소켓 API 기본
지금까지 우리는 TCP가 연결을 수립하고, 데이터를 신뢰성 있게 전달하며, 연결을 종료하는 과정을 이론적으로 살펴보았습니다. 이제 그 이론을 코드로 직접 구현해 보겠습니다. 네트워크 프로그래밍의 출발점은 소켓(Socket)입니다.
소켓이란 무엇인가
소켓은 네트워크 통신의 끝점(endpoint)입니다. 프로세스가 네트워크를 통해 다른 프로세스와 데이터를 주고받으려면, 반드시 소켓을 통해야 합니다.
애플리케이션 (웹 서버, 채팅 앱 등)
│
│ socket(), bind(), listen(), accept()
│ connect(), send(), recv()
▼
┌──────────────┐
│ 소켓 API │ ← 유저 공간과 커널의 경계
└──────┬───────┘
▼
┌──────────────┐
│ TCP / UDP │ 전송 계층
├──────────────┤
│ IP │ 네트워크 계층
├──────────────┤
│ 이더넷/WiFi │ 데이터 링크 계층
└──────────────┘
소켓 = 파일 디스크립터
→ 유닉스의 "모든 것은 파일이다" 철학
→ 파일처럼 열고(open), 읽고(recv), 쓰고(send), 닫을(close) 수 있음6장에서 소켓을 IP 주소 + 포트 번호 + 프로토콜의 조합으로 정의했습니다. 서버 소켓 (TCP, 192.168.1.10, 80)과 클라이언트 소켓 (TCP, 10.0.0.5, 52431)이 쌍을 이루어 하나의 연결을 식별합니다.
유형 상수 프로토콜 특징
──────────────────────────────────────────────────
STREAM SOCK_STREAM TCP 연결 지향, 바이트 스트림
DATAGRAM SOCK_DGRAM UDP 비연결, 데이터그램
RAW SOCK_RAW IP 직접 프로토콜 직접 구현 (ping 등)TCP 소켓의 생명주기
TCP 소켓 프로그래밍은 서버와 클라이언트가 서로 다른 흐름을 따릅니다.
서버 클라이언트
│ │
│ socket(AF_INET, SOCK_STREAM, 0) │ socket()
▼ │
│ bind(IP, Port) │
▼ │
│ listen(backlog) │
▼ │
│ accept() ←──── 3-way handshake ────── connect()
▼ → 새로운 소켓 반환 ▼
│ │
│ recv() ←───────── 데이터 ──────────── send()
│ send() ────────── 데이터 ───────────→ recv()
│ (양방향 반복) │
│ │
│ close() ←──── 4-way handshake ────── close()
▼ ▼
핵심: accept()는 새로운 소켓을 반환!
리스닝 소켓 (server_fd): 포트 8080에서 대기 지속
연결 소켓 (client_fd): 해당 클라이언트 전용 통신여기서 핵심은 accept()의 동작입니다. 서버가 listen()으로 대기하다가 클라이언트의 connect()를 받으면, accept()는 새로운 소켓을 반환합니다. 원래의 소켓은 여전히 다른 클라이언트의 연결을 기다리고, 새 소켓으로 해당 클라이언트와 통신합니다.
| API 함수 | 역할 | TCP 동작 |
|---|---|---|
socket() | 소켓 생성 | 파일 디스크립터 할당 |
bind() | 주소/포트 바인딩 | 서버의 수신 주소 설정 |
listen() | 연결 대기 시작 | backlog 큐 생성 |
accept() | 연결 수락 | 3-way handshake 완료된 연결 꺼냄 |
connect() | 연결 요청 | SYN → SYN-ACK → ACK |
send() | 데이터 전송 | TCP 세그먼트로 분할/전송 |
recv() | 데이터 수신 | 수신 버퍼에서 데이터 복사 |
close() | 연결 종료 | FIN → ACK → FIN → ACK |
C로 구현하는 에코 서버
에코 서버는 클라이언트가 보낸 데이터를 그대로 다시 돌려보내는 서버입니다. 소켓 프로그래밍의 Hello World라고 할 수 있습니다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
int main() {
int server_fd, client_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len = sizeof(client_addr);
char buffer[1024];
// 1. 소켓 생성
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// SO_REUSEADDR: TIME_WAIT 포트 재사용
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// 2. 주소 바인딩
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(8080);
if (bind(server_fd, (struct sockaddr *)&server_addr,
sizeof(server_addr)) < 0) {
perror("bind failed");
close(server_fd);
exit(EXIT_FAILURE);
}
// 3. 연결 대기
if (listen(server_fd, 5) < 0) {
perror("listen failed");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("Server listening on port 8080\n");
// 4. 클라이언트 연결 수락
client_fd = accept(server_fd, (struct sockaddr *)&client_addr,
&client_len);
if (client_fd < 0) {
perror("accept failed");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("Client connected: %s:%d\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port));
// 5. 데이터 수신 및 에코
ssize_t bytes_read;
while ((bytes_read = recv(client_fd, buffer, sizeof(buffer) - 1, 0)) > 0) {
buffer[bytes_read] = '\0';
send(client_fd, buffer, bytes_read, 0);
}
// 6. 종료
close(client_fd);
close(server_fd);
return 0;
}AF_INET → IPv4 주소 체계
AF_INET6 → IPv6 주소 체계
SOCK_STREAM → TCP (바이트 스트림)
SOCK_DGRAM → UDP (데이터그램)
htons() → Host TO Network Short (16비트, 포트)
htonl() → Host TO Network Long (32비트, IP)
ntohs() → Network TO Host Short
ntohl() → Network TO Host Long
INADDR_ANY → 0.0.0.0 (모든 인터페이스)
inet_ntoa() → 정수 IP → 문자열 ("192.168.1.1")
inet_addr() → 문자열 → 정수 IP숫자 0x1234를 메모리에 저장
리틀 엔디언 (x86, ARM): 빅 엔디언 (네트워크 표준):
주소 0: 0x34 (하위) 주소 0: 0x12 (상위)
주소 1: 0x12 (상위) 주소 1: 0x34 (하위)
포트 8080 = 0x1F90
htons(0x1F90) → 0x901F (리틀 → 빅 변환)
네트워크로 전송할 때는 반드시 빅 엔디언으로!Python으로 구현하는 에코 서버
같은 로직을 Python으로 작성하면 훨씬 간결해집니다.
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("0.0.0.0", 8080))
server.listen(5)
print("Server listening on port 8080")
client, addr = server.accept()
print(f"Connected by {addr}")
while True:
data = client.recv(1024)
if not data:
break
client.sendall(data)
client.close()
server.close()SO_REUSEADDR 옵션은 서버를 재시작할 때 Address already in use 에러를 방지합니다. 이전 연결의 TIME_WAIT 상태 때문에 같은 포트를 바로 재사용할 수 없는 문제를 해결합니다.
send()와 sendall()의 차이도 중요합니다. send()는 일부만 보낼 수 있지만, sendall()은 모든 데이터가 전송될 때까지 반복합니다.
클라이언트 구현
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(("127.0.0.1", 8080))
client.sendall(b"Hello, Socket!")
data = client.recv(1024)
print(f"Received: {data.decode()}")
client.close()클라이언트는 bind()가 필요 없습니다. connect()를 호출하면 OS가 자동으로 사용 가능한 포트(에페메럴 포트)를 할당합니다. connect() 내부에서 TCP 3-way handshake가 완료된 후 반환됩니다.
1. "Address already in use"
원인: TIME_WAIT 상태의 포트
해결: SO_REUSEADDR 설정
2. "Connection refused"
원인: 서버가 listen 상태가 아님, 포트가 다름
해결: 서버 실행 확인, 포트 번호 확인
3. recv()가 0 반환
의미: 상대방이 close()를 호출함 (FIN 수신)
처리: 연결 종료 처리 필요
4. send() 후 데이터 유실
원인: sendall() 대신 send() 사용
해결: sendall() 사용 또는 반환값 확인
5. "Broken pipe"
원인: 이미 닫힌 소켓에 쓰기
해결: recv()로 상대방 상태 확인 후 전송다음 절에서는 여러 클라이언트를 동시에 처리하는 TCP 소켓 프로그래밍을 다루겠습니다.