네트워크/개념

[NW] #11. TCP ( Transmission Control Protocol ) / IP ( Internet Protocol )

코딩하는상후니 2022. 11. 3. 19:09

 

 

 


 

* TCP(Transmission Control Protocol) / IP(Internet Protocol)

 
 
 
일전에 내부의 네트워크 통신 구조 단원에서 설명할 때 간단하게 언급했다.
 
 
TCP/IP 는 한 마디로  규약 ( 규칙 ) 이라는 말인데
TCP ( Trasmission Control Protocol ) 는 앞서본 4 계층 ( transport ) 에서 쓰는 규약이다.
IP ( Internet Protocol )3 계층 ( network ) 에서 쓰는 규약이다.
 
따라서,
TCP/IP 는 3,4 계층에서 쓰는 규약을 통틀어 말하고
데이터를 보내는 쪽에서 해당 규약, 규칙들을 이용해 송신하고
받는 쪽에서도 TCP/IP 규약을 통해서 해당 데이터를 분해해서 읽는다.
 

 

굉장히 범용성이 높고 UDP 와 마찬가지로 오늘날에 많이 쓰이고 있는 전송 계층의 프로토콜 중 하나이다.
 
 
또, TCP/IP 를 언급할 때 제일 먼저 사용되는 말들이
'연결 지향적' , '신뢰성' 의 말들이 많이 언급되는데 이것이 TCP/IP 의 대표적 특징이라 그렇다.
 
이로 인해,
TCP/IP 통신을 하면 무조건 연결되어 있으며 신뢰할 수 있는 데이터를 받을 수 있다고 착각할 수 있는데,
 
"송신자에서 정확하게 원하는 수신자에게 보내고 있는가??" 는 확실하지 않을 수 있고
내부적으로 연결되어있는 사용자가 아니라 내부 통신을 관리하는 공유기나 Gateway 로의 통신으로
실제 사용자와 연결되어있을 수 있고 패킷의 SeqNumber 를 조작해 연결될 사용자를 사칭할 수도 있다.
 
즉,
TCP/IP 의 연결 지향'논리적 연결 ( Logical Connection )' 이라는 것을 명심해야 한다.
그 반대는 '물리적 연결 ( Physical Connection )' 이 되겠다.
 
 
두 개념의 차이를 간단하게 설명하면,

 

 

 

물리적 연결 ( Physical Connection ) 은 실제로 컴퓨터 선이 서로에게 연결되어있다고 생각하면 된다.
 
논리적 연결 ( Logical Connection ) 은 실제인 것처럼 상대방과 연결되있음을 논리적으로 보장한다는 말이다.
 
예를 들어,
나의 노트북은 Wifi 에 연결되어 네트워크 통신을 하고 다른 유저와 게임을 할 수 있다.
이 때, 물리적으론 Wifi 공유기를 거쳐 전봇대의 전깃줄을 통해서 해당 게임서버와 연결되고
다른 유저도 마찬가지로 이런 과정으로 연결되어있다.
중간의 게임 서버를 통해서 나의 화면에 상대방의 캐릭터가 그려진다.
 
"위 상황을 물리적으로 상대방과 내가 연결되어있다고 말할 수 있을까 ??"
"직접적으로 내가 상대방과 연결된 것인가 ??"
이것을 '논리적 ( Logical )' 으로 연결되어있다고 말한다.
 
정말 선을 깔아서 상대방 컴퓨터와 내 컴퓨터가 직접적으로 연결된 경우를
'물리적 ( Physical )' 으로 연결되어있다고 말할 수 있겠다.
 
 
앞서 말한 착각할 수 있다는 말 '중간 과정' 이 생략될 수 있다는 것이다.
게임처럼 단지 서버와 통신하고 있는 것인데 다른 사용자의 캐릭터가 내 화면에 보이는 경우,
실제론 내가 다른 사용자와 연결되어있다고 생각했는데 연결된 곳이 End-point 가 아닌 경우,
VPN 의 사용 등 실제로 연결되었다고 생각하는 곳이 실제 그 곳이 아닐 수 있다는 말이다.
 
 
 
 
본격적으로,
TCP/IP 가 어떤 방식으로 통신을 하는지 제대로 활용할 수 있도록 자세하게 살펴보며 이해해보자.
 
 
 
 
 
 
 

