TCP는 연결이라는 개념을 사용하는 연결지향형 프로토콜 입니다. 연결 1개 당 소켓 1개를 가지게 되고, 상대방은 유일하게 식별됩니다. TCP는 연결이라는 개념을 HandShake 라는 방법으로 성립(?)하게 되는데,오늘은 그 이야기를 해보도록 하겠습니다.
요청하는 측을 Client, 요청받는 측을 Server라고 하겠습니다. TCP는 송수신이 양방향으로 다 가능하기 때문에, 지금부터는 Client - Server구조를 기반해서 설명하도록 하겠습니다. 전체적인 과정은 아래와 같고, 연결 수립시에는 3가지 Header를 교환하게 됩니다.

SYN : 연결 수립을 요청(클라)하는 신호입니다. 자신의 SequnceNumber를 보내주고, SYN bit Flag를 1로 설정합니다. 예를들어, 8000으로 SYN을 시작했다면 아래와 같은 TCP Header가 서버측으로 전송되게 됩니다.

SYN + ACK : 연결 수립 요청을 받는 입장(서버)에서도 자신의 SequnceNumber를 정해서 보내주고, SYN을 성공적으로 수신했다는 표시로 ACK(SequnceNumber + 1)를 함께 을 보내주게 됩니다. 클라이언트에서는 SYN에 대한 ACK를 받았기 때문에, Established가 됩니다. 예를들어, 10000으로 시작하고, 8000으로 syn을 받았다면 아래와 같은 Header가 전송되게 됩니다.

ACK : 서버에서 보내준 SYN을 잘 받았다는 의미로 ACK를 클라에서도 보내줍니다. ACK를 보내주게 되면, Sever도 자신의 SYN에 대한 ACK를 받았기 때문에 Established 상태가 됩니다.


SYN을 요청했는데, 상대방에게 SYN + ACK가 오지 않는다면, 이건 존재하지 않는 호스트일 가능성이 높습니다. 이때는 TCP는 똑같이 재전송을 진행하게 됩니다. SYN을 5회까지 다시 보내면서, 대기시간을 점차 늘려가는 방식으로 보내게 되다가, 연결 수립에 실패한 뒤 종료되게 됩니다.
SYN을 요청하면 받아들이는 입장(Server)에서는 SYN-Receive 상태가 되고, 이때 컴퓨터내의 리소스 (메모리)가 소모되게 됩니다. 이러한 특성을 이용해서, IP를 바꿔가면서 무수한 악수요청(SYN)을 날리고, 서버의 논페이지드 풀을 소모시켜서 서버를 마비시키게끔하는 공격이 SYN Flooding입니다.
프로그래머 입장에서는 3wayHandshake를 직접적으로 통제할 수 없기 때문에 SYN Flooding은 방화벽 차원에서 공격을 탐지하고 막아야합니다.
마지막 ACK가 오지않는다면 2가지 케이스가 있을 수 있습니다.
구현에 따라 달라질 수 있겠지만, ACK가 중간에 유실되는 경우 client 입장에서는 연결이 수립됬다고 판단이 됐기 때문에, 다음 통신부터는 그냥 Data를 쏴줄 확률이 높습니다. 따라서, Server를 향해 Data와 이전 ACK번호를 그대로 붙여서 쏘게 됩니다.
실제로, 와이어샤크로 웹 페이지를 하나 들어가는 과정을 쭉 보면, 3way handshake를 하고, 처음 데이터를 송신할 때 Ack번호를 보면, 3wayHandshake의 마지막 ACK랑 똑같은 값이 헤더에 들어있는걸 볼 수 있습니다. 저는 youtube를 들어갔을 때 wireshark로 봤습니다.


Client가 중간에 컴퓨터를 꺼버릴 수도 있습니다. TCP는 항상 재전송을 베이스로 한다는 점을 잊지않아야 합니다. Timer가 다되면, Sever 입장에서 SYN + ACK를 5번정도 재시도 하다가, 연결을 끊습니다.
TCP는 연결을 해제하는 과정에서 서로 종료를 위해서, 철저한,완벽한(?) 방식으로 서로 종료를 하게 됩니다. 여기서, FIN을 요청한 측을 클라, FIN을 수신한 측을 서버라고 하겠습니다. 늘 말씀드리지만, FIN은 서버에서도 요청할 수 있습니다.(양방향 통신)

