책정리/열혈 TCP,IP

7장 소켓의 우아한 연결종료

GONII 2015. 8. 5. 16:12
  • TCP 기반의 Half-close

    TCP에서는 연결과정보다 중요한 것이 종료과정이다. 연결과정에서는 큰 변수가 발생하지 않지만 종료 과정에서는 예상치 못한 일이 발생할 수 있기 때문이다.

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

      리눅스의 close 함수호출과 윈도우의 closesocket 함수호출은 완전종료를 의미한다. 완전종료라는 것은 데이터를 전송하는 것과 수신하는 것조차 불가능한 상황을 의미한다. 때문에 한쪽에서의 일방적인 close 또는 closesocket 함수호출은 경우에 따라서 우아해 보이지 못할 수 있다.

      호스트A가 마지막 데이터를 전송하고 나서 close 함수의 호출을 통해서 연결을 종료하였다. 때문에 이후부터 호스트A는 호스트B가 전송하는 데이터를 수신하지 못한다. 결국엔 호스트B가 전송한, 호스트A가 반드시 수신해야 하는 데이터라 할지라도 그냥 소멸되고 만다.

      이러한 문제의 해결을 위해서 데이터의 송수신에 사용되는 스트림의 일부만 종료(Half-close)하는 방법이 제공되고 있다. 일부를 종료한다는 것은 전송은 가능하지만 수신은 불가능한 상황, 혹은 수신은 가능하지만 전송은 불가능한 상황을 뜻한다.

    • 소켓과 스트림(stream)

      소켓을 통해서 두 호스트가 연결되면, 그 다음부터는 상호간에 데이터 송수신이 가능한 상태가 된다. 그리고 이러한 상태를 가리켜 '스트림이 형성된 상태'라 한다. 즉, 두 소켓이 연결되어서 데이터의 송수신이 가능한 상태를 일종의 스트림으로 보는 것이다.

      소켓의 스트림은 한쪽 방향으로만 데이터의 이동이 가능하기 때문에 양방향 통신을 위해서는 다음 그림에서 보이듯이 두 개의 스트림이 필요하다.

      때문에 두 호스트간에 소켓이 연결되면, 각 호스트 별로 입력 스트림과 출력 스트림이 형성된다. 물론 한 호스트의 입력 스트림은 다른 호스트의 출력 스트림으로 이어지고, 한 호스트의 출력 스트림은 다른 호스트의 입력 스트림으로 이어진다. 그리고 우아한 종료라는 것은 한 번에 이 두 스트림을 모두 끊어버리는 것이 아니라, 이 중 하나의 스트림만 끊는 것이다. 물론 리눅스의 close함수와 윈도우의 closesocket 함수는 두 가지 스트림을 동시에 끊어버리기 때문에 우아한 연결종료와는 거리가 멀다.

    • 우아한 종료를 위한 shutdown 함수

      Half-close에 사용되는 함수는 다음과 같다.

#include <sys/socket.h>

   

int shutdown(int sock, int howto);

// 성공 : 0

// 실패 : -1

위의 함수호출 시 두 번째 매개변수에 전달되는 인자에 따라 종료의 방법이 결정된다.

SHUT_RD

입력 스트림 종료

SHUT_WR

출력 스트림 종료

SHUT_RDWR

입출력 스트림 종료

shutdown함수의 두 번째 인자로 SHUT_RD가 전달되면 입력 스트림이 종료되어 더 이상 데이터를 수신할 수 없는 상태가 된다. 데이터가 입력버퍼에 전달되더라도 그냥 지워져 버릴 뿐만 아니라 입력 관련 함수의 호출도 더 이상 허용이 안된다.