* TCP Header

 

 

 
흔히 볼 수 있는 TCP Header 그림이다.
해당 링크에서, TCP 에 대한 항목들을 천천히 살펴보자. 자세히 설명되어있다.
 
해당 단락은 정의와 추가적인 내용을 살펴보는 방식으로 진행한다.
 
 
 

* Sourse/Destination Port Number ( 각 16bit )

 
여기서 눈여겨볼 것은 'Port' 라는 것이다.
 
결국,
IP Header 를 이용해 목적지의 IP 주소에 해당하는 컴퓨터에 도착해
Transport 계층에서 해당 프로세스에 해당하는 Port 번호를 찾아 전달되는 과정이다.
 
 
 
 
 

* Sequence Number ( 32bit )

 
순서 번호라는 이 데이터는 왜 필요한 걸까 ??
 
=> 순서 역전 방지를 위함이다.
 
해당 컴퓨터에서 만들어진 Frame 이 목적지에 도착해서 Packet 이 되고
Encapsulation 과정을 통해서 전송 계층에서 데이터를 확인할 때,
정보의 순서가 바뀔 수 있다.
 
왜냐하면,
네트워크 세상에서의 Router 들끼리의 통신에서 모두 다 같은 경로로 오는 것이 아니기 때문에
그 때의 상황에 맞춰 최적의 경로를 찾다보니 순서가 뒤바뀌는 일이 많이 일어난다.
 
=> 순서 번호를 이용해 TCP 연결 설정, 데이터를 추적하는데에 활용될 수 있다.
( TCP 의 ACK = Sequence Number + 1 , 흐름제어의 데이터 추적 )
 
 
 
Sequence Number 의 초기값을 ISN ( Initial Sequence Number ) 라고 한다.
 
초기 TCP 연결 설정을 위한 값이며 난수 발생기로 초기 값을 생성한다.
각각 연결되야할 두 지점마다 다른 ISN 이 사용된다.
 
 
 
 
 
 
 

* Acknowledgement Number ( ACK )

 

 

 

보통, Sequence Number 값을 기준으로 +1 을 한 값을 ACK 라고 생각할 수 있다.
ACK 는 해당 SequenceNumber 를 가진 데이터를 잘 수신했다는 의미가 된다.
 
 
 
 
 
 

* Flag bits ( URG, ACK, PSH, RST, SYN, FIN )

 
 
각각 간단하게 설명하자면,
 
URG ( Urgent )  :  긴급 데이터, ( 우선순위 높음, 순서에 상관없이 먼저 송신 )
ACK ( Acknowledgement )  :  수신 응답
PSH ( Push )  :  응용프로그램으로 데이터를 바로 전달 ( 메모리를 거치지 않음, Telnet->'q' )
 
( 아래 3개의 Flag 는 TCP 연결설정 & 연결종료에 주도적으로 사용됨 )
RST ( Reset )  :  강제 연결 초기화, 연결 상의 문제 발생 시 RST 전송
 
SYN ( Synchronization ) :  동기화 연결 요청
연결 요청  :  SYN = 1, ACK = 0 ( SYN 세그먼트 )
연결 허락  :  SYN = 1, ACK = 1 ( SYN + ACK 세그먼트 )
연결 설정  :  ACK = 1 ( ACK 세그먼트 )
 
FIN ( Finish )  :  연결 종료 요청
종료요청  :  FIN = 1  (  FIN 세그먼트 )
종료응답  :  FIN = 1, ACK = 1 ( FIN+ACK 세그먼트 )
 
 
 
 
 
 

* Window Size ( 윈도우 크기 )

 
흐름 제어를 위해 사용하는 16bit 필드
수신자는 자신의 수신 버퍼 여유 용량 크기를 지속적으로 알려준다.
 
이 때, 용량이 부족하다면 ??
'Wait', 대기 상태가 걸린다. 즉, 지연이 걸린다는 말이다. TCP 속도 저하의 원인이 될 수 있다.
 
