[Golang] Simple UDP server and client
golang, udp, network ·Sending and Receiving UDP datagram in Go
Go 의 UDP networking 에 대해 알아보다가 API 를 깊게 파헤쳐놓은, 매우 좋은 페이지가 있어 약간의 번역을 곁들여 소개합니다. 원본의 내용을 읽어보며 필요한 부분만을 발췌, 번역한 내용이므로 가급적 원본의 full article 을 읽어 보시길 권합니다.
원본 출처 : A UDP server and client in Go - Getting from Golang’s net package down to the Linux kernel methods invoked when UDP messages are sent.
UDP networking overview
UDP 는 기본적으로 connection-oriented 방식이 아닙니다. 따라서, 클라이언트가 보내는 메시지를 서버가 받으리라는 보장이 없고, 서버의 echo 메시지를 클라이언트가 받으리라는 보장 역시 마찬가지로 없습니다. 이상적인 경우 (양 방향 전송이 모두 성공하는 경우) 의 흐름도는 아래와 같습니다.
TIME DESCRIPTION
t0 client and server exist
client server
10.0.0.1 10.0.0.2
t1 client sends a message to the server
client ------------> server
10.0.0.1 msg 10.0.0.2
(from:10.0.0.1)
(to: 10.0.0.2)
t2 server receives the message, then it takes
the address of the sender and then prepares
another message with the same contents and
then writes it back to the client
client <------------ server
10.0.0.1 msg2 10.0.0.2
(from:10.0.0.1)
(to: 10.0.0.2)
t3 client receives the message
client server
10.0.0.1 10.0.0.2
thxx!! :D :D
ps.: ports omitted for brevity
Sending UDP packets using Go
먼저, UDP packet 을 전송하는 클라이언트의 코드부터 보겠습니다.
// client function
// reader (io.Reader) 의 입력 내용을 address (string) 주소의 서버에 UDP packet 으로 전송하고,
// 서버로부터 UDP packet 을 전송받아 그 내용을 출력한다.
// 일정 시간 이상 응답이 없을 경우 err (error) 를 반환.
func client(ctx context.Context, address string, reader io.Reader) (err error) {
// ResolveUDPAddr() 을 이용하여 *UDPAddr 구조체를 얻는다.
raddr, err := net.ResolveUDPAddr("udp", address)
if err != nil {
return
}
// DialUDP 는 반환된 소켓(혹은 connection)이 지정된 주소로만 전송/수신이 가능하도록 강제하는 역할.
conn, err := net.DialUDP("udp", nil, raddr)
if err != nil {
return
}
// 잊지말자 defer
defer conn.Close()
doneChan := make(chan error, 1)
go func() {
// io.Copy(), reader 로 들어온 input 을 output (conn) 으로 그대로 복사.
n, err := io.Copy(conn, reader)
if err != nil {
doneChan <- err
return
}
fmt.Printf("packet-written: bytes=%d\n", n)
buffer := make([]byte, maxBufferSize)
// 서버로부터의 응답을 대기할 deadline 설정.
// conn 에 대한 I/O 가 deadline 시간 동안 blocking 되어 있을 경우
// blocking 상태를 해제하고 error 를 반환한다.
deadline := time.Now().Add(time.Second * 5)
err = conn.SetDeadline(deadline)
if err != nil {
doneChan <- err
return
}
nRead, addr, err := conn.ReadFrom(buffer)
if err != nil {
doneChan <- err
return
}
fmt.Printf("packet-received: bytes=%d from=%s\n", nRead, addr.String())
// 정상 종료
doneChan <- nil
}()
select {
// Context 로 인한 종료 케이스. 여기에서는 context.Background() 를 사용했으므로,
// must not reached here.
case <-ctx.Done():
fmt.Println("Cancelled by context")
err = ctx.Err()
// 위 goroutine 이 보낸 종료 신호를 받아서 err 를 리턴
case err = <-doneChan:
}
return
}
TCP Dialing vs UDP Dialing
Go 에서 TCP 를 이용하여 통신할 때는 보통 net.Dial
을 사용하지만, UDP 의 경우에는 net.DialUDP
method 를 사용합니다. Dial
method 를 이용할 경우 Conn
interface 가 반환되는데, DialUDP
가 반환하는 UDPConn
을 이용하는 편이 UDP 에 관련한 다양한 method (특히 ReadFrom
, 이는 아래 server 에서 자세히 설명합니다.) 들을 사용할 수 있기 때문입니다.
Dial
과 DialUDP
는 리턴 타입 외에도 내부적인 동작에 차이점이 있습니다. 사실, ‘UDP’ 와 ‘Dial’ 이라는 개념은 언뜻 잘 어울리지 않아 보입니다. UDP 전송의 기본적인 개념은, 패킷 헤더에 IP address, Port 를 기록하여 network line 을 통해 전송하되, 패킷이 목적지까지 잘 도착했는지까지는 신경쓰지 않습니다. “보내고 땡” 인 거죠. TCP 의 경우 통신할 상대방과 ‘connection’ 을 맺는 과정이 있어 ‘Dial’ 과정을 거치는 것이 자연스럽지만, UDP 는 그렇지 않습니다. 그렇다면, Dial
과 DialUDP
의 차이는 무엇일까요?
- TCP
net.Dial("tcp", "1.1.1.1:53")
// strace -f -e trace=network ./main
// [pid 4891] socket(
AF_INET,
-----> SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK,
IPPROTO_IP) = 3
// [pid 4891] connect(3, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("1.1.1.1")}, 16) = -1 EINPROGRESS (Operation now in progress)
- UDP
net.Dial("udp", "1.1.1.1:53")
// strace -f -e trace=network ./main
// [pid 5517] socket(
AF_INET,
-----> SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK,
IPPROTO_IP) = 3
// [pid 5517] connect(3, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("1.1.1.1")}, 16) = 0
Dial
과 UDPDial
모두 결국에는 system call 을 통하여 소켓을 생성하고 connect(2)
를 호출합니다. 유일하게 다른 부분은 socket 을 생성할 때 주는 option 이 각각 SOCK_STREAM
/ SOCK_DGRAM
으로 다릅니다. 일반적으로, C 에서 UDP client / server 를 구현할 때, 클라이언트에서 connect(2)
를 호출하는 경우는 별로 없습니다. SOCK_DGRAM
속성이 지정된 소켓에 대하여 connect
를 호출할 경우에는, TCP (SOCK_STREAM
)의 경우에서처럼 실제 네트워크를 통해 패킷을 주고받으며 connection 을 설정하는 것이 아니라 해당 소켓이 지정한 address 의 상대방과’만’ 통신하도록 설정됩니다. 아래는 connect(2)
의 man page 발췌입니다.
If the socket sockfd is of type SOCK_DGRAM, then addr is the address to which datagrams are sent by default, and the only address from which datagrams are received.
If the socket is of type SOCK_STREAM or SOCK_SEQPACKET, this call attempts to make a connection to the socket that is bound to the address specified by addr.
실제로 Dial
과 DialUDP
호출시 kernel function call stack 이나, iptables
의 로깅 기능을 이용하여 network 패킷을 추적해 보았을 때, TCP 의 경우는 실제로 connection 을 맺기 위한 패킷이 오가는 것이 확인되지만, UDP 는 그러한 과정이 발생하지 않음을 확인할 수 있습니다. (원문의 TCP Dialing vs UDP Dialing 부분을 참고)
A UDP Server in Go
// server function
// simple echo server
// receive -> send 의 순서를 제외하고는, 클라이언트와 거의 다를 것이 없다.
func server(ctx context.Context, address string) (err error) {
// net.ResolveUDPAddr + ListenUDP 조합 대신
// ListenPacket (PacketConn 을 반환) 을 사용했다.
pc, err := net.ListenPacket("udp", address)
if err != nil {
return
}
defer pc.Close()
doneChan := make(chan error, 1)
buffer := make([]byte, maxBufferSize)
go func() {
for {
n, addr, err := pc.ReadFrom(buffer)
if err != nil {
doneChan <-err
return
}
fmt.Printf("packet-received: bytes=%d from=%s\n", n, addr.String())
deadline := time.Now().Add(time.Second * 5)
err = pc.SetWriteDeadline(deadline)
if err != nil {
doneChan <-err
return
}
n, err = pc.WriteTo(buffer[:n], addr)
if err != nil {
doneChan <-err
return
}
fmt.Printf("packet-written: bytes=%d to=%s\n", n, addr.String())
}
}()
select {
case <-ctx.Done():
fmt.Println("Cancelled by context")
err = ctx.Err()
case err = <-doneChan:
}
return
}
클라이언트의 메시지를 받아서 그대로 다시 클라이언트에 돌려주는 echo server 입니다. TCP 의 경우 accept
가 반환해주는 connection 을 이용하여 클라이언트에 메시지를 전송할 수 있지만, UDP 의 경우는 그렇지 못합니다. 또한 Conn
인터페이스에서 제공하는 Read
method 만으로는 수신한 메시지의 발신자 주소를 알아낼 수 없기 때문에 반드시 ReadFrom
method 를 제공하는 UDPConn
을 사용해야 합니다. 여기에서는 PacketConn
인터페이스 - packet-oriented network connection을 지원 - 를 반환하는 net.ListenPacket
을 이용하여 서버를 구현하였습니다.