C++/Socket 통신

TCP CLOSE_WAIT, FIN_WAIT_2 에러 - C++

초심을 찾자 2024. 10. 19. 16:36
SMALL

포스팅 계기

 최근에 같이 개발을 하고 있는 타 업체 개발자가 client가 비정상 종료되었을 때 프로그램을 다시 On 하면 TCP Connection이 되지 않는다고 도움을 요청을 받았다. 그때 발생한 TCP Error가 Server 측에서는 FIN_WIAT_2가 발생하였고, Client 측에서는 FIN_WAIT_2가 발생했다. 그래서 그때 왜 그런 문제가 발생하는지 또한, 어떻게 코드적으로 해결할 수 있는지 이야기하고자 한다. ※ 아래는 Window 환경에서 내 PC에서 TCP Connection 상태를 볼 수 있는 명령어다.

  • CMD 명령어 : netstat -nao | find "TCP"


TCP 통신이란?

 TCP는 Server와 Client 간에 데이터를 주고받을 때 신뢰성을 기반으로 전송하기 위한 프로토콜이다. TCP는 데이터를 전송하기 전에 Server와 Client가 연결이 되어야 하는 연결지향 프로토콜이기도 하다. 다른 통신을 공부하다 보면 알겠지만, 신뢰성이 있는 통신이라 하면 속도가 다른 통신에 비해 느리다. 그러면 어떨 때 TCP를 사용하고, UDP를 사용할까? ※ UDP는 추후에 따로 포스팅할 예정이다. 

TCP는 정확하게 데이터를 전달해야 할 경우 사용하고, UDP는 실시간적으로 빠르게 데이터 공유가 필요할 때 사용한다. 그러면 여기서 얻을 수 있는 힌트는 TCP는 비주기적인 데이터에 적합하고, UDP는 주기적인 데이터에 적합하다는 것이다.

이 정도면 기본적인 TCP의 개념과 언제 사용되는지는 파악이 되었을 거라 생각이 든다. 그다음에 알아야 할 것이 세그먼트이다. TCP 세그먼트는 Header와 Data로 구분되어 있다. 

  • Source Port: 데이터를 발송하는 애플리케이션의 포트 번호
  • Destination Port: 데이터를 수신하는 애플리케이션의 포트 번호
  • Sequence Number(SYN): TCP 통신 과정에서 데이터를 일정 단위 분할된 데이터의 순서
  • Acknowledgment Number(ACK): 데이터를 수신하는 애플리케이션 입장에서, 다음으로 받고 싶은 TCP 세그먼트의 Sequence Number

통신 과정에서 분할된 데이터가 순서대로 전달되지 않거나 각자 다른 경로로 전달될 수 있다. 위와 같은 TCP 헤더가 있어서 수신 애플리케이션에서 데이터를 안전하고, 정확하게 원 데이터를 만들 수 있다.

출처 : RFC793

 

 TCP 통신에서 가장 중요한 것은 3 Way HandShake다. 직역하면 3번의 주고받음이다. 3단계를 설명하면 아래와 같다.

  • SYN: Client가 Server에게 SYN플래그가 설정된 메시지를 보낸다. 이때 SYN은 Client의 임의 시퀀스 번호 A를 같이 전달한다.
  • SYN-ACK: Server는 Client의 SYN에 응답하여 SYN-ACK 메시지를 보낸다. Server는 자신만의 새로운 시퀀스 번호 B를 포함한 SYN을 전송하고, Client의 시퀀스 번호 A에 1을 더한 ACK(acknowledgment)를 함께 보낸다. ( 아래 그림에서 100 & 1011 )
  • ACK: Client는 Server로부터 받은 SYN에 대해 ACK 메시지를 전송한다. 이때 ACK는 서버의 시퀀스 번호 B에 1을 더한 값(B+1)을 포함한다.  ( 아래 그림에서 300 & 301 )

출처 : RFC793


에러 설명

 위의 TCP의 3 Way Handshake를 이해하면 CLOSE_WAIT과 FIN_WAIT_2의 문구만 보고 문제점이 무엇인지 파악이 될 거라 생각이 든다. RFC 793에 가면 아래와 같이 문제점을 정확하게 알려준다.

  • CLOSE_WAIT : Client가 연결 종료 요청을 기다리고 있는 상황
  • FIN_WAIT_2 : Server에서 연결 종료 요청을 기다리고 있는 상황

 결론은 Server와 Client가 종료가 정상적으로 되지 않은 상태라는 것이다. 보통 정상적으로 Socket이 Close 될 때에는 발생하지 않고, 비정상적으로 SW 종료되면서 발생하는 문제다.

 

 


해결 방법

Socket을 생성할 때 여러 옵션을 줄 수 있는데, 그 중 하나인 Keep_Alive 옵션을 사용하는 것이다. Keep_Alive란 특정 시간주기로 서로의 통신이 살아 있는지 확인하는 옵션이며, 응답이 없을 경우 연결을 끊어버린다. 이렇게 되면 서로 비정상 종료 문제가 발생해도 양쪽 모두 서로 Socket을 정상 종료할 수 있다. 다만 여기서 유의해야 할 점은 listen socket에 옵션을 주는 것이 아니라 connection 후 리턴 받은 소켓에 옵션을 적용해야 한다. 아래 예시 코드를 활용하여 문제를 해결에 도움이 되기를 바란다.

const INT nRetVal = connect(socket, reinterpret_cast<SOCKADDR*>(&stServerAddr), sizeof(stServerAddr)); // Socket Connection 시점

DWORD dwRet = 0;
tcp_keepalive stKeepAlive;
stKeepAlive.onoff = 1;
stKeepAlive.keepalivetime = 10000;      // Keep_alive 보내는 간격
stKeepAlive.keepaliveinterval = 1000;   // keepalive 신호를 보내고 응답이 없으면 1초 간격으로 보내기\
// 주기적으로 연결 상태 확인
if (WSAIoctl(socket, SIO_KEEPALIVE_VALS, &stKeepAlive, sizeof(stKeepAlive), nullptr, 0, &dwRet, nullptr, nullptr) == SOCKET_ERROR)
{
	nNetworkState = NET_SOCK_ERR;
	return nNetworkState;
}

 

참고 링크 : https://www.rfc-editor.org/info/rfc793

 

Information on RFC 793 » RFC Editor

 

www.rfc-editor.org