이를 해결하기 위해선
해당 프로세스에서 전송계층에 있는 수신 버퍼에 있는 데이터를 빨리 읽어들여야한다.
즉,
[ 프로세스에서 버퍼를 읽는 속도 > 송신자에게 데이터를 받는 속도 ] 이어야 한다.
 
위 쪽 참고된 유튜브 동영상에도 언급되지만,
TCP/IP 통신에서 문제가 생겼을 때, 네트워크 통신 외부에서 문제를 찾기 이전에
수신자에서 받은 데이터를 잘 읽고 있는지
송신자에서 데이터를 잘 보내고 있는지 파악하는 것이 중요하다.
 
 
 
 
 
 
 

* TCP Header 옵션 구조

 

 

 

 
앞서 본 TCP Header 의 추가적인 부분이다.
 
통상적으로 IP Header + TCP Header 를 합쳐서 40byte 라고 칭하고
MSS 가 1460 byte 로 총 MTU = 1500byte 로 생각되어진다.
 
만약 여기서 TCP Header 에 추가적인 옵션이 붙는다면
그에 따라 보내야할 데이터의 양도 줄어들 것이다.
 
일반적으로 MTU = 1500 byte 는 유지된다.
이 때, Ethernet 을 사용한다면 Frame Header 가 붙으면서 데이터가 약간 더 커진다.
 
 
 
링크된 사이트를 참고하면, 옵션 종류 필드를 통해 옵션을 설정할 수 있다.
대표적으로
윈도우 스케일 ( Window Scale factor ), 타임스태프 ( Timestamp ) , 사용자 타임아웃 (user Timeout ) 등이 존재한다.
 
 
 
 
 
 

* TCP 3-way Handshaking

 

 

 

TCP 통신에서 빼놓을 수 없는 과정이다.
 
3-way Handshaking 이란
TCP 통신을 하기 전 Client <-> Server 간 서로의 상태를 교환하고 연결하는 작업을 말한다.
 
능동 ( Active ), 수동 ( Passive ) 란 개념이 나오는데,
능동 개방 ( Active Open ) 은 주로 SYN 세그먼트를 보내며 연결 설정하는 경우로써,
주로 클라이언트 쪽에서 사용하는 형식이다. 서버 측에서도 사용할 수 있다.
즉, SYN 세그먼트를 보내는 측을 말한다.
 
반대로 수동 개방 ( Passive Open ) 은 주로 서버 쪽에서
SYN 세그먼트를 수신하며 연결 설정하는 경우로써 LISTEN 상태를 거친다.
즉, SYN 세그먼트를 받는 측을 말한다.
 
Server, Client 모두 일종의 상태 단계를 거치면서
주고 받는 패킷에 이전에 배운 TCP Header 의 Flag bits 를 사용해 서로의 상태를 확인하며
TCP 연결의 준비 완료 상태를 만든다.
 
모든 과정이 완료되었다면 Server, Client 모두 ESTABLISHED 상태가 된다.
또한 이 과정에서 ISN ( 랜덤한 초기 순서 번호, Initial Sequence Number ) 을 생성한다.
 
 
 
 
 
 
 

* TCP 4-way Handshaking

 
 

 

4-way Handshaking 데이터 전송을 완료한 후, 정상 종료될 때의 상황이다.
 
대략적인 설명으로는,
Client 가 FIN 세그먼트를 보내고 Server 는 받은 직후 ACK 세그먼트를 보내고 이후 작업들을 마무리한 후,
FIN+ACK Flag 가 담긴 패킷을 Client 에 보내고 마지막으로 Client 는 ACK 를 송신하며 TCP 를 마무리 짓는다.
 
마찬가지로,
여기서도 능동 ( Active ), 수동 ( Passive ) 란 개념이 나오는데 간단하게 정리해서 아래와 같다.
 
능동 종료 ( Active Closer )  :  연결을 끊기 위해서 FIN 세그먼트를 보내는 측.
수동 종료 ( Passive Closer )  :  FIN 세그먼트를 받는 측.
 
 
 
 
 
