[CS] 소켓(Socket)

15 분 소요


TCP/IP 네트워크 프로그래밍을 공부하다 보면 반드시 마주치는 개념이 소켓(Socket)이다. 소켓은 프로세스 간 통신(IPC)의 핵심 메커니즘으로, 네트워크 통신뿐만 아니라 같은 머신 내 프로세스 간 통신에도 사용된다. 이 글에서는 소켓의 기본 개념부터 분류 체계, 물리적 실체, 통신 메커니즘까지 살펴본다.


TL;DR

  • 소켓: 프로세스 간 통신을 위한 엔드포인트, 본질적으로 파일 디스크립터
  • 분류 기준: 도메인(어디서) × 타입(어떻게) 조합
  • 도메인: AF_UNIX(로컬), AF_INET/AF_INET6(네트워크), 기타(AF_NETLINK, AF_PACKET 등)
  • 타입: SOCK_STREAM(연결 지향), SOCK_DGRAM(비연결), SOCK_RAW(저수준), SOCK_SEQPACKET(연결+메시지)
  • 물리적 실체: 파일 시스템 상의 소켓 파일은 가상 파일이며, 실제 데이터는 커널 메모리 소켓 버퍼에 존재
  • 주요 조합: TCP(AF_INET + SOCK_STREAM), UDP(AF_INET + SOCK_DGRAM), Unix Stream(AF_UNIX + SOCK_STREAM)


배경 지식

소켓을 이해하려면 먼저 TCP와 UDP를 알아야 한다. 소켓의 분류와 동작 방식을 설명할 때 이 프로토콜을 기반으로 한 소켓이 계속 등장하기 때문이다.

TCP (Transmission Control Protocol)

TCP는 전송(Transmission)을 제어(Control)하는 프로토콜이다. 데이터가 올바른 순서로, 손실 없이 도착하도록 연결을 관리하고 흐름을 제어한다.

특징

특징 설명
연결 지향 통신 전 연결 수립 과정 필요 (3-way handshake)
순서 보장 데이터가 보낸 순서대로 도착
신뢰성 손실 시 재전송
속도 상대적으로 느림 (신뢰성 보장을 위한 오버헤드)


3-Way Handshake

TCP 연결 수립 시 3-way handshake 과정이 필요하다.

    Client                       Server
       |                            |
       |   1. SYN (seq=x)           |
       |--------------------------->|
       |                            |
       |   2. SYN-ACK (seq=y,ack=x+1)
       |<---------------------------|
       |                            |
       |   3. ACK (ack=y+1)         |
       |--------------------------->|
       |                            |
            Connection ESTABLISHED
  1. 클라이언트가 SYN 보내고
  2. 서버가 SYN-ACK 보내고
  3. 클라이언트가 ACK 보내면 연결 완료


실제 코드에서는 이 과정이 추상화되어 있다.

// 클라이언트
conn, _ := net.Dial("tcp", "example.com:80")

// 서버
listener, _ := net.Listen("tcp", ":8080")
conn, _ := listener.Accept()
  • Dial(): 이 한 줄 안에서 3-way handshake가 완료된다. handshake가 끝나야 반환되므로 블로킹이 발생한다.
  • Accept(): handshake가 완료된 연결을 반환한다. 반환 시점에 이미 연결 완료 상태다.

위 코드에서 보듯이, TCP 연결 수립(3-way handshake)은 커널의 TCP 스택에서 처리되기 때문에 개발자가 직접 다룰 일은 없다. Go뿐 아니라 Python의 socket.connect(), Java의 Socket(), C의 connect() 등 모든 언어에서 동일하다.


사용 사례

데이터가 반드시 도착해야 하는 경우에 사용한다.

  • HTTP/HTTPS
  • SSH
  • 파일 전송 (FTP, SFTP)
  • 이메일 (SMTP)


UDP (User Datagram Protocol)

UDP는 데이터그램(Datagram)을 그대로 전달하는 프로토콜이다. 연결 수립이나 순서 보장 없이, 개별 패킷(데이터그램)을 독립적으로 전송한다.

