컴퓨터 구조 운영체제 네트워크 자료구조 알고리즘 데이터베이스 내 학습
network · 02

TCP/IP

네트워크TCP핸드셰이크

TCP(Transmission Control Protocol)는 연결 지향적이고 신뢰성 있는 데이터 전송을 보장하는 프로토콜입니다. 연결 수립 시 3-way handshake, 연결 종료 시 4-way termination을 사용하며 흐름 제어·혼잡 제어·순서 보장을 통해 정확한 데이터 전달을 실현합니다.

핵심 개념

3-way Handshake

TCP 연결 수립 과정입니다. ① 클라이언트가 SYN(seq=x)을 보내 연결을 요청합니다. ② 서버가 SYN+ACK(seq=y, ack=x+1)로 응답합니다. ③ 클라이언트가 ACK(ack=y+1)를 보내 연결이 수립됩니다. 이 과정으로 양방향 통신 채널이 열립니다.

핵심 개념

4-way Termination

TCP 연결 종료 과정입니다. ① 클라이언트가 FIN을 보냅니다. ② 서버가 ACK로 응답합니다(서버가 ACK와 FIN을 합쳐 보내면 3번이 됩니다)(서버는 아직 데이터 전송 가능). ③ 서버가 FIN을 보냅니다. ④ 클라이언트가 ACK를 보내고 TIME_WAIT 상태를 거쳐 완전히 종료됩니다.

핵심 개념

TCP 헤더 구조

출발지·목적지 포트(각 16비트), 시퀀스 번호(32비트), ACK 번호(32비트), 데이터 오프셋, 플래그(SYN·ACK·FIN·RST·PSH·URG), 윈도우 크기(16비트), 체크섬(16비트), 긴급 포인터로 구성됩니다. 최소 20바이트입니다.

핵심 개념

흐름 제어 & 혼잡 제어

흐름 제어는 수신 측 버퍼 오버플로를 방지합니다. 수신 윈도우 크기로 송신 속도를 조절합니다. 혼잡 제어는 네트워크 혼잡을 방지합니다. Slow Start로 시작해 혼잡 윈도우를 조절하며, 패킷 손실 감지 시 전송 속도를 줄입니다.

핵심 개념

TCP vs UDP

TCP는 연결 지향·순서 보장·신뢰성 전송·흐름·혼잡 제어를 제공하지만 오버헤드가 큽니다. UDP는 비연결·순서 미보장·빠른 전송이 특징입니다. 실시간 스트리밍·게임·DNS·VoIP는 UDP를, 파일 전송·HTTP·이메일은 TCP를 사용합니다.

핵심 개념

TCP 상태 머신

TCP 연결은 상태 머신으로 동작합니다. CLOSED → SYN_SENT → ESTABLISHED(handshake 완료) → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED(종료). 서버 측은 LISTEN → SYN_RECEIVED → ESTABLISHED → CLOSE_WAIT → LAST_ACK → CLOSED 순서입니다.

TIME_WAIT (2MSL) — 연결을 먼저 닫은 쪽(이 그림에서는 클라이언트)이 마지막 ACK를 보낸 뒤 2×MSL(Maximum Segment Lifetime) 동안 TIME_WAIT 상태를 유지합니다. RFC 9293은 MSL을 2분으로 가정하지만 구현마다 다릅니다. 마지막 ACK가 손실되면 서버가 FIN을 재전송하며, 이때 재응답할 수 있도록 소켓을 열어둡니다.
시퀀스 번호 (ISN) — 초기 시퀀스 번호(ISN)는 예측하기 어렵게 정하도록 RFC 6528이 권고합니다. 이전 연결의 패킷이 새 연결에 영향을 미치는 것과 시퀀스 번호 추측 공격을 줄이기 위함입니다.
TCP Fast Open (TFO) — 반복 연결 시 3-way handshake 지연을 줄입니다. 첫 연결에서 받은 쿠키를 저장하고, 이후 연결 시 SYN과 함께 데이터를 전송합니다. 왕복 시간(RTT) 1회를 절약합니다.
Keep-Alive — 연결이 설정된 후 데이터 교환이 없어도 연결을 유지합니다. 일정 시간마다 탐지용 ACK 패킷을 전송해 상대방이 살아있는지 확인하는 TCP keepalive입니다. HTTP/1.1의 Connection: keep-alive는 TCP 연결을 여러 요청에 재사용한다는 뜻으로 TCP keepalive와는 다른 개념입니다.

TCP가 해결하려는 문제