SHUT_WR가 전달되면 출력 스트림이 종료되어 더 이상의 데이터 전송이 불가능해진다. 단 출력 버퍼에 아직 전송되지 못한 상태로 남아 있는 데이터가 존재하면 해당 데이터는 목적지로 전송된다.

  • Half-close가 필요한 이유

    "Half-close가 도대체 왜 필요한 거지? 그냥 데이터를 주고받기에 충분한 만큼 연결을 유지했다가 종료하면 되는 것 아닌가? 급히 종료하지만 않으면 Half-close가 필요하지는 않을 것 같은데 말이야"

    전혀 틀린 말은 아니다. 충분한 시간적 여유를 둬서 송수신을 완료하고 난 다음에 연결을 종료해도 되는 상황에서는 굳이 Half-close를 활용할 필요가 없다. 그러나 다음과 같은 상황을 생각해 보자.

    "클라이언트가 서버에 접속하면 서버는 약속된 파일을 클라이언트에게 전송하고, 클라이언트는 파일을 잘 수신했다는 의미로 문자열 "Thank you"를 서버에 전송한다."

    여기서 문자열 "Thank you"의 전달은 사실상 불필요한 일이지만, 연결종료 직전에 클라이언트가 서버에 전송해야 할 데이터가 존재하는 상황으로 해석하기 바란다. 그런데 이 상황에 대한 프로그램의 구현도 그리 간단하지만은 않다. 파일을 전송하는 서버는 단순히 파일 데이터를 연속해서 전송하면 되지만, 클라이언트는 언제까지 데이터를 수신해야 할지 알 수 없기 때문이다. 클라이언트 입장에서는 무턱대고 계속해서 입력함수를 호출할 수도 없는 노릇이다. 그랬다가는 블로킹 상태(호출된 함수가 반환하지 않는 상태)에 빠질 수 있기 때문이다.

    "서버와 클라이언트 사이에 파일의 끝을 의미하는 문자 하나를 약속하면 되잖아요?"

    이것도 어울리지 않는 상황이다. 약속으로 정해진 문자와 일치하는 데이터가 파일에 존재할 수 있기 때문이다. 이러한 문제의 해결을 위해서 서버는 파일의 전송이 끝났음을 알리는 목적으로 EOF를 마지막에 전송해야 한다. 클라이언트는 EOF의 수신을 함수의 반환 값을 통해 확인이 가능하기 때문에 파일에 저장된 데이터와 중복될 일도 없다. 그럼 이제 남은 문제는 하나다! 서버는 어떻게 EOF를 전달할 수 있겠는가?

    "출력 스트림을 종료하면 사대 호스트로 EOF가 전송됩니다"

    물론 close 함수호출을 통해서 입출력 스트림을 모두 종료해줘도 EOF는 전송되지만, 이럴 경우 상대방이 전송하는 데이터를 더 이상 수신 못한다는 문제가 있다. 즉, EOF는 전송되지만, 이럴 경우 상대방이 전송하는 데이터를 더 이상 수신 못한다는 문제가 있다. 즉, close 함수호출을 통해서 스트림을 종료하면 클라이언트가 마지막으로 보낸 문자열 "thank you"를 수신할 수 없다. 따라서 shutdown 함수 호출을 통해서 서버의 출력 스트림만 Half-close해야 하는 것이다. 이럴 경우 EOF도 전송되고 입력 스트림은 여전히 살아있어서 데이터의 수신도 가능하다.

  • Half-close 기반의 파일전송 프로그램

    위에서 언급한 파일전송 서버, 클라이언트의 데이터 흐름을 정리하면 다음과 같다.

    • file_server.c

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <unistd.h>

#include <arpa/inet.h>

#include <sys/socket.h>

   

#define BUF_SIZE 30

void error_handling(char* message);

   

int main(int argc, char* argv[])

{

int serv_sd, clnt_sd;

FILE* fp;

char* buf[BUF_SIZE];

int read_cnt;

   

struct sockaddr_in serv_adr, clnt_adr;

socklen_t clnt_adr_sz;

   

if( argc != 2 )

{

printf("Usage : %s <port>\n", argv[0]);

exit(1);

}

   

fp = fopen("07_file_server.c", "rb");

serv_sd = socket(PF_INET, SOCK_STREAM, 0);

   

memset(&serv_adr, 0, sizeof(serv_adr));

serv_adr.sin_family = AF_INET;

serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);

serv_adr.sin_port = htons(atoi(argv[1]));

bind(serv_sd, (struct sockaddr*)&clnt_adr, &clnst_adr_sz);

   

while(1)

{

read_cnt = fread((void*)buf, 1, BUF_SIZE, fp);

if( read_cnt < BUF_SIZE )

{

write(clnt_sd, buf, read_cnt);

break;

}

write(clnt_sd, buf, BUF_SIZE);

}

shutdown(clnt_sd, SHUT_WR);

read(clnt_sd, buf, BUF_SIZE);

printf("Message from client: %s\n", buf);

   

fclose(fp);

close(clnt_sd);

close(serv_sd);

   

return 0;

}

   

void error_handling(char* message)

{

fputs(message, stderr);

fputc('\n', stderr);

exit(1);

}

  • file_client.c

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <unistd.h>

#include <arpa/inet.h>

#include <sys/socket.h>

#define BUF_SIZE 30

   

void error_handling(char* message);

int main(int argc, char* argv[])

