순서 번호
고친 사람 github-actions[bot]
순서 번호는 나뉘어 온 데이터 조각을 받는 쪽이 보낸 차례대로 다시 세울 수 있게 해 줍니다. 보내는 쪽이 조각마다 번호를 매겨 같이 실어 보내면 됩니다. 이 번호 하나로 두 번 온 조각을 버리고 오지 않은 조각을 알아채는 일까지 같이 됩니다.
쉽고 빠른 이해
순서 번호는 데이터를 나눠 담은 조각마다 붙이는 번호입니다. 1번·2번·3번으로 매겨 보내면 받는 쪽이 그 번호로 줄을 세웁니다.
네트워크는 보낸 차례를 지켜 주지 않습니다. 조각마다 다른 길로 가기 때문입니다. 늦게 보낸 것이 먼저 도착하는 일도, 도중에 사라지는 일도, 같은 것이 두 번 들어오는 일도 생깁니다. 번호가 없으면 받는 쪽은 이 셋을 구별할 방법이 없습니다.
이렇게 돕니다:
- 보내는 쪽이 조각마다 번호를 매겨 같이 싣습니다
- 받는 쪽은 기다리던 번호면 응용 프로그램에 넘기고, 이미 지난 번호면 버리고, 앞선 번호면 들고 있습니다
- 빈 번호가 안 채워지면 그 번호를 다시 보내 달라고 합니다
대가는 기다림입니다. 받는 쪽은 빈 번호가 채워질 때까지 뒤에 온 조각을 붙들고 있어야 합니다. 그동안 응용 프로그램은 이미 도착한 것도 못 받습니다. 그래서 순서가 중요하지 않은 쪽은 아예 이 번호를 싣지 않습니다.
상세
이삿짐 상자 스무 개에 1번부터 번호를 적어 트럭 세 대에 나눠 싣습니다. 트럭은 저마다 다른 길로 가서 도착 순서가 뒤섞입니다. 새집에서는 상자에 적힌 번호만 보고 원래 순서대로 다시 쌓으면 됩니다. 다 쌓고 나서 7번이 비어 있으면 그 상자가 안 왔다는 것도 번호로 압니다.
보낼 데이터는 한 번에 다 가지 않습니다. 여러 덩이로 나뉘어 갑니다. 그 한 덩이를 조각이라고 부릅니다. 프로토콜마다 세그먼트·패킷·데이터그램처럼 다른 이름으로 부릅니다.
순서 번호는 보내는 쪽이 조각마다 하나씩 매겨 붙이는 정수입니다. 뒤에 보내는 조각이 앞의 것보다 큰 값을 받도록 매깁니다. 받는 쪽은 조각이 어느 길로 왔는지도, 얼마나 늦었는지도 모르는 채 이 값 하나만 보고 줄을 세웁니다.
block-beta columns 4 t1["보낸 차례"]:4 a1["1"] a2["2"] a3["3"] a4["4"] t2["닿은 차례"]:4 b1["3"] b2["1"] b3["4"] b4["2"] t3["번호로 세운 뒤"]:4 c1["1"] c2["2"] c3["3"] c4["4"]
뒤섞여 도착해도 번호만 보고 다시 세우면 보낸 차례가 돌아옵니다. 하나가 끝내 안 오면 그 번호가 비어 있는 것으로 빠진 조각도 알아냅니다.
번호가 필요한 까닭은 네트워크가 보낸 차례를 지켜 주지 않는 데 있습니다. 조각마다 다른 길로 가서 늦게 보낸 것이 먼저 닿기도 하고, 도중에 사라지기도 하고, 다시 보낸 탓에 같은 것이 두 번 들어오기도 합니다. 번호가 없으면 받는 쪽은 이 셋을 구별하지 못합니다. 도착한 차례를 그대로 보낸 차례로 믿는 수밖에 없습니다.
번호를 매기는 단위
무엇에 번호를 매기는지는 프로토콜마다 다릅니다. 조각 하나에 번호 하나를 주는 쪽이 있습니다. 조각 안의 바이트마다 번호를 매기는 쪽도 있습니다.
ICMP(Internet Control Message Protocol, 인터넷 제어 메시지 프로토콜)의 에코는 앞쪽입니다. 요청 메시지 하나에 번호 하나가 붙습니다. 답장이 같은 번호를 되돌려 줍니다. 그래서 몇 번째 요청의 답인지 알 수 있습니다.
TCP(Transmission Control Protocol, 전송 제어 프로토콜)는 뒤쪽입니다. 연결 위를 흐르는 바이트를 처음부터 끝까지 이어 셉니다. 이렇게 끊기지 않고 이어지는 바이트 흐름을 스트림이라고 합니다. 한 조각에 붙는 번호는 그 조각의 첫 바이트가 스트림의 몇 번째 바이트인가를 뜻합니다.
| 세는 단위 | 번호가 뜻하는 것 | 이렇게 쓰는 곳 |
|---|---|---|
| 조각 하나 | 몇 번째 메시지인가 | ICMP 에코 |
| 바이트 하나 | 스트림의 몇 번째 바이트인가 | TCP |
바이트를 세면 조각의 경계가 바뀌어도 번호가 흔들리지 않습니다. 보내는 쪽이 사정에 따라 한 조각을 둘로 쪼개거나 작은 것 둘을 붙여 보내도, 바이트마다 붙은 번호는 그대로입니다. 조각을 세는 방식이라면 쪼개는 순간 번호를 다시 매겨야 합니다.
block-beta columns 4 t1["처음 보낸 경계"]:4 a["조각 A · 1~100 바이트"]:2 b["조각 B · 101~200 바이트"]:2 t2["A 를 둘로 쪼갠 뒤"]:4 c["1~50"]:1 d["51~100"]:1 e["조각 B · 101~200 바이트"]:2
경계는 둘로 갈렸지만 51번 바이트는 두 줄 모두에서 51번입니다. 번호가 바이트에 붙어 있어서 조각을 어떻게 나누든 값이 안 바뀝니다.
조각이 닿을 때 받는 쪽이 하는 판단
받는 쪽은 「다음에 올 번호」를 하나 기억해 둡니다. 조각이 닿으면 그 조각의 번호를 기억해 둔 값과 견주고, 결과는 셋 중 하나입니다.
flowchart TD
A["조각이 닿는다"] --> B{"기다리던 번호인가"}
B -->|맞다| C["응용 프로그램에 넘긴다 · 기다리는 번호를 하나 올린다"]
B -->|이미 지나온 번호다| D["두 번 온 것이다 · 버린다"]
B -->|앞선 번호다| E["빈 번호가 채워질 때까지 들고 있는다"]
이미 지나온 번호가 다시 오는 것은 답장이 늦어 보내는 쪽이 같은 것을 또 보냈기 때문입니다. 앞선 번호는 버리지 않습니다. 받아 둔 것을 잠시 쌓아 두는 메모리 공간인 버퍼에 들고 있습니다. 빈 번호가 채워지면 한꺼번에 응용 프로그램에 넘깁니다.
이렇게 들고 있는 시간이 순서 번호가 치르는 대가입니다. 뒤엣것이 멀쩡히 도착해 있어도 앞의 하나가 안 오면 전부 발이 묶입니다. 이것을 헤드 오브 라인 블로킹이라고 부릅니다.
flowchart TD
subgraph C1["2번이 아직 안 온 동안"]
A1["버퍼에 3 · 4 · 5 가 쌓인다"] --> A2["응용 프로그램으로 나가는 것이 없다"]
end
subgraph C2["2번이 닿은 뒤"]
B1["2 가 도착한다"] --> B2["2 · 3 · 4 · 5 를 한꺼번에 넘긴다"] --> B3["버퍼가 빈다"]
end
C1 --> C2
빈 번호를 메우는 한 왕복
아래에서는 번호를 조각마다 하나씩 매기는 쪽으로 단순하게 그립니다. 바이트를 세는 쪽도 판단은 같습니다.
받는 쪽은 어디까지 받았는지를 번호로 알려 줍니다. 이 답장이 확인 응답입니다. 답장이 가리키는 것은 빠짐없이 이어진 데까지입니다. 3번을 들고 있어도 2번이 비어 있으면 1번까지라고 답합니다.
보내는 쪽은 답장이 안 올라오는 번호를 다시 보냅니다. 이것이 재전송입니다.
sequenceDiagram
participant 보내는쪽
participant 받는쪽
보내는쪽->>받는쪽: 1번 조각
받는쪽-->>보내는쪽: 1번까지 받았다
보내는쪽-x받는쪽: 2번 조각 · 도중에 사라진다
보내는쪽->>받는쪽: 3번 조각
Note over 받는쪽: 3번은 들고 있는다
받는쪽-->>보내는쪽: 아직 1번까지다
보내는쪽->>받는쪽: 2번 조각 다시
받는쪽-->>보내는쪽: 3번까지 받았다
보내는 쪽은 2번이 사라지는 장면을 보지 못합니다. 답장이 1번에서 더 안 올라간다는 것으로 짐작할 뿐입니다. 정해 둔 시간이 지나도 그대로면 다시 보냅니다. 이 기다리는 시간이 타임아웃입니다.
그래서 멀쩡히 간 조각이 다시 가는 일이 생깁니다. 받는 쪽에서 그 중복을 걸러 내는 것도 같은 번호입니다. 한 번호가 하는 일이 셋인 셈입니다 — 줄 세우기, 중복 버리기, 빠진 것 알아채기.
번호 값의 범위와 시작점
번호는 조각 앞머리에 붙어 다니는 제어 정보 묶음에 실립니다. 이 묶음을 헤더라고 합니다. 번호를 담는 칸은 그 안에 있습니다. 칸의 비트 수는 미리 정해져 있습니다.
칸이 정해져 있으니 번호는 언제까지나 커지지 못합니다. 가장 큰 값에 이르면 다시 0으로 돌아옵니다. 이 되돌아옴을 랩어라운드라고 합니다.
되돌아온 번호와 예전 번호가 겹치면 받는 쪽은 둘을 구별하지 못합니다. 조각 하나가 네트워크에 머물 수 있는 시간에는 한계가 있습니다. 번호가 한 바퀴 도는 데 걸리는 시간이 그 한계보다 길어야 합니다.
그래야 옛 번호를 단 조각이 먼저 사라집니다. 같은 번호가 다시 돌아와도 겹칠 것이 없습니다.
시작 번호를 늘 0으로 두지 않는 까닭도 겹침에 있습니다. 연결이 끝나고 같은 상대와 같은 포트로 새 연결이 서면, 앞 연결에서 늦게 도착한 조각이 새 연결의 것처럼 보일 수 있습니다. 연결을 열 때마다 시작 값을 다르게 잡으면 그런 조각의 번호가 새 연결이 기다리는 범위 밖이라 그냥 버려집니다. 이 시작 값을 초기 순서 번호라고 부릅니다.
sequenceDiagram
participant 보내는쪽
participant 받는쪽
보내는쪽->>받는쪽: 옛 연결의 조각을 보낸다
Note over 보내는쪽,받는쪽: 이 조각이 길에서 늦어진다
보내는쪽->>받는쪽: 옛 연결을 닫는다
보내는쪽->>받는쪽: 같은 상대 · 같은 포트로 새 연결을 연다
Note over 보내는쪽,받는쪽: 새 연결은 시작 값을 다르게 잡는다
보내는쪽->>받는쪽: 새 연결의 조각을 보낸다
Note over 받는쪽: 늦어진 옛 조각이 그제야 도착한다
Note over 받는쪽: 번호가 기다리는 범위 밖이라 버린다
시작 값을 짐작하기 어렵게 잡으면 덤으로 얻는 것이 있습니다. 바깥에 있는 제삼자가 번호를 맞춰 남의 연결에 조각을 끼워 넣기가 어려워집니다.
순서 번호가 없는 프로토콜
UDP(User Datagram Protocol, 사용자 데이터그램 프로토콜)의 헤더에는 순서 번호 칸이 없습니다. UDP 가 한 번에 보내는 조각 하나를 데이터그램이라고 합니다. 보낸 순서도 중복도 봐 주지 않아서, 도착한 데이터그램은 도착한 차례 그대로 응용 프로그램에 올라갑니다.
UDP 를 쓰면서 순서도 지켜야 하면 그 위에 번호를 직접 붙입니다. QUIC이 그렇게 합니다. 이 이름은 Quick UDP Internet Connections 를 줄인 말에서 왔습니다. 지금은 풀지 않고 이름 그대로 씁니다.
flowchart TD
subgraph S1["TCP 를 쓸 때"]
A1["응용 프로그램"] --> A2["TCP · 순서 번호 있음"] --> A3["IP"]
end
subgraph S2["QUIC 을 쓸 때"]
B1["응용 프로그램"] --> B2["QUIC · 순서 번호 있음"] --> B3["UDP · 순서 번호 없음"] --> B4["IP"]
end
S1 ~~~ S2
같은 일을 TCP 는 자기 계층에서 합니다. QUIC 은 UDP 위의 계층에서 자기 번호를 매기고 확인 응답과 재전송까지 스스로 갖춥니다.
번호를 순서 맞추기에 안 쓰는 경우도 있습니다. ICMP 에코의 번호는 줄을 세우려는 것이 아니라 어느 요청의 답인지 짝을 맞추려는 것입니다. 요청을 여럿 보내 놓고 돌아온 번호를 세면 몇 번째 것이 안 돌아왔는지 알 수 있습니다.
헷갈리는 이웃
번호처럼 생긴 값이 헤더에 여럿 실립니다. 하는 일이 서로 달라서 같은 값으로 읽으면 뜻이 어긋납니다.
| 이웃 | 무엇을 말하나 | 순서 번호와 갈리는 대목 |
|---|---|---|
| 오프셋 | 기준점에서 얼마나 떨어졌나 | 거리를 말한다. 차례를 말하지 않는다 |
| 식별자 | 누구인지 | 크기를 견주는 것이 뜻이 없다 |
| 체크섬 | 내용이 상했나 | 값이 커진다는 규칙이 없다 |
오프셋과는 특히 겹쳐 보입니다. 바이트를 세는 순서 번호는 스트림 처음부터의 거리이기도 해서 둘이 같은 값이 되기 때문입니다. 갈리는 지점은 쓰임입니다. 오프셋은 「어디를 읽을까」에 쓰고, 순서 번호는 「이것이 다음 차례인가」에 씁니다.
관련 항목
순서 번호와 함께 도는 신뢰성 장치
확인 응답 · 재전송 · 타임아웃 · 슬라이딩 윈도 · 흐름 제어 · 혼잡 제어 · 신뢰성 · 체크섬
이 번호를 싣거나 빼놓은 전송 프로토콜
TCP · UDP · ICMP · QUIC · 전송 계층
번호가 붙는 전송 단위
세그먼트 · 패킷 · 데이터그램 · 스트림 · 헤더 · 페이로드
이 번호와 헷갈리는 다른 값
오프셋 · 식별자 · 메시지 ID · 상관 관계 ID · 멱등성 키
순서를 지키느라 생기는 문제
헤드 오브 라인 블로킹 · 재정렬 · 중복 전달 · 순서 보장 · 멱등성 · 버퍼
번호 값을 정하는 규칙
초기 순서 번호 · 랩어라운드 · 단조 증가 · 난수
다른 이름: sequence number · 시퀀스 번호 · 일련 번호