네트워크 애플리케이션은 운영체제가 제공하는 소켓(Socket)을 통해 네트워크와 통신한다.
소켓은 쉽게 말해, 프로세스가 네트워크를 통해 데이터를 주고받기 위한 통신용 창구라고 할 수 있다.
내부적으로는 운영체제 커널의 TCP/IP 스택 위에서 동작하며,
데이터를 실제로 보내고 받는 건 이 커널 레벨의 소켓이 담당한다.
우리가 작성하는 코드는 결국 "커널에 통신을 부탁하는 코드"다.
그래서 send()나 recv() 같은 함수를 호출하면, 실제 전송은 커널 내부에서 비동기적으로 진행된다.
오늘은 이 소켓이 어떻게 만들어지고, 연결되고, 데이터가 오가는지
그리고 실제 서버 개발자가 알아야 할 부분까지 정리해보려 한다.
소켓 통신의 흐름 - 서버와 클라이언트

소켓 통신은 기본적으로 서버(Server) 와 클라이언트(Client) 구조로 이루어진다.
서버는 요청을 기다리고, 클라이언트는 연결을 시도한다.
서버는 다음과 같은 순서로 동작한다.
socket() → bind() → listen() → accept() → read()/write() → close()
- socket() : 소켓을 만든다. 운영체제에게 “통신할 준비하자~” 하는 요청이다.
- bind() : IP 주소와 포트를 소켓에 연결한다. 즉, “이 주소로 들어오는 요청을 받을게 ! ” 선언하는 과정이다.
- listen() : 연결 요청을 받을 준비를 한다. 이제 서버는 클라이언트를 기다리는 상태다.
- accept() : 클라이언트가 연결을 시도하면, 그 연결을 받아들이고 새로운 소켓을 만들어 준다.
(이때 accept()가 반환하는 소켓이 실제로 데이터 주고받는 소켓이다.) - read()/write() : 데이터를 주고받는다.
- close() : 연결을 종료한다.
클라이언트는 더 간단하다.
socket() → connect() → read()/write() → close()
- socket() : 소켓 생성
- connect() : 서버의 IP와 포트로 연결 요청 (TCP의 3-way handshake 과정이 여기서 이루어진다)
- read()/write() : 데이터 송수신
- close() : 연결 종료
정리하면, 서버는 “문을 열고 기다리는 역할”, 클라이언트는 “그 문을 두드리는 역할”이다.
TCP 연결의 원리
TCP는 신뢰성 있는 연결 지향형 프로토콜이다.
클라이언트와 서버는 데이터를 주고받기 전에 반드시 연결을 맺는다.
연결은 3-way handshake 과정으로 이루어진다.
- 클라이언트 → 서버 : SYN (연결 요청)
- 서버 → 클라이언트 : SYN + ACK (요청 수락)
- 클라이언트 → 서버 : ACK (최종 확인)

3-way handshake 과정
이 세 단계를 거치면 연결이 성립되고, 이제 두 프로세스는 데이터를 안전하게 주고받을 수 있다.
TCP는 이 과정에서 패킷 순서, 손실, 재전송, 혼잡 제어 등을 모두 책임진다.
연결 종료는 4단계로 이뤄진다.
- 한쪽이 FIN을 보내 종료를 요청하면,
- 상대는 ACK로 응답하고,
- 반대 방향도 같은 절차를 거쳐 완전히 닫는다.

