소켓 연결 종료 과정과 shutdown, closesocket

소켓 연결 종료 과정과 shutdown, closesocket

2023년 7월 19일
network, tcp

소켓연결종료과정 #

기본적으로 4way handshake 과정으로 소켓이 종료된다.

4wayhs

  1. 종료하고자 하는 측(A:client)에서 FIN 패킷을 전달한다. (A의 close 함수 호출)
  2. 반대편(B:server)에서 FIN 수신에 대한 ACK 패킷을 보낸다.
  3. B 는 남은 데이터가 있으면 보내고 A에게 FIN 패킷을 보낸다. (B의 send함수호출, close함수 호출)
  4. A는 대기하다가 FIN을 수신하면 ACK 패킷을 보낸다.

TCP 패킷 플래그 설명 #

RST : 즉시 세션을 끊고 비정상 종료 FIN : 연결 종료시 사용되는 플래그 ACK : 데이터를 받았다는 의미.


연결종료 함수 #

참고URL #

shutdown : https://www.sysnet.pe.kr/2/0/12533
closesocket : https://www.sysnet.pe.kr/2/0/12534

shutdown #

https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-shutdown?WT.mc_id=DT-MVP-4038148

1int shutdown(
2  [in] SOCKET s,
3  [in] int    how
4);

how에는 SD_RECEIVE(수신비활성화), SD_SEND(송신비활성화), SD_BOTH(둘다비활성화) 값이 들어갈 수 있다.

  • SD_RECEIVE : recv 함수 호출을 허용하지 않으며, 수신대기중인 데이터가 있거나 shutdown 이후 도착하면 연결이 재설정된다.
  • SD_SEND : send 함수 호출을 허용하지 않으며, 모든 데이터가 전송된 후 데이터에 대한 ACK 를 받고나서 FIN을 전송한다.

소켓을 닫는게 아니라 송수신을 비활성화 하는것이고, closesocket을 추가로 호출해줘야 소켓이 반환된다.


closesocket #

1int closesocket(
2  [in] SOCKET s
3);

소켓 연결을 닫는(shutting down a socket connection) 작업 #

기본적으로는 shutdown(SD_BOTH) 동작을 수행하지만, 별도의 옵션을 통해 다른 방식으로 동작할 수 있다.

소켓을 닫는(closing a socket descriptor) 작업 #

소켓 자원을 해제해 더이상 핸들을 재사용하지 못하도록 막는다.


연결종료 경우의수 #

테스트코드 : https://github.com/parktest0325/tcp_shutdown_testcode

0. 강제종료 #

closesocket이 아니라 닫기 버튼으로 강제종료한 경우엔 RST패킷이 전달되며 recv에서 -1 리턴됨 client강제종료


1. 수신버퍼에 데이터가 남아있을때 shutdown(SD_RECEIVE) 호출 #

  • 테스트코드
 1/****** Server ******/
 2FD_SET fds;
 3FD_ZERO(&fds);
 4
 5clntSocket = accept(ListenSocket, NULL, NULL);
 6FD_SET(clntSocket, &fds);
 7FD_SET copyFds = fds;
 8
 9send(clntSocket, "hello", 5, 0);     // 5바이트 송신 (클라이언트 수신버퍼에 1byte를 남기기위해)
10// send(clntSocket, "hello", 3, 0);    // 3바이트 송신 (클라이언트 버퍼를 채우지 않기 위해)
11
12select(0, &copyFds, nullptr, nullptr, nullptr);     // select 함수로 감시
13printf("return select\n");
14
15int recvSize = recv(clntSocket, buffer, sizeof(buffer) - 1, 0);
16printf("recvSize : %d\n", recvSize);
17
18closesocket(clntSocket);
19FD_CLR(clntSocket, &fds);
20
21/****** Client ******/
22iResult = recv(sockfd, buffer, 4, 0);     // 서버는 5바이트를 송신했지만, 클라이언트는 4바이트만 수신
23std::cout << iResult << " bytes" << std::endl;
24
25printf("Press any key to call shutdown");
26getchar();
27
28// 아직 1바이트가 수신 버퍼에 있는 상태에서 shutdown 호출
29shutdown(sockfd, SD_RECEIVE);
30
31printf("Press any key to call closesocket");
32getchar();
33
34closesocket(sockfd);
  • RST 패킷을 보내 강제로 TCP 연결해제. (serverPort=4321, clientPort=61375) 수신버퍼채워져있을때

  • select 함수에서 소켓 입력 감지 select 함수에서 감시하고 있었다면, RST 패킷을 받으면서 select 함수가 리턴된다.

  • server의 recv는 -1 반환
    비정상 종료라서 recv에서 -1 반환

  • client의 recv는 -1 반환
    금지했는데 recv를 호출하려 했기 때문에 -1이 반환된다.


