전화를 걸면 바로 용건부터 말하지 않습니다.
"여보세요?" — "네, 말씀하세요." — "아 네."
TCP도 똑같습니다. 데이터를 보내기 전에 세 번의 인사로 서로 준비됐는지 확인합니다. 이 데모는 그 과정 하나만 보여줍니다.
세 번의 인사가 하는 말
- 시작 요청(SYN) — "대화 시작할까요? 제 번호는 100부터예요."
- 서버 응답(SYN-ACK) — "좋아요, 잘 받았어요(ACK 101). 제 번호는 300부터예요."
- 마지막 확인(ACK) — "저도 확인했어요(SEQ 101, ACK 301). 시작할게요!"
ACK 값은 다음에 받고 싶은 번호입니다. 이 예제에서는 SYN이 번호 하나를 차지하므로 100 다음은 101, 300 다음은 301이 됩니다. 일반 데이터에서는 받은 바이트 길이만큼 번호가 앞으로 갑니다.
왜 세 번일까요?
두 번째 인사까지는 서버가 보낸 시작 번호가 클라이언트에 도착했는지 서버가 모릅니다. 세 번째 ACK이 와야 양쪽의 시작 번호가 모두 전달되고 확인됐음을 알 수 있습니다.
표준 문서는 이유를 더 좁혀서 설명합니다. 첫 SYN을 받은 쪽은 그것이 방금 온 새 요청인지, 예전 연결에서 떠돌다 뒤늦게 도착한 중복인지 구별할 방법이 없습니다. 그래서 보낸 쪽에게 "이 SYN이 진짜 지금 것인지" 확인을 요구해야 하고, 그 확인이 세 번째 ACK입니다.
The receiver of the first SYN has no way of knowing whether the segment was an old one or not (…) and so it must ask the sender to verify this SYN. — RFC 9293 §3.5
이때 교환한 시작 번호(SEQ)는 이후 데이터가 잘리고 뒤섞여 도착해도 순서를 맞추는 기준이 됩니다. 네트워크 7계층 랩에서 예시로만 보여줬던 "순서 번호"가 여기서 정해지는 것입니다.
양쪽이 지나는 상태
인사 세 번 동안 두 컴퓨터는 각자 다른 상태를 지나갑니다. 표준 문서가 정의한 경로는 이렇습니다.
| 시점 | 연결을 거는 쪽 | 기다리는 쪽 |
|---|---|---|
| 시작 전 | CLOSED | LISTEN |
| SYN을 보낸 뒤 | SYN-SENT | LISTEN |
| SYN-ACK을 보낸 뒤 | SYN-SENT | SYN-RECEIVED |
| 마지막 ACK이 도착한 뒤 | ESTABLISHED | ESTABLISHED |
이 표가 실무에서 쓸모 있는 이유는, 어느 칸에서 멈췄는지가 곧 어디를 볼지를
정해주기 때문입니다. 내 쪽이 SYN-SENT에서 안 움직이면 첫 인사가 상대에게
닿지 않은 것이고, 서버에 SYN-RECEIVED가 잔뜩 쌓여 있으면 서버는 답했는데
마지막 확인이 돌아오지 않는 것입니다.
직접 인사를 주고받아 보기
아래 파란 버튼만 누르세요
세 문장만 주고받으면 연결돼요
- 1시작 요청
- 2서버 응답
- 3마지막 확인
- 4연결 완료
‘첫 인사 보내기’를 누르면 세 번의 인사와 연결 완료를 차례로 보여드려요.
이유 세 가지만
왜 두 번이 아니라 세 번일까요?
두 번째 인사만으로는 서버가 보낸 시작 번호가 상대에게 닿았는지 알 수 없어요. 세 번째 ACK이 와야 양쪽의 시작 번호가 모두 확인돼요.
서로의 SEQ를 알아두면 데이터가 여러 조각으로 잘리고 뒤섞여 도착해도 순서대로 복원할 수 있어요.
일반적인 응용 데이터는 연결이 확인된 뒤 프로그램에 전달돼요. 오래된 중복 연결 요청을 새 연결로 착각할 가능성도 줄여요.
데모만 따로 보려면 전체 화면으로 열기.
- 첫 인사 보내기를 누르고 세 문장만 따라갑니다.
- 양쪽 카드가 연결 완료로 바뀌는 순간을 확인합니다.
- 숫자가 궁금할 때만 말풍선의 SEQ·ACK 번호 보기를 펼칩니다.
인사가 실패하면 어떤 모습으로 보일까
세 번의 인사는 잘될 때보다 안 될 때 더 자주 마주칩니다. 접속이 안 되는 증상은 크게 둘로 갈리는데, 갈리는 지점이 바로 첫 번째 SYN입니다.
- 바로 거절당하는 경우 — 상대가 그 포트에서 아무것도 듣고 있지 않으면 연결을
끊자는 신호(RST)를 곧바로 돌려보냅니다.
Connection refused가 거의 즉시 뜹니다. 서버까지는 도달했는데 프로세스가 죽었거나 다른 포트를 쓰고 있을 때의 모습입니다. - 한참 멈춰 있는 경우 — 중간의 방화벽이 SYN을 조용히 버리면 답 자체가 오지
않습니다. 보낸 쪽은 첫 인사가 유실됐다고 보고 SYN을 몇 번 더 보내면서 간격을
두 배씩 늘립니다. 리눅스 기본값인
tcp_syn_retries = 6이면 포기할 때까지 약 127초가 걸립니다.
즉 즉시 거절이면 서버 안쪽, 오래 멈추면 중간 경로나 방화벽을 먼저 의심할 수 있습니다. 증상을 이렇게 한 번 나누는 것만으로 확인할 범위가 크게 줄어듭니다.
"한참"이 얼마인지는 대충이 아니라 정해진 값입니다. 리눅스 tcp(7) 문서는
tcp_syn_retries의 기본값을 6, 그때 포기까지 걸리는 시간을 약 127초로
적어 둡니다. 재전송 간격이 1초에서 시작해 두 배씩 늘어난다고 보면 아래처럼
쌓이고, 문서가 말하는 총합과 맞아떨어집니다.
| 시도 | 이때까지 흐른 시간 |
|---|---|
| 첫 SYN | 0초 |
| 1차 재전송 | 1초 |
| 2차 | 3초 |
| 3차 | 7초 |
| 4차 | 15초 |
| 5차 | 31초 |
| 6차(마지막) | 63초 |
| 포기 | 127초 |
그래서 "접속이 2분쯤 멈췄다가 실패한다"는 제보는 그 자체로 단서입니다. 거의
정확히 127초라면 SYN이 어딘가에서 조용히 버려지고 있다는 뜻이고, 애플리케이션
타임아웃이 그보다 짧으면 그 시간에 맞춰 끊깁니다. 반대로 서버가 답을 보내고도
마지막 확인을 못 받는 상황이라면 서버 쪽 tcp_synack_retries(기본값 5)가
같은 방식으로 SYN-ACK을 다시 보냅니다.
첫 인사에는 데이터를 못 싣는다는 오해
"인사가 끝나야 데이터를 보낼 수 있다"는 설명은 기본 동작으로는 맞습니다. 하지만
언제나 그런 것은 아닙니다. TCP Fast Open(RFC 7413)은
첫 SYN에 데이터를 함께 실어 보냅니다. 리눅스에서는 TCP_FASTOPEN 소켓 옵션으로 켜고, 유효한 쿠키가
있으면 핸드셰이크가 끝나기 전에 데이터 교환이 시작됩니다.
공짜는 아닙니다. 쿠키는 이전에 같은 서버와 정상적으로 연결한 적이 있어야 받을 수 있으므로 첫 방문에는 쓸 수 없고, 중간 장비가 낯선 옵션이 붙은 SYN을 버리면 오히려 연결이 더 느려집니다.
이 반례가 중요한 이유는, 세 번의 인사가 "TCP의 물리 법칙"이 아니라 안전하게 연결을 시작하기 위한 최소 절차라는 걸 보여주기 때문입니다. 그 안전을 다른 방법(쿠키)으로 확보하면 절차는 줄어들 수 있습니다.
내 컴퓨터에서 직접 확인하기
데모는 그림이지만 같은 일이 지금 내 컴퓨터에서도 일어나고 있습니다.
# 세 번의 인사만 골라 보기 (관리자 권한 필요)
sudo tcpdump -n 'tcp[tcpflags] & (tcp-syn|tcp-ack) != 0 and host example.com'
# 지금 맺어진 연결과 상태 보기
ss -tan
ss 출력의 SYN-SENT는 첫 인사를 보내고 답을 기다리는 중이라는 뜻이고,
ESTAB은 세 번의 인사가 모두 끝난 상태입니다. 위의 "한참 멈춰 있는 경우"에
ss를 보면 SYN-SENT가 오래 남아 있습니다.
이 인사를 아낄 수 있을까
핸드셰이크는 데이터를 한 바이트도 보내기 전에 왕복 한 번을 씁니다. 왕복 시간이 100ms인 서버라면 연결을 새로 만들 때마다 0.1초를 먼저 쓰는 셈입니다.
그래서 브라우저와 HTTP 클라이언트는 한 번 만든 연결을 재사용합니다(keep-alive). 이미지 50장을 받으려고 연결을 50번 새로 만들지 않는 이유입니다. 연결을 재사용하면 인사는 처음 한 번만 하고 그 위로 요청을 계속 흘려보냅니다.
연결이 만들어진 다음 조각이 사라지면 어떻게 될까요? 패킷 손실/재전송 랩에서 이어집니다.
이 인사를 내 코드에서는 어떤 함수로 부를까요? connect() 한 줄입니다.
그 앞뒤로 서버가 무엇을 하는지는
소켓 통신 랩에서 볼 수 있습니다.
두 번으로는 왜 모자란가
SYN → SYN-ACK 까지 오면 클라이언트는 서버의 시작 번호를 알게 됩니다. 그런데 서버 쪽은 아직 모릅니다. 자기가 보낸 번호가 상대에게 닿았는지를요. 세 번째 ACK 이 그것을 확인해 줍니다.
인사가 세 번인 이유는 결국 양쪽 모두가 상대의 시작 번호를 확인해야 하기 때문입니다. 그렇게 맞춘 번호가 이후 모든 데이터의 순서를 지켜 줍니다.
이 데모가 보여주지 않는 것
한계를 먼저 알고 봐야 데모를 잘못 읽지 않습니다.
- 번호는 예시값입니다. 데모의 시작 번호 100·300은 읽기 쉽게 고른 값입니다. 실제 구현은 공격자가 예측하기 어렵게 고릅니다.
- 시간은 시뮬레이션입니다. 화면의 장면 전환은 보기 좋은 속도로 맞춘 것이고 실제 왕복 시간이 아닙니다. 위 127초 표는 데모가 아니라 리눅스 문서에서 온 값입니다.
- 연결 종료(FIN·RST 교환), 재전송, 혼잡 제어, TLS는 다루지 않습니다. 데모는 연결이 만들어지는 구간 하나만 봅니다.
- 한쪽이 동시에 연결을 거는 경우처럼 표준이 정의한 드문 경로도 생략했습니다.
실제 핸드셰이크를 보고 싶다면 Wireshark에서 tcp.flags.syn == 1 필터를 걸면
됩니다. 화면의 네 장면과 같은 순서로 찍힙니다.