특징

특징 설명
비연결 연결 수립 없이 바로 전송
순서 비보장 도착 순서가 보낸 순서와 다를 수 있음
재전송 없음 손실되어도 재전송 안 함
속도 빠름 (오버헤드 적음)


사용 사례

속도가 중요하고 약간의 손실이 허용되는 경우에 사용한다.

  • DNS
  • 실시간 스트리밍 (비디오, 오디오)
  • 온라인 게임
  • VoIP


Stream vs Datagram

TCP와 UDP는 StreamDatagram의 차이로도 이해할 수 있다.

용어 정의

  • Stream (스트림): 연속적인 바이트 흐름. 데이터의 시작과 끝 경계가 없고, 물 흐르듯 연속적으로 전달된다.
  • Datagram (데이터그램): 독립적인 메시지 단위. 각 패킷이 개별적으로 전송되며, 메시지 경계가 보존된다.

비교

구분 Stream (SOCK_STREAM) Datagram (SOCK_DGRAM)
데이터 단위 바이트 스트림 (경계 없음) 메시지 단위 (경계 있음)
연결 연결 지향 비연결
순서 보장 비보장
신뢰성 신뢰성 있음 신뢰성 없음
비유 전화 통화 (연결하고 대화) 우편 (그냥 보내기만)
대표 프로토콜 TCP UDP


소켓 기본 개념

정의

소켓은 프로세스 간 통신(IPC)을 위한 엔드포인트다.

  • 프로세스 간: 네트워크 너머 원격 프로세스일 수도 있고, 같은 머신 내 로컬 프로세스일 수도 있음
  • 엔드포인트: 통신의 끝점, 데이터를 보내고 받는 지점

네트워크 통신이든 로컬 통신이든, 데이터를 주고받을 수 있도록 추상화된 인터페이스다.


소켓도 파일이다

Unix/Linux의 “Everything is a file” 철학에 따라 소켓도 파일이다 ([CS] Everything is a File 참고).

ls -l 출력에서 첫 번째 문자가 s이면 소켓 파일이다.

기호 종류 설명 예시
- 일반 파일 텍스트, 바이너리, 이미지 등 -rw-r--r-- file.txt
d 디렉토리 폴더 drwxr-xr-x /home/
l 심볼릭 링크 다른 파일을 가리키는 링크 lrwxrwxrwx rtc -> rtc0
c 문자 장치 한 바이트씩 전송 crw-rw-rw- /dev/null
b 블록 장치 블록 단위 전송 brw-rw---- /dev/sda
s 소켓 프로세스 간 통신 srwxrwxrwx /run/systemd/notify
p 파이프 프로세스 간 데이터 전달 Named pipe


본질: 파일 디스크립터

소켓은 본질적으로 파일 디스크립터다. 따라서 read()/write() 시스템 콜로 다룰 수 있다.

대상 read() write() close()
일반 파일 데이터 읽기 데이터 쓰기 fd 해제
소켓 데이터 받기 데이터 보내기 연결 종료 + fd 해제


소켓 타입별로 read()/write()/close()의 의미는 다음과 같다.

소켓 타입 read() write() close()
TCP 상대방이 보낸 데이터 받기 상대방에게 데이터 보내기 4-way handshake로 연결 종료
Unix Domain 다른 프로세스가 보낸 데이터 받기 다른 프로세스에게 데이터 보내기 소켓 파일 연결 해제
UDP 누군가 보낸 패킷 받기 목적지에 패킷 보내기 fd 해제 (연결 개념 없음)


소켓 전용 I/O: send()/recv()

소켓은 파일 디스크립터이므로 read()/write()로 다룰 수 있지만, 소켓 전용 시스템 콜도 있다.

시스템 콜 대상 특징
read()/write() 모든 fd 범용 I/O
send()/recv() 소켓 전용 플래그 옵션 제공
sendto()/recvfrom() 소켓 전용 주소 지정 (UDP용)
close() 모든 fd fd 해제 (소켓은 연결 종료)
shutdown() 소켓 전용 방향별 종료 (half-close 가능)
// 범용 파일 I/O
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

