TCP 는 무엇을 보장하려고 무엇을 포기했나
Esc

    TCP 는 무엇을 보장하려고 무엇을 포기했나

    –

    앞 편에서 다섯 개를 보내고 1·2·5·3 을 받았습니다. 4는 아예 오지 않았습니다.

    받는 쪽이 이걸 그대로 쓸 수는 없습니다. 순서가 뒤집혀 있고 중간이 비어 있는데, 그 사실을 받는 쪽이 알아서 알아내야 합니다.

    TCP 는 이 일을 전부 대신 해 줍니다. 보낸 순서 그대로, 빠짐없이, 중복 없이 올려 줍니다. 그래서 TCP 위에서는 네트워크가 아니라 파일을 읽는 것처럼 코드를 쓸 수 있습니다.

    공짜일 리가 없습니다. 이 편은 그 보장을 무엇으로 만들었고 무엇을 대가로 냈는지를 봅니다.

    연결이라는 말의 뜻

    TCP 를 “연결형” 이라고 부릅니다. 선이 새로 깔린다는 뜻이 아닙니다. 선은 그대로입니다.

    양쪽이 서로에 대한 상태를 들고 있다는 뜻입니다.

    • 지금까지 몇 번까지 보냈는지
    • 상대가 몇 번까지 받았다고 했는지
    • 답이 안 온 것이 무엇이고 언제 보냈는지
    • 지금 얼마나 보내도 되는지

    이 상태가 있어야 “빠진 것” 이라는 말이 성립합니다. 아무 상태도 없으면 안 온 것과 원래 없던 것을 구별할 방법이 없습니다.

    상태를 들고 있다는 말에는 두 가지가 따라옵니다. 시작할 때 그 상태를 맞춰야 하고, 끝날 때 정리해야 합니다. 그게 아래의 세 번과 네 번입니다.

    시작할 때 왜 세 번인가

    연결을 열 때 세 번 주고받습니다. 여는 쪽이 클라이언트, 문을 열어 두고 기다리는 쪽이 서버입니다.

    세 번인 이유는 번호를 알려주는 쪽이 둘이기 때문입니다.

    한쪽이 번호를 알려주면, 상대가 그걸 받았는지 확인해야 합니다. 알려주기 한 번과 확인 한 번이 양쪽에 필요하니 네 번이어야 하는데, 가운데 둘이 한 패킷에 합쳐져서 세 번이 됩니다.

    시작 번호를 0 부터 세지 않고 매번 다르게 고르는 이유도 여기 있습니다. 항상 0 부터 시작하면, 예전 연결에서 늦게 도착한 패킷이 새 연결의 번호와 겹쳐서 엉뚱한 데이터로 섞여 들어갈 수 있습니다.

    두 번이면 안 되는 이유도 같은 자리에서 나옵니다. 클라이언트가 보낸 SYN 이 길에서 한참 헤매다 도착하는 경우가 있습니다. 서버가 그걸 새 요청으로 알고 바로 연결을 열어 버리면, 아무도 원하지 않는 연결이 서버에 하나 생깁니다. 세 번째 확인이 있으면 클라이언트가 “나는 그런 것 보낸 적 없다” 는 사실이 드러나서 그 연결은 정리됩니다.

    번호를 붙이고, 받았다고 답한다

    연결이 열리면 보내는 것마다 번호가 붙습니다. 정확히는 바이트 단위로 번호가 매겨집니다. 몇 번째 패킷이 아니라 몇 번째 바이트인지가 기준입니다.

    받는 쪽은 답할 때 “여기까지는 빠짐없이 받았다” 를 말합니다. 받은 것 하나하나를 지목하는 게 아니라, 끊기지 않고 이어진 끝을 말합니다. 이것을 누적 ACK 라고 합니다.

    앞 편의 상황을 여기에 대 보면 이렇게 됩니다.

    도착한 것 받는 쪽이 답하는 값 뜻
    1 2 1까지 받았으니 다음은 2
    2 3 2까지 받았으니 다음은 3
    5 3 여전히 3
    3 4 3까지 받았으니 다음은 4

    5가 도착했는데도 답하는 값이 3에서 안 움직입니다. 받은 것이 없어서가 아니라, 이어진 끝이 거기까지라서입니다. 5는 버리지 않고 들고 있습니다. 다만 아직 이어붙일 수가 없습니다.

    이 “안 움직이는 답” 이 뒤에 나오는 이야기 전부의 씨앗입니다.

    사라진 것을 되찾는 두 가지 방법

    보낸 쪽 입장에서, 답이 안 오면 두 가지 중 하나입니다. 가는 길에 사라졌거나, 오는 답이 사라졌거나. 어느 쪽이든 다시 보내야 합니다. 언제 다시 보낼지가 문제입니다.

    첫째, 시간을 재고 기다립니다. 보낼 때 시계를 켜 두고, 정해진 시간 안에 답이 안 오면 다시 보냅니다. 그 시간을 RTO 라고 하고, 그동안 잰 왕복 시간을 바탕으로 잡습니다. 안전하게 잡아야 하니 실제 왕복보다 넉넉합니다. 느립니다.

    둘째, 같은 답이 반복되는 것을 신호로 씁니다. 위 표에서 5가 도착했을 때 받는 쪽은 3을 또 말했습니다. 뒤엣것이 계속 도착하면 같은 답이 계속 옵니다. 보낸 쪽은 이걸 보고 짐작할 수 있습니다. 뒤엣것은 가고 있는데 3만 빠졌구나.

    그래서 같은 답이 세 번 겹치면 시계가 끝나기를 안 기다리고 바로 다시 보냅니다. 이것을 빠른 재전송이라고 합니다.

    왜 하필 세 번인가 하면, 한두 번은 순서가 뒤바뀐 것만으로도 생기기 때문입니다. 앞 편에서 본 재정렬이 그것입니다. 그 정도로 재전송을 걸면 멀쩡한 것을 또 보내게 됩니다.

    가장 좋은 경우에도 왕복 한 번이 더 듭니다. 이게 뒤에서 문제가 됩니다.

    얼마나 보낼지를 정하는 손잡이가 둘이다

    여기서 자주 섞이는 두 가지를 갈라 둡니다. 보낼 양을 줄이는 이유가 두 개고, 서로 다른 문제입니다.

    TCP 는 이 둘 중 작은 쪽을 따릅니다. 상대가 벅차든 길이 벅차든, 벅찬 쪽에 맞춥니다.

    혼잡 제어 쪽을 한 줄 더 봅니다. 아무도 알려 주지 않으니 짐작해야 하고, 짐작의 근거는 손실입니다. 보내는 양을 조금씩 늘리다가 손실이 나면 크게 줄이고 다시 늘립니다.

    이 짐작에는 전제가 있습니다. 손실은 길이 막혀서 생긴다는 것입니다. 유선에서는 대체로 맞습니다. 큐가 넘치는 것이 손실의 주된 이유니까요.

    무선에서는 틀립니다. 신호가 깨져서 생긴 손실도 혼잡으로 읽고 보내는 양을 줄입니다. 길은 비어 있는데 스스로 좁혀 놓는 상태가 됩니다.

    작은 것을 모으는 습관

    TCP 에는 작은 패킷을 아껴 쓰려는 장치가 둘 있습니다. 게임에서는 둘 다 방해가 됩니다.

    Nagle 알고리즘은 보내는 쪽에 있습니다. 보낼 것이 작고 아직 답을 못 받은 것이 있으면, 바로 안 보내고 조금 모읍니다. 1바이트를 보내려고 헤더 40바이트를 붙이는 낭비를 막으려는 것입니다.

    지연 ACK 는 받는 쪽에 있습니다. 받자마자 답하지 않고 잠깐 기다립니다. 마침 보낼 데이터가 생기면 거기에 얹어서 한 번에 보내려고요.

    둘이 만나면 서로를 기다립니다. 보내는 쪽은 답이 와야 보내고, 받는 쪽은 보낼 것이 생겨야 답합니다. 그 결과로 수십 밀리초가 그냥 사라집니다.

    그래서 게임에서는 대개 Nagle 을 끕니다. 소켓 옵션 이름이 TCP_NODELAY 입니다. 작은 것을 자주 보내는 것이 목적인 쪽에서는, 모아 주는 친절이 그냥 지연입니다.

    끝낼 때는 네 번

    열 때 세 번이었으니 닫을 때도 셋일 것 같지만 넷입니다. 한쪽 방향씩 따로 닫기 때문입니다.

    TCP 의 연결은 방향이 둘입니다. 내가 보낼 것이 없어진 것과 상대가 보낼 것이 없어진 것은 다른 사건입니다. 그래서 알리기와 답하기가 방향마다 한 번씩, 둘 곱하기 둘로 넷이 됩니다.

    열 때는 가운데 둘을 한 패킷에 합쳐 셋으로 줄였습니다. 닫을 때는 못 합칩니다. “알았습니다” 와 “나도 끝입니다” 사이에 상대가 보낼 데이터가 낄 수 있기 때문입니다.

    닫은 뒤에도 마지막 답을 보낸 쪽은 잠시 상태를 들고 있습니다. TIME_WAIT 입니다. 서버에서 연결을 자주 열고 닫으면 이 상태가 쌓입니다. 포트가 모자라는 문제가 여기서 나옵니다.

    그래서 남는 문제: 줄 서서 막힌다

    지금까지가 TCP 가 해 주는 것들입니다. 이제 대가를 봅니다.

    받는 쪽은 순서대로만 올려 줍니다. 5가 도착해도 3이 없으면 5를 못 올립니다. 들고만 있습니다. 3이 다시 와서 자리를 메워야 5도 같이 올라갑니다.

    앞 편의 그림과 같은 요소인데 기다리게 하는 것 하나만 켰습니다. 도착한 것이 넷인데 쓸 수 있는 것은 둘에서 멈춰 있습니다.

    이것을 줄 서서 막히는 것(head-of-line blocking)이라고 합니다. 줄 맨 앞에 선 사람이 안 움직이면 뒤가 다 못 움직이는 상황입니다.

    이게 왜 게임에서 특히 아픈가 하면, 기다린 끝에 받은 것이 이미 쓸모없는 경우가 많기 때문입니다.

    캐릭터 위치를 초당 20번 보낸다고 해 봅시다. 하나가 빠져서 왕복 한 번을 더 기다리는 동안, 뒤이어 두세 개가 더 도착합니다. 기다림이 끝나면 그 네 개가 한꺼번에 올라옵니다.

    그중 앞의 셋은 볼 필요가 없습니다. 위치는 최신만 있으면 됩니다. 없어도 되는 것을 기다리느라 최신 것까지 늦게 본 셈입니다.

    파일을 받을 때는 이 보장이 정확히 필요한 것입니다. 한 바이트라도 빠지면 파일이 깨지니까요. 위치처럼 덮어쓰면 그만인 값에서는 그 보장이 그냥 비용입니다.

    정리

    • 연결은 선이 아니라 양쪽이 들고 있는 상태입니다. 그래서 열 때와 닫을 때 절차가 있습니다
    • 세 번 주고받는 이유는 번호를 알려주는 쪽이 둘이고, 서로 받았음을 확인해야 해서입니다
    • 답은 “여기까지 빠짐없이” 를 말합니다. 그래서 중간이 비면 답하는 값이 안 움직입니다
    • 되찾는 방법은 둘. 시계를 기다리는 쪽은 느리고, 같은 답 세 번으로 걸리는 쪽이 빠릅니다. 그래도 왕복 한 번이 더 듭니다
    • 흐름 제어는 상대가 벅찬 것, 혼잡 제어는 길이 벅찬 것. 다른 문제입니다
    • 혼잡 제어는 손실을 혼잡으로 읽습니다. 무선에서 오판이 생기는 이유입니다
    • Nagle 과 지연 ACK 는 작은 것을 모으다가 서로를 기다립니다. 게임에서 TCP_NODELAY 를 켜는 이유입니다
    • 대가는 줄 서서 막히는 것. 앞의 하나가 늦으면 뒤에 도착한 것이 전부 묶입니다

    여기서 갈림길이 하나 생깁니다. 보장을 통째로 사거나, 통째로 안 사거나 둘밖에 없는가.

    다음 편의 UDP 는 아무것도 안 해 줍니다. 그래서 오히려 필요한 것만 골라서 직접 얹을 수 있습니다.