[Computer Network] - TCP & Connection-oriented

오현석·2024년 11월 27일

Computer Network

목록 보기
20/25

🤔 TCP (Transmission control protocol)

TCP는 Reliable Connection-oriented한 프로토콜이다.

여기서 말하는 Reliable은 Layer 2인 Datalink layer에서 하는 것처럼 ARQ 방식으로 구현한다. Stop and Wait 방식으로 Reliable함을 충족시킬 수 있지만, 성능을 향상시키기 위해 Window size를 이용하는 Go back N 방식이나 Selective Repeat 방식을 사용할 수 있었다. 또한, Layer 2처럼 Piggybacking을 지원하여 Full-duplex한 통신이 가능하다.

타이머와 Ack를 활용하여 Error Control을 구현할 수 있고, Window size를 활용하여 보내는 데이터의 양을 조절하기에 Flow Control도 구현할 수 있다. 이때, Layer 2에서는 Window size를 미리 정해두고 통신을 이어갔는데, Layer 4에서는 Receiver가 Sender에게 자신의 Window size를 알려준다. 즉, Receiver가 Congestion없이, 무리없이 받을 수 있는 양을 Sender에게 알려준다는 점이 Layer 2와 구별되는 특징이다.

또한, Datalink layer에서는 물리적으로 연결이 되어있었지만, Transport layer에서는 Host 간에는 선으로 연결된 것이 아니라 인터넷을 통해 연결되어 있다. 때문에, Receiver에 있는 buffer에서 발생하는 오버플로우 이외에도 인터넷에서 오버플로우가 발생할 수 있다. 이를 방지하기 위해 TCP는 Congestion Control도 지원한다.

Stream delivery service

TCP에서는 Connection-Oriented Service, Reliable Service, Full-duplex Service 외에 다른 서비스도 지원한다. TCP에서 사용하는 이 서비스는 UDP와 달리 Datagram service가 아닌 Stream delivery service를 사용한다.

먼저 Datagram service를 알아보자. 우리가

Hello
world!

라는 메시지를 보낸다고 해보자. 이렇게 되면 Hello를 보내고 world!를 보낸거니까 각 부분 앞에 UDP 헤더를 붙이게 된다. 그러면 Hello가 하나의 데이터그램이 되어 전송되고, world!가 하나의 데이터그램이 되어 전송될 수 있다. Receiver는 이걸 받아 처리하면 된다.

반면, Stream delivery service라면 Helloworld!가 이렇게 붙어 버퍼에 들어가게 되고, 이걸 하나의 헤더를 붙여서 한번에 전송한다. UDP에서는 보내고자 하는 데이터의 경계를 구분할 수 있지만, TCP에서는 이 경계를 구분하지 못하고 한번에 보내기 때문에 Receiver는 들어온 데이터에서 Sender가 어떻게 데이터를 나눴는 지를 알 수가 없다. 그러나, 이렇게 구현하면 네트워크 상에 존재하는 Segment의 수가 줄어드므로 이런 방식을 채택해서 사용한다.

Numbering system

TCP에서는 Sender와 Receiver가 Connection을 형성한 후 보내는 데이터의 Byte마다 번호가 매겨진다. 프레임마다 번호를 매겼던 Datalink layer와는 달리 데이터의 바이트마다 번호를 매긴다는 점이 Transport Layer의 특징이다.
이 번호를 활용하여 Sequence Number, Ack Number를 정의할 수 있다. Sequence number는 Segment에 들어가는 첫번째 바이트의 번호가 Sequence number로 들어가게 되고, Ack number는 Receiver에서 받기를 기대하는 다음 바이트의 번호가 Ack number로 들어간다. Ack number는 Cumulative한 특성을 가져, Connection 이 Setup된 이후부터 내가 지금 받은 Ack 번호까지는 이미 잘 받았다고 생각할 수 있게 된다.

TCP Segment

TCP Segment는 다음과 같이 구성되어 있다. 역시 지원하는 서비스가 UDP에 비해 훨씬 많다보니 헤더에 들어있는 필드들도 상당히 많은 모습이다.

우선 Port 번호를 기입해주고, Sequence number와 Ack number도 같이 정의해준다. 이외에도 Data offset, flag들이 있고, Receiver가 받을 수 있는 데이터의 크기, 즉 Receive buffer에 남은 공간을 나타내는 Window size 필드도 있다. 그 뒤로는 Checksum과 Urgent 필드들, 옵션들이 자리하고 있다. Checksum 과정은 UDP와 마찬가지로 진행된다.

각 옵션이나 flag, Urgent 등에 대한 내용은 추후 포스트들에서 자세히 다뤄질 예정이다.