지금까지의 과정에서 알 수 있는 사실 중 하나는
 
TCP 의 전체적인 동작 방식은 수신자나 송신자이건 간에 데이터를 받았으면
"데이터를 받아서 자신이 알고 있다" 라는 사실을 상대방에게 알려주어야 한다.
왜냐하면, 논리적 연결을 지향하기 때문이다.
 
 
송신자 측에서 수신측이 지금 메세지를 받았는지 아닌지 수신자의 상태를 판별하기를 원한다.
상태를 판별해 자신의 다음 과정을 수행하기 위해서이기 때문이다.
 
 
TCP 과정은 항상 논리적 연결을 위해 상대방의 정확한 상태를 인지하도록 노력한다.
 
 
 
 
 
 
 

* TCP 연결 종료 구분

 
앞서 4-way Handshaking 은 정상 종료 되었을 때에 대한 상황의 과정을 말한 것이라고 했다.
 
연결 부분과는 다르게 종료 상황은 여러 상황으로 나뉠 수 있다.
왜냐하면, 여러 이유로 인해 한 쪽에서 일방적으로 연결을 끊어버릴 수 있기 때문이다.
 
 
1. 정상 종료
 
=> 4-way Handshaking 을 통해 정상적으로 종료된다.
 
 
2. 반 종료 ( Half Close )
 
=> 한쪽이 일방적으로 종료함으로써 한쪽 연결은 열린채로 종료되는 것이다.
 
 
3. 동시 종료 ( Simultaneous Close )
 
=> 동시에 양측에서 FIN 세그먼트를 보내는 경우.
 
 
4. 강제 종료 ( RESET )
 
=> TCP Reset 요구 ( RESET 세그먼트 ) 기능.
 
 
 
 
3-way, 4-way Handshaking 모두 송,수신측에서의 상태마다 행동 양식이 다르다.
결과적으로 상태 변화에 따라 관리된다는 것을 알 수 있다.
 
앞서 살펴본 Handshaking 과정을 상기하면서 상태 개념을 파악해보자.
 
 
 
 
 
 
 

* TCP 상태

 
 

 

 

 
앞서 설명한 Active ( 능동 ), Passive ( 수동 ) 표현이 다시 등장한다.
 
Open ( 개방 ) 에서의 Active 는 SYN 세그먼트를 보내는 측, 수신 측은 Passive.
Close ( 종료 ) 에서의 Active 는 FIN 세그먼트를 보내는 측, 수신 측은 Passive.
 
알아두어야 할 것은 각각의 상황이 서버, 클라이언트 모두 발생할 수 있는 점이다.
서버간의 연결, Server to Server 에서도 발생할 수 있다.
 
 
 
또, 한 가지 유심히 보아야할 것은
최종적으로 Close Active 가 FIN, ACK 세그먼트를 수신한 이후 Wait 시간을 갖는다는 점이다.
해당 상태를 Time-Wait 상태라고 말하고
이유는 서버가 FIN 세그먼트를 보내기 전의 세그먼트들이 아직 도착하지 않았을 경우를 대비하기 때문이다.
일정 시간 동안 기다린 후 종료된다.
 
 
 
 
 
 
 

* Time-Wait 상태

 
 
 
위에서 말했듯이,
Time-Wait 는 단순히 클라이언트의 상황만이 아니라 서버에서도 충분히 일어날 수 있다.
서버의 상황에서 다른 서버들과도 연결될 수 있다. 이 상황을 Server to Server 라고 한다.
 
 
이 상태가 주목받는 이유는 Time-Wait 상태가 꽤 길기 때문이다.
 
참고된 링크에 따르면 ( 리눅스 Server 기준 )
RFC793 = 2MSL, CentOs = 60초로 지정되어있다고 한다. 더불어, 이 값은 수정할 수 없다.
 
 
 
Time-Wait 상태가 이렇게 길게 설정된 이유에 대해 2가지가 언급된다.

 

 

 

 

 

1. 지연 패킷 발생
 