인터넷의 기반인 IP는 패킷을 최선을 다해 전달할 뿐, 도착과 순서를 보장하지 않습니다. TCP는 그 위에서 번호 매기기, 확인 응답, 재전송, 흐름 제어, 혼잡 제어로 '순서대로 빠짐없이 도착하는 바이트 스트림'처럼 보이게 만드는 프로토콜입니다. 현재 사양은 2022년 RFC 9293에 정리되어 있습니다.

숫자로 확인하기

시퀀스 번호가 한 바퀴 도는 시간 — 시퀀스 번호는 32비트라서 2³² = 4,294,967,296바이트마다 한 바퀴를 돕니다. 초당 125MB(1Gbps)로 보내면 약 34초, 10Gbps면 약 3.4초입니다. 그래서 고속 환경에서는 오래된 패킷을 가려내는 별도 장치가 필요합니다.
윈도우 크기의 한계 — 헤더의 윈도우 필드는 16비트라서 최대 65,535바이트입니다. 왕복 시간이 100ms이면 한 번에 65,535바이트만 보낼 수 있어 처리량이 대략 65,535 / 0.1 ≈ 655KB/s(약 5.2Mbps)로 제한됩니다. 윈도우 스케일 옵션을 쓰면 이 한계를 크게 넘길 수 있습니다.
연결 설정 비용 — 3-way handshake는 데이터를 보내기 전에 왕복을 한 번 쓰는 셈입니다. 왕복 시간이 100ms이면 연결 설정이 끝나는 데 100ms가 듭니다.
세그먼트 크기 — 이더넷의 MTU 1,500바이트에서 IPv4 헤더 20바이트와 TCP 헤더 20바이트(옵션 없음)를 빼면 한 세그먼트에 담는 데이터는 최대 1,460바이트입니다.

자주 하는 오해

TCP는 메시지 경계를 지켜 준다 — TCP는 바이트 스트림입니다. 한 번 send한 데이터가 한 번에 recv되리라는 보장이 없어서 응용 계층에서 길이나 구분자로 메시지 경계를 정해야 합니다.
연결 종료는 항상 4번의 패킷이다 — 양쪽이 각자 FIN을 보내고 각각 ACK를 받으면 4번이지만, RFC의 예시는 4개의 세그먼트이지만, 수신 쪽이 ACK와 자기 FIN을 한 세그먼트에 합치면 3번이 됩니다. 한쪽만 닫는 반 닫기(half-close)도 가능합니다.
TCP는 데이터가 반드시 전달된다고 보장한다 — 재전송으로 전달을 시도하지만 연결이 끊기면 실패를 알려 줄 뿐입니다. 상대가 데이터를 처리했는지는 응용 계층의 확인이 있어야 압니다.
UDP는 신뢰성이 필요한 곳에 쓸 수 없다 — UDP 자체는 보장이 없지만 그 위에 직접 재전송과 순서 보장을 올릴 수 있습니다. QUIC가 대표적입니다.

면접에서 이렇게 물어요

면접 질문

연결을 왜 3-way handshake로 맺나요?

양쪽이 서로의 초기 시퀀스 번호를 확인하고 동기화해야 해서 양방향으로 SYN과 ACK가 오가야 합니다. RFC 9293은 오래된 중복 SYN이 혼란을 일으키지 않게 하는 것도 이유로 설명합니다.

면접 질문

TIME_WAIT 상태는 왜 있나요?

마지막 ACK가 손실되었을 때 상대가 FIN을 다시 보내면 응답할 수 있게 하고, 같은 주소 쌍의 이전 연결에서 늦게 도착한 패킷이 새 연결에 섞이는 것을 막기 위해서입니다. 보통 연결을 먼저 닫은(능동 종료) 쪽이 TIME_WAIT에 들어가고, 동시에 닫으면 양쪽이 들어갑니다.

면접 질문

흐름 제어와 혼잡 제어의 차이는 무엇인가요?

흐름 제어는 수신자의 버퍼를 넘치지 않게 수신 윈도우로 송신 속도를 맞추는 것이고, 혼잡 제어는 네트워크 전체가 막히지 않게 혼잡 윈도우로 송신 속도를 조절하는 것입니다.

면접 질문

TCP와 UDP는 언제 각각 쓰나요?

순서와 신뢰성이 중요한 웹, 파일 전송, 메일은 TCP를 씁니다. 지연이 중요하고 약간의 손실을 허용하는 실시간 음성·영상, 짧은 질의응답인 DNS는 UDP를 흔히 씁니다.

실제 시스템에서는