// 소켓 전용 (flags 파라미터 추가)
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

// UDP용 (주소 지정)
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);

// 연결 종료
int close(int fd);                    // 양방향 종료 + fd 해제
int shutdown(int sockfd, int how);    // 방향별 종료 (SHUT_RD, SHUT_WR, SHUT_RDWR)

send()/recv()flags 파라미터로 추가 동작을 지정할 수 있다.

플래그 설명
MSG_PEEK 데이터를 버퍼에서 제거하지 않고 읽기
MSG_DONTWAIT 논블로킹으로 동작
MSG_OOB Out-of-band 데이터 처리

플래그 없이 사용하면 read()/write()와 동일하게 동작한다.


read() 반환값과 연결 상태

TCP 소켓에서 read()의 반환값은 연결 상태를 나타낸다.

반환값 의미 원인
> 0 정상 수신 데이터 도착
0 (EOF) 정상 종료 상대방이 close() 또는 shutdown(SHUT_WR) 호출 (FIN 전송)
-1 + ECONNRESET 비정상 종료 RST 패킷 수신, 네트워크 끊김
-1 + EAGAIN 데이터 없음 논블로킹 모드에서 버퍼 비어있음

주요 에러/상태 코드

코드 의미 발생 상황
EOF (0 반환) End of File 상대방이 정상 종료 (FIN 전송)
EPIPE Broken Pipe 이미 닫힌 연결에 write() 시도
ECONNRESET Connection Reset 상대방이 RST 패킷 전송 (비정상 종료)
EAGAIN / EWOULDBLOCK 재시도 필요 논블로킹 모드에서 데이터 없음
ECONNREFUSED Connection Refused 상대방이 연결 거부 (포트 안 열림)
ETIMEDOUT Timeout 연결 또는 응답 타임아웃

참고: Half-Close

close()는 양방향 종료지만, shutdown(fd, SHUT_WR)은 송신만 종료하고 수신은 유지한다 (half-close). 이때 상대방의 read()는 EOF를 반환하지만, 상대방은 여전히 데이터를 보낼 수 있다.


생성 방법: socket() 시스템 콜

소켓은 도메인, 타입, 프로토콜을 조합해서 생성한다.

int socket(int domain, int type, int protocol);
파라미터 의미 예시
domain 어디서 통신하냐 AF_INET, AF_UNIX
type 어떻게 통신하냐 SOCK_STREAM, SOCK_DGRAM
protocol 어떤 프로토콜 보통 0 (domain+type 기본 프로토콜)


생성 과정

socket() 호출 시 내부적으로 다음이 일어난다.

  1. 커널에서 소켓 구조체 생성: 커널 메모리에 소켓을 위한 자료구조 할당
    • struct socket: 소켓 메타데이터
    • struct sock: 송신/수신 버퍼, 프로토콜 상태 정보
  2. 파일 디스크립터 반환
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    // sockfd = 3 (예시) → 이후 read/write/close 등에 사용
    
    • 성공 시: 음이 아닌 정수(파일 디스크립터) 반환
    • 실패 시: -1 반환 + errno 설정
  3. 생성만 된 상태: 통신 준비는 아직 안 됨
    • 소켓이 프로세스 간 통신을 위한 엔드포인트이므로, 엔드포인트만 생성된 것
    • 실제 통신하려면 추가 설정 필요


다음 단계

소켓 생성 후 역할에 따라 다른 단계가 필요하다.

소켓 역할 다음 단계
서버 (TCP) bind()listen()accept()
클라이언트 (TCP) connect()
UDP bind() (선택) → sendto()/recvfrom()
Unix Domain 위와 동일한 패턴


소켓 분류 체계

소켓은 도메인(어디서 통신)과 타입(어떻게 통신)의 조합으로 분류된다.

도메인 (Domain)

“어디서” 통신하냐를 결정한다. Domain은 ‘관할 영역’, ‘지배 범위’라는 의미다.