새로운 연결 도중, 이미 Close 된 연결의 데이터가 지연되서 들어올 수 있다.
물론 일반적인 상황에서는 Seq 번호가 전혀 다르기 때문에 상관없다.
하지만 이 때, 공교롭게도 Seq 번호가 겹친다면 데이터 처리에 문제가 발생한다.
 

 

 

 

 

 
 
2. ACK 유실로 LAST_ACK 상태 지속 시 문제 발생
 
ACK 유실로 Passive Close 측에서 LAST_ACK 상태를 계속 가지게 된다.
또, Timeout 으로 설정된 시간동안 ACK 를 받지 못하면 자동적으로 Close 된다.
문제는 LAST_ACK 상태를 가지는 동안이다.
 
만약 Time-wait 가 짧다면,  Time-wait 구간이 끝난 후 새로운 접속이 시도된다.
새로운 연결을 위해 새로운 SYN 세그먼트를 보낼 수 있다.
 
이 때, Passive Close 측은 아직도 LAST_ACK 상태이기 때문에 RST Flag 를 전송해 연결을 막는다.
RST 를 보내는 것은 논리적으론 아직 기존의 연결이 끊기지 않았기 때문이다.
때문에 연결을 시도하는 클라이언트는 RST 를 받고 접속 오류를 낸다.
RST 는 비정상 종료 시 보내는 패킷이다.
 
 
이 상황을 예방하기 위해서
Active Close 측이 Time-wait 상태에 되도록 길게 머물러야 한다.
Time-Wait 상태를 지속함으로써 연결은 Close 되지 않고
Close 되지 않는 동안 새로운 연결은 들어오지 않고 서버는 새로운 SYN 세그먼트를 받지 않는다.
 
 
 
이것은 결국
Time-Wait 상태가 LAST_ACK 상태보다 더 빨리 끝날 때 발생할 수 있는 문제라고 볼 수 있다.
 
한가지 예방 방법은 Timestamp 를 사용하는 것이다. ( 아래에 설명된다. )
 
위 상황들이 드문 경우이지만, TCP 는 일말의 여지도 남기지 않으려고 한다.
 
 
 
 
 
 
한 가지 알아두어야할 과정은,
 
서버는 로컬 포트를 사용하지 않는다.
서버가 할당하는 것은 소켓 ( Socket ) 이며 포트는 최초 bind 시 하나만 사용한다.
클라이언트가 로컬 포트를 할당 후, 소켓을 연다. 그 다음 해당 소켓과 서버의 소켓이 bind 한다.
 
소켓이 생성되면 각각의 고유번호 부여되는데 이것을 '디스크립터' 라고 말하며
리눅스에선 파일에 설정된 디스크립터의 수만큼 소켓을 생성할 수 있다. ( 참고링크에선 26만개. )
 
 
만약 서버가 다른 서버의 클라이언트 역할로 접속하면 ( 서버 to 서버 )
클라이언트 입장의 서버는 자신에게 연결된 클라이언트들의 포트를 할당 후 소켓을 열고
클라이언트의 데이터를 받기 위해 서버도 소켓을 열고 서로 바인딩한다.
 
 
 
 
 
 
 

* Time-Wait Reuse ( 재사용 )

 
 
만약 Time-Wait 시간이 1분이라면,
클라이언트 측인 서버에서 요청 가능한 수는 500 RPS * Time-Wait 시간 이다.
지속적으로 1초에 500번 요청이 가능하다는 말이다.
( 참고 링크에선 리눅스의 포털 범위가 3만개로 설정되어있다고 나온다. )
( 이 경우는 Server to Server 로 대용량 접속이 발생할 경우를 예시한다. )
 
 
위에서 말한 클라이언트 요청 가능 횟수를 초과하는 상태를 대비하여
아래의 옵션을 사용해 Time-Wait 상태를 재사용한다.

 

SO_REUSEADDR          ( Passive close )
net.ipv4.tcp_tw_reuse ( Active close )

 

 

Active, Passaive close 각각 Time-wait 를 재사용하는 키워드가 다르다.
보통, Active close ( 클라 ) 쪽의 Time-Wait 의 재사용을 필요로하는 경우가 많다.
 
 
Time-wait 의 재사용을 위해서는 서버/클라 양쪽 모두 Timestamp 옵션이 On 되어있어야 한다.
 
 
 
