CS/Network

전송 계층: UDP, TCP

olsohee 2024. 3. 26. 15:22

전송 계층

전송 계층은 어플리케이션 계층 바로 아래에 위치하며, 좀 더 구체화된 개념이다. 전송 계층에서의 전송 단위는 세그먼트이며, 세그먼트는 데이터와 헤더 부분으로 나뉜다. 애플리케이션 계층으로부터 받은 메시지가 데이터 부분에 들어가고, 부가 정보가 헤더 부분에 들어간다. 전송 계층의 대표적인 프로토콜로 TCP와 UDP가 있다. 

UDP

UDP는 비연결형 통신 방법으로, TCP와 달리 신뢰성을 보장하지 않는다. 이때 비연결형이라는 것은 다음 사진과 같이 소켓과 소켓 간의 일대일 매핑이 이뤄지지 않는 것은 말한다.

즉, 송신지 소켓과 수신지 소켓이 연결되었는지 확인하지 않고 보낸다(connectionless). 그리고 TCP와 달리 전송된 세그먼트에 대한 ACK 응답을 받는 과정이 없다. 따라서 데이터가 제대로 도착했는지 알 수 없으며, 데이터가 유실되어도 재전송하지 않는다.
 
따라서 UDP는 데이터 전송은 빠르지만 신뢰성을 보장하지 않는다. 이런 특성으로 UDP는 실시간 스트리밍, 음성 통화 등에서 주로 사용된다. 이들은 속도와 실시간성이 중요하기 때문에 신뢰성은 떨어지지만 빠른 통신을 보장하는 UDP를 사용한다.

UDP 세그먼트

  • checksum: 전송 중에 데이터에 에러가 발생했는지 안했는지 판단할 수 있는 부분이다.
    • 받는 쪽의 UDP가 checksum 필드를 확인한 후 에러가 있으면 애플리케이션 계층으로 데이터를 올리지 않고 버린다.
    • 즉, UDP는 중간에 데이터가 유실될 수는 있어도 에러가 있는 데이터를 전송하지는 않는다.

TCP

TCP는 연결형 통신 방법으로,  다음과 같이 소켓 간의 일대일 매핑이 이뤄진 후(연결 확립) 데이터 통신이 이뤄진다. 이때 두 소켓 간 연결 확립을 위해 3-Way Handshake가 일어난다.

참고로 이때 P4, P5, P6는 모두 같은 IP 주소(B)와 포트 번호(80)를 갖는다. 즉 포트 번호가 같다고 다 같은 소켓이 아니라는 것이다. 각 소켓들은 고유의 id를 갖는데 id는 다음 4가지 정보를 기반으로 생성된다. 즉, 4가지 중 하나만 달라도 다른 소켓이 된다.

  • source IP 주소
  • sourse port 번호
  • dest IP 주소
  • dest port 번호

예를 들어 4명이 동시에 네이버에 접속하면, 모두 같은 80번 포트로 접속하지만(dest port = 80) 4명을 위한 각각의 소켓이 생성되어 있다. 그리고 각 소켓을 통해 통신이 이뤄진다.

TCP 세그먼트

  • sequence number
    • 데이터는 여러 개의 세그먼트로 나누어서 전송된다. (여러 개의 세그먼트로 나누면 전체 데이터가 한 번에 네트워크에 쏟아져 혼잡을 유발하는 것을 막을 수 있다.)
    • 이 필드에는 세그먼트 데이터 부분의 첫 번째 바이트의 번호가 담긴다. 즉, send buffer용 데이터로, 몇 번째 데이터를 보내는지를 의미한다. 
    • 여러 개의 세그먼트가 전송 순서대로 도착하지 않을 수 있는데, 이 필드를 통해 세그먼트의 순서를 파악해 재조립할 수 있다.
  • acknowledge number
    • 상대방으로부터 받은 데이터의 바로 다음에 받을 데이터 번호를 의미한다. 즉 성공적으로 받은 데이터의 sequence number + 1이다. 
    • 즉, recieve buffer용 데이터로, 몇 번째 데이터까지 성공적으로 받았으니, 다음으로 몇 번째 데이터를 보내달라는 것을 알리기 위한 용도이다.
    • 예를 들어 마지막으로 받은 데이터의 sequence number가 100이라면, 이에 대한 응답으로 ack 101 세그먼트가 전송된다.
  • Flags: 세그먼트의 용도와 내용을 의미한다. 각 비트마다 역할이 정해져 있으며, 초기 값이 0이고 비트가 활성화되면 1이 된다. 
    • ACK(Acknowledgement)
      • acknowledgement number 필드에 유효한 값이 채워져 있으면 1, 아니면 0이다.
      • SYN 세그먼트 전송 이후(TCP 연결 시작 이후) 모든 세그먼트에는 항상 이 비트가 1로 수신된다.
      • 즉, 몇 번째 데이터까지 성공적으로 받았으니, 다음으로 몇 번째 데이터를 보내달라는 것을 의미한다.
    • SYN(Synchronize)
      • 연결을 시작하기 위한 비트이다. 
    • FIN(Finish)
      • 연결을 종료하기 위한 비트이다.