Transport Layer에서 생길 수 있는 문제 상황

다음 그림을 참고하여 예시 상황을 생각해보자. A라는 사용자가 물건을 구매하기 위해 10만원이라는 돈을 이체하려고 한다. 이체를 위해 사용자는 쇼핑몰 사이트에 Connection Setup Request를 보낸다. 이때, Setup Request에 대한 Ack가 오지 않는다면, Reliable Service이기 때문에 다시 Setup Request를 보내게 된다.

이제는 Ack가 잘 도착하여 Connection을 형성했다. 이제 A는 쇼핑몰 사이트에 대해 10만원을 이체했다. 그러나, 이번에도 타이머가 타임아웃 될 때까지 Ack가 도착하지 않아 다시 10만원을 이체했다. 이번에는 10만원을 잘 받았다는 Ack가 잘 도착했다면 A는 이체를 성공했다고 생각하며 커넥션을 종료하게 될 것이다.

그러나, 인터넷에서는 보낸 패킷이 Receiver로 가기 위한 경로가 여러 개이고, 이에 따라 도착하는 패킷의 순서가 달라질 수 있다. 위의 상황에서는 사실 Receiver는 모든 요청을 잘 받았는데, Ack가 인터넷 상에서 여러 상황들로 인해 Sender에게 전달이 너무 늦게 되었다면, A는 10만원이 아니라 20만원을 이체한 것이 된다.

이렇게 Receiver가 의도치 않은 중복 수신 문제를 일으킬 수 있으니 TCP는 이런 상황을 막아야 한다.

Connection Establishment

TCP에서는 연결을 설정할 때 이런 상황을 방지하기 위해 3-way handshaking이라는 과정을 거친다.

이 단계에서는 Header의 SYN flag를 1로 설정하여 Setup request를 먼저 보낸다. 이 Segment를 Sync Segment라고 한다. 이때, Sequence Number는 임의의 숫자를 잡아서 보내게 된다.

서버는 이 요청을 받아 Ack flag를 1로 설정하고, 들어온 Sequence number에 대한 Ack number를 설정한다. 또한, 서버 쪽에서도 클라이언트 쪽으로 데이터 전송을 위한 Connection을 setup하기 위해 SYN flag를 1로 설정하고 임의의 Sequence number를 설정한다. 이때, Flow control을 위해 서버는 클라이언트에게 자신의 Window size를 알려준다.

이 SYN ACK Segment를 받은 클라이언트는 서버가 보낸 SYN 요청에 대해 Ack를 보내면서 자신의 Window size를 알려주며 Connection을 만든다.
그림과 달리, 이 과정에서 보내는 Ack segment의 Sequence number는 8001이 맞다

그렇다면 이 3-way handshaking이 중복 수신 문제를 어떻게 해결할 수 있을까?

먼저 a가 정상 상황이라고 해보자. a 상태처럼 Connection이 연결된 상태가 지속되다가, 한참 뒤에 다시 Setup을 요청하는 Sync Segment가 서버에 들어왔다고 해보자. 이러면 클라이언트에서 예전에 보냈던 요청이 다시 들어온 상황인 것이다. 이때, 서버는 이 Sync Segment에 대한 응답을 해주게 된다. 그러나, Ack Segment에서 Sequence number를 설정하면 초기 Sequence number(이하 ISN)는 임의로 선택한다고 했다. Sequence number의 bit가 총 32개이므로 같은 ISN값을 선택할 확률은 1/(2^32)로 0에 가깝다.

그러면 ACK 단계에서 다른 ISN값을 선택해서 클라이언트에게 보내주기 때문에 클라이언트 입장에서는 자기는 새로운 연결 요청을 보낸 적이 없는데 답이 들어왔으니 해당 ACK에 대해 REJECT를 보내어 무시하게 된다.

Data transfer

데이터 전송은 이런 식으로 이뤄진다. 앞서 설명한 것처럼 SN는 Segment의 가장 첫 바이트 번호를 사용한다.

이 과정에서는 두 가지 포인트를 살펴봐야 한다.

  • Pushing data
  • Urgent data
    프로그램 실행 중에 강제 종료를 원하는 경우, ^C와 같은 종료 문자, 즉 Urgent data를 입력해서 빠져나오게 된다. 그러나, 이런 종료 문자가 다른 Segment들과 동일하게 처리된다면 지금 당장 강제 종료를 시킬 수 없게 되는 문제가 생긴다. TCP를 사용하는 Receiver에게 지금 종료를 원한다는 것을 알려줘야 하는 것이다.
    이를 위해 Urgent data를 Application data 맨 앞에 넣고, 옵션과 Urgent pointer를 설정하여 Receiver가 이를 빨리 처리할 수 있도록 한다.
    URG flag와 Urgent pointer는 각각 TCP Segment에 Urgent data가 포함되어 있다는 것을 알려주고, Urgent Data의 끝 위치를 알려준다. 예를 들어, Urgent pointer에 3이 있다면 0부터 2까지는 3바이트만큼 Urgent data가 들어있고, 3부터는 Normal한 데이터가 들어있다고 표시할 수 있는 것이다.

