09. TCP 헤더 32비트 완전분해
지난 글에서 스펙 본문이 어떻게 바뀌었는지 봤습니다. 이번엔 시선을 좁혀서, TCP 헤더 20바이트를 비트 단위로 완전히 분해합니다. RFC 9293 Section 3.1의 다이어그램을 그대로 따라갑니다.
헤더 전체 구조
기본 20바이트(옵션 없이)의 필드 배치는 아무렇게나 정해진 게 아닙니다. 크게 네 단계로 나뉩니다 — (1) 무조건 필요한 큰 필드 → (2) 32비트가 안 되는 자잘한 필드들을 한 줄에 압축 → (3) 상대적으로 덜 핵심적인 부가 필드 → (4) 있어도 되고 없어도 되는 가변 길이 옵션.
1. Source/Destination Port (32비트) — 어느 프로그램에게 전달할지 정하는 값이라, 다른 어떤 필드보다도 먼저 필요합니다. 그래서 헤더 맨 앞에 옵니다. 각각 16비트라 0~65535 범위입니다.
2. Sequence Number, Acknowledgment Number (각각 32비트) — 신뢰성 있는 전송을 지탱하는 핵심 상태값이라, 다른 필드와 욱여넣지 않고 32비트(한 줄 전체)를 통째로 씁니다. 표현 범위는 약 43억이고, 이게 한 바퀴 도는 문제를 다음 글(10편)에서 다룹니다.
3. Data Offset · Reserved · 제어 비트 · Window (합쳐서 32비트) — 여기부터는 32비트씩 쓸 만큼 큰 값이 없는 필드들이라, 여러 개를 한 줄에 압축해서 담았습니다.
- Data Offset(4비트): 헤더가 정확히 몇 바이트인지. 옵션이 없으면 5(=20바이트), 있으면 더 큽니다. 4비트라 최댓값 15 — 옵션을 아무리 붙여도 헤더는 60바이트를 못 넘습니다.
- Reserved(4비트): 미래를 위해 비워둔 자리. 원래 6비트였는데 2비트가 CWR·ECE로 넘어갔습니다 (바로 다음 섹션에서 다룸).
- 제어 비트(8비트): 이 세그먼트가 뭘 하려는 건지(연결 시작·종료·확인 등) 나타냅니다.
- Window(16비트): 수신자가 지금 받을 수 있는 양. 65535바이트가 한계인데, Window Scale 옵션으로 우회할 수 있습니다.
4. Checksum, Urgent Pointer (합쳐서 32비트) — 연결의 핵심 동작에는 필수가 아닌 부가 필드라 뒤로 밀렸습니다. Checksum은 헤더와 데이터 전체(및 IP 주소까지 포함한 “pseudo header”)를 검증하는 값이고, Urgent Pointer는 URG 비트가 켜졌을 때만 의미 있는, 실제로는 거의 안 쓰이는 필드입니다.
5. Options (가변 길이) — 크기가 정해져 있지 않으니, 고정 크기 필드들이 전부 끝난 다음·실제 데이터가 시작되기 전에 옵니다.
제어 비트 8개, 확대해서 보기
위 다이어그램에서 강조된 8비트를 하나씩 뜯어보면 이렇습니다.
각 비트가 하는 일을 한 줄씩 정리하면:
- SYN: 연결을 시작하겠다는 신호
- FIN: 더 이상 보낼 데이터가 없다는 신호
- ACK: Acknowledgment Number 필드가 유효하다는 신호 (연결 중에는 거의 항상 켜져 있음)
- RST: 연결을 즉시 강제로 끊는다는 신호
- URG: Urgent Pointer 필드가 유효하다는 신호
- PSH: 버퍼에 모아두지 말고 지금까지 받은 데이터를 즉시 애플리케이션에 올려보내라는 신호
새로 나온 건 CWR과 ECE입니다. 6편 곁다리에서 “라우터가 IP 헤더에 혼잡 조짐을 표시하고, 수신자는 그걸 ACK에 실어 보낸다”고 했는데, 그 “ACK에 싣는” 부분이 바로 이 두 비트입니다.
수신자가 라우터의 ECN 표시를 발견하면 ECE를 켜서 송신자에게 “혼잡 신호 받았어”라고 알리고, 송신자는 cwnd를 줄인 뒤 CWR을 켜서 “나 줄였어, 그만 알려줘도 돼”라고 응답합니다.
이 두 비트가 RFC 793 시절엔 그냥 “Reserved”로 비어 있었다는 걸 생각하면, “예약 비트는 미래를 위해 비워둔다”는 말이 정확히 이런 식으로 실현된 셈입니다.
Options — 옵션 필드는 왜 있는가
Data Offset이 5보다 크면, Acknowledgment Number와 실제 데이터 사이에 Options 영역이 끼어듭니다. 옵션이 존재하는 이유는 간단합니다 — 고정 20바이트 헤더를 한 번 정해놓고 나니, 나중에 뭔가 새로 추가하고 싶을 때 헤더 구조 자체를 바꿀 수 없었기 때문입니다.
대신 “옵션”이라는 확장 슬롯을 만들어서, 필요한 기능만 골라 붙이는 방식을 택했습니다. 지금까지 나온 옵션 중 이 시리즈와 관련된 것만 추리면:
- MSS(Maximum Segment Size): handshake의 SYN에만 실려서, “나는 한 세그먼트에 최대 이만큼만 담을 수 있어”를 상대에게 알립니다.
- Window Scale: Window 필드가 16비트라 65535바이트가 한계라고 했는데, 이 옵션은 실제 윈도우 값에 곱할 배율(shift count)을 알려줘서 훨씬 큰 윈도우를 쓸 수 있게 해줍니다. 4편에서 다룬 rwnd가 사실은 이 옵션 덕분에 65535바이트보다 커질 수 있는 겁니다.
- SACK Permitted / SACK: 5편 곁다리에서 다룬 그 SACK입니다. handshake 때 “나 SACK 지원해”를 SACK Permitted 옵션으로 확인하고, 이후 실제 데이터 전송 중에는 SACK 옵션에 “이 구간도 받았어”를 실어 보냅니다.
- Timestamps: 세그먼트를 보낼 때 타임스탬프를 같이 실어서, RTT(왕복 시간)를 정밀하게 측정할 수 있게 해줍니다. RTO 계산(12편에서 다룰 예정)의 정확도가 여기 달려 있습니다.
실전: 헥사덤프 하나 읽어보기
지금까지 배운 걸 직접 써먹어봅니다. 다음은 옵션 없는(20바이트) SYN 세그먼트의 헤더를 16진수로 나타낸 것입니다.
C7 38 01 BB 00 00 03 E8 00 00 00 00 50 02 FA F0 12 34 00 00
바이트 위치를 따라가며 해석하면:
| 바이트 | 값 | 의미 |
|---|---|---|
| 0~1 | C7 38 |
Source Port = 0xC738 = 51000 |
| 2~3 | 01 BB |
Destination Port = 0x01BB = 443 |
| 4~7 | 00 00 03 E8 |
Sequence Number = 1000 |
| 8~11 | 00 00 00 00 |
Acknowledgment Number = 0 (아직 확인할 게 없음 — SYN이니까) |
| 12 | 50 |
Data Offset=5(상위 4비트), Reserved=0(하위 4비트) → 0101 0000 |
| 13 | 02 |
제어 비트 = 0000 0010 → SYN만 켜짐 |
| 14~15 | FA F0 |
Window = 0xFAF0 = 64240 |
| 16~17 | 12 34 |
Checksum (IP 주소까지 포함해서 계산하므로 여기서는 임의 값) |
| 18~19 | 00 00 |
Urgent Pointer = 0 (URG 꺼져 있으니 의미 없음) |
목적지 포트가 443(HTTPS), 시퀀스 넘버가 1000, SYN 비트만 켜져 있는 걸 보면 — 3편에서 본 handshake의 첫 번째 메시지, 그것도 우리가 1편에서 처음 그린 그림 그대로라는 걸 바이트 레벨에서 확인할 수 있습니다.
다음 글
Sequence Number가 32비트라는 걸 이번 글에서 확인했습니다. 32비트면 언젠가 최댓값을 넘어서 다시 0으로 돌아갈 텐데, 그때 무슨 일이 일어날까요?
다음 글에서 wraparound과 PAWS를 다룹니다.