TCP 소켓 간 연결과 연결 해제과정

TCP는 신뢰성 있는 통신을 위해 데이터 전송 전, 소켓 간 연결을 확립하기 위해 3-Way Handshake가 일어난다. 

  1. 클라이언트가 서버로 SYN 세그먼트를 보낸다.
    • sequence number 필드: 난수(random number)
    • 애플리케이션의 데이터는 포함되지 않는다.
  2. 서버는 해당 클라이언트와의 TCP 통신에서 사용할 버퍼 및 윈도우 크기 등을 초기화하고, 헤더의 SYN과 ACK 필드 비트가 1로 설정된 SYNACK 세그먼트를 전송한다.
    • acknowledgement number 필드: 클라이언트로부터 받은 seq 값 + 1
    • sequence number 필드: 난수
    • 애플리케이션의 데이터는 포함되지 않는다.
  3. 클라이언트는 해당 서버와의 TCP 통신에서 사용할 버퍼와 윈도우 사이즈 등을 초기화하고, ACK 세그먼트를 전송한다.
    • acknowledgement number 필드: 클라이언트로부터 받은 seq 값 + 1
    • 이제부터는 TCP 연결이 된 상태이므로 애플리케이션 데이터도 포함하여 전송할 수 있다.

데이터 전송이 끝난 뒤에는 연결을 종료하기 위한 4-Way Handshake가 일어난다. 그렇다면 송신 측 TCP는 모든 데이터를 보냈다는 것을 어떻게 알 수 있을까? 바로 애플리케이션 계층의 소켓에서 close() 시스템 콜이 호출되면 데이터 전송이 끝난 것이다.

  1. close() 시스템 콜이 발생하여, 클라이언트는 TCP 연결 종료를 위해 FIN 세그먼트를 전송한다.
  2. 서버는 ACK 세그먼트를 전송한다.
  3. 서버도 연결을 종료할 준비가 완료되면 FIN 세그먼트를 전송한다.
  4. 클라이언트는 ACK 세그먼트를 전송한다.
    • 이때 ACK 세그먼트를 보낸 클라이언트는 일정 시간(기본 값은 240초)을 기다린 후 연결을 종료한다. 일정 시간 기다리는 이유는, ACK 세그먼트가 유실될 수 있기 때문이다. 만약 ACK 세그먼트를 보내고 바로 종료했는데 ACK 세그먼트가 유실되면, 서버는 FIN 세그먼트에 대한 ACK 응답이 오지 않았기 때문에 FIN 세그먼트를 재전송한다. 그러나 이미 클라이언트는 종료했기 때문에 FIN 세그먼트는 전송되지 않고 서버는 ACK 응답을 받지 못하며, 계속 FIN 세그먼트를 재전송하게 된다. 따라서 이런 경우를 방지하기 위해 ACK 세그먼트를 보낸 뒤 일정 시간 기다렸다가 최종적으로 TCP 연결을 종료하게 된다. 
    • TCP 연결이 완전히 종료되면 TCP 통신을 위해 할당되었던 모든 자원의 할당이 해제된다.

유실된 세그먼트 판단 방법 (TCP가 데이터 유실을 해결하는 방법)

TCP는 유실된 세그먼트를 찾아 재전송하기 위해 타임아웃/재전송 매커니즘을 사용한다.

  • Sample RTT(Round Trip Time)는 세그먼트가 송신되고 긍정 응답이 도착한 시간까지의 시간이다. (참고로 재전송한 세그먼트에 대해서는 Sample RTT를 측정하지 않는다. 만약 세그먼트를 보냈는데 확인 응답이 오지 않는 경우 세그먼트를 재전송하는데, 이 경우 세그먼트 전송 시점이 여러 개가 되어 RTT 측정이 어려워지기 때문이다.)
  • 재전송하는 경우를 제외하고, 세그먼트를 전송하고 응답을 받을 때마다 Sample RTT를 측정하는데, 다음과 같이 Sample RTT는 매번 다르다. 그 이유는 각 세그먼트가 겪게 되는 라우터 내에서의 딜레이가 다르기 때문이다.
  • 따라서 Sample RTT 값의 평균인 Estimated RTT 값을 구한다. 그리고 새로운 Sample RTT를 얻자마자 Estimated RTT를 갱신한다. 
  • 그리고 Estimated RTT를 통해 타임아웃 값이 정해진다.
  • 결과적으로 세그먼트를 송신한 이후 타임아웃 값을 넘기면 세그먼트를 재전송한다.

