-> 블로그 이전

[Network] 트랜스포트 계층 : TCP

2022. 2. 15. 15:55Major`/컴퓨터 네트워크

TCP

1. Client와 Server 서로 1:1 통신을 한다

- Server는 Client별로 소켓을 보유하고 있기 때문에, 1:1로 양방향 통신이 가능하다

- 3-way handshake를 통해서 TCP 연결을 하면 소켓이 생성된다

 

2. 신뢰적이고, 순차적인 Data를 전달한다

- UDP와 달리, TCP는 Data를 바이트 스트림으로 인식

  • 각 Data별로 Boundary가 존재하지 않는다 

  1. 일단 Data를 TCP Buffer에 저장해둔다
  2. 혼잡/흐름 제어에 따라서 Data를 전송한다
    • 이때, Data는 순차적인 바이트 스트림이므로 Window Size씩 전송이 된다

>> TCP는 Data의 Boundary가 존재하지 않고, 하나의 연속적인 바이트 스트림으로 인식

 

3. 연결지향형 서비스를 제공한다

- Client - Server가 서로 핸드셰이킹을 통해 연결을 확립하고, 핸드셰이킹을 통해 연결을 종료한다

 

4. 흐름/혼잡 제어가 TCP 세그먼트의 Window Size를 결정한다

- 최대한 한번에 많이 전송하기 위해 파이프라인을 통해서 Window Size를 결정 :: Throughput Enhance 

 

흐름 제어 

  • 수신측의 Buffer Size를 고려

혼잡 제어 

  • Network 상(:: Network Code->Router)의 Buffer Size를 고려


TCP 세그먼트 구조

- 고정 헤더 : Source Port # / Dest Port # / Sequence # / Acknowledgement # / Header Length / Unused / Flag / Receive Window(Window Size : rwnd) / Checksum / Urgent Data Pointer

  • 고정 헤더는 반드시 세그먼트내에 존재 / 가변 헤더는 존재하지 않을 수도 있다
  • 고정 Header Length : 32bit/8bit × 5 = 4byte × 5 = 20byte

 

- 가변 헤더 : Options

 

- 페이로드 : Data

 

Sequence Number

- Sender → Receiver

- Connetion이 연결된 이후로,

  • "이번 세그먼트 페이로드에 실린 Data의 첫번째 byte는 우리가 Connect 한 이후로, 전체 Packet의 n번째 byte를 보내는 거야" 

 

>> 이번에 보내는 Data가 전체 Data의 몇 번째 byte라는 것을 알려준다

  • 이번 차례에서 580번째 byte 이후의 Data를 보낼 경우, Sequence Number는 580이다

 

Acknowledgement Number

- Receiver → Sender

- 만약, Ack-Number가 M이라면

  • "M-1번째 Data까지는 잘 받았고, 다음에는 M번째 Data를 보내줘"라고 요청하는 것이다

 

1. User(Client)가 'C' 타이핑

  • Seq=42, data='C' : "C의 Sequence Number는 42"
  • ACK=79 : "나는(Client) 너에게(Server) 받는 Sequence Number가 79이기를 기대할게"

2. 수신(Server)측이 ACK 신호 보냄

  • Seq=79 : "너가 원하는 Sequence Number 달아서 보낼게"
  • ACK=43 : "42번째 Data까지는 잘 받았고, 다음에 보낼때는 43번째 Data 보내줘"

3. 송신(Client)측이 ACK 신호 받고 다음 데이터 보냄

  • Seq=43 : "너가 원하는 43번째 Data 보내줄게"
  • ACK=80 : "나는(Client) 너에게(Server) 받는 Sequence Number가 80이기를 기대할게"

 

Header Length

- 고정 헤더 + 가변 헤더의 총 길이

  • 전체 TCP 세그먼트 길이 - Header Length = Data 길이

 

 

Flag :: URG / ACK / PSH / RST / SYN / FIN

▶ SYN / FIN / ACK / RST

- TCP 연결 확립/종료/리셋할때 쓰이는 Flag들이다

 

연결 확립 : SYN - ACK :: 3-way handshake

1. Client가 Server에게 연결 확립을 요청하자는 의미로 SYN 패킷 + Client의 초기 순서 번호를 보낸다 (Client → Server)

2. Server는 Client의 초기 순서번호를 인식하고, Client에게 (SYN + ACK) 패킷 + Server의 초기 순서 번호를 보낸다 (Server → Client)

3. Client는 Server로부터 Server 초기 순서 번호를 잘 받았다는 걸 응답하기 위해서 ACK 패킷을 보낸다 (Client → Server) 

  • ACK과 함께 Data를 전달할 수도 있다 :: Data의 첫번째 byte : Seq = 11

 

연결 종료 : FIN - ACK :: 4 way handshake

1. Client가 Server에게 연결을 종료하기 위해 FIN 패킷을 보낸다

2. Server는 Client에게 응답으로 ACK 패킷을 보낸다

3. Server는 남은 Data를 모두 Client에게 전송 후, FIN 패킷을 보낸다

4. Clinet는 Server에게 응답으로 ACK 패킷을 보낸다

  • FIN 패킷을 보내면, 그 이후부터는 Data 전송이 불가능하다
  • FIN 패킷을 보기전에 보낸 패킷(A)이 라우팅 지연, 패킷 유실로 인한 재전송 때문에 FIN 패킷보다 늦게 도착하면 Client는 A 패킷을 받아야 하기 때문에 일정시간 동안 세션을 열어두어야 한다 :: TIME_WAIT 

 

연결 즉시 종료 : RST

- 재설정(Reset)을 하는 과정이고, 양방향에서 동시에 일어난다

- 비정상적인 세션 연결 끊기

  • 현재 접속하고 있는 곳과 즉시 연결을 끊고자 할 때 사용

 

▶ PSH

송신

  • PSH 플래그가 세팅되면, 현재 Data + Buffer 내의 Data전부 Network-Layer로 내보내야 한다

수신

  • PSH 플래그가 세팅된 세그먼트를 수신하면, 받은 Data + Buffer 내의 Data전부 Application-Layer로 전달해야 한다

 

▶ URG

- PSH와 동일하다

  • 그러나 Urgent Data를 따로 표시해서, 우선순위를 가지게 해준다

 

Receive Window :: Window Size (rwnd)

- 흐름 제어(Flow Control)을 위한 필드이다

- Receiver의 Buffer Size

- Sender는 Receiver로부터 ACK 패킷을 받지 않아도, Window Size만큼의 Data를 일단 전송할 수 있다


TCP 왕복 시간(RTT) & 타임아웃(Timeout)

- rdt처럼 세그먼트의 손실에 대응하기 위한 Timer

 

타임아웃 주기는 왕복 시간보다 커야 한다 (타임아웃 > 왕복 시간)

▶ 너무 짧은 타임아웃

  • RTT도 안됐는데 Timeout이 발생해서 Sender가 "왜 ACK 패킷이 안오지? 세그먼트에 문제가 있나?"라고 생각을 해서 불필요한 재전송을 하게된다

 

▶ 너무 긴 타임아웃

  • 세그먼트가 Drop이 되었는데도 불구하고 아직 Timeout이 발생하지 않아서, Sender는 "아직 RTT가 안되었으니 더 기다리자"라고 생각을 해서 쓸데없이 기다리게 되어서 세그먼트 손실에 대한 대응이 느려진다 

왕복 시간(RTT) 예측

"미래는 예측이 불가능하다. 따라서, 과거의 Sample을 활용해야 한다"

 

- 세그먼트에 대한 ACK 패킷을 받을때마다 SampleRTT를 구한다

  • 이때, 재전송된 세그먼트에 대한 SampleRTT는 무시한다

 

EstimatedRTT = (1-∂)×EstimatedRTT + ∂×SampleRTT

- 2번째 EstimatedRTT : 이전 세그먼트의 RTT

- SampleRTT : 현재 세그먼트의 RTT

  • 일반적으로 ∂ = 0.125


TCP 타임아웃 값 설정

DevRTT = (1-§)×DevRTT + §×|SampleRTT - EstimatedRTT|

  • 일반적으로 § = 0.25
  • SampleRTT EstimatedRTT에서 얼마나 벗어났는지를 예측한 값이다

TimeoutInterval = EstimatedRTT + 4×DevRTT

  • DevRTT : 여유 값
  • EstimatedRTT가 SampleRTT에서 벗어나면 TimeoutInterval값에 여유 값(DevRTT)를 더해서 설정해준다

간소화 TCP 전송/재전송 이벤트 (특별 조건)

- 중복 ACKs 무시

- 흐름/혼잡 제어 무시

 

상위 Application으로부터 수신된 Data (Application Layer → Transport Layer) :: Sender 입장

1. Transport Layer에서는 수신된 Data를 이용해서 세그먼트로 만들어야 한다 :: 세그먼트 = TCP 헤더 + Data 

2. 체크섬을 계산해서 해당 체크섬 값을 insert한 세그먼트를 Receiver한테 전달해야 한다

 

>> Timer : 내보낸 세그먼트 중에서 ACK를 못받은 가장 오래된 세그먼트에 대해서 Timer를 세팅한다

  • if Timer가 이미 작동
    • 내보낸 세그먼트 중에서 ACK를 아직도 못받은 세그먼트가 존재
  • if Timer 작동 X
    • 내보낸 세그먼트에 대해서는 전부 ACK를 받았다
    • 새로 내보낸 세그먼트에 대해서 Timer 설정

 

타이머 타임아웃

- 타임아웃을 유발한 세그먼트를 재전송한다

  • 재전송하고, Timer를 처음부터 다시 시작한다

TCP ACK 생성

  • in-order segment : 순서에 맞게 차례대로 도착한 세그먼트
  • out-of-order segment : 순서에 맞지 않게 도착한 세그먼트 :: 세그먼트 사이에 gap 발생

 

Example) in-order segment

- Receiver 입장에서는 M + 1번째 Data를 원한다

  • Sender로 보내는 세그먼트에 ACK = M+1을 기입하고 보낸다

 

- Receiver는 원하는 M + 1번째 Data를 받았다

- 여기서 다음 세그먼트가 연달아서 올수도 있기 때문에 약 500ms까지 기다린다 ::: 지연된 ACK 

  • 만약 다음 세그먼트가 500ms이내에 안오면 ACK = M + n + 1을 기입하고 보낸다

 

- 만약 500ms이내에 또다른 세그먼트가 들어왔다면

  • 해당 세그먼트 까지의 누적 ACK = K + 1을 기입해서 Sender에게 보낸다

 

 

Example) out-of-order segment

- Receiver는 ACK = K + 1을 보냄으로써 다음 Data가 K + 1일거라고 기대했는데 Seq = Z인 segment가 전달되었다

  • 해당 segment를 out-of-order segment라고 한다

- 그러면 Receiver는 여전히 K + 1의 segment를 원하기 때문에 ACK = K + 1을 다시 보낸다 :: 중복 ACK 

 

 

Example) gap filling segment - segment T

(1) segment T :: in-order segment

- segment T가 in-order segment이다

  • 여기서는 약 500ms동안 다음 세그먼트를 기다리지 않고, Receiver는 바로 ACK = Q + 1을 통해서 다음 segment를 받는다

 

(2) segment T :: out-of-order segment

- Receiver는 여전히 K + 1인 segment를 원하기 때문에 ACK = K + 1을 다시 보낸다 :: 중복 ACK 


빠른 재전송 (Fast Retransmit)

- 타임아웃 주기가 상대적으로 길어지면 :: 손실된 Packet에 대한 대응이 느려진다

  • 중복 ACK를 사용해서 손실된 Packet들을 감지한다
  • Sender가 같은 Data에 대해서 3개의 중복 ACK를 수신하면 "해당 Data가 손실되었구나"라고 생각해서 Timer가 만료되기 전에 재전송한다

TCP 흐름제어 (Flow Control)

Receiver의 Buffer상에서 발생하는 오버플로우 문제

 

- Sender → Receiver로 보내는 Data 양 : A

- Receiver의 Transport Layer → Application Layer로 보내는 Data 양 : B

  • 만약, A > B :: Receiver 입장에서 내보내는 양보다 자신한테 들어오는 양이 더 많다
    • Receiver의 Buffer가 오버플로우된다
    • 이를 방지하기 위해 Flow Control을 수행한다

- 소켓을 생성할 때 :: OS는 소켓을 위한 Buffer를 할당해준다 (default : 4096 bytes)

- 전체 버퍼 용량 = buffered Data + free Buffer Space (버퍼된 데이터 양 + 아직 버퍼안된 공간(여유 공간))

 

Receiver항상 자신의 rwnd(:: Window size)를 Sender에게 알려줘야 한다

  • Sender 입장에서는, 아직 ACK가 안온 Data의 양이 rwnd보다 작도록 유지시켜야만 한다
  • ACK 안온 Data의 양 < rwnd 

TCP 혼잡 제어 (Congestion Control)

- Network 상에서 발생하는 혼잡 문제

  • Sender가 너무 많은 Data를 보내가지고, Network상에서 감당을 할 수 없는 경우 혼잡 발생
    • Packet Loss :: Router Buffer의 오버플로우 발생
    • Queueing Delay : Router Buffer에서의 큐잉
    • 따라서 "Cost of Congetion" :: 혼잡 비용이 발생하게 된다
  • 혼잡을 발생시키는 Sender들을 억제하는 Mechanism이 필요하다

 

혼잡 비용

(1) 불필요한 재전송

- 실제로는 Data도 잘 전달이 되었고, ACK도 Sender로 가고있는 중이다

  • but, Network가 너무 혼잡하고 그에 따라서 Delay가 매우 길어져서 해당 세그먼트에 대해서 Timeout이 발생하였다

>> 이런 경우, 해당 세그먼트는 이미 잘 전달이 되었고, ACK도 오고있음에도 불구하고, 재전송이 발생하게된다

 

(2) 상위 라우터에서 사용된 전송 용량 낭비

- Packet이 Source로부터 시작해서 R1 → R2 → R3 → R3 → Destination으로 전달될 예정이다

  • 하지만 R3에서 Packet Loss가 발생하였다

>> 이러면 S → R1 → R2 → R3에서 해당 Packet을 보내기 위해서 사용한 전송 용량아무런 쓸모가 없게 되었다


혼잡제어 해결 접근법

- 실제로 Congestion이 발생하는 곳은 Network의 내부 :: Network Core의 Router에서 발생한다

 

(1) end-to-end 혼잡제어

- Congesion이 발생하였는데도, Router에서는 End System들에게 "지금 Congestion이니까 Rate 낮춰서 보내줘라"라는 요청을 하지 않는다

  • End System들이 Packet의 Loss/Delay를 통해서 "해당 Packet에 Congestion이 발생했구나"라고 추측을 해서 혼잡 제어를 한다

- 이러한 방법은 TCP에서 사용한다

  • TCP는 Internet에서 사용하는 프로토콜로써, TCP의 역할은 오로지 Packet의 전달이다
  • 그렇기 때문에, 모든 복잡한 일(혼잡 제어)들은 Network에게 맡긴다

 

(2) Network-Assisted 혼잡제어

1. RouterCongestion에 대해서 가장 먼저 파악한다

2. Router가 End System들에게 혼잡 제어에 관한 Feedback을 제공한다

  1. 싱글 bit
    • 그냥 Congestion이 있다/없다만 알려준다
    • SNA, DECbit, TCP/IP ECN, ATM
  2. Explicit Rate
    • Router가 Explicit Rate를 계산해서 End System에게 "이 Congestion 제어하려면 Explicit Rate로 Packet들을 전송해라"라고 알려준다

※ rwnd / cwnd

Sender는 Packet을 전달할 때, 수신측의 Window Size(Flow Control) + Network 상황(Congestion Control)을 고려해서 Sender 본인의 Window Size를 결정한다

 

▶ rwnd (Receiver 측의 Buffer Size)

- 수신측이 현재 받을 수 있는 Packet의 Size

 

▶ cwnd (Network 상의 Buffer Size)

- Network상에서 현재 허용할 수 있는 Packet의 Size

  • Network의 Congestion에 따라서 동적이다

- LastByteSent - LastByteACKed = 보냈는데 아직 ACK 못받은 Packet의 양 :: K

  • TCP는 반드시 K ≤ Min(cwnd, rwnd)가 되도록 파이프라이닝해야 한다
  • 이래야지 Network도 Data의 양에 대해서 허용할 수 있고, Receiver 또한 허용할 수 있다

혼잡 회피 방법

AIMD :: Additive Increase/Multicative Decrease

▶ Network에 별 문제가 없는 경우

- 매 RTT마다 cwnd를 1MSS씩 증가 :: cwnd += 1 

 

▶ Network에 Congestion 발생

- Congestion이 발생한 경우 :: Packet Loss / Packet Delay

  • cwnd를 반으로 줄인다 :: cwnd /= 2 

- Network의 Bandwidth를 효율적으로 사용하려고 cwnd에 변동을 준다

>> Network 대역폭이 많이 남는 상황에서도 cwnd를 너무 조금씩 늘려서 Network를 효율적으로 활용하기 힘들다

 

Slow Start

- ssthresh라는 특정 임계치를 설정해준다 :: default Value는 OS가 임의로 정해준다

  • ssthresh를 기준으로 cwnd의 증가량을 결정한다
    • "Network에 Congestion이 발생할 수 있는 경계"

 

▶ cwnd < ssthresh

- 매 RTT마다 cwnd를 2배씩 증가시킨다 :: cwnd *= 2 

  • ACK를 받을때마다 cwnd를 1MSS씩 증가시킨다 :: 결국에는 RTT마다 cwnd는 2배씩 증가

- cwnd가 ssthresh보다 낮은 경우 = Network를 효율적으로 사용하지 못한다는 증거

  • Network를 효율적으로 사용하기 위해서 cwnd를 지수적으로 증가

 

▶ cwnd ≥ ssthresh

- 매 RTT마다 cwnd를 1MSS씩 증가시킨다 :: cwnd += 1 

 

▶ Network에 Packet Loss 발생

1. ssthresh를 cwnd/2로 재설정해준다 :: ssthresh = cwnd/2 

2. cwnd는 1로 줄여버린다 :: cwnd -> 1 

>> Loss가 발생하면 ssthresh는 반드시 cwnd/2로 재설정된다


혼잡 제어 정책 :: Packet Loss에 대한 대처

Packet Loss

  • 타임아웃 or 3 ACK Duplicated 발생

타임 아웃 

  • 해당 Packet에 대한 Timer 종료

3 ACK Duplicated 

  • 3개의 중복된 ACK가 전달되었다

 

 

>> TCP Tahoe / TCP Reno 둘 다 Slow Start의 시작을 전제로 한다

 

 

TCP Tahoe

- 어떤 Loss이던 간에 결국 Loss가 발생하게 되면, cwnd를 1로 만든다

1. Congestion이 발생할 때 까지 Slow Start

2. Congestion이 발생하면 AIMD

3. Loss가 발생하면 cwnd = 1로 설정하고 다시 Slow Start부터

  • 이때, ssthresh는 Loss가 발생할 당시의 cwnd값의 반으로 설정된다

>> Congestion 이후, Slow Start를 통해서 cwnd를 증가시킬 때 너무 시간이 오래걸린다

 

TCP Reno

- 빠른 회복 방식을 활용 :: TCP Tahoe의 단점을 극복

▶ 3 ACK Duplicated

- cwnd를 반으로 줄이기 :: cwnd /= 2

  • 이때, ssthresh도 cwnd/2 값으로 설정이 된다

>> 따라서, cwnd == ssthresh가 되기 때문에 이 시점부터는 AIMD 방식을 통해서 cwnd를 1MSS씩 증가시킨다

 

▶ Timeout

- cwnd를 1로 만든다

  • cwnd < ssthresh 동안은 다시 Slow Start 방식을 통해서 cwnd를 증가시키고, cwnd ≥ ssthresh 시점부터는 AIMD 방식을 통해서 cwnd를 증가시킨다