[CS] 소켓(Socket)

· 22 분 소요


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)
  • 확인 도구: ss 명령어로 소켓 상태 확인 (ss -tan, ss -xan 등)


배경 지식

소켓을 이해하려면 먼저 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하게 처리 (패닉 대신 로깅 후 연결 정리)
  • 서버 측에도 적절한 타임아웃 설정
  • 오래 걸리는 작업은 진행 상황을 주기적으로 전송해서 연결 유지


소켓 확인 및 디버깅: ss 명령어

ss 명령어를 통해 지금까지 살펴본 소켓 개념을 실제 시스템에서 확인할 수 있다.

ss 명령어 개요

ssSocket Statistics의 약자로, 시스템의 소켓 상태를 확인하는 명령어다.

특징 설명
빠른 속도 커널에서 직접 정보를 가져와 netstat보다 빠름
상세한 정보 TCP 상태, 타이머, 메모리 사용량 등 상세 정보 제공
필터링 소켓 타입, 상태, 포트 등 다양한 조건으로 필터링 가능


기본 사용법

ss [옵션] [QUERY] [FILTER]

주요 옵션

옵션 설명 대상
-t, --tcp TCP 소켓 AF_INET + SOCK_STREAM
-u, --udp UDP 소켓 AF_INET + SOCK_DGRAM
-x, --unix Unix Domain 소켓 AF_UNIX (모든 타입)
-w, --raw Raw 소켓 AF_INET + SOCK_RAW
-l, --listening LISTENING 상태만 -
-a, --all 모든 소켓 (LISTENING 포함) -
-n, --numeric 숫자로 표시 (포트 이름 해석 안 함) -
-p, --processes 프로세스 정보 표시 -


QUERY: 어떤 소켓?

--query 옵션으로 소켓 타입을 더 세밀하게 지정할 수 있다.

QUERY 도메인 타입 설명
tcp AF_INET SOCK_STREAM + TCP TCP 소켓
udp AF_INET SOCK_DGRAM + UDP UDP 소켓
unix AF_UNIX 모든 타입 Unix Domain 전체
unix_stream AF_UNIX SOCK_STREAM Unix Stream 소켓
unix_dgram AF_UNIX SOCK_DGRAM Unix Datagram 소켓
raw AF_INET SOCK_RAW Raw 소켓
packet AF_PACKET 모든 타입 패킷 소켓
netlink AF_NETLINK - Netlink 소켓
# 명시적 지정
$ ss --query=tcp,udp          # TCP와 UDP 모두
$ ss --query=unix_stream      # Unix Stream만
$ ss --query=unix_stream,tcp  # Unix Stream + TCP


FILTER: 어떤 상태?

TCP 소켓은 상태(state)로 필터링할 수 있다. TCP만 상태가 있기 때문이다.

상태 설명
listening 연결 대기 중
established 연결됨
syn-sent SYN 전송함
syn-recv SYN 받음
fin-wait-1 FIN 전송, ACK 대기
fin-wait-2 FIN ACK 받음, 상대 FIN 대기
time-wait 종료 후 대기
close-wait 상대 FIN 받음, 로컬 종료 대기
last-ack FIN 전송, 마지막 ACK 대기
closing 동시 종료

상태 그룹도 사용 가능하다:

그룹 포함 상태
connected established, syn-sent, syn-recv, fin-wait-{1,2}, time-wait, close-wait, last-ack, closing
synchronized established, syn-recv, fin-wait-{1,2}, time-wait, close-wait, last-ack, closing
bucket syn-recv, time-wait (임시 상태)
# 상태 필터링
$ ss -t state established     # ESTABLISHED 상태만
$ ss -t state listening       # LISTENING 상태만
$ ss -t state connected       # 모든 연결 상태
$ ss -t state time-wait       # TIME_WAIT 상태만


옵션 조합 규칙

옵션은 OR 조건으로 동작한다.

# -x와 -t는 OR 조건: unix OR tcp
$ ss -x -t

# 명확하게 하려면 --query 사용
$ ss --query=unix_stream,tcp

권장: 단일 타입은 -옵션을, 여러 타입 조합은 --query를 사용하자. 혼용하면 헷갈릴 수 있다.

# 혼용 예시: "Unix Stream만 보고 싶다"는 의도
$ ss -x --query=unix_stream
# 실제 결과: -x(unix 전체) OR --query=unix_stream
# → unix_stream + unix_dgram + unix_seqpacket 모두 나옴!