도메인 범위 주소 형식 설명
AF_UNIX 같은 호스트 (로컬) 파일 경로 Unix Domain Socket
AF_INET 네트워크 (IPv4) IP:Port 인터넷 소켓
AF_INET6 네트워크 (IPv6) IP:Port 인터넷 소켓 (IPv6)
AF_NETLINK 커널-유저 공간 - 커널과 통신
AF_PACKET 링크 레벨 - raw 패킷 접근
AF_BLUETOOTH 블루투스 범위 Bluetooth MAC 블루투스 통신
AF_CAN 차량 내부 - CAN 버스 통신

용어 참고: “Unix Domain Socket”의 “Domain”도 같은 맥락이다. 통신 범위(domain)가 Unix 시스템 내부로 한정되기 때문에 그렇게 불린다. 즉, 소켓 API의 domain 파라미터는 분류 기준이고, Unix Domain Socket이라는 이름은 해당 소켓의 범위를 설명한다.


AF_UNIX (Unix Domain Socket)

같은 호스트 내 프로세스 간 통신에 사용한다.

// 서버
listener, _ := net.Listen("unix", "/tmp/app.sock")

// 클라이언트
conn, _ := net.Dial("unix", "/tmp/app.sock")
  • 주소: 파일 경로 (/tmp/app.sock)
  • 특징: 네트워크 스택을 거치지 않아 빠름
  • 용도: Docker, MySQL, systemd 등 로컬 IPC


AF_INET / AF_INET6 (Internet Socket)

네트워크를 통한 통신에 사용한다.

// TCP
conn, _ := net.Dial("tcp", "192.168.1.100:8080")

// UDP
conn, _ := net.Dial("udp", "192.168.1.100:9000")
  • 주소: IP 주소 + 포트 번호
  • 용도: HTTP, SSH, DNS 등 네트워크 통신


타입 (Type)

“어떻게” 통신하냐를 결정한다.

타입 특징 예시 비유
SOCK_STREAM 연결 지향, 순서 보장, 신뢰성 TCP, Unix Stream 전화 통화
SOCK_DGRAM 비연결, 순서 비보장, 빠름 UDP, Unix Datagram 우편
SOCK_RAW IP 레벨 직접 접근 ping (ICMP), 패킷 분석 -
SOCK_SEQPACKET 연결 지향, 메시지 경계 보존 SCTP -


도메인 × 타입 조합

모든 조합이 가능한 것은 아니다. 의미 있는 조합만 지원된다.

자주 사용하는 조합

// TCP 소켓 (90% 이상 사용)
socket(AF_INET, SOCK_STREAM, 0)

// UDP 소켓
socket(AF_INET, SOCK_DGRAM, 0)

// Unix Domain Stream 소켓 (로컬 IPC)
socket(AF_UNIX, SOCK_STREAM, 0)

// Unix Domain Datagram 소켓
socket(AF_UNIX, SOCK_DGRAM, 0)


특수 목적 조합

// ping 구현 (ICMP)
socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)

// tcpdump, wireshark (패킷 캡처)
socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))

// ip 명령어 (라우팅 테이블 조작)
socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)

// SCTP (통신 시스템)
socket(AF_INET, SOCK_SEQPACKET, IPPROTO_SCTP)


전체 조합표

도메인 타입 가능 프로토콜 용도
AF_UNIX SOCK_STREAM O - Docker, MySQL 소켓
AF_UNIX SOCK_DGRAM O - systemd 로깅
AF_UNIX SOCK_SEQPACKET O - 메시지 경계 보존 IPC
AF_UNIX SOCK_RAW X - 의미 없음
AF_INET / AF_INET6 SOCK_STREAM O TCP HTTP, SSH
AF_INET / AF_INET6 SOCK_DGRAM O UDP DNS, 스트리밍
AF_INET / AF_INET6 SOCK_SEQPACKET O SCTP 통신 시스템
AF_INET / AF_INET6 SOCK_RAW O ICMP/IP ping, traceroute
AF_NETLINK SOCK_STREAM X - 의미 없음
AF_NETLINK SOCK_DGRAM O NETLINK_ROUTE 라우팅 테이블
AF_NETLINK SOCK_SEQPACKET X - 의미 없음
AF_NETLINK SOCK_RAW O NETLINK_* 커널 이벤트 수신
AF_PACKET SOCK_STREAM X - 의미 없음
AF_PACKET SOCK_DGRAM O ETH_P_* 헤더 제거된 패킷
AF_PACKET SOCK_SEQPACKET X - 의미 없음
AF_PACKET SOCK_RAW O ETH_P_ALL tcpdump, wireshark