{

int sd;

FILE* fp;

   

char buf[BUF_SIZE];

int read_cnt;

struct sockaddr_in serv_adr;

   

if( argc != 3 )

{

printf("Usage: %s <IP> <port>\n", argv[0]);

exit(1);

}

   

fp = fopen("receive.dat", "wb");

   

sd = socket(PF_INET, SOCK_STREAM, 0);

memset(&serv_adr, 0, sizeof(serv_adr));

serv_adr.sin_family = AF_INET;

serv_adr.sin_addr.s_addr = inet_addr(argv[1]);

serv_adr.sin_port = htons(atoi(argv[2]));

   

connect(sd, (struct sockaddr*)&serv_adr, sizeof(serv_adr));

   

while((read_cnt = read(sd, buf, BUF_SIZE)) != 0)

fwrite((void*)buf, 1, read_cnt, fp);

   

puts("Received file data");

write(sd, "Thank you", 10);

fclose(fp);

close(sd);

   

return 0;

}

   

   

void error_handling(char* message)

{

fputs(message, stderr);

fputc('\n', stderr);

exit(1);

}

  • 윈도우 기반으로 구현하기

    윈도우 기반에서도 Half-close를 위해서 shutdown함수를 호출한다 단 인자로 전달되는 상수의 이름에 약간 차이가 있다

#include <winsock2.h>

   

int shutdown(SOCKET sock, int howto);

// 성공 : 0

// 실패 : SOCKET_ERROR

두 번째 매개변수에 전달되는 인자는 다음과 같다

SD_RECEIVE

입력 스트림 종료

SD_SEND

출력 스트림 종료

SD_BOTH

입출력 스트림 종료

  • file_server_win.c

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <winsock2.h>

#pragma comment(lib, "ws2_32.lib")

   

#define BUF_SIZE 30

void error_handling(char* message);

   

int main(int argc, char* argv[])

{

WSADATA wsaData;

SOCKET servSock, clntSock;

SOCKADDR_IN servAdr, clntAdr;

int clntAdrSz;

   

FILE* fp;

char buf[BUF_SIZE];

int readCnt;

   

if( argc != 2 )

{

printf("Usage: %s<port>\n", argv[0]);

exit(1);

}

   

if( WSAStartup(MAKEWORD(2,2), &wsaData) != 0 )

error_handling("WSAStartup() error");

   

fp = fopen("file_server_win.c", "rb");

servSock = socket(PF_INET, SOCK_STREAM, 0);

   

memset(&servAdr, 0, sizeof(servAdr));

servAdr.sin_family = AF_INET;

servAdr.sin_addr.s_addr = htonl(INADDR_ANY);

servAdr.sin_port = htons(atoi(argv[1]));

   

bind(servSock, (SOCKADDR*)&servAdr, sizeof(servAdr));

listen(servSock, 5);

   

clntAdrSz = sizeof(clntAdr);

clntSock = accept(servSock, (SOCKADDR*)&clntAdr, &clntAdrSz);

   

while(1)

{

readCnt = fread((void*)buf, 1, BUF_SIZE, fp);

if( readCnt < BUF_SIZE)

{

send(clntSock, (char*)&buf, readCnt, 0);

break;

}

send(clntSock, (char*)&buf, BUF_SIZE, 0);

}

   

shutdown(clntSock, SD_SEND);

recv(clntSock, (char*)buf, BUF_SIZE, 0);

printf("Message from client: %s\n", buf);

   

fclose(fp);

closesocket(clntSock);

closesocket(servSock);

WSACleanup();

return 0;

}

   

void error_handling(char* message)

{

fputs(message, stderr);

fputc('\n', stderr);

exit(1);

}

  • file_client_win.c

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <winsock2.h>

#pragma comment(lib, "ws2_32.lib")

   

#define BUF_SIZE 30

void error_handling(char* message);

   

int main(int argc, char* argv[])

{

WSADATA wsaData;

SOCKET sock;

SOCKADDR_IN servAdr;

FILE* fp;

   

char buf[BUF_SIZE];

int readCnt;

   

if( argc != 3 )

{

printf("Usage: %s <IP> <port>\n", argv[0]);

exit(1);

}

   

if( WSAStartup(MAKEWORD(2,2), &wsaData) != 0 )

error_handling("WSAStartup() error");

   

fp = fopen("receive.dat", "wb");

sock = socket(PF_INET, SOCK_STREAM, 0);

   

memset(&servAdr, 0, sizeof(servAdr));

servAdr.sin_family = AF_INET;

servAdr.sin_addr.s_addr = inet_addr(argv[1]);

servAdr.sin_port = htons(atoi(argv[2]));

   

connect(sock, (SOCKADDR*)&servAdr, sizeof(servAdr));

   

while((readCnt = recv(sock, buf, BUF_SIZE, 0)) != 0 )

fwrite((void*)buf, 1, readCnt, fp);

   

puts("Received file data");

send(sock, "thank you", 10, 0);

fclose(fp);

closesocket(sock);

WSACleanup();

return 0;

}

   

void error_handling(char* message)

{

fputs(message, stderr);

fputc('\n', stderr);

exit(1);

}

 

반응형