마지막으로 연결을 닫은 쪽은 TIME_WAIT 상태를 잠시 유지하는데,
이건 늦게 도착한 패킷을 처리하기 위해서다.
그래서 서버를 재시작할 때 Address already in use 에러가 날 수 있고, 이를 방지하려면 SO_REUSEADDR 옵션을 설정한다.
TCP와 UDP의 차이
두 프로토콜 모두 전송 계층에서 동작하지만,
TCP는 연결을 맺고 신뢰성을 보장하는 대신 속도가 느리고, UDP는 연결 없이 빠르지만 데이터 손실이 발생할 수 있다.
| 구분 | TCP | UDP |
| 연결 방식 | 연결형 (3-way handshake) | 비연결형 |
| 신뢰성 | 순서, 손실 복구 보장 | 보장하지 않음 |
| 데이터 단위 | 바이트 스트림 (프레이밍 필요) | 데이터그램 (메시지 단위 유지) |
| 속도 | 느림 | 빠름 |
| 대표 사용 예 | HTTP, 파일 전송, DB 연결 | 스트리밍, DNS, 게임, 음성통화 |
TCP는 데이터를 연속적인 바이트 스트림으로 다루기 때문에, “어디서부터 어디까지가 한 메시지인지”를 프로그래머가 직접 구분해야 한다. 이를 프레이밍(Framing) 이라고 한다.
예를 들어, 메시지 길이 + 본문 형태로 보내거나, '\n' 같은 구분자를 사용하는 방식이 있다.
UDP는 반대로, 한 번 보낸 메시지가 한 덩어리로 유지된다.
하지만 도착을 보장하지 않기 때문에, 실시간성이 필요한 게임이나 스트리밍에 주로 사용된다.
커널 버퍼와 전송 과정
데이터는 단순히 프로그램 사이를 "직접" 오가는 게 아니다.
실제로는 이렇게 흐른다.
유저 공간 → 커널 소켓 버퍼 → 네트워크 카드 → 인터넷
send() 함수를 호출하면, 우리가 보낸 데이터는 커널의 송신 버퍼에 저장되고,
커널이 네트워크 스택을 통해 실제 전송을 처리한다.
이때 버퍼가 가득 차면,
- 블로킹 모드에서는 send() 함수가 멈춘다.
- 논블로킹 모드에서는 EAGAIN 에러를 반환한다.
TCP에는 작은 데이터를 효율적으로 묶어 보내는 *Nagle 알고리즘이 있다.
하지만 채팅처럼 지연이 민감한 프로그램에서는 TCP_NODELAY 옵션으로 이 기능을 끈다고한다.
*Nagle 알고리즘
: 네트워크를 통해 전송되는 패킷의 수를 줄여 TCP/IP 네트워크의 효율성을 향상시키는 방법/네트워크 효율성은 높아지지만 ACK를 기다리는 시간 때문에 데이터 전송에 지연이 발생하는 단점이 있음
블로킹과 논블로킹 / 멀티플렉싱
소켓은 두 가지 방식으로 동작할 수 있다.
- 블로킹 소켓: 데이터가 도착할 때까지 기다린다. 구현은 단순하지만, 하나의 연결만 처리할 수 있다.
- 논블로킹 소켓: 즉시 반환하며, “지금은 읽을 게 없다면” EAGAIN 에러를 낸다.
논블로킹을 쓰려면 "지금 어떤 소켓이 준비됐는지"를 감시해야 한다. 이 역할을 하는게 바로 I/O 멀티 플렉싱이다.
I/O멀티 플렉싱이란, 하나의 스레드가 여러 소켓을 동시에 감시하기 위해 사용하는 기술이다.
대표적인 방식으로는 :
- select() : 오래된 방식, 파일 디스크립터 개수 제한이 있다.
- poll() : 제한은 없지만 매번 전체를 순회해야 한다.
- epoll(리눅스), kqueue(BSD/macOS) : 이벤트 기반으로, 수천 개 이상의 연결을 효율적으로 처리할 수 있다.
이 구조를 잘 이해하면,
Nginx, Redis, Node.js 같은 고성능 서버의 기본 원리를 이해할 수 있다.
데이터 송수신의 세부 동작
TCP는 "한 번에 전송된 데이터가 한 번에 읽힌다"는 보장이 없다.
예를들어 4KB를 보냈다고 해도, 수신 측에서는 2KB + 2KB로 나눠서 읽을 수 있다.
이걸 부분 읽기(Partial Read)라고 한다.
반대로, 여러 번 write()한 데이터가 한 번의 read()로 합쳐질 수도 있는데, 이건 스티키 패킷(Sticky Packet) 현상이다.
이런 이유로 프레이밍 규칙이 반드시 필요하다.
shutdown()을 이용하면 송신만 닫거나, 수신만 닫는 half-close도 가능하다.
예를들어 HTTP 1.0에서 서버가 응답 후 송신을 닫는 구조가 이런 방식이다.
예시코드 - TCP 에코 서버
# TCP 에코 서버
import socket
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # TIME_WAIT 대비
srv.bind(("0.0.0.0", 9000))
srv.listen(128)
while True:
conn, addr = srv.accept()
data = conn.recv(4096)
if not data:
conn.close()
continue
conn.sendall(data) # 받은 데이터를 그대로 되돌려줌
conn.close()
이 코드는 TCP 에코 서버다.
SO_REUSEADDR 옵션은 서버를 재시작할 때 “Address already in use” 오류를 피하기 위해 설정한다.
사실 recv()는 한 번에 데이터를 다 읽어오지 못하는 경우도 있어서 반복문으로 돌려야 하지만,
아직 그 부분은 완전히 감을 못 잡았다..... 그냥 지금은 구조만 이해해보자는 느낌으로 적어봤다.
클라이언트 예시 - Java
// TCP 클라이언트
import java.io.*;
import java.net.*;
public class EchoClient {
public static void main(String[] args) throws Exception {
try (Socket s = new Socket("127.0.0.1", 9000)) {
s.setTcpNoDelay(true); // 작은 데이터 지연 최소화
OutputStream out = s.getOutputStream();
InputStream in = s.getInputStream();
out.write("hello\n".getBytes());
out.flush();
byte[] buf = new byte[4096];
int n = in.read(buf);
System.out.println(new String(buf, 0, n));
}
}
}
여 기서 setTcpNoDelay(true) 는 Nagle 알고리즘을 끄는 설정이다. 작은 메시지를 보낼 때 딜레이 없이 즉시 전송되도록 만든다.
운영 환경에서 고려할 점
- 작은 패킷을 자주 보낸다면 Nagle 알고리즘 비활성화 또는 배칭(batch) 설계를 고려해야 한다.
- 대용량 데이터를 보낼 때는 송신 버퍼가 꽉 차면 백프레셔(backpressure) 처리가 필요하다.
- TCP 연결은 한 번 끊어지면 재사용되지 않으므로, 재시도 정책과 타임아웃 설정이 중요하다.
- 대규모 서버에서는 epoll 기반 비동기 구조와 함께 커널 파라미터(somaxconn, rmem_max, wmem_max)를 조정한다.
- EC2나 클라우드 환경에서는 반드시 보안 그룹 인바운드 규칙에 포트를 허용해야 외부 접속이 가능하다.
정리 및 요약
소켓은 단순히 send() 와 recv() 함수가 아니다.
프로세스가 운영체제의 네트워크 스택을 거쳐 실제로 데이터를 주고받는 표준 인터페이스다.
TCP는 신뢰성과 순서를 보장하지만, 그만큼 개발자가 알아야 할 디테일(버퍼, 프레이밍, 부분 읽기)이 많다.
소켓을 이해하면 네트워크의 동작 원리가 눈에 들어오고, 그 위에서 만들어진 WebSocket 같은 기술이 훨씬 쉽게 보인다.
이제 소켓의 원리를 어느 정도 이해했으니,
다음 글에서는 웹 환경에서 이 소켓을 확장한 WebSocket 구조를 정리해보려 한다.
'Study > 인터넷 & 네트워크 기초' 카테고리의 다른 글
| 호스팅이란(Hosting)? - 서버에 웹사이트를 올리는 과정 (0) | 2025.11.24 |
|---|---|
| 웹소켓(WebSocket)의 원리와 동작 방식 (0) | 2025.11.10 |
| DNS와 그 작동원리 (0) | 2025.11.04 |
| 3. 브라우저의 작동 원리 (0) | 2025.11.03 |
| 2. HTTP의 작동 원리 (0) | 2025.11.03 |