그런데 위 방법에는 한계가 있다. 타임아웃 값은 여유있게 계산되었기 때문에 타임아웃 시간이 되려면 생각보다 오래 기다려야 한다는 것이다. 따라서 "타임아웃 시간이 되기 전에 좀 더 빨리 손실 세그먼트를 판단하는 방법은 없을까?"라는 생각에서 TCP Fast Retransmit이라는 개념이 등장했다.

  • TCP Fast Retransmit은 타이머의 타임아웃 시간이 길기 때문에, 타이머가 종료되기 전에 중복된 ACK을 3번 받으면 바로 재전송을 하는 기능이다. 결과적으로 손실된 패킷의 재전송을 빠르게 처리할 수 있다. 

정리하면, TCP는 다음 두 가지 근거를 기반으로 세그먼트가 유실되었다고 판단한다. (ex, 200~399 바이트의 데이터가 손실된 경우)

  • 타임아웃 시간이 될 때까지 전송한 세그먼트에 대한 ACK을 받지 못한 경우 (ACK 400을 받지 못한 경우)
  • 타임아웃 시간이 되지는 않았지만 중복된 ACK을 3번 이상 받은 경우 (ACK 200을 3번 이상 받은 경우)

TCP 소켓의 입출력 버퍼

입출력 버퍼는 소켓이 생성될 때 각 소켓마다 생성된다. 예를 들어 A 소켓과 B 소켓이 연결되어 있다고 가정하면, 이는 A의 send buffer와 B의 receive buffer가 연결된 것이고, B의 send buffer와 A의 receive buffer가 연결된 것이다.
 
송신 측의 애플리케이션 계층의 소켓이 write() 시스템 콜을 하면 데이터가 send buffer로 내려간다. 그리고 윈도우 사이즈만큼 세그먼트를 전송하고, 위에서 살펴본 것과 같이 타임아웃 또는 중복된 3번의 ACK을 받으면 세그먼트가 유실되었다고 판단하여, send buffer에서 해당 세그먼트를 재전송한다. 그리고 전송한 세그먼트에 대한 ACK을 받으면 send buffer에서 전송했던 세그먼트를 삭제한다. 
 
마찬가지로 수신 측 애플리케이션 계층의 소켓이 read() 시스템 콜을 하면 데이터가 receive buffer에서 애플리케이션 계층으로 올라간다. 그런데 이때 순차적으로 데이터가 있지 않고 중간에 데이터가 유실되면, 해당 데이터가 도착할 때까지 애플리케이션 계층으로 올라가지 않는다. 즉, TCP 세그먼트의 헤더에 sequence number 필드가 있고, 이 필드를 통해 순서대로 데이터를 재조립하여 애플리케이션 계층으로 올리기 때문에 TCP에서 데이터의 순서가 보장되는 것이다.

세그먼트의 크기

데이터를 여러 개의 세그먼트로 나누어 보낼 때 너무 많은 세그먼트로 나누면, 그 세그먼트 수만큼 고정된 크기의 헤더가 붙기 때문에 오버헤드가 발생한다. 따라서 세그먼트에 들어가는 데이터의 크기는 클수록, 즉 세그먼트의 크기는 클수록 좋다. 세그먼트의 최대 크기를 MSS라고 하며, 이는 약 1500바이트이다.
 
그런데 만약 송신 측의 애플리케이션 계층의 소켓에서 write() 하는 속도가 매우 느려서 send buffer에 데이터가 적게 있다면 어떨까? send buffer로 데이터가 들어올 때마다 그 작은 데이터를 세그먼트로 만들어 전송한다면 매우 비효율적일 것이다.
 