Time-Wait 재사용 방법에는 reuse 방법 말고도 Recycle, linger 옵션 등이 존재한다.
아래 참고한 사이트들에서 자세히 설명되어있다.
 
 
 
 
 
 
 
 
 

* 타임스탬프 ( timestamps )

 

 

 

time-wait 의 재사용을 위해서 해당 옵션이 서버/클라 모두 On 되어있어야 한다.
( 리눅스 커널 기준 Default On Value )
 
요약하자면,
'Timestamp'단방향으로 증가하는 값이며 RTT ( 왕복시간 ) 를 측정하기 위한 주된 요소 이다.
각각 내부적으로 System Clock 을 사용하고 해당 시간을 송신자에서 도장찍듯이 보낸다.
각각의 프로세스마다 다른 값일 수 있다.
 
 
위 링크와 그림을 참고해 설명하자면,
 
먼저, TCP 통신을 위해 처음 SYN 세그먼트를 보낼 때 Timestamp를 찍고 보낸다.
이 때, echo reply 항목이 있는데 짐작할 수 있듯이 응답된 패킷의 Timestamp 데이터를 쓰는 곳이다.

각각을 TCP 패킷에서는 "TSVal", "TSecr" 라고 표현한다.

 
당연히 처음 보낼 때는 응답된 패킷이 없기 때문에 '0' 으로 설정된다.
 
반대로 수신측에서 송신자의 패킷을 받을 때,
수신자는 받은 패킷의 Timestamp 데이터를 echo reply 항목에 적고 자신의 Timestamp 데이터를 기록해 보낸다.
이 후 송신자가 수신자의 응답을 받았을 때, Timestamp 를 비교해서 해당 패킷이 응답 패킷인지 판단한다.
 
 
 
여기서 중요한 것은
송신자는 자신이 보낸 Packet 정보를 가지고 있다는 것이다.
 
보낸 Packet 의 Timestamp Value 와 받은 응답 Packet 의 echo reply 값을 비교해 차이를 낸 값이
RTT ( Round Trip Time ), 왕복시간 이 되겠다.
( 이 시간을 근거로 RTO ( Retransmission Time-Out ) 를 설정할 수 있다. )
 
 
 
Timestamp 를 사용함으로써 가지는 장점
Sequence Number 가 중복된 혹은 순서상의 문제가 있는 패킷들을 판별할 수 있다는 점 이다.
순서 번호로만 패킷의 순서를 판별함으로써 생기는 문제들을 방지할 수 있다.
 
예를 들어,
송신한 패킷의 Sequence Number 는 100 이고 101 번인 Sequence Number 를 기다리는 상황에서
순간 엄청난 패킷들이 들어오게 된다면 ( Sequence Number bit 범위를 초과한 )
Sequence Number 가 같은 동일한 패킷이 들어올 수 있다.
이 때 둘 중 하나는 버려지게 된다.
또한, Sequence Number 가 낮은 새로 들어오게 되는 패킷들도 마찬가지로 버려지게 된다.
순서가 맞지 않기 때문이다.
 
 
Timestamp 로 판별할 수 있는 데이터가 하나가 더 생김으로써 새로운 패킷을 구별 가능하다.
( 새로운 패킷 => echo reply = '0' 으로 설정됨으로 )
때문에 Seq 번호가 낮게 들어올 땐 새로운 패킷을 구별할 수 있고
Seq 번호가 중복될 때에는 받아야할 패킷과 새로운 패킷을 구별할 수 있다.
 
 
 

 

 

 

 
 

* Time-Wait reuse 옵션을 위해 Timestamp 는 왜 필요한가 ??

 
 
reuse 옵션으로 Time-Wait 상태에 있는 연결들을 재사용할 수 있다.
 
이 때, Timestamp 를 이용해 재사용 여부를 결정한다.
새로운 타임스탬프가 가장 최근 연결에 사용된 타임스탬프보다 확실히 큰 경우 결정된다.
 