# 의도대로 하려면
$ ss --query=unix_stream  # 이게 맞음

-xAF_UNIX 전체(stream, dgram, seqpacket)를 의미하고, --query=unix_streamAF_UNIX + SOCK_STREAM만 의미한다. 둘을 함께 쓰면 OR 조건이 되어 결국 -x와 같아진다.


결과 해석

출력 컬럼

$ ss -tan  # -t(tcp) -a(all, listening 포함) -n(numeric, 포트 이름 해석 안 함)
Netid  State   Recv-Q  Send-Q  Local Address:Port   Peer Address:Port
tcp    ESTAB   0       0       127.0.0.1:8080       127.0.0.1:54321
tcp    LISTEN  0       128     0.0.0.0:80           0.0.0.0:*
컬럼 설명
Netid 소켓 타입 (tcp, udp, u_str, u_dgr 등)
State TCP 상태 (ESTAB, LISTEN 등) 또는 UNCONN
Recv-Q 수신 버퍼 (상태에 따라 의미 다름)
Send-Q 송신 버퍼 (상태에 따라 의미 다름)
Local Address:Port 로컬 주소와 포트
Peer Address:Port 상대방 주소와 포트


Netid 종류

Netid 의미
tcp TCP 소켓
udp UDP 소켓
u_str Unix Stream 소켓
u_dgr Unix Datagram 소켓
u_seq Unix Seqpacket 소켓
raw Raw 소켓
nl Netlink 소켓
p_raw Packet Raw 소켓


Recv-Q / Send-Q 의미

소켓 타입과 상태에 따라 의미가 다르다.

연결 지향 소켓 (TCP, Unix Stream)

연결 지향 소켓은 LISTENING, ESTABLISHED 등의 상태를 가진다.

LISTENING 상태 (연결 요청 관리):

컬럼 의미 단위
Recv-Q Accept 대기 중인 연결 수
Send-Q Accept queue 최대 크기 (backlog)
# TCP
$ ss -tln
State   Recv-Q  Send-Q  Local
LISTEN  3       128     0.0.0.0:8080
        ↑       ↑
      대기 중   최대 대기
      3개      128개

# Unix Stream
$ ss -xln
State   Recv-Q  Send-Q  Local
LISTEN  0       128     /var/run/docker.sock
  • 정상: Recv-Q < Send-Q
  • 문제: Recv-Q ≈ Send-Q → Accept queue 가득 참, 새 연결 거부될 수 있음

ESTABLISHED 상태 (데이터 전송 관리):

컬럼 의미 (TCP) 의미 (Unix Stream) 단위
Recv-Q 앱이 아직 안 읽은 수신 데이터 앱이 아직 안 읽은 수신 데이터 바이트
Send-Q ACK 안 받은 송신 데이터 송신 버퍼에 있는 데이터 바이트
# TCP
$ ss -tn
State  Recv-Q  Send-Q  Local           Peer
ESTAB  1024    512     127.0.0.1:8080  127.0.0.1:54321
       ↑       ↑
    읽을      ACK 대기
    1024B     512B

# Unix Stream
$ ss -x
State  Recv-Q  Send-Q  Local                    Peer
ESTAB  0       0       /var/run/docker.sock     *
  • 정상: 둘 다 0 또는 작은 값
  • 문제: 큰 값 → 데이터 처리 병목

TCP vs Unix Stream의 Send-Q 차이

  • TCP: 네트워크를 통해 전송되므로 ACK를 기다리는 데이터
  • Unix Stream: 로컬 커널 버퍼 간 복사이므로 ACK 개념 없음, 단순히 송신 버퍼에 남은 데이터

비연결 소켓 (UDP, Unix Datagram)

비연결 소켓은 상태가 항상 UNCONN이다.

컬럼 의미 단위
Recv-Q 앱이 아직 안 읽은 수신 데이터 바이트
Send-Q 송신 버퍼에 있는 데이터 바이트
# UDP
$ ss -uan
State   Recv-Q  Send-Q  Local        Peer
UNCONN  0       0       0.0.0.0:53   0.0.0.0:*

# Unix Datagram
$ ss -x
State   Recv-Q  Send-Q  Local                         Peer
UNCONN  0       0       /run/systemd/journal/dev-log  *