이에 대한 효율적인 방안이 네이글(Nagle) 알고리즘이다. 네이글 알고리즘은 네트워크를 통해 보내야 하는 패킷 수를 줄여 TCP 효율성을 향상시킨다. 

  1. 처음으로 send buffer로 내려온 데이터는 그 크기가 작더라도 무조건 세그먼트로 만들어 전송한다.
  2. 전송에 대한 ACK 응답을 받기 전에
    1. send buffer의 데이터가 최대 세그먼트 사이즈보다 크면, 세그먼트로 만들어 전송한다.
    2. send buffer의 데이터가 최대 세그먼트 사이즈보다 작으면, ACK 응답이 오기 전까지 send buffer에 데이터를 모은다.
  3.  ACK 응답이 오면 send buffer의 데이터가 최대 세그먼트 사이즈보다 작더라도 세그먼트를 만들어 전송한다.

윈도우 사이즈 조절 방법

세그먼트를 전송할 때 하나씩 전송하고 전송에 대한 ACK 세그먼트를 받고, 다음 세그먼트를 전송하는 것은 매우 비효율적이다. 따라서 송신 측은 자신의 윈도우 사이즈만큼 여러 개의 세그먼트를 한 번에 전송할 수 있다. 즉, 윈도우 사이즈는 송신 측이 한 번에 전송할 수 있는 최대 크기이다.
 
이때 송신 측의 윈도우 사이즈는 다음 두 가지를 기준으로 결정된다. 둘 중 더 작은 값이 송신 측이 한 번에 보낼 수 있는 데이터의 사이즈, 즉 윈도우 사이즈가 된다.

  • 수신 측의 receive buffer의 윈도우 사이즈 (수신할 수 있는 남은 사이즈)
  • 현재 네트워크가 받아들일 수 있는 데이터 사이즈

수신 측의 윈도우 사이즈를 고려해야 하는 이유(흐름 제어)

수신 측이 송신 측보다 데이터 처리 속도가 빠르면 문제 없지만, 송신 측의 속도가 더 빠르면 문제가 된다. 즉, 수신 측의 애플리케이션 계층의 소켓이 receive buffer의 데이터를 read()하는 속도보다 송신 측에서 데이터를 전송하는 속도가 더 빠르면? receive buffer에 데이터가 꽉 차고, 그 이후에 전달되는 데이터는 정상적으로 도착하지 못할 것이다. 따라서 송신 측은 수신 측의 ACK 세그먼트에 담긴 윈도우 사이즈를 확인하여, 윈도우 사이즈에 맞춰 데이터를 보낸다. 예를 들어 송신 측의 윈도우 사이즈가 2000 바이트인데 수신 측의 윈도우 사이즈가 1000 바이트이면, 송신 측은 한 번에 2000 바이트를 보내는 것이 아니라 수신 측의 윈도우 사이즈에 맞춰 데이터를 보낸다.

네트워크 상황을 고려해야 하는 이유(혼잡 제어)

송신 측에서 한 번에 많은 양의 데이터를 보낸다고 해서 그 데이터들이 빠르게 도착하는 것은 아니다. 처음에는 데이터를 한 번에 많이 보낼수록 수신 측이 데이터를 많이 받는다. 그러나 일정 수준 이상이 되면, 그렇지 않다. 그 이유는 네트워크 혼잡이 발생하기 때문이다.
 
네트워크는 공용 자원이다. 따라서 각 소켓에서 많은 양의 데이터를 보내면 라우터에 데이터가 쌓이게 되고 처리 속도가 느려지거나 데이터가 유실된다. 즉, 송신 측의 전송 속도에 네트워크가 처리할 수 있는 수준이 못미치게 되고, 결국 실제로 데이터가 유실되거나 유실되지는 않았지만 데이터 전송 속도가 느려서 전송 완료 전에 타임아웃 시간을 초과하게 된다. 따라서 송신 측은 데이터를 재전송하게 된다.
 
즉, 데이터를 한 번에 많이 보낼수록 많은 데이터를 정상 수신할 수 있는 것은 아니며, 오히려 재전송해야 하는 데이터가 늘어나 비효율적인 상황이 일어난다. 따라서 네트워크 상황을 고려하여 위와 같은 비효율적인 상황이 일어나지 않을 수 있는 최대의 전송량을 찾는 것이 핵심이다.
 
