네트워크 버퍼와 소켓 차이: send·recv 사이에서 데이터가 머무는 곳

반응형

버퍼(buffer)는 데이터를 잠시 보관하는 메모리 영역이고, 소켓(socket)은 프로세스가 운영체제의 통신 기능을 사용하는 통신 끝점과 그 핸들이다. 둘은 자주 함께 등장하지만 같은 개념은 아니다.

애플리케이션이 소켓에 데이터를 쓸 때 사용자 메모리의 버퍼와 커널의 소켓 송신 버퍼를 거칠 수 있다. 반대로 수신 데이터는 커널의 수신 버퍼에 도착한 뒤 recv를 통해 애플리케이션 버퍼로 읽힌다.

소켓은 IP 주소와 포트 번호만을 뜻하지 않는다

Linux의 socket() 시스템 호출은 통신 끝점을 만들고 그 끝점을 가리키는 파일 디스크립터를 돌려준다. 생성할 때 주소 체계, 소켓 타입, 프로토콜을 선택한다.

int fd = socket(AF_INET, SOCK_STREAM, 0);
  • AF_INET: IPv4 주소 체계
  • SOCK_STREAM: 순서 있는 바이트 스트림 형태
  • 0: 주소 체계와 타입에 맞는 기본 프로토콜 선택

그 뒤 역할에 따라 bind, listen, accept, connect, send, recv 같은 호출을 사용한다.

인터넷 소켓은 로컬·원격 IP와 포트로 식별되는 경우가 많지만 모든 소켓이 IP와 포트를 갖는 것은 아니다. AF_UNIX 소켓은 같은 호스트 안의 프로세스 통신에 파일 시스템 경로나 추상 이름을 사용할 수 있다.

TCP 서버에서도 listening socket과 연결된 socket을 구분해야 한다. 서버가 하나의 로컬 포트에서 대기해도 accept()가 반환한 연결별 소켓은 서로 다른 원격 주소·포트와 상태를 갖는다.

데이터 경로에는 여러 버퍼와 큐가 있다

전형적인 송신 흐름을 단순화하면 다음과 같다.

애플리케이션 버퍼
  → send()/write()
  → 커널 소켓 송신 버퍼
  → TCP/IP 처리와 네트워크 장치 큐
  → 네트워크

수신은 반대 방향이다.

네트워크
  → 네트워크 장치와 TCP/IP 처리
  → 커널 소켓 수신 버퍼
  → recv()/read()
  → 애플리케이션 버퍼

실제 운영체제는 offload, zero-copy, 메모리 압력, 프로토콜과 드라이버 구현에 따라 복사와 큐 처리 방식이 달라질 수 있다. 이 그림은 계층을 이해하기 위한 모델이지 모든 패킷이 같은 횟수로 복사된다는 뜻은 아니다.

send 성공은 상대 애플리케이션의 수신 완료가 아니다

send()가 양수를 반환하면 그만큼의 데이터를 이 호출에서 처리했다는 뜻이다. 보통 로컬 커널이 전송을 위해 받아들인 상태이지, 상대 프로세스가 이미 recv()로 읽었다는 확인은 아니다.

소켓이 blocking 모드이고 송신 버퍼 공간이 부족하면 호출이 기다릴 수 있다. non-blocking 모드라면 EAGAIN 또는 EWOULDBLOCK이 반환될 수 있다. 한 번의 호출이 요청한 길이보다 적은 바이트만 처리할 가능성도 고려해야 한다.

수신도 마찬가지다. recv()가 반환한 길이만큼만 처리하고, 필요한 데이터가 더 있다면 반복해서 읽어야 한다.

TCP는 메시지 경계를 보존하지 않는다

TCP의 SOCK_STREAM은 순서 있는 바이트 스트림이다. 송신 측에서 send()를 두 번 호출했다고 수신 측 recv()도 같은 두 덩어리로 나뉘는 것은 아니다.

송신: "HELLO" + "WORLD"

가능한 수신 예:
"HELLOWORLD"
"HEL" + "LOWORLD"
"H" + "ELLO" + "WORLD"

애플리케이션은 길이 필드, 구분자, 고정 크기 헤더 같은 자체 프레이밍 규칙을 두어야 한다. UDP datagram은 메시지 경계를 가진다는 점에서 다르지만, 신뢰성·순서·크기 제한과 오류 처리 방식도 함께 달라진다.

TCP 연결과 ACK·재전송 흐름은 TCP 연결 동작 순서, 그 위에서 HTTP 요청과 응답이 오가는 구조는 HTTP 요청과 응답에서 이어서 볼 수 있다.

소켓 버퍼 크기는 점유량과 같은 값이 아니다

Linux에서는 SO_RCVBUFSO_SNDBUF로 소켓 수신·송신 버퍼 한도를 조회하거나 설정할 수 있다. 다만 커널은 관리 오버헤드를 위해 설정값을 내부적으로 조정할 수 있고, 표시된 한도가 현재 실제로 채워진 바이트 수와 같지는 않다.

Linux에서 연결과 소켓 메모리 상태를 관찰할 때는 다음 명령을 사용할 수 있다.

ss -ltn
ss -tnm

ss -tnmskmem에는 할당된 수신 메모리와 버퍼 한도, 송신 관련 값 등이 구분되어 나온다. 숫자가 크다는 이유만으로 버퍼를 먼저 줄이거나 늘리기보다 다음을 함께 본다.

  1. 애플리케이션이 제때 읽고 쓰는가?
  2. blocking과 non-blocking 오류를 올바르게 처리하는가?
  3. RTT와 대역폭, TCP 윈도 크기가 어떤가?
  4. 패킷 손실과 재전송이 있는가?
  5. 프로세스 전체와 시스템 전체의 메모리 압력은 어떤가?

이 글의 코드와 명령은 인터페이스를 설명하기 위한 예다. 특정 서버에서 버퍼를 튜닝해 성능이 개선됐다는 결과를 의미하지 않는다.

참고 자료

반응형
KEEP READING
카테고리 전체 보기 →

댓글