간략히 설명하자면,
Active 측의 연결되지 않은 다른 프로세스가 서버 측에 연결을 하고자할 때
모든 포트가 Time-Wait 상태에 있어서 재사용을 해야하는 경우,
현재 내보내야할 새로운 Timestamp 값을 Active 측은 저장하고 있고
각 Time-Wait 상태에 있는 연결들의 아주 최근에 받은 타임스탬프 값을 알고 있다.
 
이 때,
보내야할 패킷에 찍어서 보낼 Timestamp 값
Time-Wait 상태에 있는 연결들 최근에 받은 타임스탬프 값의 값이
 
"차이가 확실하게 크다고 판단할 때" Time-Wait 상태에 있는 연결을 재사용한다.
( 최대한 충분한 시간을 확보하기 위해서 )
 
 
또한,
Timestamp 로 Time-Wait 이 짧을 때 발생할 수 있는 문제를 해결할 수 있다.
앞전에 언급한 Time-Wait 가 길게 설정된 2가지 이유와 Timestamp 로의 해결을 설명한다.
 
 
1. 지연 패킷 발생
 
=> 기존의 Time-Wait 상태에 있는 포트를 재사용하여 새롭게 TCP 연결이 되었음을 가정할 때,
 
이전의 연결에 해당하는 지연 패킷이 도착하면
 
지연된 패킷의 Timestamp 는 현재 송신한 패킷의 값과는 작을 수 밖에 없기 때문에 버려진다.
 
작을 수밖에 없는 이유는 Timestamp 의 값은 단방향으로 증가하는 값이고
 
이전의 연결을 재사용했기 때문에 Timestamp 가 기존 값이 계승되기 때문이다.
 
또한, 위 상황은 Sequence Number 가 중복된 상황이란 점을 알아야한다.
 
ISN 은 3-way handshaking 시 설정되기 때문에 애초에 순서 번호가 다를 가능성이 높다.
 
 
 
 
2. ACK 유실로 LAST_ACK 상태 지속 시 문제 발생
 
=> Passive 쪽에서 새로운 SYN 세그먼트를 무시한다.
 
새로운 연결은 응답을 받지 않았으므로 echo reply = '0' 이다.
 
 
그 순간 동안, Passive 는 ACK 를 받지 못해 FIN 를 다시 재전송하고
 
FIN 을 받은 Active 는 RST 를 Passive 에 보내면서 Passive 의 LAST_ACK 상태를 빠져나와 Close 상태가 된다.
 
( LAST_ACK 상태에 RST 를 받으면 상태를 빠져나온다는 사실을 알 수 있다. )
 
 
 
서버가 Close 상태가 될 때까지의 시간동안만 클라이언트는 접속하지 못하게 되고
 
이 후, 정상적으로 서버와 연결될 것이다.
 
결과적으로 약간의 딜레이만 발생한다.
 
 
 
2번 상황을 해결하기 위해,
 
Active 측뿐만 아니라 Passive 측에도 Timestamp 옵션이 켜져있어야하는 이유이다.
 
 
 
 
 
 
 
 
 
 
 

* 정상적이지 않은 TCP 연결 종료

 
정상적인 종료 시 발생하는 4-way Handshaking 과정과 동시에
TCP 연결 종료 구분 단락에서 살펴보았던 '반 종료 ( Half Close ) 상황' 도 빈번하다.
 
즉,
한 쪽이 Established 상태를 유지하면서 다른 한쪽의 연결이 끊어지는 것이다.
 
그로 인해,
Established 상태를 유지하는 측은 상대방이 아직 연결되어있고 패킷이 손실된다고 알고 있다.
상대의 ACK 를 기다리다 Timeout 되어 재전송한다.
 
상대방의 연결은 끊어졌는데 패킷을 받지 못하는 상황으로 오해해
계속해서 응답을 기다리거나 데이터를 재전송하게 되면서 불필요하게 소켓을 낭비하게 된다.
 
또한,
연결과 종료가 빈번해지면서 위에서 살펴본 Time-Wait 상태가 많아지는 현상도 생긴다.
 
 
 