불가능한 조합과 이유

AF_UNIX + SOCK_RAW: 로컬 IPC는 네트워크를 거치지 않아 raw하게 접근할 패킷 계층이 없다.

socket(AF_UNIX, SOCK_RAW, 0)  // X

AF_NETLINK + SOCK_STREAM: 커널 메시지는 스트림이 아니라 메시지 단위로 전달된다.

socket(AF_NETLINK, SOCK_STREAM, 0)  // X

AF_PACKET + SOCK_STREAM: 링크 계층에는 연결(connection) 개념이 없다.

socket(AF_PACKET, SOCK_STREAM, 0)  // X


소켓의 물리적 실체

소켓 파일은 가상 파일이다. 실제 데이터를 담고 있지 않다.

가상 파일

  • 정의: 실제 디스크에 데이터를 저장하지 않고, 커널 메모리의 자료 구조를 가리키는 파일
  • 특징:
    • 파일은 존재함
    • 디스크 블록을 차지하지 않음
    • inode는 있지만 데이터 블록은 없음
    • 단지 네임스페이스 상의 이름일 뿐


Unix Domain Socket: 파일 시스템에 보임

Unix Domain Socket은 파일 시스템에 소켓 파일로 나타난다. 하지만 가상 파일이다.

소켓 파일을 생성한다.

listener, _ := net.Listen("unix", "/tmp/test.sock")

ls -la로 파일 타입과 크기를 확인한다. 파일 타입은 s(소켓)이고, 크기는 0이다.

$ ls -la /tmp/test.sock
srwxr-xr-x 1 user user 0 Feb  3 10:00 /tmp/test.sock # 크기 0 바이트

du로 실제 디스크 사용량을 확인한다. 0이다.

$ du -h /tmp/test.sock
0       /tmp/test.sock

stat으로 블록 할당을 확인한다. 디스크 블록이 0개다.

$ stat /tmp/test.sock
  File: /tmp/test.sock
  Size: 0         	Blocks: 0          IO Block: 4096   socket # 디스크 블록 0개

ls -i로 inode 번호를 확인한다. inode는 존재한다 (파일 시스템 엔트리는 있음).

$ ls -i /tmp/test.sock
1234567 /tmp/test.sock
  • 타입: s (소켓)
  • 크기: 0 바이트
  • 블록: 0개
  • 역할: 커널 소켓 구조체를 찾는 “이름표”


TCP Socket: 파일 시스템에 안 보임

TCP 소켓은 파일 시스템에 아예 나타나지조차 않는다. 완전히 커널 메모리에만 존재한다.

conn, _ := net.Dial("tcp", "example.com:80")
// 파일 시스템에 아무것도 안 생김!

/proc을 통해서만 확인할 수 있다.

# 프로세스의 파일 디스크립터 목록
$ ls -l /proc/self/fd/3
lrwx------ 1 user user 64 Feb  3 10:00 3 -> socket:[45678] # 45678: inode 번호

# 소켓 연결 정보
$ ss -tn
State   Local Address:Port    Peer Address:Port
ESTAB   192.168.1.100:54321   93.184.216.34:80


실제 데이터 위치: 커널 버퍼

소켓의 실제 데이터는 커널 메모리의 소켓 버퍼에 있다.

커널 메모리 구조

