TCP 4-way Handshake: 양방향 데이터 스트림을 독립적으로 종료하는 절차
TCP 4-way Handshake는 연결 양쪽이 `FIN`과 `ACK`를 교환해 송신 스트림을 각각 종료하는 절차다. 한쪽의 `FIN`은 더 보낼 데이터가 없다는 뜻이며, 반대 방향 데이터 전송은 계속할 수 있다. 종료한 쪽은 지연된 패킷을 처리하기 위해 일정 시간 `TIME-WAIT` 상태를 유지한다.
By 송민성
1. 개념
TCP 4-way Handshake는 수립된 TCP 연결을 정상 종료하는 절차다. 일반적인 흐름은 다음 네 개 세그먼트로 구성된다.
- 종료 요청 측 → 상대 측:
FIN - 상대 측 → 종료 요청 측:
ACK - 상대 측 → 종료 요청 측:
FIN - 종료 요청 측 → 상대 측:
ACK
TCP 연결은 전이중 통신(Full-duplex Communication)을 지원한다. 양쪽 송신 스트림이 독립적이므로 한쪽이 송신을 끝내도 반대쪽은 데이터를 계속 보낼 수 있다. 각 방향을 따로 종료하기 때문에 연결 수립의 3-way Handshake보다 단계가 하나 더 많다.
먼저 FIN을 보내는 쪽을 능동 종료 측(Active Close), 이를 받는 쪽을 수동 종료 측(Passive Close)이라고 한다.
2. 왜 사용하는가
TCP는 신뢰성 있는 바이트 스트림(Byte Stream)을 제공한다. 연결을 즉시 제거하면 아직 전달되지 않은 데이터나 확인 응답을 처리할 수 없다.
4-way Handshake는 다음 조건을 보장한다.
- 능동 종료 측이 데이터 전송을 마쳤음을 알린다.
- 수동 종료 측이
FIN을 받았음을 확인한다. - 수동 종료 측에 남은 데이터를 전송할 시간을 준다.
- 수동 종료 측도 데이터 전송을 마쳤음을 알린다.
- 마지막
ACK로 양쪽 송신 스트림의 종료를 확인한다.
한쪽 송신만 닫고 반대쪽 데이터를 계속 받는 상태를 반종료(Half-close)라고 한다. 요청 전송을 끝낸 뒤 서버 응답을 계속 받아야 하는 프로토콜에서 사용할 수 있다.
3. 동작 원리
능동 종료 측을 클라이언트, 수동 종료 측을 서버라고 가정한다. 클라이언트의 FIN 시퀀스 번호(Sequence Number)는 u, 서버의 FIN 시퀀스 번호는 v다.
1단계: FIN
클라이언트가 더 보낼 데이터가 없음을 알린다.
클라이언트 → 서버
FIN = 1
SEQ = u클라이언트 상태는 ESTABLISHED에서 FIN-WAIT-1로 바뀐다.
FIN은 SYN처럼 시퀀스 번호 공간을 1만큼 소비한다.
2단계: ACK
서버가 클라이언트의 FIN을 확인한다.
서버 → 클라이언트
ACK = 1
ACK 번호 = u + 1서버 상태는 ESTABLISHED에서 CLOSE-WAIT로 바뀐다. 클라이언트가 이 ACK를 받으면 FIN-WAIT-2 상태가 된다.
이 시점에 클라이언트의 송신 방향은 닫혔지만 서버는 남은 데이터를 클라이언트로 보낼 수 있다.
3단계: FIN
서버가 남은 데이터 전송을 마친 뒤 자신의 송신 스트림을 종료한다.
서버 → 클라이언트
FIN = 1
SEQ = v서버 상태는 CLOSE-WAIT에서 LAST-ACK로 바뀐다.
첫 번째 ACK와 서버의 FIN 사이에는 애플리케이션이 남은 데이터를 처리하는 시간이 포함될 수 있다.
4단계: ACK
클라이언트가 서버의 FIN을 확인한다.
클라이언트 → 서버
ACK = 1
ACK 번호 = v + 1서버는 이 ACK를 받으면 CLOSED 상태가 된다. 클라이언트는 TIME-WAIT 상태에 들어간 뒤 2MSL 동안 기다리고 CLOSED 상태로 바뀐다.
MSL(Maximum Segment Lifetime)은 TCP 세그먼트가 네트워크에 존재할 수 있는 최대 시간을 뜻한다.
클라이언트 서버
ESTABLISHED ESTABLISHED
| |
| --- FIN, SEQ=u ---------------------> |
FIN-WAIT-1 CLOSE-WAIT
| <--- ACK, ACK=u+1 ------------------- |
FIN-WAIT-2 |
| 서버의 남은 데이터 전송 |
| <--- FIN, SEQ=v --------------------- |
TIME-WAIT LAST-ACK
| --- ACK, ACK=v+1 -------------------> |
| CLOSED
|
2MSL
|
CLOSED4. 코드 예제
Node.js의 net 모듈에서 socket.end()를 호출하면 운영체제가 남은 데이터를 전송한 뒤 FIN을 보낸다. 다음 코드는 클라이언트가 송신을 먼저 종료하고 서버 응답을 계속 받는 반종료를 보여준다.
import {
AddressInfo,
createConnection,
createServer,
} from "node:net";
const host = "127.0.0.1";
const server = createServer({ allowHalfOpen: true }, (socket) => {
socket.on("data", (data) => {
console.log(`서버 수신: ${data.toString()}`);
});
socket.on("end", () => {
console.log("서버: 클라이언트 FIN 수신");
socket.end("서버의 마지막 응답", () => {
console.log("서버: 응답 전송 후 FIN 전송");
});
});
socket.on("error", (error) => {
console.error(`서버 소켓 오류: ${error.message}`);
});
});
server.listen(0, host, () => {
const address = server.address() as AddressInfo;
const client = createConnection(
{
host,
port: address.port,
},
() => {
console.log("클라이언트: 연결 수립 완료");
client.end("클라이언트 요청");
console.log("클라이언트: 요청 전송 후 FIN 전송");
},
);
client.on("data", (data) => {
console.log(`클라이언트 수신: ${data.toString()}`);
});
client.on("end", () => {
console.log("클라이언트: 서버 FIN 수신");
});
client.on("close", () => {
server.close();
});
client.on("error", (error) => {
console.error(`클라이언트 오류: ${error.message}`);
server.close();
});
});
server.on("error", (error) => {
console.error(`서버 오류: ${error.message}`);
});tsx가 설치된 환경에서 실행한다.
npx tsx tcp-close-demo.tsmacOS에서 패킷을 관찰하려면 별도 터미널에서 다음 명령을 실행한다.
sudo tcpdump -i lo0 -nn "tcp"리눅스에서는 루프백 인터페이스 lo를 사용한다.
sudo tcpdump -i lo -nn "tcp"5. 시간 복잡도 또는 성능 특성
4-way Handshake는 입력 크기에 따라 반복되는 알고리즘이 아니다. 정상 종료에 필요한 제어 세그먼트 수는 개념적으로 최대 4개이므로 처리량은 O(1)이다.
종료 시간은 고정된 1 RTT가 아니다. 수동 종료 측이 첫 번째 ACK를 보낸 뒤 남은 데이터를 처리하는 시간이 포함될 수 있기 때문이다.
종료 지연
= 네트워크 왕복 지연
+ 수동 종료 측의 남은 데이터 처리 시간
+ 재전송 지연능동 종료 측은 마지막 ACK를 보낸 뒤에도 2MSL 동안 TIME-WAIT 상태를 유지한다. 짧은 TCP 연결을 대량 생성하면 TIME-WAIT 소켓과 임시 포트(Ephemeral Port)가 많이 사용될 수 있다.
6. 실무 사용 사례
HTTP 연결 종료
HTTP/1.1에서 Connection: close를 사용하거나 지속 연결(Persistent Connection)의 유지 시간이 끝나면 TCP 연결 종료가 진행된다. 응답 본문을 모두 보낸 서버가 먼저 FIN을 보낼 수도 있다.
요청 스트림 종료
클라이언트는 요청 데이터를 모두 보낸 뒤 송신 스트림을 닫고 서버 응답을 계속 받을 수 있다. TCP 반종료를 사용하는 형태다.
데이터베이스 연결
애플리케이션이나 데이터베이스 서버가 연결을 정상 종료하면 TCP 종료 절차가 수행된다. 연결 풀(Connection Pool)은 반복되는 연결 수립과 종료 비용을 줄인다.
로드 밸런서와 프록시
로드 밸런서가 TCP 연결을 종료하는 구조에서는 클라이언트 연결과 백엔드 연결이 별도로 종료된다. 한쪽 연결 종료가 다른 쪽 연결 종료 시점과 일치하지 않을 수 있다.
7. 주의할 점
항상 네 개 패킷으로 보이지는 않는다
수동 종료 측에 남은 데이터가 없으면 ACK와 FIN을 하나의 세그먼트로 합칠 수 있다. 패킷 캡처에서는 다음과 같은 세 단계로 보일 수 있다.
FIN → FIN-ACK → ACK4-way Handshake는 두 송신 방향을 독립적으로 닫는 논리적 절차를 뜻한다.
TIME-WAIT는 오류가 아니다
TIME-WAIT는 마지막 ACK가 손실됐을 때 상대가 재전송한 FIN에 다시 응답하고, 이전 연결의 지연 세그먼트가 사라질 시간을 확보한다.
CLOSE-WAIT가 오래 유지될 수 있다
CLOSE-WAIT는 상대의 FIN을 받았지만 로컬 애플리케이션이 소켓을 닫지 않은 상태다. 이 상태가 계속 쌓이면 애플리케이션의 소켓 종료 누락이나 장시간 작업을 확인해야 한다.
RST는 정상 종료가 아니다
리셋(Reset)을 뜻하는 RST는 연결을 즉시 중단한다. 아직 읽지 않은 데이터가 있거나 비정상 소켓 상태가 발생하면 FIN 기반 정상 종료 대신 RST가 전송될 수 있다.
양쪽이 동시에 종료할 수 있다
양쪽이 거의 동시에 FIN을 보내는 동시 종료(Simultaneous Close)도 가능하다. 이 경우 일반적인 FIN-WAIT-2, CLOSE-WAIT 흐름과 다른 상태 전이가 발생한다.
8. 핵심 정리
- TCP 4-way Handshake는
FIN → ACK → FIN → ACK순서로 연결을 정상 종료한다. - TCP는 전이중 통신이므로 각 송신 방향을 독립적으로 닫는다.
FIN은 시퀀스 번호 공간을 1만큼 소비하므로 확인 번호는FIN의 시퀀스 번호에 1을 더한 값이다.- 능동 종료 측은 마지막
ACK이후 2MSL 동안TIME-WAIT상태를 유지한다. ACK와FIN이 합쳐지면 실제 패킷은 세 개만 관찰될 수 있다.