TCP 소켓 통신

네트워크2025. 04. 24.
카테고리
게시일
시리즈

배경지식

  • 리눅스 소켓 라이브러리는 Berkeley Sockets (BSD Sockets) 표준을 따름
  • sys/socket.h 에 라이브러리 정의
  • 소켓 통신은 IP protocol suite에서, Transport 계층에 해당 (TCP/UDP)
    • TCP의 패킷은 세그먼트, UDP의 패킷은 데이터그램
  • TCP / UDP 별도의 소켓 존재
  • 소켓통신은 전이중 지원
  • sys/socket.h → syscall → net stack → NIC driver 순서로 처리
  • 코드에서 Listen Socket, Accept Socket 둘 로 나뉨
다시보는 IP protocol suite
IP protocol suite layer - Wikipedia
IP protocol suite layer - Wikipedia

왜 TCP를 연결 지향이라고 하는가?

  1. handshake로 연결을 설정 - 연결 보장
  1. seq 시퀀스 번호 존재 - 순서 보장

TCP State

TCP FSM

TCP FSM
TCP FSM
TCP FSM을 참조하면, Handshake 등의 과정이 한 눈에 보인다.
TCP FSM은 커널의 TCP 스택이 내부적으로 구현하고 관리하며, 소켓 사용자는 위 FSM을 참고하여 코딩하면 된다.

연결 시도, 3-way handshake

Client Server | -------- SYN ------------> | | | | <------- SYN + ACK ------- | | | | -------- ACK ------------> | | |
연결 성립 (ESTABLISHED 상태)

Estabilished

Client Server | --- data(seq=x+1) --------> | | | | <--- ack(ack=x+1+len) ----- | | ... | | <--- data(seq=y+1) -------- | | --- ack(ack=y+1+len) ----> |

연결 해제, 4-way handshake

Client Server | ------- FIN ------------> | | | | <------ ACK ------------- | | | | <------ FIN ------------- | | ------- ACK ------------> |
그림에서는 클라이언트가 FIN을 보내는 것으로 되어있지만, 클라이언트, 서버 양측에서 모두 Close가 가능하다.
Close 요청을 보내는 쪽
  • FIN-WAIT-1
    • FIN을 보내고 ACK를 기다리는 상태
  • FIN-WAIT-2
    • ACK를 받은 뒤, FIN을 받기를 기다리는 상태
  • CLOSING
    • 양 측에서 모두 FIN을 동시에 보냈을 때 상태
  • TIME-WAIT
    • 연결을 닫은 뒤에도, 임시 시간(리눅스 기본 60s)동안 작동하지 않는 소켓을 유지한다. 이유는 후술
Close를 닫는 쪽
  • CLOSE-WAIT
    • FIN 요청에 대한 ACK를 보냈고, FIN을 다시 보냄
  • LAST-ACK
    • 보낸 FIN에 대한 ACK를 받음
 

소켓 통신 과정

notion image
  • 많이 하는 오해 : 서버의 로컬 포트는 accept 될 때 마다 새로 할당된다? - X
    • 당연히 서버의 로컬 포트는 최초에 서버에서 socket() 으로 열었을 때 생성된 것이 전부이다.

Listen Socket vs Established Socket

  • Listen Socket → socket() 으로 얻음
socket->state = LISTENING
  • Established Socket (connected socket 등)
socket->state = ESTABLISHED

Server-Side 소켓 코드

socket()

socket(domain, type, protocol)
옵션
  • domain
    • AF_INET : IPv4 네트워크
    • AF_INET6 : IPv6 네트워크
    • AF_UNIX | AF_LOCAL : IPC
  • type
    • SOCK_STREAM : TCP
    • SOCK_DGRAM : UDP
  • protocol
    • 0 으로 자동선택
Listen 소켓의 File Descriptor를 얻는다. 서버 사이드의 이 소켓은 LISTEN 소켓이고, 실질적으로는 이후 accept() 함수의 결과로 나온 FD로 통신한다.
STATE: CLOSED

bind()

IP와 포트 설정
STATE : CLOSED

listen()

오픈 대기큐 (backlog)를 준비
STATE : LISTEN

accept()

Client-Side에서 connect() 가 진행되었을 때 나타난다.
backlog 큐에 들어간 연결을 꺼내 새 연결 소켓 FD 반환, 해당 소켓으로 통신이 일어난다.
STATE : ESTABLISHED

write()

데이터를 보낸다. 한 번에 최대 ssize_t 만큼 보낼 수 있다. write()로 보낸 데이터는 통신 소켓 버퍼로 보내지고, 이후 TCP network stack에서 패킷 별로 쪼개진 뒤 보내진다.

read()

데이터를 받는다. 블로킹으로, 필요한 데이터가 버퍼로 모두 들어올 때 까지 대기한다.

recv()

read()는 File Descripter를 이용한 것이지만, 실제로는 recv()를 이용하는 것이 더 좋다.

Client-Side 소켓 코드

connect()

3-way handshake의 진행
STATE : ESTABLISHED (성공시)

bind()

클라이언트 사이드에서 bind() 를 생략

다중 연결과 Backlog

  • Established 상태인 연결을 대기시키는 큐인 Backlog 존재
  • accept

fork() 를 이용해 멀티 프로세싱에서 처리하기

int srv = socket(...); bind(srv, ...); listen(srv, 128); while (1) { int conn = accept(srv, NULL, NULL); if (fork() == 0) { // 자식 프로세스: 연결 하나 처리 handle_client(conn); close(conn); exit(0); } // 부모 프로세스: 다음 연결 대기 close(conn); // 부모는 자식이 맡았으니 FD 닫음 }

TIME_WAIT 문제?

  • TCP는 연결지향이므로, 통신이 끝난 이후 연결을 끊음
  • 연결 종료 시 지연 패킷으로 인한 혼란을 막기 위해, 원격 종단의 연결을 막기
  • 결론적으로 클라이언트의 포트가 고갈되지 않는 이상, 서버 측에서는 TIME_WAIT로 인한 오버헤드를 걱정할 필요는 없다. 문제가 되었던 것은 과거 메모리 자원량이 적었던 시절의 이야기
 

File 구조

struct file
 
struct socket