그렇다면 TCP는 어떻게 윈도우 사이즈를 조절하며 그 최적의 전송량을 구할까? 이 방법은 TCP 버전에 따라 다르다. 우선 TCP 버전에 따른 윈도우 사이즈 조절 방법을 알아보기 전에 공동으로 쓰이는 개념을 알아보자.

  • Slow Start
    • 세그먼트를 1개만 보낸다. 즉, 윈도우 사이즈는 1 MSS라고 할 수 있다. (세그먼트 크기는 클수록 좋아서, 대부분 최대 세그먼트 크기(MSS = 1500 바이트)로 세그먼트를 만들어 보내기 때문)
    • 전송한 세그먼트에 대한 ACK 응답이 오면, 전송한 세그먼트 사이즈의 2배만큼 윈도우 사이즈를 늘린다.
  • AIMD(Additive Increse/Multicative Decrease)
    • 전송한 세그먼트에 대한 ACK 응답이 오면, 전송한 세그먼트 사이즈만큼 윈도우 사이즈를 늘린다.
  • Slow Start Threshold(ssthresh) 
    • Slow Start 임계점으로, 여기까지만 Slow Start를 사용하겠다는 의미이다.
    • 이러한 개념이 필요한 이유는, Slow Start를 통해 윈도우 사이즈가 세그먼트 크기의 2배로 기하급수적으로 커진다. 따라서 어느순간부터는 네트워크 혼잡이 발생할 확률이 높아지기 때문에, 윈도우 사이즈를 천천히 늘리기 위함이다.

TCP 버전에 따른 윈도우 사이즈 조절 방식에 대해 알아보자.

  • TCP Tahoe
    1. 처음에는 Slow Start를 사용하여 윈도우 사이즈를 기하급수적으로 늘린다.
    2. 윈도우 사이즈가 임계점이 되면, AIMD를 사용하여 비교적 천천히 윈도우 사이즈를 늘린다.
    3. 타임아웃되거나 3 ACK Duplicated가 발생하면, 윈도우 사이즈는 1로 줄이고, 임계점은 네트워크 혼잡이 발생한 시점의 윈도우 사이즈의 절반으로 줄인다. 그리고 다시 Slow Start를 시작한다.
  • TCP Reno
    1. 처음에는 Slow Start를 사용하여 윈도우 사이즈를 기하급수적으로 늘린다.
    2. 윈도우 사이즈가 임계점이 되면, AIMD를 사용하여 비교적 천천히 윈도우 사이즈를 늘린다.
    3. TCP Reno는 타임아웃과 3 ACK Duplicated을 구분하여 처리한다.
      1. 타임아웃: Tahoe와 마찬가지로 윈도우 사이즈는 1로 줄이고, 임계점은 타임아웃이 발생한 시점의 윈도우 사이즈의 절반으로 줄인다. 그리고 다시 Slow Start를 시작한다.
      2. 3 ACK Duplicated: 이는 타임아웃에 비해 네트워크가 혼잡하다고 판단하기에는 어렵다. 왜냐면 이는 특정 세그먼트가 유실되었으며 그 뒤 순서의 세그먼트는 정상 도착한 경우이기 때문이다. 따라서 이 경우에는 윈도우 크기를 반으로 줄이고, 임계점도 줄어든 윈도우 크기와 동일하게 설정한다. 따라서 Slow Start 없이 바로 AIMD가 실행된다.

혼잡 제어 기능을 통한 공평성

TCP에서 데이터 전송 속도는 네트워크 상황에 따라 결정된다고 할 수 있다. 반면 UDP의 데이터 전송 속도는 애플리케이션에 의해 결정된다.
 
만약 애플리케이션 소켓이 write()를 통해 10GB의 데이터를 send buffer로 보냈다고 가정하자. send buffer는 네트워크 상황을 보며 윈도우 사이즈를 조절하고, 뿐만 아니라 수신 측의 윈도우 사이즈까지 살피며 10GB의 데이터를 나눠서 전송한다. 반면 UDP는 10GB의 데이터가 즉시 전송된다.
 
TCP 연결이 된 TCP 소켓들은 각각 내부적으로 네트워크 상황을 봐가면서 send buffer의 윈도우 사이즈를 조절한다. 즉, 그 이상의 데이터를 보낼 수 있음에도 네트워크 상황을 봐가면서 조절하는 것이다. 이는 네트워크는 공용 자원이기 때문에 한 소켓이 데이터를 한 번에 많이 보낸다고 빠르게 도착하는 것이 아니기 때문이다. 즉, 네트워크라는 공용 자원을 적당히 양보해가면서 나눠 사용하기 위함이다. 따라서 이로 인해 TCP 통신에서 각 소켓들은 유사한 윈도우 사이즈를 가지게 되고, 결국 공평하게 네트워크 자원을 1/N으로 나눠 사용하게 된다.


Reference

'CS > Network' 카테고리의 다른 글

링크 계층  (0) 2024.03.30
네트워크 계층: IP  (0) 2024.03.28
소켓(Socket)  (0) 2024.03.26
애플리케이션 계층: DNS  (1) 2024.03.26
애플리케이션 계층: HTTP  (0) 2024.03.26