[네트워크] TCP 연결 지향 프로토콜 구조와 동작 방식 - Connection-oriented transport: TCP
Connection-oriented transport: TCP의 특징
- point-to-point: 하나의 송신자와 하나의 수신자 연결
- reliable, in-order byte stream: "메시지 경계"가 없는 바이트 스트림 방식
- full duplex data: 동일한 연결에서 양방향 데이터 흐름, MSS (maximum segment size)
- cumulative ACKs: 누적 확인응답 방식
- pipelining: TCP 혼잡 제어와 흐름 제어가 윈도우 크기 설정
- connection-oriented: 핸드셰이킹으로 데이터 교환 전 송수신자 상태 초기화
- flow controlled: 송신자가 수신자를 압도하지 않도록 제어
TCP 세그먼트 구조
TCP 세그먼트는 다음과 같은 필드로 구성된다:
- 소스 포트 번호와 목적지 포트 번호(32비트)
- Sequence number: 바이트 스트림의 바이트 카운팅
- Acknowledgement number: 다음에 기대되는 바이트의 sequence number
- 헤더 길이(head len), 플래그(CWR, ECE, URG, ACK, PSH, RST, SYN, FIN)
- Receive window: 수신자가 받을 수 있는 바이트 수
- Checksum: 오류 검출을 위한 체크섬
- 옵션(Options): 가변 길이
- 응용 계층 데이터(application data): 가변 길이
TCP는 이러한 구조를 통해 신뢰성 있는 데이터 전송, 흐름 제어(flow control), 혼잡 제어(congestion control)를 제공하며, sequence number와 ACK는 이러한 메커니즘의 핵심 요소이다.
TCP 시퀀스 번호와 ACK
TCP는 신뢰할 수 있는 데이터 전송을 위한 연결 지향적 프로토콜이다.
TCP가 데이터의 신뢰성을 보장하기 위해 사용하는 핵심 메커니즘 중 하나가 sequence number(시퀀스 번호)와 acknowledgement(확인응답)이다.
송신자의 Sequence Number 공간
송신자에서는 sequence number 공간을 다음과 같이 구분한다:
- 이미 ACK 받은 부분(sent ACKed)
- 전송했으나 아직 ACK를 받지 않은 부분(sent, not-yet ACKed, "in-flight")
- 아직 사용 가능하지만 전송하지 않은 부분(usable but not yet sent)
- 아직 사용할 수 없는 부분(not usable)
이 중에서 window size(N)는 "in-flight" 상태의 데이터의 최대 양을 제한한다.
TCP의 Sequence Number
Sequence number는 TCP 통신에서 다음과 같은 의미를 갖는다:
- byte stream "number"의 첫 번째 바이트를 segment의 데이터에 할당한다
- 실제로는 segment 내의 첫 번째 바이트가 전체 데이터 스트림에서 몇 번째 바이트인지를 나타낸다
- 이를 통해 수신자는 받은 데이터의 순서를 정확히 파악할 수 있다
TCP는 바이트 스트림 프로토콜이기 때문에 메시지 경계가 없다. 대신 데이터를 바이트 스트림으로 취급하며, 각 바이트는 고유한 번호를 가진다. 이 번호가 바로 sequence number이다.
TCP의 ACK(Acknowledgement)
ACK는 수신자가 송신자에게 다음에 받기를 기대하는 byte의 sequence number를 알려주는 역할을 한다:
- 다른 측에서 다음에 예상되는 바이트의 sequence number를 나타낸다
- TCP는 cumulative ACK(누적 확인응답) 방식을 사용한다
- ACK 번호는 지금까지 성공적으로 수신한 마지막 바이트의 다음 번호를 의미한다
예를 들어, ACK 번호가 100이라면 이는 "99번 바이트까지 모두 성공적으로 수신했으니, 이제 100번부터 보내달라"는 의미이다.
TCP 통신의 간단한 예시
telnet 시나리오를 통해 TCP sequence number와 ACK의 동작을 살펴보자:
- Host A의 사용자가 'C' 문자를 입력한다
- Host A는 sequence number 42, ACK 79, data = 'C'를 포함한 패킷을 전송한다
- Host B는 'C'를 수신하고, ACK 43(다음에 받기 원하는 sequence number)을 보내며, 'C'를 echo back한다
- Host B는 sequence number 79, ACK 43, data = 'C'를 포함한 패킷을 Host A에게 전송한다
- Host A는 echo된 'C'를 수신하고, ACK 80(다음에 받기 원하는 sequence number)을 Host B에게 보낸다
이 과정에서 각 호스트는 상대방이 보낸 데이터의 수신을 확인하고, 다음에 받기 원하는 바이트의 sequence number를 ACK를 통해 알려준다.
TCP Round Trip Time과 Timeout 메커니즘
TCP는 신뢰할 수 있는 데이터 전송을 위해 timeout 메커니즘을 사용한다.
이 메커니즘은 패킷이 손실되었을 때 패킷을 재전송하기 위한 핵심 요소이다. TCP의 timeout 값을 어떻게 설정하는지, 그리고 RTT(Round Trip Time)를 어떻게 추정하는지에 대해 살펴보자.
TCP Timeout 값 설정의 중요성
TCP timeout 값을 설정할 때는 다음과 같은 고려사항이 있다:
- timeout은 RTT보다 길어야 한다. 그러나 RTT는 네트워크 상황에 따라 계속 변한다.
- 너무 짧은(too short) timeout: 조기 타임아웃(premature timeout)이 발생하여 불필요한 재전송(unnecessary retransmissions)이 일어난다.
- 너무 긴(too long) timeout: 세그먼트 손실에 대한 반응이 느려진다.
적절한 timeout 값 설정은 네트워크 효율성에 큰 영향을 미친다.
RTT(Round Trip Time) 추정 방법
RTT를 추정하기 위해 TCP는 다음과 같은 방법을 사용한다:
- SampleRTT: 세그먼트 전송부터 ACK 수신까지 측정된 시간
- 재전송된 세그먼트는 무시한다
- SampleRTT는 네트워크 상황에 따라 크게 변할 수 있으므로, 단일 측정값보다 더 "부드러운(smoother)" 추정치가 필요하다
- 최근 여러 측정값의 평균을 사용한다
- 단순히 현재의 SampleRTT만 사용하지 않는다
EstimatedRTT 계산 방법
TCP는 다음 공식을 사용하여 EstimatedRTT를 계산한다:
EstimatedRTT = (1- α)*EstimatedRTT + α*SampleRTT
이 공식은 다음과 같은 특징을 가진다:
- 지수 가중 이동 평균(Exponential Weighted Moving Average, EWMA)
- 과거 샘플의 영향이 지수적으로 빠르게 감소한다
- 일반적인 α 값: 0.125
이 방식을 사용하면 최근 측정값에 더 많은 가중치를 두면서도 과거 값들의 영향도 일정 부분 유지할 수 있다.
Timeout Interval 계산
실제 TCP timeout 간격(TimeoutInterval)
단순히 EstimatedRTT만 사용하지 않고, "안전 마진(safety margin)"을 추가한다:
TimeoutInterval = EstimatedRTT + 4*DevRTT
여기서 DevRTT는 RTT의 변동성을 측정하는 값이다:
DevRTT = (1-β)*DevRTT + β*|SampleRTT-EstimatedRTT|
- EstimatedRTT의 변동이 클 경우 더 큰 안전 마진이 필요하다
- 일반적인 β 값: 0.25
- DevRTT는 SampleRTT가 EstimatedRTT에서 얼마나 벗어나는지를 측정한다
이 방식으로 계산된 timeout 간격은 네트워크 상황의 변동성에 적응할 수 있으며, 불필요한 재전송을 최소화하면서도 패킷 손실에 신속하게 대응할 수 있다.
Timeout 메커니즘은 TCP가 신뢰할 수 있는 데이터 전송을 보장하기 위한 핵심 요소이며, 효율적인 네트워크 성능을 위해 적절한 timeout 값을 설정하는 것이 중요하다.
TCP 송신자와 수신자의 재전송 메커니즘
TCP(Transmission Control Protocol)는 신뢰할 수 있는 데이터 전송을 위해 다양한 재전송 메커니즘을 사용한다.
TCP 송신자와 수신자의 동작 방식, 다양한 재전송 시나리오, 그리고 빠른 재전송(Fast Retransmit) 기법에 대해 살펴보겠다.
TCP 송신자(Sender)의 동작 방식
TCP 송신자는 다음과 같은 주요 이벤트에 따라 동작한다:
1. 애플리케이션으로부터 데이터 수신 시
- 시퀀스 번호(sequence number)와 함께 세그먼트(segment) 생성
- 시퀀스 번호는 세그먼트 내 첫 데이터 바이트의 바이트 스트림 번호이다
- 타이머가 아직 동작 중이 아니라면 타이머 시작
- 타이머는 가장 오래된 미확인(unACKed) 세그먼트에 대해 설정
- 타이머 만료 간격은 TimeoutInterval로 설정
2. 타임아웃(timeout) 발생 시
- 타임아웃을 발생시킨 세그먼트 재전송
- 타이머 재시작
3. ACK 수신 시
이전에 미확인된 세그먼트에 대한 확인응답인 경우:
- 확인된 세그먼트 정보 업데이트
- 아직 미확인 세그먼트가 있다면 타이머 시작
TCP 수신자(Receiver)의 ACK 생성
RFC 5681에 따르면 TCP 수신자는 다음과 같은 상황에서 확인응답(ACK)을 생성한다:
1. 예상 시퀀스 번호의 in-order 세그먼트 도착 시
이전에 모든 데이터가 이미 확인된 경우:
- 지연된 ACK(delayed ACK) 전송
- 최대 500ms까지 다음 세그먼트를 기다림
- 다음 세그먼트가 오지 않으면 ACK 전송
2. 예상 시퀀스 번호의 in-order 세그먼트 도착, 하나의 세그먼트가 ACK 대기 중일 때
- 즉시 단일 누적(cumulative) ACK를 전송
- 두 개의 in-order 세그먼트 모두 확인
3. 예상보다 높은 시퀀스 번호의 out-of-order 세그먼트 도착 시(갭 감지)
- 즉시 중복 ACK(duplicate ACK) 전송
- 다음에 예상되는 바이트의 시퀀스 번호 표시
TCP 재전송 시나리오
TCP의 다양한 재전송 시나리오를 이해하는 것은 중요하다:
1. ACK 손실 시나리오
- 호스트 A가 시퀀스 번호 92, 8바이트 데이터를 전송
- 호스트 B가 ACK 100을 보내지만 손실됨
- 타임아웃 발생 후 호스트 A가 동일한 세그먼트 재전송
- 호스트 B가 ACK 100 재전송
- 호스트 A가 ACK 수신 후 다음 데이터 전송 계속
2. 조기 타임아웃(premature timeout) 시나리오
- 호스트 A가 시퀀스 번호 92(8바이트), 100(20바이트) 연속 전송
- 호스트 B가 각각 ACK 100, ACK 120 응답
- ACK 100이 지연되어 호스트 A에서 타임아웃 발생
- 호스트 A가 시퀀스 번호 92 세그먼트 재전송
- 호스트 B가 이미 수신했으므로 ACK 120으로 응답
- 호스트 A가 ACK 120을 받아 SendBase를 120으로 업데이트
3. 이전에 손실된 ACK를 위한 누적 ACK
- 호스트 A가 시퀀스 번호 92(8바이트), 100(20바이트) 연속 전송
- 호스트 B가 두 세그먼트 모두 수신, ACK 100 송신(손실됨), ACK 120 송신
- 호스트 A는 ACK 120만 수신하지만, 이는 92, 100 모두 수신되었음을 의미
- 누적 ACK 덕분에 첫 번째 ACK 손실 문제가 해결됨
TCP 빠른 재전송(Fast Retransmit)
TCP 빠른 재전송은 타임아웃을 기다리지 않고 손실된 세그먼트를 신속하게 감지하고 재전송하는 메커니즘이다:
- 송신자가 동일한 데이터에 대한 3개의 추가 ACK를 수신하면("triple duplicate ACKs"), 가장 작은 시퀀스 번호를 가진 미확인 세그먼트를 재전송한다
- 타이머 만료를 기다리지 않음
- 세그먼트가 손실된 것으로 간주하여 즉시 재전송
- 3개의 중복 ACK는 3개의 세그먼트가 누락된 세그먼트 이후에 도착했음을 의미
예를 들어, 세그먼트 92와 100을 전송했을 때 100이 손실되고, 이후 더 높은 시퀀스 번호의 세그먼트가 3개 도착하면, 수신자는 모두 ACK 100을 보낸다. 송신자는 3개의 중복 ACK 100을 받으면 타임아웃을 기다리지 않고 세그먼트 100을 즉시 재전송한다.
이러한 빠른 재전송 메커니즘은 TCP의 성능을 크게 향상시키며, 특히 높은 지연 시간을 가진 네트워크에서 유용하다.
TCP 흐름 제어(Flow Control) 메커니즘
TCP 흐름 제어는 송신자(sender)가 수신자(receiver)의 처리 능력을 초과하는 데이터를 전송하지 않도록 하는 중요한 메커니즘이다. 네트워크 계층이 애플리케이션 계층보다 빠르게 데이터를 전달할 때 발생할 수 있는 버퍼 오버플로우(buffer overflow) 문제를 해결하기 위해 설계되었다.
수신 버퍼 오버플로우 문제
네트워크 계층이 애플리케이션 계층보다 빠르게 데이터를 전달하면 어떻게 될까? 이 상황에서는 다음과 같은 문제가 발생할 수 있다:
- TCP 소켓 수신 버퍼가 점점 차게 된다
- 버퍼가 완전히 채워지면 추가로 들어오는 데이터는 폐기된다
- 이로 인해 데이터 손실이 발생하고 재전송이 필요해진다
- 결과적으로 네트워크 효율성이 저하된다
TCP의 흐름 제어 해결책
TCP는 이 문제를 해결하기 위해 수신자가 송신자에게 자신의 버퍼 상태를 알려주는 방식을 사용한다:
1. TCP 수신자는 가용 버퍼 공간을 알림:
- TCP 헤더의 rwnd(receive window) 필드를 통해 여유 버퍼 공간을 광고(advertise)한다
- RcvBuffer는 소켓 옵션을 통해 설정된 크기(일반적인 기본값은 4096 바이트)이다
- 많은 운영 체제에서 RcvBuffer를 자동으로 조정한다
2. 수신 윈도우 계산 방법:
- rwnd = RcvBuffer - (LastByteRcvd - LastByteRead)
- 즉, 전체 버퍼 크기에서 아직 애플리케이션이 읽지 않은 데이터 크기를 뺀 값이다
3. 송신자의 데이터 제한:
- 송신자는 미확인(unACKed) 상태의 "in-flight" 데이터 양을 수신자가 알려준 rwnd 값으로 제한한다
- 이를 통해 수신 버퍼가 절대 넘치지 않도록 보장한다
특수 상황 처리 (추가)
- rwnd = 0인 경우:
- 수신 버퍼가 완전히 찬 상태
- 데드락(deadlock) 방지를 위해 송신자는 주기적으로 1바이트 크기의 "probe 패킷"을 전송하여 수신자의 버퍼 상태 변화를 확인한다
- 실제로는 헤더(40바이트) + 데이터(1바이트) 구조의 패킷이 사용된다
- Silly Window Syndrome (SWS):
- 송신자가 데이터를 1바이트씩 보내거나 수신자가 1바이트씩 매우 늦게 처리할 때 발생
- IP 헤더(20바이트)와 TCP 헤더(20바이트)에 비해 데이터(1바이트)가 극히 적어 오버헤드가 큰 비효율적 상황
- 해결책:
- Nagle의 알고리즘: 큰 사이즈의 세그먼트를 한 번에 전송
- Clark's solution: 수신 버퍼의 절반 이상 또는 MSS(Maximum Segment Size)를 받을 수 있을 때까지 대기
- Delayed ACK: 충분한 버퍼 공간을 확보할 때까지 ACK 지연
흐름제어 요약
TCP 흐름 제어는 송신자가 수신자의 처리 능력을 초과하는 데이터를 전송하지 않도록 함으로써 버퍼 오버플로우를 방지한다. 수신자는 자신의 버퍼 상태(rwnd)를 TCP 헤더에 포함시켜 송신자에게 알리고, 송신자는 이 정보를 바탕으로 전송 속도를 조절한다. 이 메커니즘은 TCP의 신뢰성 있는 데이터 전송을 보장하는 핵심 요소 중 하나이다.
TCP connection management
TCP(Transmission Control Protocol)에서 데이터 교환 전에 송신자(sender)와 수신자(receiver)는 반드시 "handshake" 절차를 거친다. 이 과정에서 다음과 같은 두 가지를 합의한다.
- 연결을 맺을 의사가 서로 있음을 확인한다(agree to establish connection).
- 연결의 파라미터(예: 시작 sequence number 등)를 합의한다.
이 과정을 통해 양쪽 애플리케이션은 connection state가 ESTAB(Established) 상태가 되고, seq #(sequence number), rcvBuffer size(수신 버퍼 크기)와 같은 연결 변수(connection variables)를 각각 저장한다.
클라이언트에서는 다음과 같이 소켓을 생성한다.
Socket clientSocket = newSocket("hostname", "port number");
서버에서는 다음과 같이 연결 요청을 받아들인다.
Socket connectionSocket = welcomeSocket.accept();
2-way handshake 방식
연결을 맺는 과정에서 가장 단순한 handshake 방식은 2-way handshake이다.
2-way handshake에서는 한 쪽이 "Let's talk"라고 요청하면, 상대방이 "OK"라고 응답하여 연결이 성립된다.
이를 네트워크 관점에서 보면, 클라이언트가 req_conn(x)라는 연결 요청 메시지를 보내고, 서버가 acc_conn(x)로 응답하면 양쪽 모두 ESTAB 상태가 된다.
하지만 2-way handshake가 항상 네트워크에서 잘 동작하지는 않는다. 그 이유는 다음과 같다.
- 네트워크 지연(variable delays)이 발생할 수 있다.
- 메시지 손실로 인해 요청 메시지(req_conn(x))가 재전송될 수 있다(retransmitted messages).
- 메시지 순서가 바뀔 수 있다(message reordering).
- 한 쪽이 상대방의 상태를 직접적으로 볼 수 없다(can't "see" other side).
이러한 문제 때문에 실제 TCP에서는 3-way handshake를 사용한다.
2-way handshake의 실패 시나리오
TCP(Transmission Control Protocol)에서 2-way handshake를 사용하지 않고 3-way handshake를 사용하는 이유는 2-way handshake가 가진 근본적인 문제점 때문이다.
2-way handshake에서는 다양한 실패 시나리오가 발생할 수 있는데, 이러한 문제들이 안정적인 연결 설정을 방해한다.
1. half open connection(no client)
Half open connection은 클라이언트가 없는 상태에서 서버 측에 연결이 반만 열려있는 상태를 의미한다. 이 상황이 발생하는 과정은 다음과 같다:
- 클라이언트가 서버에게 연결 요청 메시지인 req_conn(x)를 보낸다.
- 서버는 이 요청을 받고 acc_conn(x)로 응답하면서 ESTAB(Established) 상태가 된다.
- 그러나 클라이언트가 연결을 종료하거나 크래시가 발생하면, 서버는 이 사실을 알지 못한 채 계속 연결을 유지한다.
- 결과적으로 서버는 리소스를 낭비하게 된다.
또 다른 시나리오에서는:
- 클라이언트가 req_conn(x)를 보내지만 네트워크 지연으로 서버에 도착하지 않는다고 판단한다.
- 클라이언트는 같은 메시지를 다시 전송한다(retransmit req_conn(x)).
- 서버가 원래 요청을 뒤늦게 받고 응답하면, 클라이언트는 이미 ESTAB 상태가 된다.
- 이후 서버에 도착한 두 번째 req_conn(x)에 대해 서버는 또다시 연결을 설정하게 되어 불필요한 connection이 생성된다.
2. dup data accepted!
또 다른 심각한 문제는 중복 데이터가 수락되는 상황이다:
- 클라이언트가 req_conn(x)를 보내고 서버는 acc_conn(x)로 응답하여 양쪽 모두 ESTAB 상태가 된다.
- 클라이언트가 일부 지연으로 인해 req_conn(x)를 재전송한다.
- 클라이언트가 데이터(x+1)를 전송한다.
- 서버가 재전송된 req_conn(x)를 받고 또 다른 연결로 인식하여 다시 ESTAB 상태가 된다.
- 클라이언트가 전송한 동일한 데이터(x+1)를 서버가 두 번 수락하게 된다.
이러한 문제들은 3-way handshake를 사용함으로써 해결할 수 있다. 3-way handshake에서는 클라이언트가 서버의 SYN+ACK에 대해 ACK로 응답하는 세 번째 단계를 추가함으로써 양쪽 모두 연결 상태를 명확히 인지할 수 있게 된다. 이 과정에서 각 측은 상대방의 sequence number를 확인하고 중복 요청을 식별할 수 있어 half-open connection과 중복 데이터 문제를 방지할 수 있다.
TCP 3-way handshake
TCP 3-way handshake는 다음 단계로 진행된다.
- 클라이언트가 서버에게 SYN 패킷(SYNbit=1, Seq=x)을 보낸다. 이때 클라이언트는 SYN_SENT 상태가 된다.
- 서버는 클라이언트의 SYN 패킷을 받고 SYN_RCVD 상태가 되며, SYN+ACK 패킷(SYNbit=1, Seq=y, ACKbit=1, ACKnum=x+1)을 클라이언트에게 응답으로 보낸다.
- 클라이언트는 서버의 SYN+ACK 패킷을 받고, ACK 패킷(ACKbit=1, ACKnum=y+1)을 서버에게 보낸다. 이 단계 이후 클라이언트는 ESTAB 상태가 되고, 서버도 ACK 패킷을 받고 ESTAB 상태가 된다.
이 과정을 통해 양쪽 모두 서로의 상태를 확인하고, 초기 sequence number를 동기화하여 안전한 통신을 준비하게 된다.
Closing a TCP connection
TCP 연결 종료 시에는 각 측에서 자신의 연결을 닫아야 한다. 이 과정에서는 FIN 비트가 1로 설정된 TCP 세그먼트를 전송하고, 상대방은 이에 대해 ACK로 응답한다. FIN을 받은 측에서는 ACK와 함께 자신의 FIN을 보낼 수 있으며, 이를 통해 양방향 통신을 동시에 종료할 수 있다.
FIN을 받은 후에 ACK를 보내는 것은 "나는 더 이상 데이터를 보내지 않겠다"는 의미이지만, 여전히 상대방으로부터 데이터를 받을 수 있는 상태를 유지한다. 이런 특성 때문에 TCP 연결 종료는 보통 4-way handshake로 이루어진다.
- 클라이언트가 서버에게 FIN 패킷을 보내 연결 종료를 요청한다.
- 서버는 FIN에 대한 ACK를 보낸다.
- 서버도 모든 데이터 전송을 마치면 FIN 패킷을 클라이언트에게 보낸다.
- 클라이언트는 서버의 FIN에 대해 ACK를 보내고, 일정 시간(TIME_WAIT) 동안 대기한 후 연결을 완전히 종료한다.
이러한 과정을 통해 양쪽 모두 안전하게 연결을 종료할 수 있다.