핵심은 버퍼가 연결 리스트로 연결되어 있다는 점이다. 패킷이 도착하면 sk_buff 구조체에 담겨 큐에 연결되고, read()하면 큐에서 꺼내서 유저 공간으로 복사한다.

struct socket (VFS)
    └─► struct sock (Protocol)
            ├─► sk_receive_queue ─► [sk_buff] ─► [sk_buff] ─► ...  (수신 버퍼)
            └─► sk_write_queue   ─► [sk_buff] ─► [sk_buff] ─► ...  (송신 버퍼)
구조체 레벨 역할
struct socket VFS 파일 디스크립터와 연결, 소켓 타입 저장
struct sock Protocol 버퍼 큐 관리, 연결 상태, 버퍼 크기 제한
sk_buff Data 실제 패킷 데이터 (연결 리스트로 체이닝)


커널 자료구조

struct socket {
    struct sock *sk;           // 프로토콜별 정보
    struct file *file;         // 파일 디스크립터 연결
    ...
}

struct sock {
    struct sk_buff_head receive_queue;  // 수신 버퍼
    struct sk_buff_head write_queue;    // 송신 버퍼
    int state;                          // TCP 상태 등
    ...
}
구성요소 위치 설명
struct socket 커널 힙 (kmalloc) VFS와의 연결 담당
struct sock 커널 객체 캐시 프로토콜별 전용 캐시에서 할당
sk_buff 커널 객체 캐시 skbuff_head_cache에서 할당
실제 패킷 데이터 커널 페이지 alloc_pages() 또는 kmalloc()으로 할당


소켓 확인 방법

Unix Domain Socket

# ss 명령어로 확인 (-x: unix)
$ ss -x | grep test.sock
u_str  ESTAB  0  0  /tmp/test.sock

# lsof로 확인
$ lsof -U


TCP Socket

# ss 명령어로 확인 (-t: tcp, -n: 숫자로 표시)
$ ss -tn
State   Local Address:Port    Peer Address:Port
ESTAB   127.0.0.1:8080       127.0.0.1:54321

# lsof로 확인
$ lsof -i


결론: 소켓 파일에 데이터가 없다

# 소켓 파일을 cat으로 읽어도 의미 없음
$ cat /tmp/test.sock
# 아무것도 안 나옴 또는 에러

소켓 파일은 통신 채널을 가리키는 이름표일 뿐이다.

  • 파일 시스템 경로(Unix Domain)나 FD는 핸들일 뿐
  • 실제 데이터는 커널 메모리 소켓 버퍼에 있음
  • “Everything is a file” 철학의 추상화 덕분에 동일한 인터페이스로 접근 가능


소켓 통신 메커니즘

소켓의 Read/Write

소켓 타입에 따라 Write()Read()의 데이터 경로가 다르다.

TCP

네트워크를 거쳐 데이터가 전달된다.

  Sender          Kernel        Network        Kernel        Receiver
     |               |              |              |              |
     | Write()       |              |              |              |
     |-------------->|              |              |              |
     |          [Send Buffer]       |              |              |
     |               |  "Hello"     |              |              |
     |               |------------->|              |              |
     |               |         TCP Packet          |              |
     |               |              |------------->|              |
     |               |              |         [Recv Buffer]       |
     |               |              |              |  "Hello"     |
     |               |              |              |<-------------|
     |               |              |              |    Read()    |

Write 과정: 커널 송신 버퍼에 복사 → TCP 패킷화 → 네트워크 전송 → 수신 버퍼 도착

Read 과정: 커널 수신 버퍼에서 복사 (버퍼가 비어있으면 blocking)


Unix Domain Socket

TCP/IP 프로토콜 스택을 거치지 않고, VFS를 통해 커널 내에서 직접 버퍼 간 복사가 일어난다.

  Sender          Kernel          Receiver
     |               |                |
     | Write()       |                |
     |-------------->|                |
     |          [Send Buffer]         |
     |               |                |
     |          [Recv Buffer]         |
     |               |    "Hello"     |
     |               |--------------->|
     |               |     Read()     |