이러한 상황을 미리 예방할 수 있는 방법도 존재한다.
 
 
=>  지속적인 연결 확인 패킷 ( Heart Beat )
'Keep-Alive' 라고도 한다.
 
 
설정된 값을 통해서 일정 기간마다 세션에 연결된 클라이언트가 살아있는지
아주 작은 TCP Keep Alive 패킷을 전송한다.
 
Socket 프로그래밍에서 SO_KEEPALIVE 옵션을 통해서 쉽게 사용할 수 있다.
( Keep-Alive 시간 간격  /  응답 대기 시간 ) 을 설정할 수 있다.
 
 
 
 
 

* 랜선 뽑기

 
게임을 하면서 한 번쯤 들어봤을법한 단어이다.
필자가 어렸을 때 많이 들어봤던 이야기인 것 같다.
 
이 방법은 굉장히 재미있다.
물리적 연결은 끊어져도 논리적으로는 연결되어있는 상황을 악용한 방법 이다.
 
 
이것을 악용해 게임 내에서 부당한 이득을 취하는 것은 불법인걸 명심하자.

 

 

 

그림을 기준으로 대략적으로 설명하자면,
Player2 는 고가의 아이템을 떨어뜨리고 다시 픽업한다.
픽업할 때, LAN 케이블을 분리함으로써
논리적으론 연결되어있지만 실제 데이터는 Server 가 받지 못해 동기화하지 못했고
그로 인해 Player1 이 동기화되지 않은 아이템을 먹는다.
이 후, Player2 가 아이템을 픽업한 사실과 Player1 이 아이템을 픽업한 사실이 동시에 동기화되면서
아이템이 복사가 되는 현상이 벌어졌다.
 
"이 모든 작업들을 Keep-Alive 시간만큼 해야한다."

 

 
 
여기서 얻은 교훈은 모든 네트워크 통신에 있어서
"L1, L2 수준에서의 '물리적 충격' 에 어떻게 대응할 것인가 ?? ( 성능을 잃지 않으면서도 )"
이것은 깊게 생각해볼 주제이다.
 
당장 위 예시에서 생각해볼 수 있는 해결법은
순차적으로 동기화를 해야할 필요성이 보인다.
 
정확히는 모르겠지만 무언가 로직이 잘못되서
플레이어1 이 검을 주운 상황과 플레이어2 가 검을 주운 상황이 동시에 실행된 것처럼 보인다.
 
해당 동기화 작업들을 큐를 이용해 순차적 처리한다.
결국 플레이어1, 2 누가 검을 줍던간에
호출이 먼저 진행된 쪽의 아이템 작업이 끝난 후에 다른 한쪽을 진행해야할 것이다.
 
이렇게 동기화를 위해 순차적으로 처리하게 되면
접속자가 많아질 때 트래픽도 같이 증가하게 되면서 당연히 성능은 떨어질 수 밖에 없는데
하나의 서버에 몰리지 않게 여러 개의 서버를 두는 방법을 고려할 수 있겠다.
 
다시 한번 얘기하지만,
위 예시로 든 버그가 아니라도 치명적인 버그를 발견하면 악용하지말고 개발사에 문의해서 알려주자.
 
 
 
 
 

* 결론

한마디로 정리하자면,
TCP/IP'논리적인 연결을 우선시 하는 프로토콜' 이 되겠다.
 
다음 단원에는 TCP/IP 의 혼잡제어에 대해서 알아보겠다.
 
 
 
TCP 에 대해 이해하고 정리하다보니 글이 길어진 것 같다.
그만큼 복잡하고 어렵다는 말이지만 알고 있어야하는 개념들을 추려봤다.
해당 단원에서 살펴보지 못한 내용들이 존재하면 이후 짧게 정리해 올리려고한다.
 
 
 
p.s
제가 직접 서버를 열고 TCP 통신을 한 것이 아니라 많은 글들을 참고해서
중요한 것이라고 생각한 것들을 이해하고 정리한 것입니다.
따라서, 틀린 내용이 분명히 존재할 수 있을테니 지적해주시길 바랍니다 :)

 

 

 

 

 

 


 

참고 자료