GNPS (Go Network Programming School)의 내용을 정리한 글입니다.
Go 언어를 활용한 네트워크 프로그래밍 - 책으로 스터디를 진행합니다.

UDP의 특징
UDP는 통신에 대한 신뢰성이 없습니다. 전송 확인 메커니즘이 없기 때문에 수신자도 수신 확인 패킷을 보내지 않습니다. 패킷 순서도 보장되지 않습니다.
단순하기 때문에 빠르며, 이런 특징이 필요한 애플리케이션에서 UDP를 사용하는 것이 유리합니다.
안정적인 연결과 연속적인 패킷의 흐름을 가지는 TCP와 다르게 UDP는 투박합니다. 안정적인 연결 확인도 없고, 세션 확인도 없으며, 데이터 수신 확인도 없습니다.
net.Conn 인터페이스는 TCP 연결을 사용하는 데는 유용하지만 UDP를 사용할 때는 적합하지 않습니다. 따라서 패킷 지향적인 인터페이스인 net.PacketConn 인터페이스를 주로 사용합니다.
UDP 에코 서버 구현
TCP 기반 listener에서는 Accept 과정이 있었지만 UDP listener는 Accept 메서드가 없습니다. 핸드셰이크 없이 특정 포트로 연결을 대기하면서 모든 입력을 받습니다. 따라서 클라이언트의 주소가 중요하며 어떤 노드에서 메시지가 왔는지를 리턴해줍니다. 동일한 이유로 WriteTo 메서드 또한 클라이언트 주소를 메서드의 매개변수로 전달받아 패킷을 전송합니다.
package udpbasic
import (
"context"
"fmt"
"net"
)
func echoServerUDP(ctx context.Context, addr string) (net.Addr, error) {
s, err := net.ListenPacket("udp", addr)
if err != nil {
return nil, fmt.Errorf("binding to udp %s: %w", addr, err)
}
go func() {
go func() {
<-ctx.Done()
_ = s.Close()
}()
buf := make([]byte, 1024)
for {
n, clientAddr, err := s.ReadFrom(buf)
if err != nil {
return
}
_, err = s.WriteTo(buf[:n], clientAddr)
if err != nil {
return
}
}
}()
return s.LocalAddr(), nil
}
받은 데이터를 그대로 되돌려 보내는 에코 서버는 net.PacketConn으로부터 데이터를 읽을 때 반환되는 클라이언트 주소를 처리해야 합니다. UDP 연결을 맺고 net.ListenPacket 함수는 net.PacketConn 인터페이스를 리턴합니다. ReadFrom과 WriteTo 메서드를 호출하여 클라이언트에서 읽어온 데이터를 클라이언트의 주소로 다시 전달합니다.
UDP 에코 서버 테스트
package udpbasic
import (
"bytes"
"context"
"net"
"testing"
)
func TestEchoServerUDP(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
serverAddr, err := echoServerUDP(ctx, "127.0.0.1:")
if err != nil {
t.Fatal(err)
}
defer cancel()
client, err := net.ListenPacket("udp", "127.0.0.1:")
if err != nil {
t.Fatal(err)
}
defer func() { _ = client.Close() }()
msg := []byte("ping")
_, err = client.WriteTo(msg, serverAddr)
if err != nil {
t.Fatal(err)
}
buf := make([]byte, 1024)
n, addr, err := client.ReadFrom(buf)
if err != nil {
t.Fatal(err)
}
if addr.String() != serverAddr.String() {
t.Fatalf("received reply from %q instead of %q", addr, serverAddr)
}
if !bytes.Equal(msg, buf[:n]) {
t.Errorf("expected reply %q; actual reply %q", msg, buf[:n])
}
}
에코 서버에 통신을 요청하는 클라이언트 코드는 서버의 주소를 반환받아 서버로 메시지를 전송하고, 클라이언트에서 데이터를 읽어올 때 ReadFrom 메서드에서 반환된 주소를 사용하여 에코 서버가 메시지를 보냈는지 확인할 수 있습니다.
UDP의 송신자 검증 필요성
UDP는 세션 기능이 없기 때문에 커넥션을 통해서 패킷을 수신한 후 애플리케이션 상에서 처리해야 할 일이 더 많습니다. Conn으로부터 수신되는 패킷이 같은 송신자에서 오지 않을 수 있기 때문에 반드시 송신자의 주소를 검증해야 합니다.
package echo
import (
"bytes"
"context"
"net"
"testing"
)
func TestListenPacktUDP(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
serverAddr, err := echoServerUDP(ctx, "127.0.0.1:")
if err != nil {
t.Fatal(err)
}
defer cancel()
client, err := net.ListenPacket("udp", "127.0.0.1:")
if err != nil {
t.Fatal(err)
}
defer client.Close()
// ----
interloper, err := net.ListenPacket("udp", "127.0.0.1:")
if err != nil {
t.Fatal(err)
}
interrupt := []byte("pardon me")
n, err := interloper.WriteTo(interrupt, client.LocalAddr())
if err != nil {
t.Fatal(err)
}
_ = interloper.Close()
if l := len(interrupt); l != n {
t.Fatalf("wrote %d bytes of %d", n, l)
}
// ----
ping := []byte("ping")
_, err = client.WriteTo(ping, serverAddr)
if err != nil {
t.Fatal(err)
}
buf := make([]byte, 1024)
n, addr, err := client.ReadFrom(buf)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(interrupt, buf[:n]) {
t.Errorf("expected reply %q; actual reply %q", interrupt, buf[:n])
}
if addr.String() != interloper.LocalAddr().String() {
t.Errorf("expected message from %q; actual sender is %q", interloper.LocalAddr(), addr)
}
n, addr, err = client.ReadFrom(buf)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(ping, buf[:n]) {
t.Errorf("expected reply %q; actual reply %q", ping, buf[:n])
}
if addr.String() != serverAddr.String() {
t.Errorf("expected message from %q; actual sender is %q", serverAddr, addr)
}
}
이 코드는 UDP 패킷 통신에서 여러 송신자로부터 메시지를 수신할 때, 각 메시지가 올바른 송신자로부터 왔는지 확인하는 테스트 코드입니다.
먼저 콘텍스트를 생성하고 UDP 에코 서버를 시작합니다. 에코 서버는 받은 메시지를 그대로 되돌려주는 서버입니다. 그다음 클라이언트 UDP 연결을 생성합니다.
테스트의 핵심은 "interloper"라는 방해자 역할을 하는 또 다른 UDP 연결을 만드는 것입니다. 이 방해자는 클라이언트에게 "pardon me"라는 메시지를 먼저 보내고 즉시 연결을 닫습니다. 그 후 클라이언트는 에코 서버에 "ping" 메시지를 보냅니다.
클라이언트는 두 번의 ReadFrom 호출을 통해 메시지를 수신합니다. 첫 번째로 받은 메시지는 방해자가 보낸 "pardon me"이어야 하고, 송신자 주소도 방해자의 주소와 일치하는지 검증합니다. 두 번째로 받은 메시지는 에코 서버가 되돌려준 "ping"이어야 하고, 송신자 주소가 서버 주소와 일치하는지 확인합니다.
이 테스트는 UDP의 비연결성 특성상 하나의 소켓으로 여러 송신자로부터 메시지를 받을 수 있으며, ReadFrom 메서드가 각 메시지의 출처를 정확하게 식별할 수 있는지를 검증합니다.
이는 net.PacketConn 이 아닌 net.Conn 을 사용해서 응답마다 송신자의 주소를 확인하지 않아도 되는 방식으로 개선할 수 있습니다.
net.Conn을 사용한 개선
이는 net.PacketConn이 아닌 net.Conn을 사용해서 응답마다 송신자의 주소를 확인하지 않아도 되는 방식으로 개선할 수 있습니다.
client, err := net.Dial("udp", serverAddr.String())
net.Conn 인터페이스를 사용한 연결은 Read와 Write를 할 때 이미 연결된 주소로만 메시지를 읽고 쓰게 됩니다. net.Conn을 사용하면 지정되지 않은 송신자 주소에서 오는 데이터에 대한 확인 코드가 없어도 되기 때문에 코드를 깔끔하게 관리할 수 있습니다. 다만, net.Conn을 사용하더라도 UDP의 특성인 정상적으로 수신되지 않은 패킷에 대한 확인은 애플리케이션에서 반드시 해야 합니다.
파편화? 피해보자
파편화란 네트워크 상에서 효율적인 데이터 전송을 위해 패킷을 작게 조각내 전송하는 3 계층 인터넷 프로토콜의 절차입니다. 모든 네트워크에는 MTU라는 최대 전송 단위라는 패킷 크기의 제한이 존재합니다. MTU 이상의 패킷을 수신하게 되면 파편화가 필요합니다. 조각난 패킷 파편들이 전송될 노드 간 MTU보다 작아야 합니다. 운영체제는 파편화된 패킷을 재조립하고 애플리케이션에게 전달합니다.
패킷의 파편화는 UDP에서 문제입니다. TCP와는 달리 패킷을 복구할 수 있는 기능이 없기 때문입니다. 패킷이 누락되거나 손실된 경우 전체 패킷을 다시 보내야 하기 때문에 문제가 됩니다. 파편화를 줄이고 효과적인 데이터 전송을 하기 위해서는 호스트와 원격 노드 간의 MTU를 알아내고, 이를 기반으로 페이로드의 크기를 적절히 조정하여 파편화 가능성을 낮출 수 있습니다.
'School > GNPS' 카테고리의 다른 글
| [GNPS] 챕터 7 유닉스 도메인 소켓! (0) | 2025.11.08 |
|---|---|
| [GNPS] 챕터 6 UDP 통신의 신뢰성 확보 (0) | 2025.11.07 |
| [GNPS] 챕터 4 TCP 데이터 전송하기 (0) | 2025.11.05 |
| [GNPS] 챕터 2 리소스의 위치와 트래픽 라우팅 (0) | 2025.10.23 |
| [GNPS] 챕터 1 네트워크 시스템 개요, 챕터 3 신뢰성 있는 TCP 데이터 스트림 (0) | 2025.10.03 |