tcp를 신뢰성 있게 만드는 것... ~핸드셰이크 절차 등 추가
go 에서 TCP 서버는 net.Listen
표준 라이브러리 함수를 사용해 작성할 수 있다. 아래에 코드로는 리스너를 생성하는 테스트 코드이다.
package TCP
import (
"net"
"testing"
)
func TestListener(t *testing.T) {
// 리스너 생성
/* net.Listen 함수
net.Listen 함수는 매개변수로 네트워크 종류와 콜론으로 구분된 IP 주소와 포트 문자열을 받는다.
실습 코드에서는 네트워크 종류는 tcp 로 설정해주고, 주소는 루프백 주소, 포트는 0으로 주었다.
포트가 0이거나 비워져있으면 Go가 리스너에 사용가능한 무작위 포트 번호를 할당한다.
IP 주소를 생략하면 리스너는 시스템상의 모든 유니캐스트와 애니캐스트 IP 주소에 바인딩된다.
반환 값으로 net.Listener 인터페이스와 에러 인터페이스를 반환한다.
이미 바인딩된 포트에 리스너가 바인딩을 시도하는 경우 net.Listen 함수는 에러를 반환한다.
*/
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
/*
Close 메서드를 사용하여 항상 리스너를 graceful close 해준다.
리스너를 종료하는 데 실패하면 메모리 누수가 발생하거나
코드상에서 리스너의 Accept 메서드가 무한정 블로킹되며 deadlock 이 발생 할 수 있다.
*/
defer func() { _ = listener.Close() }()
// Addr 리스너를 아용하여 리스너의 주소를 얻어 올 수 있다.
t.Logf("bound to %q", listener.Addr())
아래의 코드는 리스너가 TCP의 수신 연결 요청을 수락하는 과정을 보여준다.
// 리스너가 TCP 의 수신 연결 요청을 수락하는 과정
/*
하나 이상의 수신 연결을 처리하기 위해서는 for 루프를 사용하여 서버가 계속해서 수신 연결 요청을 수락하고,
goroutine 에서 해당 연결을 처리하고, 다시 for 루프로 돌아와서 다음 연결 요청을 수락할 수 있도록 대기해야 한다.
*/
for {
// 리스너의 Accept() 메서드는 수신 연결을 감지하고 클라이언트와 서버 간의 TCP 루프의 처음을 시작한다.
// 이 메서드는 리스너가 수신 연결을 감지하고 클라이언트와 서버 간의 TCP 핸드셰이크 절차가 완료될 때까지 블로킹된다.
// 메서드는 net.Conn 인터페이스와 에러를 반환한다, 가령 TCP 핸드셰이크가 실패하거나 리스너가 닫힌 경우 에러 인터페이스의 값이 nil 외의 값을 갖게 된다.
// 현재 예시에서는 TCP 수신 연결을 수락했기 때문에 net.TCPConn 객체의 포인터가 된다. 연결 인터페이스는 서버 측면에서의 TCP 연결을 나타낸다.
// net.TCPConn 객체는 net.Conn 인터페이스에서 제공하는 것보다 더 많은 기능을 제공해 세밀한 제어가 가능하다.
conn, err := listener.Accept()
if err != nil {
return err
}
// 클라이언트의 연결을 동시에 처리하기 위해 고루틴을 사용해 각 연결을 비동기적으로 처리하도록 하여 리스너가 다음 수신 연결을 처리할 수 있도록 한다.
//고루틴을 사용하지 않고 코드를 작성하여 처리하는것도 가능은 하지만, Go 언어의 장점을 살리지 못해 비효율적이다.
go func(c net.Conn) {
// 연결 객체의 close 메서드를 호출하여 고루틴이 종료되기 전에 호출하여 서버로 FIN 패킷을 보내 연결이 graceful close 될 수 있도록 해준다.
defer c.Close()
// TCP 연결을 사용하여 비즈니스 로직을 작성
}(conn)
}
}
아래의 코드는 클라이언트 측면에서 TCP 서버로 연결하고 연결을 수립하는 절차를 보여주는 테스트이다.
package TCP
import (
"io"
"net"
"testing"
)
func TestDial(t *testing.T) {
// 랜덤 포트에 리스너 생성
listener, err := net.Listen("tcp", "127.0.0.1:")
if err != nil {
t.Fatal(err)
}
// 병렬로 실행 중인 다른 함수가 끝날 때까지 대기시키기 위해 chan 사용, struct{}형이 Go의 자료형 중 가장 공간을 작게 차지하기 때문에 struct{} 형을 받게 한다.
done := make(chan struct{})
go func() { // 리스너를 고루틴에서 시작해서 이후의 테스트에서 클라이언트 측에서 연결할 수 있도록한다.
defer func() { done <- struct{}{} }()
for {
/*
리스너의 Accept 메서드는 리스너가 종료되면, 즉시 블로킹이 해제되고 에러를 반환한다.
이 때 에러는 무언가 실패했다는 의미가 아니다.
*/
conn, err := listener.Accept()
if err != nil {
t.Log(err)
return // 리스너의 고루틴이 종료되며 테스트가 완료된다.
}
go func(c net.Conn) {
defer func() {
c.Close() // 커넥션 핸들러는 연결 객체의 close 메서드를 호출하며 종료, close 메서드는 FIN 패킷을 전송하며 TCP 의 graceful close 를 마무리
done <- struct{}{}
}()
buf := make([]byte, 1024)
for {
n, err := c.Read(buf) // FIN 패킷을 받고 나면 Read 메서드는 io.EOF 에러를 반환, 리스너 측에서는 반대편 연결이 종료되었다는 의미
if err != nil {
if err != io.EOF {
t.Error(err)
}
return
}
t.Logf("reveived: %q", buf[:n])
}
}(conn)
}
}()
/*
net.Dial 함수는 tcp 같은 네트워크 종류와 IP 주소, 포트의 조합을 매개변수로 받는다.
Dial 함수에서 두 번째 매개변수로 받은 IP 주소, 포트를 이용해 리스너로 연결을 시도.
IP 주소 대신 호스트명을 사용할 수도 있고, 포트 대신에 http 와 같은 서비스명을 사용할 수도 있다.
호스트명이 하나 이상의 주소로 해석되면 go는 성공할 때까지 각각의 IP 주소에 연결을 시도한다.
*/
conn, err := net.Dial("tcp", listener.Addr().String()) // Dial 함수는 연결 객체와 에러 인터페이스의 값을 반환.
if err != nil {
t.Fatal(err)
}
conn.Close() // 리스너 연결을 성골적으로 수립한 후, 클라이언트 측에서 graceful close 를 시작한다.
<-done
listener.Close() // 마지막으로 리스너를 종료
<-done
}
코드를 올려둔 리포지토리: https://github.com/dong5854/network-programming-golang
참고자료:
애덤 우드벡 저/김찬빈 역, Go 언어를 활용한 네트워크 프로그래밍 - 제이펍