Linux 소켓 설정 — tcp(7) 매뉴얼은 tcp_keepalive_time의 기본값이 7200초(2시간)라고 설명합니다. ss 명령으로 소켓 상태(TIME_WAIT 등)를 볼 수 있습니다.
TIME_WAIT 길이 — RFC 9293은 MSL을 2분으로 가정해 2MSL을 4분으로 설명하지만 구현마다 다릅니다. 리눅스 커널 소스(include/net/tcp.h)는 TIME_WAIT 길이를 약 60초로 정의합니다.
HTTP 연결 재사용 — HTTP/1.1의 keep-alive는 TCP 연결을 여러 요청에 재사용하는 것이고, TCP keepalive(주기적인 탐지 패킷)와는 다른 개념입니다. HTTP 토픽에서 이어서 봅니다.

함께 보면 좋은 토픽

OSI 7계층 — TCP가 속한 전송 계층의 위치를 봅니다.
TCP 혼잡 제어 — 혼잡 윈도우가 변하는 과정을 봅니다.
HTTP — TCP 위에서 도는 응용 프로토콜을 봅니다.
HTTPS / TLS — TCP 위에 보안 채널을 얹는 방식을 봅니다.
WebSocket — TCP 연결을 오래 유지하는 사용 예를 봅니다.

참고 자료

Java — TCP 서버 / 클라이언트 소켓 (3-way handshake 흐름)

import java.net.*;
import java.io.*;

/* ── 서버: LISTEN → SYN_RECEIVED → ESTABLISHED ────── */
public class TcpServer {
    public static void main(String[] args) throws Exception {

        // ServerSocket.bind() → 서버 LISTEN 상태
        ServerSocket server = new ServerSocket(8080);
        System.out.println("서버 LISTEN — 포트 8080");

        // accept()는 블로킹 — 클라이언트 SYN을 기다림
        // SYN 수신 → OS가 SYN+ACK 자동 전송 → ACK 수신
        // → 3-way handshake 완료 → ESTABLISHED
        Socket client = server.accept(); // 여기서 handshake 완료
        System.out.println("ESTABLISHED: " + client.getRemoteSocketAddress());

        try (var out = new PrintWriter(client.getOutputStream(), true);
             var in  = new BufferedReader(
                           new InputStreamReader(client.getInputStream()))) {
            String msg = in.readLine();
            out.println("Echo: " + msg);
        }
        // try-with-resources → close() → 4-way termination 자동 수행
        server.close();
    }
}

/* ── 클라이언트: CLOSED → SYN_SENT → ESTABLISHED ───── */
public class TcpClient {
    public static void main(String[] args) throws Exception {

        // new Socket() → SYN 전송 → SYN+ACK 수신 → ACK 전송
        // → ESTABLISHED (3-way handshake를 생성자에서 완료)
        try (Socket sock = new Socket("localhost", 8080)) {
            System.out.println("ESTABLISHED");

            // TCP 옵션 설정 예시
            sock.setTcpNoDelay(true);          // Nagle 알고리즘 비활성
            sock.setSoTimeout(5000);          // read 타임아웃 5초
            sock.setKeepAlive(true);          // TCP Keep-Alive 활성

            var out = new PrintWriter(sock.getOutputStream(), true);
            var in  = new BufferedReader(
                          new InputStreamReader(sock.getInputStream()));
            out.println("Hello TCP");
            System.out.println(in.readLine());
        }
        // 소켓 닫힘 → FIN 전송 → 4-way termination
    }
}

Java에서 new Socket("host", port)를 호출하는 순간 OS가 TCP 3-way handshake를 수행합니다.
개발자는 생성자가 반환되면 이미 ESTABLISHED 상태임을 알 수 있습니다.
ServerSocket.accept()는 OS의 백로그 큐에서 완료된 연결을 꺼냅니다. SYN_RECEIVED → ESTABLISHED 과정은 OS 커널이 자동으로 처리합니다.
Socket.close()를 호출하면 OS가 4-way termination을 수행합니다.
try-with-resources를 사용하면 예외 발생 시에도 FIN이 전송됩니다.
setTcpNoDelay(true)는 Nagle 알고리즘을 끄는 옵션으로, 소량의 패킷을 즉시 전송해야 하는 실시간 애플리케이션에 사용합니다.

Python — TCP 상태 확인 및 Raw 시퀀스 번호 분석

import socket
import struct
import subprocess

# ── TCP 상태 확인 (ss 명령어 래핑) ─────────────────────
def show_tcp_states():
    result = subprocess.run(
        ['ss', '-tn', 'state', 'established'],
        capture_output=True, text=True
    )
    print(result.stdout)
    # Recv-Q Send-Q LocalAddr:Port PeerAddr:Port
    # Recv-Q: 수신 버퍼 미처리 데이터 (0이어야 정상)
    # Send-Q: 미전송 데이터 (0이면 전송 완료)