Connection Termination

Connection을 종료하는 방법에도 2가지 방법이 존재한다.

  • 3-way handshaking

Finish 플래그를 사용하여 Connection을 종료할 수 있다. 이 flag는 SYN flag처럼 데이터를 보내지 않아도 SN을 하나 소모한다. Connection 생성 때와 유사하게 종료가 이루어진다.

그러나, 이런 3-way handshaking 방식에서는 서버가 아직 보내야 할 데이터가 남은 상황에서 연결이 끊길 수가 있다. 이런 상황을 방지하기 위해서는 4-way handshaking을 사용하면 된다.

  • 4-way handshaking

4-way handshaking 방식은 Finish flag에 대한 ACK를 클라이언트가 받으면 서버 쪽에서 보낼 데이터를 모두 보내고 서버가 Finish flag를 보낼 때까지 클라이언트는 대기한다.

Half-open connection problem

이번에는 다음과 같은 상황을 생각해보자.

클라이언트는 서버와 연결된 TCP 연결을 종료하기 위해 FIN flag를 가지는 Segment를 보내 연결 종료를 요청했다. 그러나, 이 과정에서 이 연결 종료 요청이 네트워크 단에서 사라지거나 서버에 도달하지 못한다면, 서버는 클라이언트가 종료를 원한다는 것을 알지 못한다. 물론 클라이언트는 서버에게로부터 ACK가 오지 않았으니 타이머가 타임아웃 되서 다시 연결 종료 요청을 보내겠지만, 계속해서 ACK가 오지 않는 상황이 되는 것이다.

이러면 클라이언트는 계속해서 종료 요청을 보내 ACK를 하염없이 기다려야 하는걸까? 그건 아니다. 클라이언트는 이 종료 요청을 보내는 횟수를 제한하여, 이 횟수에 도달하면 자체적으로 TCP 연결을 끊는다.

그러나, 이렇게 되면 클라이언트가 연결을 종료했음에도 불구하고 서버는 이 사실을 모른 채 계속 리소스를 점유하고 있게 되므로 문제가 발생한다. 그렇다고 서버는 클라이언트로부터 요청이 일정 시간 들어오지 않을 때 연결을 끊도록 설정하면 해결이 될까? 진짜 클라이언트가 보낼 데이터가 없어서 안 보내고 있던거라면 또 문제가 발생할 수 있다.

이를 해결하기 위해서는 클라이언트가 서버에 보낼 데이터가 없어도 더미 Segment를 타이머를 돌려 가끔 보내줘야 한다. 나 아직 살아있으니까 연결을 끊지 말라고 서버에 알리는 것이다.

Connection resetting

Connection reset은 현재 연결이 비정상적으로 종료되었다는 것을 의미한다. 이것이 발생하는 상황은 주로 3가지이다.

  • 존재하지 않는 포트에 연결 요청을 보냈을 때 -> 요청을 취소시킨다.
  • 비정상적으로 연결이 끊어졌을 때 -> 연결을 닫는다.
  • 일정 시간 동안 데이터가 안 왔을 때 -> 연결을 종료시킨다.

이 과정은 RST flag를 1로 설정해서 연결을 다시 설정할 수 있다.

지금까지의 과정을 정리하면 다음과 같다. / 기준으로 왼쪽은 Event 이름이고, 오른쪽은 그 event에 대한 Action이다. 또한, 점선은 서버의 과정, 실선은 클라이언트의 과정이다.

여기서 서버는 이런 state들을 저장하고 있어야 한다. 이 저장 공간은 역시 메모리 공간을 잡아먹게 되는데, 이 메모리 공간을 노리는 공격이 DDoS 공격이 있는 것이다. TCP 연결을 계속해서 요청하게 되면 서버에서 state를 저장하는 메모리 공간이 부족해지고 이 메모리 공간이 부족해지면 reset되기 때문에 서버가 동작하지 않게 된다.

profile
다함께 성장하는 개발자 세상을 꿈꾸는 MLOps 엔지니어입니다😁 작성 당시 제 생각의 흐름을 독자 모두가 공감하고 이해할 수 있게 적으려고 노력합니다. 조언이나 질문은 언제든 환영입니다!

0개의 댓글