2. 수신버퍼에 데이터가 없을때 shutdown(SD_RECEIVE) 호출 #

  • 테스트코드 위의 테스트코드의 서버 send 부분에서 3byte만 보내도록 변경

  • 아무일도 일어나지 않음 말 그대로 아무일도 일어나지 않는다. 패킷 자체를 전송하지 않기 때문에 서버측에서는 소켓에 입력도 감지되지않아서 select 조차 호출되지 않는다. 이후 closesocket 함수를 호출해서 연결을 종료하면 4way handshake 과정이 진행되어 정상 종료되기 때문에 select 함수도 감지되고, recv 함수도 0이 리턴된다.

수신버퍼가비워져있을때

  • client의 recv는 -1 반환 금지했는데 recv를 호출하려 했기 때문에 -1이 반환된다.

3. 송신버퍼에 데이터가 있을때 shutdown(SD_RECEIVE) 호출 #

shutdown(SD_RECEIVE)는 수신버퍼만 생각하기 때문에 송신버퍼는 동작에 아무런 영향도 주지 않는다. 송신버퍼를 비우고나서 수신버퍼에 따라 동작한다.


4. 송신버퍼에 데이터가 있을때 shutdown(SD_SEND) 호출 #

  • 테스트코드
 1/****** Server ******/
 2const int DEFAULT_BUFLEN = 1000;
 3char recvbuf[DEFAULT_BUFLEN];
 4clntSocket = accept(ListenSocket, NULL, NULL);
 5
 6// 연결 수락 후, 키가 눌리면 클라이언트로부터 전송한 데이터를 모두 수신
 7printf("Press any key to call recv");
 8getchar();
 9
10int totalReceive = 0;
11while (true)
12{
13	iResult = recv(clntSocket, recvbuf, DEFAULT_BUFLEN, 0);
14	if (iResult == -1 || iResult == 0)
15	{
16		errorCode = WSAGetLastError();
17		printf("recv: %d(%d)\n", iResult, errorCode);
18		break;
19	}
20
21	totalReceive += iResult;
22	printf("recvLen: %d (total: %d)\n", iResult, totalReceive);
23}
24
25/****** Client ******/
26const int DEFAULT_BUFLEN = 1024 * 1024 * 10;
27char* recvbuf = new char[DEFAULT_BUFLEN];
28
29// 10MB를 서버 측으로 전송
30iResult = send(clntSocket, recvbuf, DEFAULT_BUFLEN, 0);
31printf("send: %d\n", iResult);
32
33// 키가 눌리면 shutdown(SD_SEND) 호출
34printf("Press any key to call shutdown");
35getchar();
36shutdown(clntSocket, SD_SEND);
37
38delete[] recvbuf;
39
40// 키가 눌리면 closesocket 호출
41printf("Press any key to call closesocket");
42getchar();
43closesocket(clntSocket);
44printf("closesocket\n");
  • 딱히 어떤일도 수행하지 않음

  • 데이터를 받아주지 않으면 수신버퍼가 가득차서 송신이 불가능하고, 클라이언트측에서는 송신버퍼에 데이터가 남아있어 비워질때까지 서버의 수신버퍼를 체크함
    (closesocket이 호출되고, 클라이언트 프로그램이 종료된 상황에도 운영체제가 보내준다)

zerowindow

  • 모든 데이터가 전달되고 서버측에서 호출한 recv 값은 0이 리턴된다. (사실 shutdown(SD_SEND) 때문은 아님)

5. 송신버퍼에 데이터가 없을때 shutdown(SD_SEND) 호출 #

  • FIN 패킷을 보내서 정상 종료를 시작한다.

6. 수신버퍼에 데이터가 있을때 shutdown(SD_SEND) 호출 #

  • 송신버퍼여부에 따라 동작한다. 대신 closesocket을 호출하면서 RST가 발생한다.

7. closesocket과 shutdown(SD_BOTH) #

  • SD_RECEIVE + SD_SEND 두개를 합쳐놓은것이다.
  • closesocket은 내부적으로 shutdown(SD_BOTH)을 호출한다.

서버에서 호출 #

서버에서 호출했다고 다르게 동작하지는 않는다.


소켓을 정상적으로 종료시키기 #

결과적으로 보면, 양쪽 모두 수신이 끝난 뒤 데이터가 없을때 종료한다면 정상종료가 된다.

  1. 종료하고싶은녀석이 shutdown(SD_SEND)를 호출 (Client - FIN)
  2. recv로 남은 데이터 전부 읽기 => 어차피 이미 FIN을 보내놔서 서버측에선 데이터 전부 전송후 FIN을 보내기때문에 recv가 읽을 데이터가 없어도 0이 리턴된다.
  3. closesocket 호출
comments powered by Disqus