본문으로 건너뛰기

TCP 바이트 스트림과 Seq/ACK

RFC 9293에서 TCP는 신뢰할 수 있고 순서가 보장되는 바이트 스트림 서비스로 정의됩니다. 여기서 기준은 메시지나 파일이 아니라 바이트입니다.

애플리케이션이 소켓에 데이터를 두 번 나눠 썼다고 해보겠습니다.

text
write("ABC")
write("DEF")

수신 애플리케이션이 같은 경계로 ABC, DEF를 읽는다는 보장은 없습니다. 한 번에 ABCDEF를 읽을 수도 있고, ABCDEF로 나눠 읽을 수도 있습니다. TCP는 두 번의 write() 호출을 두 개의 메시지로 보존하지 않습니다. 바이트의 내용과 순서를 보존할 뿐입니다. 메시지 경계가 필요하면 길이 필드나 구분자처럼 애플리케이션 프로토콜에서 정해야 합니다.

연결마다 두 개의 시퀀스 공간이 있다

TCP 연결은 양방향입니다. 클라이언트가 서버로 보내는 바이트와 서버가 클라이언트로 보내는 바이트는 서로 다른 시퀀스 공간을 사용합니다. 연결을 시작할 때 주고받는 SYN에는 각 방향의 초기 시퀀스 번호가 담깁니다.

text
클라이언트 서버
| -------- SYN, Seq=x ------------> |
| <--- SYN+ACK, Seq=y, Ack=x+1 ----- |
| -------- ACK, Ack=y+1 -----------> |

SYN은 데이터가 없어도 시퀀스 번호 하나를 사용합니다. 일반적인 3-way handshake가 끝난 뒤 클라이언트가 보내는 첫 데이터 바이트의 번호는 x+1, 서버가 보내는 첫 데이터 바이트의 번호는 y+1부터 시작합니다. 연결을 닫을 때 사용하는 FIN도 시퀀스 번호 하나를 사용합니다.

TCP 헤더에서 먼저 볼 값

RFC 9293의 TCP 헤더 형식에는 여러 필드가 있습니다. 손실 감지 흐름을 읽을 때는 다음 값을 먼저 보면 됩니다.

필드확인할 내용
Sequence NumberSYN이 없는 데이터 세그먼트에 실린 첫 바이트의 번호입니다. 일반적인 handshake 뒤 첫 데이터 바이트는 ISN+1부터 시작합니다.
Acknowledgment NumberACK 플래그가 설정됐을 때 유효하며, 수신자가 다음으로 기대하는 시퀀스 번호입니다.

Sequence Number와 Acknowledgment Number는 모두 32비트이며 계산은 2^32를 기준으로 순환합니다. 실제 캡처에서는 값이 크게 보이기 때문에 Wireshark가 연결 시작점을 0으로 맞춘 상대 시퀀스 번호를 보여주기도 합니다. 어떤 표시 방식을 사용하든 두 필드가 가리키는 것은 시퀀스 공간의 위치입니다.

데이터 구간에서 Seq는 첫 바이트의 번호다

TCP 세그먼트가 Seq=1000, Len=400이라고 해보겠습니다. 이 세그먼트가 담은 바이트 범위는 다음과 같습니다.

text
[1000, 1400)

대괄호 쪽의 1000은 포함하고, 소괄호 쪽의 1400은 포함하지 않습니다. 실제로 담긴 마지막 바이트 번호는 1399입니다. 이렇게 반열린 구간으로 적으면 길이는 1400 - 1000 = 400으로 바로 계산할 수 있고, 다음 구간도 [1400, 1800)처럼 빈틈없이 이어집니다.

SYN이 없는 데이터 세그먼트에서 Seq는 세그먼트의 순번이 아니라 첫 데이터 바이트의 번호입니다. Seq=1000인 세그먼트를 잃었다고 해서 다음 세그먼트의 Seq가 1001이 되는 것은 아닙니다. 첫 세그먼트가 400바이트였다면 다음 세그먼트는 보통 1400에서 시작합니다. 세그먼트 크기가 달라져도 바이트 범위를 기준으로 보면 어느 구간이 이어지고 비었는지 확인할 수 있습니다.

ACK는 다음에 받을 바이트를 가리킨다

Ack=1400은 1400번 바이트를 받았다는 뜻이 아닙니다. 1400 바로 전까지 연속해서 받았고, 다음에는 1400번 바이트를 기대한다는 뜻입니다. 따라서 [1000, 1400)을 정상적으로 받았다면 수신자는 Ack=1400을 보낼 수 있습니다.

이 ACK는 누적 방식입니다. Ack=2200으로 값이 진행됐다면 수신 TCP가 2200 미만의 바이트를 연속된 구간으로 받았다는 뜻입니다. 앞서 보낸 세그먼트마다 ACK 번호를 따로 나열할 필요가 없습니다. ACK 번호 하나로 그보다 앞선 연속 구간 전체를 확인할 수 있습니다.

ACK가 왔다고 애플리케이션 처리까지 끝난 것은 아닙니다. ACK는 상대 TCP가 바이트를 받았다는 뜻입니다. 상대 애플리케이션이 데이터를 읽었는지, 데이터베이스에 반영했는지는 알 수 없습니다.

중간 구간이 비면 ACK가 앞으로 가지 못한다

누적 ACK는 연속된 바이트 범위만 표현합니다. 중간 구간이 비어 있으면 그 뒤의 데이터를 먼저 받더라도 ACK 번호를 빈 구간 너머로 진행할 수 없습니다.

text
송신자 수신자
| -- Seq=1000, Len=400 [1000, 1400) ----------> |
| <------------------------------- Ack=1400 ----- |
|
| -- Seq=1400, Len=400 [1400, 1800) --X | 전달되지 않음
| -- Seq=1800, Len=400 [1800, 2200) ----------> |
| <------------------------------- Ack=1400 ----- | 1400부터 비어 있음
|
| -- Seq=1400, Len=400 [1400, 1800) ----------> |
| <------------------------------- Ack=2200 ----- | 구간이 연결됨

수신자는 [1800, 2200)을 보관하고 있더라도 [1400, 1800)을 받기 전에는 다음 기대값을 2200이라고 말할 수 없습니다. 빠져 있던 구간이 도착하면 보관 중이던 뒤쪽 구간과 이어지고, ACK가 한 번에 2200으로 진행합니다.

실제로는 지연 ACK 때문에 모든 데이터 세그먼트에 ACK가 하나씩 돌아오지 않을 수 있습니다.

같은 ACK가 반복돼도 손실이 확정된 것은 아니다

같은 ACK가 반복되면 수신자가 아직 연속해서 받지 못한 구간이 있다는 뜻입니다. 다만 세그먼트의 순서가 바뀌거나 데이터가 중복된 경우에도 같은 현상이 나타날 수 있으므로, 이것만으로 손실을 확정할 수는 없습니다.