파일 및 표준 입출력, 그리고 현재 다루고 있는 네트워크 프로그램밍에서 흔히 등장하는 개념인 스트림물의 흐름을 의미한다. 그런데 물의 흐름은 한쪽 방향으로만 형성된다. 마찬가지로 소켓의 스트림 역시 한쪽 방향으로만 데이터의 이동이 가능하기 때문에 양방향 통신을 위해서는 다음 그림에서 보이듯이 두 개의 스트림이 필요하다.

한 소켓의 입력 스트림은 맞은편 소켓의 출력 스트림으로 이어진다.

이번 문서에서 다루는 Half-close는 한 번에 두 개의 스트림을 모두 끊어버리는 게 아닌, 이 중 하나의 스트림만 끊는 것이다. 리눅스의 close()는 두 스트림을 동시에 끊어버리기 때문에 shutdown이라는 별도의 시스템콜을 사용해야 한다.

Half-close가 필요한 이유

일방적인 연결 종료의 문제점

close를 사용하면 출력 버퍼에 남아있는 데이터는 모두 전송하지만, 입력 버퍼를 사용해서는 어떠한 데이터도 수신할 수 없게 된다. 따라서 일방적으로 close를 사용하면 상대가 송신한 데이터를 수신하지 못하고 연결을 종료하는 경우가 발생하게 된다.

이와 같은 문제의 해결을 위해 사용되는 방법은 보통 다음과 같다.

  1. 특정 문자를 통신 종료의 신호로 삼아 해당 문자를 수신하기 전에는 close하지 않는다.
  2. 충분히 여유를 종료한다.

하지만 위의 방법은 근본적으로 문제를 해결할 수 없다. 1의 경우 텍스트만을 주고 받는 간단한 케이스의 경우 텍스트를 작성하는 사람이 해당 규칙을 인지하고 있으므로 어느정도 유효하지만, 파일 송수신을 하는 경우 해당 파일에 종료를 의미하는 문자가 포함되어 있을 수 있어 중간에 연결이 갑작스럽게 끊길 수 있다.

두번째 방법의 경우, 어느 정도의 시간만큼 대기해야 안전하게 종료할 수 있는지에 대한 기준이 불명확하다.

이러한 문제의 해결을 위해서 데이터의 송수신에 사용되는 **데이터의 일부만 종료(Half-close)**하는 방법이 제공되는 것이다.

eof를 사용한 연결 제어

다음과 같은 상황이 있다고 가정하자.

  1. 서버는 클라이언트에게 파일을 보낸다.
  2. 클라이언트는 파일을 ‘완전히’ 수신한 후 서버 측으로 스스로의 상태를 보고하는 메시지를 보낸다.

여기서 문제가 되는 것은 2번 과정이다. 만약에 1번 과정만 존재했다면 서버 측에서 파일 전송을 완료하고 close를 사용하면 문제가 해결되었을 것이다. 앞서 언급했지만 close를 사용해도 출력버퍼에 남아있는 데이터의 전송은 보장받기 때문이다. 클라이언트는 소켓이 닫히면서 eof를 전송받아 연결이 종료되었음을 인지할 수 있다.

하지만 1번 과정 이후의 프로세스가 남아 있으므로, 서버 측은 close를 사용할 수 없다. 클라이언트 측은 eof를 받지 못하므로 파일의 전송이 끝났다는 것을 인지하지 못해 2번 과정으로 넘어갈 수 없다.

그러면 다음과 같은 해결책을 제안할 수 있다.

서버와 클라이언트 사이에 파일의 끝을 의미하는 문자를 하나 약속한다.

하지만 상술했듯이, 위 방법은 유효하지 않은 방법이다. 약속으로 정해진 문자와 일치하는 데이터가 파일 중간에 존재할 수도 있기 때문이다. 근본적으로 이러한 문제를 해결하는 half-close를 해결할 수 있는 방법은 half-close를 사용하는 것이다.

위의 예시로 언급한 상황의 경우, 아래와 같은 프로세스로 문제를 해결할 수 있다.

  1. 서버는 클라이언트에게 파일을 보낸다.
  2. 파일을 완전히 보내면 서버 측에서 shutdown을 사용해 출력 버퍼만을 닫는다.
  3. 서버 측의 출력 버퍼는 클라이언트의 입력 버퍼와 연결되어 있으므로 클라이언트는 eof를 수신한다.
  4. 클라이언트는 파일 전송이 끝났음을 인지하고 서버에게 스스로의 상태를 보고하는 메시지를 보낸다.
  5. 서버 측은 출력 버퍼만 닫혀있고 입력 버퍼는 열려있으므로 클라이언트의 보고를 수신할 수 있다.
  6. 모든 프로세스가 종료되었으므로 연결을 완전히 끊는다.

위 예시 말고도 소켓의 일부만 닫는다는 개념은 다양한 상황에 유용하게 사용할 수 있다.

close와 shutdown은 다른 시스템콜입니다.

shutdown에 SHUT_RDWT를 전달하면 입력 버퍼와 출력 버퍼를 모두 닫을 수 있다.
이 점은 close를 사용했을 때와 동일하므로, SHUT_RDWT 인자로 shutdown을 사용하면 close를 대체할 수 있다는 오해를 부를 수 있다는 생각이 든다.

shutdown은 소켓의 버퍼만을 닫을 뿐, 리소스는 해제하지 않는다. 따라서 shutdown으로 입출력 버퍼를 모두 닫아도 버퍼와 관계없는 동작은 실행이 가능하다.

반면에 close는 소켓의 리소스를 완전히 해제하므로 close 이후에는 어떠한 조작도 할 수 없다.

그러므로 shutdown으로 입출력 버퍼를 모두 닫았더라도 차후 close를 사용해 소켓의 리소스를 완전히 해제해주어야 한다.


참고자료

윤성우의_열혈_TCP_IP_소켓_프로그래밍