차이점: TCP/IP 스택을 거치지 않아 프로토콜 오버헤드가 없다. 같은 호스트 내 통신에서 TCP보다 빠르다.


송신자와 수신자가 서로 찾는 방법

주소 체계

소켓 타입 주소 형식 네임스페이스 찾는 방법
Unix Domain 파일 경로 파일 시스템 파일 시스템 → inode → 소켓
TCP/UDP IP:Port 네트워크 네트워크 패킷 → 포트 테이블 → 소켓


Unix Domain Socket: 파일 시스템 경로로 찾기

// 서버: 특정 경로에 소켓 생성
listener, _ := net.Listen("unix", "/tmp/app.sock")
conn, _ := listener.Accept()

// 클라이언트: 같은 경로로 연결
conn, _ := net.Dial("unix", "/tmp/app.sock")

connect("/tmp/app.sock") 내부 동작:

  1. VFS(Virtual File System)로 경로 전달
  2. 파일 시스템에서 inode 검색
  3. inode 타입 확인 (S_IFSOCK)
  4. inode → socket 구조체 포인터 획득
  5. listening socket 찾음


TCP Socket: IP:Port와 3-way Handshake로 찾기

// 서버: IP:Port에 바인딩
listener, _ := net.Listen("tcp", "127.0.0.1:8080")
conn, _ := listener.Accept()

// 클라이언트: 같은 IP:Port로 연결
conn, _ := net.Dial("tcp", "127.0.0.1:8080")

connect(IP:Port) 내부 동작:

  1. TCP/IP 스택으로 전달
  2. 포트 번호로 해시 테이블 검색
  3. listening socket 찾음
  4. 3-way handshake 수행
  5. 연결 수립


UDP Socket: Connectionless

// UDP 서버
addr, _ := net.ResolveUDPAddr("udp", ":9000")
conn, _ := net.ListenUDP("udp", addr)

buf := make([]byte, 1024)
n, clientAddr, _ := conn.ReadFromUDP(buf)

// UDP 클라이언트
addr, _ := net.ResolveUDPAddr("udp", "192.168.1.100:9000")
conn, _ := net.DialUDP("udp", nil, addr)

conn.Write([]byte("Hello"))  // 그냥 쏨! 연결 없음!

UDP는 connectionless다:

  • Dial() 시 실제 연결 수립 없음
  • 소켓 생성 + 기본 목적지 주소 저장만 함
  • Write() 시 바로 패킷 전송
  • 3-way handshake 없음, 응답 확인 없음


소켓 상태

“열려 있다”의 의미

일반 파일과 소켓에서 “열려 있다”의 의미가 다르다.

구분 일반 파일 소켓 (TCP)
조건 FD 유효 + 파일 시스템 리소스 접근 가능 FD 유효 + 연결 활성 + 상대방도 유지
범위 로컬 리소스 네트워크 너머 상대방 존재
제어 open(), close()로 완전 제어 내 쪽 FD만으론 부족


소켓이 열려 있다: 여러 레벨의 조건

  1. FD 레벨 (모든 소켓 공통)
    • 프로세스가 소켓 FD 보유
    • close() 미호출
    • read()/write() 시스템 콜 가능
  2. 연결 상태 레벨 (TCP 등 연결 지향 소켓)
    • 3-way handshake 완료 (ESTABLISHED)
    • 커널 TCP 상태 머신에서 연결 유지
    • 양쪽 peer가 연결 유지 중
  3. 버퍼 레벨
    • 송신/수신 버퍼 할당됨
    • 데이터 송수신 준비 완료


소켓 타입별 “열려 있다”

Unix Domain Socket

# 확인 명령
$ ss -x | grep test.sock
  • FD 유효
  • 파일 시스템에 소켓 파일 존재
  • 양쪽 프로세스가 연결 유지
  • 네트워크 경로 검증 불필요 (로컬)

TCP Socket

# 확인 명령
$ ss -tn | grep ESTABLISHED
  • FD 유효
  • TCP 상태 ESTABLISHED
  • 양쪽 peer 연결 유지
  • 네트워크 경로 정상

