• shutdown()과 closesocket()

    2015. 6. 15.

    by. xxx123

     

    shutdown(socket, SD_BOTH);

    closesocket(socket);

     

    우리는 소켓을 종료 시킬 때 위와 같은 구문으로 소켓을 종료 시킨다.

    그럼 각각의 함수가 어떤기능을 하는지 부터 살펴 본 후, 종료 과정에서 어떤일이 일어나는지 알아보자.

     

     

     

     

    1. 접속 종료 과정

     

    나중에 따로 다루겠지만 위 과정이 4-way handshake이다. 위 과정에 대해 순서대로 설명하자면,

     

    ① closesoket()이나 shutdown(SD_SEND)을 호출할 경우, 세션을 종료한다는 의미로 FIN 패킷을 전송한다.

     

    ② client는 FIN_WAIT_1 상태가 될것이고, server는 FIN을 받고 종료할 준비를 하게 될것이다. 준비가 완료되면 치CLOSE_WAIT 상태로 변경하고 잘 받았다는 의미로 ACK패킷을 client에 전송한다.

     

    ③ CLOSE_WAIT 상태가 된 server는 closesocket()함수를 호출하여 client에 FIN 패킷을 전송한다. 이 FIN 패킷의 결과가 클라이언트 Application에서의 0byte를 recv라고 할 수 있다.

     

    ④ FIN을 받은 client는 이에 대한 ACK를 보내고 나면 세션을 종료 할 수 있는 상태가 된다. 위 그림에서 TIME_WAIT 상태라고 되어 있는데 말 그대로 데이터의 전송을 을 위해 FIN을 수신하더라도 일정시간(디폴트 240초) 동안 패킷 상대가 패킷을 다 보낼 때까지 기다리는 작업을 하게 되는 것이다. 이것을 "TIME_WAIT"라 한다.

     

    여기서 알아둬야 할것은 4-way handshake란 우아한 종료를 했을때 일어나는 과정이다. 강제로 종료를 하는 경우 4-way handshake는 일어나지 않는다.

     

    또한 위 그림의 close 같은 부분은 어플리케이션에서 실행시켜줘야 하는 부분임을 기억하자. (close만 한번 호출한다고 해서 자기가 알아서 쿵짝쿵짝하는게 아니라는 뜻이다.)

     

     

     

     

     

     

     

     

     

     

    2. shutdown()과 closesocket()의 차이?

     

    먼저 각 함수에 대해서 알아보자.

    - shutdown(how) 

    how를 인자로 받으며, SD_SEND(혹은 SD_BOTH)를 인자로 넣을 경우. "더 이상 내가 보낼 데이터는 없다. 란 의미로 FIN 패킷을 상대방에게 전송하게 되어 있다." 

    소켓의 소멸과는 상관이 없다.

     

    한번 셧다운 된 소켓을 다시 셧다운 해제하는 API는 존재하지 않는다. 그말인즉, 한번 소켓을 셧다운 했다면, 소켓을 재활용 할 수 없고 새로 생성해야 한다는 뜻이다. (만들기 나름이겠지만 세션 로그인 테스트를 할때 세션을 재활용 할 생각 말고, 세션을 새로 생성하는게 여러모로 편하다는 뜻이다.)

    만약 소켓을 재활용 하고 싶다면 shutdown()이 아닌 DiconnectEx() 함수를 활용해야 한다.

     

    여기서 오해하지 말아야 할 부분은 shutdown()은 어플리케이션의 Write()/Read()함수가 동작하지 않도록 막는것이라는 점이다. 다르게 말하자면 송신의 경우 송신 어플리케이션 버퍼의 데이터를 송신 소켓 버퍼로 복사 할 수 없게 만드는 것이며, 수신 소켓 버퍼의 데이터는 모두 비워 버리고 입력 관련 함수의 호출을 막는것이다.

    송신 소켓 버퍼에 차있는 데이터가 상대쪽으로 넘어가는 것을 막는것은 아니다.

     

    shutdown(SD_SEND)의 호출은 바로 FIN을 상대에게 날림으로 4-way handshake를 유도하게 된다.

    이런 동작은 half-close라 하여 클라이언가 자신이 보내고 싶은 데이터를 모두 보내고 종료 할 수 있도록 해준다. (graceful shutdown)

    정상적인 종료의 시나리오는 다음과 같다.

     

    - closesocket()

    shutdown()과 마찬가지로 종료 동작을 수행한다. 하지만 shutdown(SD_SEND)이 바로 FIN을 보내는 반면, closesocket()은 자신의 소켓 버퍼에 쌓인 데이터와 소켓의 옵션에 따라 다르게 동작한다. 이 뿐만 아니라 closesocket() 함수 호출이 끝나는 시점에 socket리소스를 반환하는 동작 또한 수행한다.

     

    onoff 값이 0이면,  소켓 큐의 데이터처리를 전부 처리하지 않고 바로 소켓을 종료한다. 이 경우 RST 패킷이 상대에게 날라가며, IOCP를 사용할 경우 상대는 비정상 종료의 의미로 GQCS는 false를 리턴, 수신바이트는 0이 될것이며 "ERROR_NETNAME_DELETED"라는 이름의 에러를 뱉어낼 것이다.

    onoff 값이 1이면, linger 값에 따라 동작이 달라지는데 linger값이 0이면 위와 동일하게 동작하고, 0이 아닌 값이면 해당 값만큼 소켓 버퍼가 지워지기를 기다린다. 이는 양 쪽 버퍼가 다 비워질 때까지 기다리며, 모두 비워지면 상대에게 FIN을 전송하게 된다.

    그렇기 때문에 많은 사람들이 얘기하는 것처럼 socketclose() 함수를 단순히 "shutdown(종료 동작 수행) + 리소스 정리"라고 정의하기엔 무리가 있다.

     

     

    - 두 함수의 차이 정리

    ① shutdown(SD_SEND)은 무조건 FIN 패킷을 보내는 반면, closesocket()은 자신의 소켓버퍼 상태와 소켓옵션에 따라 RST 패킷을 보낼 수도 있고, FIN 패킷을 보낼 수도 있다.

    ② shutdown()은 함수의 호출을 막는데 반해, closesocket()은 소켓 버퍼 자체를 종료 시킨다.

    ③ closesocket()은 socket리소스의 반환 동작까지 수행한다.

     

     

     

     

     

    3. 관용 코드의 정체

     

    가장 위에서 설명한

    shutdown(socket, SD_BOTH);

    closesocket(socket);

     

    코드의 의미는 상대방에게 정상 종료를 수행하는 FIN을 날리고, 자신의 소켓은 바로 닫아 버리는 상황이라고 할 수 있겠다.

    이 경우 상대방은 0byte recv를 하고 정상 종료의 처리를 수행하겠지만, 이미 보낸쪽은 socket을 close한 상태이므로 상대로 부터의 FIN에 대해서는 별도의 처리를 하지 않겠다는 의미이다. 

    온라인 게임에서 딱히 상대방의 종료 OK에 대해 처리해줘야 할 이슈가 과연 있을까..?

     

     

     

     

     

     

     

     

     

     

    4. 고찰

    ① shutdown()을 SD_BOTH로 걸어 놓았을 경우, shotdown()함수가 호출되기 이전에 이미 걸어놓은 WSARecv()에 대해서는 어떻게 될까? Recv에 대해 막았기 때문에 상대방이 shutdown()할 경우 0byte는 날아오지 않을것인가?

    - 테스트 결과. 이미 걸어놓은 WSARecv()에 대해서는 GQCS()가 0byte를 받아 낸다. 위에서 설명한대로 함수의 호출을 막는 것이지. 결과의 통보를 막는것은 아니다.

     

     

     

    ② linger 옵션을 통해 time_wait를 주고 closesocket() 함수를 호출할 경우 FIN이 날라가지 않고 계속 대기하게 된다.

    결국 time-out되고 RST 패킷이 날라가게 되는데, Send 중인 데이터가 없어서 소켓 SendBuffer에는 데이터가 없을텐데 왜 정상 종료가 이루어지지 않는 것일까?

    다음 코드를 보자.

    이 코드는 main 스레드가 아닌 별도의 스레드에서 동기적 recv만을 수행하는 코드이다.

     

    상황은 이렇다. 동기 타입 클라이언트에서 Recv()를 다른 스레드에서 호출하고 있는 중에 closesocket()을 호출할 경우, RST 패킷(hard close)를 상대에게 전송한다.

    분명 send버퍼는 비워졌을테지만, 강제 종료의 의미인 RST 패킷이 날라간다.

    http://stackoverflow.com/questions/28941572/strange-closesocket-behavior

    http://cs.ecs.baylor.edu/~donahoo/practical/CSockets/TCPRST.pdf

    이 두 링크를 보면, recv버퍼의 데이터가 아직 소진 되지 않은 상태에서 closesocket()을 호출하면 비정상 종료로 인식. FIN이 아닌 RST 패킷을 날린다는 것이다.

     

    다음 코드를 보자.

     

    미리 데이터를 다 받아낸 후에 바로 closesocket()을 호출할 경우. 정상 종료되는걸 볼 수 있다.

    즉, SendBuffer 뿐만 아니라 RecvBuffer의 데이터도 모두 처리되어야 정상적인 종료가 가능하다는 뜻이다.

     

     

    ③ 4way - handshake라는건 TCP 단에서 자동으로 일어나는 Application 단과는 전혀 상관없는 동작인가? 

    - 아니다. 위에서 설명했듯이 클라에서 종료를 요청한다고 할때, shutdown(SD_SEND) 혹은 조건을 만족하는 closesocket() 함수가 호출 되었을 때 4-way handshake의 첫 시작인 FIN이 날라가는 것이다. 이렇게 결국 Application 단에서 직접 합수를 호출해주고, 이의 응답으로 ACK를 건내 준후 서버 쪽에서도 Application 단에서 shutdown(SD_SEND) 혹은 closesocket()을 호출해 줘야만 정상적인 4-way handshake가 완료되는 것이다.

     

     

     

    ④ 정상 종료시 0byte를 어플리케이션으로 올리는 작업은 언제 일어나는가?

    - 바로 정상종료를 통해 FIN을 날렸을 때, 즉 상대로부터 shutdown(SD_SEND)나 closesocket()이 호출된 시점이다.

    위 내용은 클라이언트가 shutdown(SD_SEND)를 호출했을 때의 클라이언트 상황을 와이어샤크를 통해 캡춰한 것이다.

    보는것과 같이 FIN 패킷을 보냈고 그 응답으로 ACK 패킷이 왔는데, 그 두 패킷을 제외하고는 주고 받은 패킷은 없는 상태이다.

     

     

    그 상태에서 서버쪽 코드의 0byte recv부분에 break point를 걸어보면, 정상 종료의 상징인 0byte 패킷은 FIN이 날라갔을때 발생한다는 것을 알 수 있다.

     

     

     

     

     

     

     

     

     

    [참고]

    http://chokuto.ifdef.jp/advanced/function/shutdown.html // 일본 shutdown 관련

    http://stackoverflow.com/questions/28941572/strange-closesocket-behavior // TCP Reset 관련 오버 플로우

    http://cs.ecs.baylor.edu/~donahoo/practical/CSockets/TCPRST.pdf   // TCP Reset 관련 글.

    http://www.viper.pe.kr/wiki2/wiki.php/Socket%20Programming  // 정상적인 종료.

     

     

    'Programming > Network' 카테고리의 다른 글

    SO_SNDBUF 0  (0) 2016.12.04
    TCP Header  (0) 2015.06.19
    page-locking & non-paged pool  (0) 2015.06.19
    IOCP - 1 (I/O CompletionPort)  (0) 2014.12.14
    Overlapped I/O  (0) 2014.09.18

    댓글