왜 상태마다 다른가?

  • LISTENING: 연결 요청을 받아들이는 것이 중요 → 연결 개수 관리
  • ESTABLISHED/UNCONN: 데이터를 주고받는 것이 중요 → 데이터 바이트 관리


특수 문자

표시 의미 예시
0.0.0.0 모든 인터페이스 0.0.0.0:8080 (모든 IP에서 듣기)
* 아직 연결 안 됨 또는 익명 0.0.0.0:* (peer 없음)
- 해당 없음 Unix 소켓의 protocol 등
# LISTENING: 아직 연결된 peer 없음
tcp  LISTEN  0  128  0.0.0.0:80  0.0.0.0:*
                                       ↑ peer 없음

# UDP: connectionless라 항상 * 
udp  UNCONN  0  0    0.0.0.0:53  0.0.0.0:*

# Unix: 클라이언트 경로 표시 안 됨
u_str  ESTAB  0  0  /var/run/docker.sock  *
                                           ↑ 익명 소켓


활용 예시

자주 쓰는 명령어

# TCP 소켓 (listening + established)
$ ss -tan

# TCP listening 소켓만
$ ss -tln

# TCP established 소켓만
$ ss -tn state established

# UDP 소켓
$ ss -uan

# Unix Domain 소켓
$ ss -xan

# 프로세스 정보 포함
$ ss -tlnp


TCP 서버 디버깅

# 1. 서버가 8080 포트에서 듣고 있나?
$ ss -tln | grep 8080
LISTEN  0  128  0.0.0.0:8080  0.0.0.0:*

# 2. 어떤 클라이언트가 연결되어 있나?
$ ss -tn state established | grep 8080
ESTAB  0  0  192.168.1.100:8080  192.168.1.200:54321

# 3. TIME_WAIT 소켓이 많나? (서버 재시작 문제 가능)
$ ss -tn state time-wait | wc -l

참고: TIME_WAIT과 서버 재시작

TCP 연결을 먼저 종료한 쪽은 TIME_WAIT 상태로 2MSL 동안 대기한다. MSL(Maximum Segment Lifetime)은 패킷이 네트워크에서 살아있을 수 있는 최대 시간으로, RFC 793에서는 2분으로 정의하지만 실제 구현에서는 30초~2분으로 다양하다. 따라서 2MSL은 1~4분 정도다. 이 기간 동안 해당 포트를 재사용할 수 없어서, 서버 재시작 시 address already in use 에러가 발생할 수 있다.

# 서버 종료 후 즉시 재시작 시
$ ./server
Error: bind: address already in use

# TIME_WAIT 확인
$ ss -tan | grep 8080
TIME-WAIT  0  0  127.0.0.1:8080  127.0.0.1:54321

이와 같은 상황이 발생했을 때 해결 방법으로 다음의 것들을 시도해 볼 수 있다.

  • SO_REUSEADDR 소켓 옵션 설정
  • 클라이언트가 먼저 종료하게 설계
  • TIME_WAIT 끝날 때까지 대기


Unix Domain Socket 확인

# Docker daemon 소켓
$ ss -x | grep docker
u_str  ESTAB  0  0  /var/run/docker.sock  *

# systemd 소켓들
$ ss --query=unix_stream | grep systemd

# MySQL 소켓
$ ss -x | grep mysql
u_str  ESTAB  0  0  /var/run/mysqld/mysqld.sock  *


성능 문제 진단

# Accept queue 확인 (LISTENING 소켓의 Recv-Q)
$ ss -tln
LISTEN  125  128  0.0.0.0:8080  0.0.0.0:*
        ↑    ↑
     거의 가득 참 → 서버가 accept를 빨리 못 하고 있음

# 데이터 쌓임 확인 (ESTABLISHED 소켓의 Recv-Q/Send-Q)
$ ss -tn
ESTAB  50000  0  127.0.0.1:8080  127.0.0.1:54321
       ↑
    앱이 read를 안 하고 있음 (처리 병목)


정리

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

개념 설명
소켓 정의 프로세스 간 통신을 위한 엔드포인트
소켓 본질 파일 디스크립터 (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로 확인
확인 도구 ss 명령어 (Socket Statistics) - 소켓 상태 조회 및 디버깅


참고 자료

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




hit count

댓글남기기