UDP Socket

# 확인 명령
$ ss -u  # 상태 없음
  • FD 유효
  • 연결 개념이 약함 (connectionless)
  • connect() 호출해도 실제 연결 수립 안 됨
  • 그냥 기본 목적지 설정만 함


연결 끊김 감지

연결 지향 소켓의 경우, 연결 끊김이 자동으로 감지되지 않는다. 능동적으로 확인해야 한다.

conn, _ := net.Dial("tcp", "example.com:80")
// 이 시점에 상대방이 연결을 끊어도 모름 - fd는 여전히 유효

실제로 Read()Write()를 시도할 때 비로소 연결 끊김을 알게 된다. 앞서 설명한 대로 정상 종료면 EOF, 비정상 종료면 에러가 반환된다.

n, err := conn.Read(buf)
if err == io.EOF {
    // 정상 종료: 상대방이 close() 호출
}
if errors.Is(err, syscall.ECONNRESET) {
    // 비정상 종료: RST 패킷 수신
}

n, err := conn.Write(data)
if errors.Is(err, syscall.EPIPE) {
    // 상대방이 이미 끊음 (broken pipe)
}


감지 방법

방법 감지 시점 비용
Read/Write 시도 실제 I/O 할 때 낮음
TCP Keepalive 주기적 (커널) 중간
Read Timeout Read 호출 시 낮음
App-level Ping 주기적 (앱) 높음


TCP Keepalive

conn, _ := net.Dial("tcp", "example.com:80")
tcpConn := conn.(*net.TCPConn)

// Keepalive 활성화
tcpConn.SetKeepAlive(true)
tcpConn.SetKeepAlivePeriod(30 * time.Second)

// 이제 30초마다 커널이 자동으로 확인
// 상대방이 응답 안 하면 연결 끊김 감지


Read Timeout

conn, _ := net.Dial("tcp", "example.com:80")

// 5초 안에 데이터 안 오면 타임아웃
conn.SetReadDeadline(time.Now().Add(5 * time.Second))

n, err := conn.Read(buf)
if err != nil {
    // timeout 또는 연결 끊김
}


참고: Broken Pipe (EPIPE) 에러

서버 개발을 하다 보면 종종 “broken pipe” 에러를 마주치는 경우가 있다. 이 경우는 클라이언트가 타임아웃 등으로 먼저 연결을 종료했는데, 서버가 뒤늦게 응답을 보내려 할 때 발생한다. 위에서 언급했듯이 연결 끊김은 자동 감지되지 않으므로, Write() 시점에 비로소 알게 되는 것이다.

  • 오래 걸리는 API 응답 중 클라이언트가 탭 닫음
  • 클라이언트 타임아웃 설정(예: 5분)이 응답 전에 먼저 발동

대응 방법:

  • 에러를 graceful하게 처리 (패닉 대신 로깅 후 연결 정리)
  • 서버 측에도 적절한 타임아웃 설정
  • 오래 걸리는 작업은 진행 상황을 주기적으로 전송해서 연결 유지


정리

소켓의 핵심 개념을 정리하면 다음과 같다.

개념 설명
소켓 정의 프로세스 간 통신을 위한 엔드포인트
소켓 본질 파일 디스크립터 (Everything is a file)
도메인 어디서 통신하냐 (AF_UNIX, AF_INET, …)
타입 어떻게 통신하냐 (SOCK_STREAM, SOCK_DGRAM, …)
TCP 특징 연결 지향, 순서 보장, 신뢰성, 3-way handshake
UDP 특징 비연결, 순서 비보장, 빠름
물리적 실체 소켓 파일은 가상 파일, 실제 데이터는 커널 버퍼
주소 찾기 Unix Domain (파일 경로), TCP/UDP (IP:Port)
연결 끊김 자동 감지 안 됨, Read/Write나 Keepalive로 확인


참고 자료

추후 더 확인해 보면 좋을 참고 자료 목록을 정리해 둔다.




hit count

댓글남기기