서로 FIN을 교환하는게 어떻게 보면, 비효율적이라고 생각할 수도 있습니다. TCP종료 과정에 대한 이론을 세울 때, 종료를 요청한 측은 언제든지 발생할 수 있기 때문에 종료를 요청했다고 바로 연결을 끊는게 꽤 부자연스럽다고 생각이 들었나 봅니다.
따라서, 상대방이 종료를 대비할 수 있다는 신호로 FIN을 다시 주고, ACK를 수신하면 이제 서로 연결이 완전히 종료된다는 개념입니다.

네트워크에서는 언제든지 유실이 일어날 수 있습니다. 따라서, 클라가 보낸 마지막 ACK가 보내는 도중에 유실될 수있습니다. 또, Client측에서 서버가 FIN을 보냈는데 클라가 그 사이에 컴퓨터를 꺼버려서 ACK를 보내지 않을 수도 있습니다.
클라 측에서 FIN을 받은 후, ACK를 보낸 뒤 바로 종료를 하게 되면, ACK가 유실되는 상황에 대해 대비할 수 없습니다. 따라서, 일정시간 동안 소켓을 살려놓을 필요가 있는데 그 상태가 TIME-WAIT 상태입니다.
TIME-WAIT에 대한 자료를 찾아보면, TIME-WAIT에 대한 이유가 2가지가 나옵니다.
- ACK가 유실됐을 때, FIN에 대한 ACK를 재전송해주기 위해서.
- 새롭게 시작되는 연결이 최대한 중복되지 않게 하기 위해서
CASE 2가 진짜 일어날 수 있는 일인지 생각을 해봅시다. 네트워크 측에서 생각해볼 수 있고, 소켓을 만들고 관리하는 컴퓨터 입장에서 생각해볼 수 있습니다.

하지만, 이 사이에 똑같은 포트에 똑같은 주소로 똑같은 SEQ 번호로 연결된 연결이 있다면, 해당 소켓으로 이 세그먼트가 갈수는 있습니다.
결론적으로, 위 같은 케이스는 매우매우매우 드물게 일어날겁니다.
이론상 불가능한 것은 아니기 때문에.. 아마 대비하지 않았나 싶습니다.
이 경우는 꽤 심각할 수 있습니다. FIN-WAIT2상태는 상대방이 FIN을 보낸다는 가정하에 무작정 기다리는 상태입니다. 하지만, 해당 컴퓨터가 꺼졌거나 악의적인 이유로 일부로 FIN을 주지 않는다거나 등의 어떤 사유든 FIN을 주지않으면 FIN-WAIT2상태에서 무한정 대기해야하는 상황이 생길 수 있습니다.
따라서, 대부분의 구현들은 FIN-WAIT2 상태일 때, 타이머를 설정해서 FIN-WAIT2상태에서 일정시간 지나면 CLOSE 상태로 가거나, TCP 연결수립의 제한시간을 두어서 연결이 끊어지면 자연스럽게 끊어지도록 유도하는 방법을 채택하고 있습니다.
SOCKET에서 LINGER Option을 써서, 연결 해제시 정상종료를 선택하지 않고, 연결정보를 파괴하는 RST를 보내는 방법을 이용할 수 있습니다.
RST는 재설정 요청으로, 즉시 연결을 끊어버리는 동작을 수행하게 됩니다. 연결정보를 양측에서 서로 파괴하게 됩니다. RST는 다양한 사유로 발생할 수 있는데, Socket option을 건드려서 종료시 바로 RST를 쏘는 경우, 수신측에서 다양한 사유로 연결을 성립할 수 없는 경우 등이 있습니다. RST를 쏘는 경우를 한번 직접 보겠습니다.
RST를 보내는 상황을 보기 위해서 다소 억지스러운 상황을 가정해봅시다.
사실, Backlog Queue가 작아도 위 같은 현상은 일어날 확률이 매우 적습니다. 왜냐하면 정상적인 서버인 경우 BackLog Queue에 쌓이지 않도록 최대한 Accpet를 통해서 BacklogQueue를 비우도록 노력하기 때문입니다.
서버가 BackLog Queue에서 Accept로 데이터를 안뽑아갈 때, 서버는 SYN-Receive에 돌입조차 하지 않아야합니다. 따라서, SYN을 보내는 경우 서버측은 RST를 보내 현재는 연결이 불가능한 상황이라는 것을 알려주어야 합니다. 아래는 패킷 캡쳐입니다.

다음 글에서는 TCP의 Window관리와 데이터 전송에 대해서 글을 쓰겠습니다.