# ── TCP 헤더 파싱 (raw socket, root 권한 필요) ─────────
def parse_tcp_header(raw: bytes):
    # IP 헤더 이후 TCP 헤더 파싱
    ip_ihl = (raw[0] & 0x0F) * 4          # IP 헤더 길이
    tcp    = raw[ip_ihl:]

    # !HHLLBBHHH: big-endian, 각 필드 언팩
    src_port, dst_port, seq, ack_num, \
    offset_flags, flags, window, checksum, urg = \
        struct.unpack('!HHLLBBHHH', tcp[:20])

    syn = (flags & 0x02) != 0
    ack = (flags & 0x10) != 0
    fin = (flags & 0x01) != 0
    rst = (flags & 0x04) != 0

    print(f"포트: {src_port} → {dst_port}")
    print(f"seq={seq}, ack={ack_num}")
    print(f"SYN={syn} ACK={ack} FIN={fin} RST={rst}")
    print(f"window={window}, checksum=0x{checksum:04X}")

    if syn and not ack:
        print("→ 3-way handshake Step 1: SYN")
    elif syn and ack:
        print("→ 3-way handshake Step 2: SYN+ACK")
    elif fin:
        print("→ 4-way termination: FIN")

# ── TIME_WAIT 확인 ──────────────────────────────────────
# $ ss -tn state time-wait
# TIME_WAIT는 2MSL(~60초) 후 자동 해제
# SO_REUSEADDR 옵션으로 TIME_WAIT 중 포트 재사용 가능

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('', 8080))

ss -tn 명령어로 현재 시스템의 TCP 연결 상태를 확인할 수 있습니다. Recv-Q와 Send-Q가 지속적으로 0이 아니라면 버퍼 병목을 의심해야 합니다.
raw socket으로 TCP 헤더를 파싱하면 SYN·ACK·FIN 플래그를 비트 마스킹으로 추출할 수 있습니다. Wireshark·tcpdump가 이 방식으로 동작합니다.
SO_REUSEADDR 옵션을 서버 소켓에 설정하면 TIME_WAIT 상태의 포트를 즉시 재사용할 수 있습니다.
개발 서버 재시작 시 "Address already in use" 오류를 방지합니다.
TIME_WAIT는 OS가 관리하며 약 60초(2MSL) 후 자동 해제됩니다.
대용량 단기 연결이 많은 서버에서는 TIME_WAIT 소켓이 누적되어 포트 고갈이 발생할 수 있습니다.

Q1

TCP 3-way Handshake의 순서는?

연결을 요청하고, 승인하고, 확인합니다.
클라이언트가 SYN을 보내면 서버가 SYN-ACK으로 응답하고, 클라이언트가 다시 ACK을 보내 연결이 수립됩니다(3-way Handshake).

Q2

TCP 3-way Handshake의 순서는?

연결을 요청하고, 승인하고, 확인합니다.
클라이언트가 SYN을 보내면 서버가 SYN-ACK으로 응답하고, 클라이언트가 다시 ACK을 보내 연결이 수립됩니다(3-way Handshake).

Q3

TCP 연결 종료가 4단계(4-way)인 이유는?

연결은 양방향이라 양쪽 다 종료 의사를 밝혀야 합니다.
TCP는 양방향 연결이라 클라이언트와 서버가 각자 FIN을 보내고 상대의 ACK을 받아야 완전히 종료되므로 총 4단계가 필요합니다.

Q4

TCP 연결 종료가 4단계(4-way)인 이유는?

연결은 양방향이라 양쪽 다 종료 의사를 밝혀야 합니다.
TCP는 양방향 연결이라 클라이언트와 서버가 각자 FIN을 보내고 상대의 ACK을 받아야 완전히 종료되므로 총 4단계가 필요합니다.

Q5

TCP가 UDP보다 신뢰성이 높은 이유로 가장 적절한 것은?

TCP는 '연결형', UDP는 '비연결형'입니다.
TCP는 세그먼트 순서를 보장하고 손실된 데이터를 재전송하며 흐름/혼잡 제어까지 수행해 신뢰성을 보장하지만, 그만큼 오버헤드가 있습니다. UDP는 이런 보장 없이 빠르게 보냅니다.

Q6

TCP가 UDP보다 신뢰성이 높은 이유로 가장 적절한 것은?

TCP는 '연결형', UDP는 '비연결형'입니다.
TCP는 세그먼트 순서를 보장하고 손실된 데이터를 재전송하며 흐름/혼잡 제어까지 수행해 신뢰성을 보장하지만, 그만큼 오버헤드가 있습니다. UDP는 이런 보장 없이 빠르게 보냅니다.