소켓과 전송 계층 (2) - TCP 연결의 상태 머신 - soo:bak
작성일 :
연결이란 무엇인가
Part 1에서 소켓이 무엇인지, TCP와 UDP의 차이를 살펴보았습니다. TCP는 “연결”을 맺는다고 했는데, 연결이란 정확히 무엇일까요?
물리적으로 전용 회선이 생기는 것이 아닙니다. 인터넷은 패킷 교환 네트워크입니다. 각 패킷은 독립적으로 라우팅되며, 같은 연결의 패킷이라도 다른 경로로 갈 수 있습니다. TCP의 연결은 양쪽 끝점의 상태 동기화입니다.
TCP 연결이 수립되면 양쪽 호스트의 커널은 동일한 정보를 공유합니다.
| 동기화되는 정보 | 용도 |
|---|---|
| 시퀀스 번호 | 데이터 순서 추적 |
| 윈도우 크기 | 수신 가능한 데이터량 |
| 연결 상태 | ESTABLISHED, CLOSED 등 |
시퀀스 번호는 전송하는 각 바이트에 붙이는 번호입니다. 패킷이 순서대로 도착하지 않아도 수신측이 올바른 순서로 재조립할 수 있습니다. 윈도우 크기는 “나는 지금 N바이트까지 받을 수 있다”는 정보입니다. 송신측은 이 값을 보고 수신측 버퍼가 넘치지 않도록 전송 속도를 조절합니다.
이 정보가 동기화되어 있으면 “연결되어 있다”고 말합니다.
반면 UDP는 상태를 유지하지 않습니다. 각 데이터그램은 독립적이며, 이전에 데이터를 주고받았는지 상대방이 준비되어 있는지 알지 못합니다. 그래서 UDP를 비연결형(Connectionless) 프로토콜이라고 합니다.
TCP 상태 전이 다이어그램
TCP 연결은 11가지 상태를 가집니다. 각 상태는 연결 수립, 데이터 전송, 연결 종료 과정에서 나타납니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
┌─────────────────────────────────────────────────────────────────┐
│ CLOSED │
└───────────────────────────┬─────────────────────────────────────┘
│
┌───────────────────┴───────────────────┐
↓ ↓
[서버 경로] [클라이언트 경로]
LISTEN SYN_SENT
↓ SYN 수신 ↓ SYN+ACK 수신
SYN_RCVD │
↓ ACK 수신 │
└───────────────┬───────────────────────┘
↓
ESTABLISHED ← 데이터 전송
│
┌───────────────┴───────────────┐
↓ ↓
[능동 종료] [수동 종료]
FIN_WAIT_1 CLOSE_WAIT
↓ ACK 수신 ↓ close()
FIN_WAIT_2 LAST_ACK
↓ FIN 수신 ↓ ACK 수신
TIME_WAIT ──── 2MSL 대기 ────→ CLOSED
↓
CLOSED
각 상태의 의미입니다.
연결 수립 단계
| 상태 | 주체 | 설명 | 대기 중인 이벤트 |
|---|---|---|---|
| CLOSED | - | 연결 없음 | - |
| LISTEN | 서버 | 클라이언트 연결 요청 대기 | SYN 수신 |
| SYN_SENT | 클라이언트 | SYN 전송 완료 | SYN+ACK 수신 |
| SYN_RCVD | 서버 | SYN 수신, SYN+ACK 전송 완료 | ACK 수신 |
| ESTABLISHED | 양쪽 | 연결 수립 완료, 데이터 전송 가능 | - |
연결 종료 단계
| 상태 | 주체 | 설명 | 대기 중인 이벤트 |
|---|---|---|---|
| FIN_WAIT_1 | 능동 종료 측 | close() 호출, FIN 전송 완료 | ACK 수신 |
| FIN_WAIT_2 | 능동 종료 측 | 자신의 FIN에 대한 ACK 수신 | 상대방 FIN 수신 |
| TIME_WAIT | 능동 종료 측 | 상대방 FIN 수신, ACK 전송 완료 | 2MSL 타이머 만료 |
| CLOSE_WAIT | 수동 종료 측 | 상대방 FIN 수신, ACK 전송 완료 | 애플리케이션 close() 호출 |
| LAST_ACK | 수동 종료 측 | close() 호출, FIN 전송 완료 | ACK 수신 |
| CLOSING | 양쪽 | 양쪽이 동시에 FIN 전송 (드문 경우) | ACK 수신 |
3-Way Handshake: 왜 3번인가
TCP 연결을 수립할 때 3-Way Handshake가 필요합니다. 왜 2번이 아니라 3번일까요?
2-Way Handshake의 문제
2번의 교환으로 연결을 수립한다고 가정합니다.
1
2
3
4
5
6
7
클라이언트 서버
│ │
│──── 연결 요청 (seq=100) ────→ │
│ │
│←── 연결 수락 (ack=101) ───── │
│ │
연결 수립! 연결 수립!
정상적인 경우에는 문제가 없어 보입니다. 하지만 네트워크에서는 오래된 패킷이 늦게 도착할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
클라이언트 서버
│ │
│──── SYN (seq=100) ────────────────────────→ X │ ← 패킷이 네트워크에서 지연
│ │
│ (타임아웃, 재시도) │
│ │
│──── SYN (seq=200) ────────────────────────→ │
│←─── SYN+ACK ────────────────────────────── │
│──── ACK ──────────────────────────────────→ │
│ │
│ [정상 통신 후 연결 종료] │
│ │
│ 오래된 SYN (seq=100) 도착 →│
│ │
│←─── SYN+ACK (새 연결로 오해) ───────────── │
│ │
??? 연결 수립!
서버는 오래된 SYN을 새 연결 요청으로 착각하고 자원을 할당합니다. 클라이언트는 이 연결에 대해 아무것도 모르므로 응답하지 않습니다. 서버는 존재하지 않는 연결을 위해 자원을 낭비합니다.
3-Way Handshake의 해결책
1
2
3
4
5
6
7
8
클라이언트 서버
│ │
│─── SYN (seq=x) ────────────────→ │ 1단계: 클라이언트 → 서버
│ │
│←── SYN+ACK (seq=y, ack=x+1) ──── │ 2단계: 서버 → 클라이언트
│ │
│─── ACK (ack=y+1) ──────────────→ │ 3단계: 클라이언트 → 서버
│ │
세 번째 ACK가 핵심입니다. 서버는 SYN+ACK를 보낸 후 바로 연결을 수립하지 않고, 클라이언트의 ACK를 기다립니다.
앞서 본 오래된 SYN 문제를 다시 봅시다.
1
2
3
4
5
6
7
8
9
클라이언트 서버
│ │
│ 오래된 SYN (seq=100) 도착 →│
│ │
│←─── SYN+ACK (seq=y, ack=101) ───────────── │ 서버: ACK 대기 상태
│ │
│ (클라이언트: 요청한 적 없음, 무시 또는 RST) │
│ │
│ 타임아웃, 연결 취소
클라이언트는 자신이 보낸 적 없는 SYN에 대한 SYN+ACK를 받습니다. 이 패킷을 무시하거나 RST(연결 거부)를 보냅니다. 서버는 ACK를 받지 못하면 연결을 수립하지 않습니다. 세 번째 단계가 클라이언트의 의도를 확인하는 역할을 합니다.
시퀀스 번호 동기화
3-Way Handshake의 또 다른 목적은 시퀀스 번호(ISN, Initial Sequence Number)를 교환하는 것입니다.
1
2
3
4
5
6
7
8
클라이언트 서버
│ │
│─── SYN (seq=1000) ────────────→ │ "내 시작 번호는 1000"
│ │
│←── SYN+ACK (seq=5000, ack=1001) ─ │ "알겠어. 내 시작 번호는 5000"
│ │
│─── ACK (ack=5001) ────────────→ │ "알겠어"
│ │
TCP는 전송하는 각 바이트에 시퀀스 번호를 붙입니다. 패킷이 순서대로 도착하지 않아도 수신측이 번호를 보고 재정렬할 수 있습니다.
그런데 시퀀스 번호는 왜 0이 아니라 임의의 값(1000, 5000 등)으로 시작할까요?
| 이유 | 설명 |
|---|---|
| 보안 | 예측 가능한 시퀀스 번호는 TCP 세션 하이재킹 공격에 취약 |
| 구분 | 이전 연결의 지연된 패킷과 새 연결의 패킷을 구분 |
현대 운영체제는 암호학적으로 안전한 난수 생성기로 ISN을 만듭니다.
4-Way Handshake: 연결 종료
연결 수립은 3번, 연결 종료는 4번이 필요합니다. 왜 더 많을까요?
3-Way Handshake에서는 서버가 SYN과 ACK를 동시에 보낼 수 있었습니다. 클라이언트의 SYN을 받으면 서버도 바로 자신의 SYN을 보낼 준비가 되어 있기 때문입니다.
하지만 종료는 다릅니다. 클라이언트가 FIN을 보내도 서버는 아직 보낼 데이터가 남아있을 수 있습니다. 서버는 ACK만 먼저 보내고, 자신의 데이터 전송이 끝난 후에야 FIN을 보냅니다.
반이중 종료(Half-Close)
TCP 연결은 양방향입니다. A→B와 B→A, 두 개의 독립적인 데이터 흐름이 있으며, 각 방향을 독립적으로 종료합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
클라이언트 (능동 종료) 서버 (수동 종료)
[ESTABLISHED] [ESTABLISHED]
│ │
│─── FIN ───────────────────────────→ │
│ │
[FIN_WAIT_1] │ FIN 수신
│ ↓
│←── ACK ──────────────────────────── [CLOSE_WAIT]
│ │
[FIN_WAIT_2] │
│ │ (남은 데이터 전송)
│←── 데이터... ────────────────────── │
│ │
│ │ close() 호출
│←── FIN ──────────────────────────── [LAST_ACK]
│ │
[TIME_WAIT] │
│─── ACK ───────────────────────────→ │
│ │
│ (2MSL 대기) [CLOSED]
↓
[CLOSED]
| 단계 | 방향 | 의미 |
|---|---|---|
| 1단계 | 클라이언트 → 서버 | FIN: “나는 더 보낼 데이터가 없습니다” |
| 2단계 | 서버 → 클라이언트 | ACK: “알겠습니다” (서버는 아직 보낼 수 있음) |
| 3단계 | 서버 → 클라이언트 | FIN: “나도 더 보낼 데이터가 없습니다” |
| 4단계 | 클라이언트 → 서버 | ACK: “알겠습니다” |
2단계와 3단계 사이에 서버가 남은 데이터를 보낼 수 있습니다. 이것이 반이중 종료(Half-Close)입니다. 한쪽은 송신을 끝냈지만 수신은 계속하는 상태입니다.
CLOSE_WAIT 상태
CLOSE_WAIT은 상대방이 FIN을 보냈지만 자신은 아직 close()를 호출하지 않은 상태입니다.
1
2
3
4
$ netstat -an | grep CLOSE_WAIT
tcp 0 0 192.168.1.100:8080 10.0.0.1:52000 CLOSE_WAIT
tcp 0 0 192.168.1.100:8080 10.0.0.2:52001 CLOSE_WAIT
... (수백 개)
CLOSE_WAIT이 쌓이면 애플리케이션 버그의 징후입니다.
| 원인 | 설명 |
|---|---|
| close() 누락 | 소켓을 열고 닫지 않는 코드 |
| 예외 처리 미흡 | try 블록에서 예외 발생 시 close()를 건너뜀 |
| 리소스 누수 | 소켓 객체가 가비지 컬렉션되지 않음 |
CLOSE_WAIT은 커널이 아닌 애플리케이션이 해결해야 하는 문제입니다. 커널은 애플리케이션이 close()를 호출할 때까지 기다릴 수밖에 없습니다. 파일 디스크립터가 고갈되면 새 연결을 받을 수 없게 됩니다.
TIME_WAIT: 왜 바로 닫지 않는가
능동 종료 측은 마지막 ACK를 보낸 후 TIME_WAIT 상태에서 일정 시간 대기합니다.
| 용어 | 의미 | 값 |
|---|---|---|
| MSL | Maximum Segment Lifetime, 패킷이 네트워크에서 살아있을 수 있는 최대 시간 | 보통 60초 |
| 2MSL | TIME_WAIT 대기 시간 | 2분 |
왜 바로 CLOSED로 가지 않고 2분이나 기다릴까요?
이유 1: 지연된 패킷으로부터 새 연결 보호
Part 1에서 살펴본 5-tuple을 떠올려 봅시다. 연결이 종료되어도 네트워크에 해당 연결의 패킷이 떠돌 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
시간 →
[연결 1: (TCP, A:5000, B:80)]
│
│─── 데이터 (seq=1000) ───→ X ← 패킷이 네트워크에서 지연
│
종료
[연결 2: (TCP, A:5000, B:80)] ← 같은 5-tuple로 새 연결
│
지연된 패킷 도착 → │ ← 연결 2의 데이터로 오해!
│
데이터 오염
TIME_WAIT 동안 같은 5-tuple로 새 연결을 맺지 않으면 지연된 패킷이 만료될 시간을 확보합니다.
이유 2: 마지막 ACK 손실 대비
1
2
3
4
5
6
7
8
9
10
11
A (능동 종료) B (수동 종료)
│ │
│←─── FIN ─────────────────────── │ [LAST_ACK]
│ │
│──── ACK ──────────────────→ X │ ← 마지막 ACK 손실
│ │
[CLOSED]? │ (ACK 안 옴, FIN 재전송)
│ │
│←─── FIN (재전송) ────────────── │
│ │
??? │ ← A가 CLOSED면 응답 불가
A가 TIME_WAIT에서 대기하면 재전송된 FIN에 ACK로 응답할 수 있습니다. B가 정상적으로 CLOSED 상태로 전이하도록 보장합니다.
| 이유 | TIME_WAIT가 해결하는 문제 |
|---|---|
| 지연된 패킷 | 같은 5-tuple의 새 연결에 오래된 데이터가 섞이는 것 방지 |
| ACK 손실 | 상대방이 FIN을 재전송할 경우 응답 가능 |
TIME_WAIT와 서버 재시작 문제
서버가 연결을 먼저 끊으면(능동 종료) 서버 측에 TIME_WAIT이 쌓입니다. 이 상태에서 서버를 재시작하면 문제가 생깁니다.
1
2
3
4
$ ./server # 8080 포트에서 실행
^C # 종료 (연결된 클라이언트에 FIN 전송 → TIME_WAIT)
$ ./server # 즉시 재시작
Error: Address already in use
TIME_WAIT 상태의 소켓이 8080 포트를 점유하고 있어서 새 서버가 bind()할 수 없습니다.
SO_REUSEADDR 옵션이 이 문제를 해결합니다.
1
2
3
int optval = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
bind(sockfd, ...);
| 옵션 | 효과 |
|---|---|
| SO_REUSEADDR | TIME_WAIT 상태의 주소에도 바인딩 허용 |
이 옵션은 앞서 설명한 TIME_WAIT의 안전장치를 우회하는 것이 아닙니다. 새 연결의 시퀀스 번호가 이전 연결과 다르기 때문에 지연된 패킷은 여전히 구분됩니다. 서버 개발에서 거의 필수적으로 사용됩니다.
2MSL의 수학적 근거
왜 하필 2MSL일까요? 최악의 시나리오를 고려합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
시간 →
─────────────────────────────────────────────────────────→
A B
│ │
│──── ACK ─────────────────────────────────────→ X │
│ (거의 1MSL 직전에 손실) │
│ │
│ ← FIN 재전송 ────────────│
│ (거의 1MSL 걸려서 도착) │
│ │
├─────────────── 1MSL ──────────┼─────── 1MSL ─────────┤
= 2MSL
| 구간 | 소요 시간 | 상황 |
|---|---|---|
| A → B | 최대 1MSL | ACK가 거의 도착할 때쯤 손실 |
| B → A | 최대 1MSL | B가 FIN 재전송, A에 도착 |
| 합계 | 2MSL | A가 재전송된 FIN을 받을 수 있는 최대 시간 |
A는 2MSL 동안 대기하면 B의 FIN 재전송을 받을 수 있습니다.
동시 연결과 동시 종료
드물지만 양쪽이 동시에 연결을 시작하거나 종료할 수 있습니다. TCP는 이런 상황도 처리할 수 있도록 설계되어 있습니다.
동시 열기(Simultaneous Open)
양쪽이 동시에 connect()를 호출하면 발생합니다. P2P 애플리케이션에서 NAT 홀 펀칭 시 나타날 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
A B
[SYN_SENT] [SYN_SENT]
│ │
│─── SYN (seq=x) ─────────────→ │
│ │
│ ←───────────── SYN (seq=y) ───│
│ │
[SYN_RCVD] [SYN_RCVD]
│ │
│─── SYN+ACK (ack=y+1) ───────→ │
│ │
│ ←─────── SYN+ACK (ack=x+1) ───│
│ │
[ESTABLISHED] [ESTABLISHED]
일반적인 3-Way Handshake와 달리 4개의 세그먼트가 교환됩니다. 양쪽 모두 클라이언트이자 서버 역할을 합니다.
동시 닫기(Simultaneous Close)
양쪽이 동시에 close()를 호출하면 발생합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
A B
[FIN_WAIT_1] [FIN_WAIT_1]
│ │
│─── FIN ─────────────────────→ │
│ │
│ ←───────────────────── FIN ───│
│ │
[CLOSING] [CLOSING] ← ACK 대신 FIN을 먼저 받음
│ │
│─── ACK ─────────────────────→ │
│ │
│ ←───────────────────── ACK ───│
│ │
[TIME_WAIT] [TIME_WAIT]
│ │
(2MSL 대기) (2MSL 대기)
│ │
[CLOSED] [CLOSED]
CLOSING 상태는 FIN을 보낸 후 ACK 대신 상대방의 FIN을 먼저 받았을 때 진입합니다. 일반적인 4-Way Handshake에서는 나타나지 않습니다.
소켓 버퍼: 데이터의 대기실
Part 1에서 write()와 read()가 소켓 파일 디스크립터를 통해 데이터를 주고받는다고 했습니다. 하지만 write()가 반환되었다고 데이터가 네트워크로 전송된 것은 아닙니다.
커널의 소켓 구조에는 버퍼가 있습니다. 각 연결마다 송신 버퍼와 수신 버퍼가 존재합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
┌─────────────────────────────────────────────────────────────────────┐
│ 사용자 공간 │
│ ┌───────────┐ │
│ │ 프로세스 │ │
│ └─────┬─────┘ │
│ │ write() / read() │
├────────┼────────────────────────────────────────────────────────────┤
│ 커널 공간 │ │
│ ↓ │
│ ┌─────────────────────────────────────┐ │
│ │ 소켓 버퍼 │ │
│ │ ┌─────────────────┐ │ │
│ │ │ 송신 버퍼 │ ──→ TCP 세그먼트로 전송 ──→ 네트워크 │
│ │ │ [전송 대기 데이터]│ │ │
│ │ └─────────────────┘ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ 수신 버퍼 │ ←── 도착한 세그먼트 ←─── 네트워크 │
│ │ │ [도착한 데이터] │ │ │
│ │ └─────────────────┘ │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
애플리케이션과 네트워크는 서로 다른 속도로 동작합니다. 버퍼가 이 속도 차이를 흡수합니다.
송신 버퍼
write()를 호출하면 데이터가 송신 버퍼에 복사됩니다.
1
2
3
4
5
6
7
8
9
애플리케이션: write(fd, data, 1000)
│
↓
┌───────────────────────────────┐
송신 버퍼: │ data ████████░░░░░░░░░░░░░░░░ │ ← 복사 완료, write() 반환
└───────────────────────────────┘
│
↓ (커널이 적절한 시점에)
TCP 세그먼트로 전송
write()가 반환되어도 데이터가 상대방에게 도착한 것이 아닙니다. 버퍼에 복사되었을 뿐입니다.
| 송신 버퍼 상태 | blocking 모드 | non-blocking 모드 |
|---|---|---|
| 여유 공간 있음 | 복사 후 즉시 반환 | 복사 후 즉시 반환 |
| 가득 참 | 공간이 생길 때까지 대기 | EAGAIN 에러 반환 |
수신 버퍼
네트워크에서 데이터가 도착하면 커널이 수신 버퍼에 저장합니다. read()를 호출하면 버퍼에서 데이터를 꺼내옵니다.
1
2
3
4
5
6
7
8
9
TCP 세그먼트 도착
│
↓ (커널이 자동으로)
┌───────────────────────────────┐
수신 버퍼: │ data ████████░░░░░░░░░░░░░░░░ │ ← 애플리케이션이 read() 호출 전
└───────────────────────────────┘
│
↓ read(fd, buf, 1000)
애플리케이션: buf에 데이터 복사
| 수신 버퍼 상태 | blocking 모드 | non-blocking 모드 |
|---|---|---|
| 데이터 있음 | 데이터 복사 후 반환 | 데이터 복사 후 반환 |
| 비어 있음 | 데이터가 도착할 때까지 대기 | EAGAIN 에러 반환 |
버퍼와 윈도우 크기
TCP 흐름 제어에서 사용하는 윈도우 크기(Window Size)는 수신 버퍼의 여유 공간입니다.
1
2
3
4
5
6
7
수신측 버퍼 (64KB)
┌────────────────────────────────────────────────────────────────┐
│████████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│
│← 사용 중: 20KB → │← 여유 공간: 44KB →│
└────────────────────────────────────────────────────────────────┘
↓
TCP 헤더의 Window 필드로 광고: "44KB 더 보내도 됨"
수신측은 매 ACK에 현재 윈도우 크기를 포함합니다. 송신측은 이 값을 보고 전송량을 조절합니다.
| 상황 | 윈도우 크기 | 송신측 동작 |
|---|---|---|
| 수신측이 빠르게 처리 | 큼 | 계속 전송 |
| 수신측이 느리게 처리 | 작아짐 | 전송 속도 감소 |
| 수신 버퍼 가득 참 | 0 | 전송 중단 (Zero Window) |
버퍼 크기 조정
버퍼 크기는 소켓 옵션으로 조정할 수 있습니다.
1
2
3
int bufsize = 1024 * 1024; // 1MB
setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize)); // 송신 버퍼
setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize)); // 수신 버퍼
고대역폭 장거리 연결에서는 큰 버퍼가 필요합니다. BDP(Bandwidth-Delay Product)는 “파이프에 채울 수 있는 데이터량”입니다.
1
2
3
4
BDP = 대역폭 × RTT (왕복 시간)
예: 1Gbps 링크, RTT 100ms
BDP = 1Gbps × 0.1s = 100Mb = 12.5MB
송신측은 ACK를 받기 전까지 BDP만큼의 데이터를 “날려 보내놓을” 수 있어야 합니다. 버퍼가 BDP보다 작으면 ACK를 기다리느라 대역폭을 완전히 활용하지 못합니다.
1
2
3
4
5
버퍼 < BDP: 전송 ──→ [대기] ──→ ACK 도착 ──→ 전송 ──→ [대기] ...
↑ 대역폭 낭비
버퍼 ≥ BDP: 전송 ──→ 전송 ──→ 전송 ──→ ACK 도착 ──→ 전송 ──→ ...
↑ 연속 전송으로 대역폭 최대 활용
마무리
TCP 연결은 물리적 회선이 아니라 상태의 동기화입니다.
Part 1에서 소켓이 5-tuple로 식별되는 통신 끝점이라고 했습니다. 이번 글에서는 그 끝점들이 어떻게 상태를 맞추고, 유지하고, 정리하는지 살펴보았습니다.
| 주제 | 핵심 내용 |
|---|---|
| 11가지 상태 | 연결 수립 → 데이터 전송 → 연결 종료 과정의 상태 전이 |
| 3-Way Handshake | 시퀀스 번호 동기화, 오래된 연결 요청 구분 |
| 4-Way Handshake | 양방향 데이터 흐름을 독립적으로 종료 (Half-Close) |
| TIME_WAIT | 지연된 패킷과 손실된 ACK에 대비 (2MSL 대기) |
| CLOSE_WAIT | 애플리케이션이 close()를 호출해야 해결되는 상태 |
| 소켓 버퍼 | 애플리케이션과 네트워크 사이의 속도 차이 흡수 |
이 모든 상태 전이는 커널에서 자동으로 처리됩니다. 애플리케이션은 connect(), accept(), close()만 호출하면 됩니다.
1
2
3
4
5
애플리케이션: connect() ─────────────────────────────→ close()
│ │
커널: SYN → SYN+ACK → ACK ... FIN → ACK → FIN → ACK
│ │
상태: SYN_SENT → ESTABLISHED ─────────→ FIN_WAIT → CLOSED
Part 3에서는 멀티플렉싱과 패킷 흐름을 살펴봅니다. 하나의 네트워크 인터페이스가 어떻게 수천 개의 연결을 동시에 처리하는지, 애플리케이션 데이터가 어떻게 패킷이 되어 네트워크로 나가는지 알아봅니다.
관련 글
시리즈
- 소켓과 전송 계층 (1) - 소켓의 탄생과 추상화
- 소켓과 전송 계층 (2) - TCP 연결의 상태 머신 (현재 글)
- 소켓과 전송 계층 (3) - 멀티플렉싱과 패킷 흐름