Chapter 07 - 소켓의 우아한 연결종료

07-1 : TCP 기반의 Half-Close

리눅스의 close()와 윈도우의 closesocket() 함수는 완전종료로써 데이터의 송수신을 완전히 끊어버림을 의미
이러한 송수신 과정에서의 일방적 연결종료로 인해 문제가 생길 수 있으므로 일부 스트림만 종료시키는 Half-close 방법을 사용할 수 있음

  • 스트림(Stream) : 두 호스트가 소켓을 통해 상호간에 데이터 송수신이 가능한 상태
  • 스트림은 단방향을 가리키기 때문에 양방향 통신을 위해서는 두 개의 스트림이 필요
  • 즉, 소켓 연결시 각 호스트별로 입력 스트림과 출력 스트림이 생성


Half-close에는 shutdown() 함수를 사용

#include <sys/socket.h>

int shutdown(int sock, int howto);
// 성공시 0, 실패시 -1 반환

howto 인자로는 스트림의 종료방법을 전달

  • SHUT_RD : 입력 스트림 종료, 데이터 수신 불가 및 입력함수 호출 불가
  • SHUT_WR : 출력 스트림 종료, 데이터 송신은 불가능하지만 출력 버퍼에 남아있던 데이터는 목적지로 전송됨
  • SHUT_RDWR : 입출력 스트림 종료

클라이언트가 서버로부터 데이터를 전송받고, 연결종료 직전에 클라이언트가 서버에 전송해야 할 데이터가 존재할 경우

  • 클라이언트는 서버로부터 EOF가 전송될경우 데이터 수신을 중단함
  • 서버의 출력 스트림을 종료할시 상대 호스트로 EOF가 전송
  • 이 때 EOF를 보내기 위해 입출력 스트림을 모두 종료할경우 클라이언트가 마지막에 보내는 데이터를 서버가 받을 수 없음
  • 따라서 서버의 출력 스트림만을 종료하는 Half-close가 필요


Half-close 기반의 파일전송 서버 예시

// file_server.cpp

#include <iostream>
#include <fstream>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

using namespace std;

#define BUF_SIZE 30
void	error_handling(char *message);

int main(int argc, char *argv[])
{
	int					serv_sd, clnt_sd, read_cnt;
	char				buf[BUF_SIZE];
	FILE				*fp;
	socklen_t			clnt_adr_sz;
	struct sockaddr_in	serv_adr, clnt_adr;

	if (argc != 2)
	{
		cout << "Usage : " << argv[0] << " <port>\n";
		exit(1);
	}
	
	fp = fopen("file_server.cpp", "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*)&serv_adr, sizeof(serv_adr));
	listen(serv_sd, 5);

	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);
	cout << "Message from client : " << buf << endl;

	fclose(fp);
	close(clnt_sd);
	close(serv_sd);
	return 0;
}

void	error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

Half-close 기반의 파일전송 클라이언트 예시

// file_client.cpp

#include <iostream>
#include <fstream>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

using namespace std;

#define BUF_SIZE 30
void	error_handling(char *message);

int main(int argc, char *argv[])
{
	int					sd, read_cnt;
	char				buf[BUF_SIZE];
	FILE				*fp;
	struct sockaddr_in	serv_adr;

	if (argc != 3)
	{
		cout << "Usage : " << argv[0] << " <IP> <port>\n";
		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);

	cout << "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);
}

07-2 : 윈도우 기반으로 구현하기

윈도우에서도 Half-close를 위해 shutdown() 함수를 사용
단, howto 인자로 전달되는 상수의 이름에 차이가 있음

#include <winsock2.h>

int shutdown(SOCKET sock, int howto);
// 성공시 0, 실패시 SOCKET_ERROR 반환
  • SD_RECEIVE : 입력 스트림 종료
  • SD_SEND : 출력 스트림 종료
  • SD_BOTH : 입출력 스트림 종료

윈도우/Half-close 기반의 파일전송 서버 예시

// file_server_win.cpp

#include <iostream>
#include <winsock2.h>

using namespace std;

#define BUF_SIZE 30
void	ErrorHandling(char* message);

int main(int argc, char *argv[])
{
	WSADATA		wsaData;
	SOCKET		hServSock, hClntSock;
	FILE		*fp;
	int			readCnt, clntAdrSz;
	char		buf[BUF_SIZE];
	SOCKADDR_IN	servAdr, clntAdr;

	if (argc != 2)
	{
		cout << "Usage : " << argv[0] << " <port>\n";
		exit(1);
	}

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		ErrorHandling("WSAStartup() error");

	fp = fopen("file_server_win.cpp", "rb");
	hServSock = socket(PF_INET, SOCK_STREAM, 0);

	memset(&servAddr, 0, sizeof(servAdr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servAddr.sin_port = htons(atoi(argv[1]));

	::bind(hServSock, (SOCKADDR*)&servAdr, sizeof(servAdr));
	::listen(hServSock, 5);

	clntAdrSz = sizeof(clntAdr);
	hClntSock = accept(hServSock), (SOCKADDR*)&clntAdr, &clntAdrSz;

	while (1)
	{
		readCnt = fread((void*)buf, 1, BUF_SIZE, fp);
		if (readCnt < BUF_SIZE)
		{
			send(hClntSock, (char*)&buf, readCnt, 0);
			break;
		}
		send(hClntSock, (char*)&buf, BUF_SIZE, 0);
	}

	shutdown(hClntSock, SD_SEND);
	recv(hClntSOck, (char*)buf, BUF_SIZE, 0);
	cout << "Message from client : " << buf << endl;

	fclose(fp);
	closesocket(hClntSock);
	closesocket(hServSock);
	WSACleanup();
	return 0;
}

void	ErrorHandling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

윈도우/Half-close 기반의 파일전송 클라이언트 예시

// file_client_win.cpp

#include <iostream>
#include <winsock2.h>

using namespace std;

#define BUF_SIZE 30
void	ErrorHandling(char *message);

int		main(int argc, char *argv[])
{
	WSADATA		wsaData;
	SOCKET		hSocket;
	FILE		*fp;
	char		buf[BUF_SIZE];
	int			readCnt;
	SOCKADDR_IN	servAdr;

	if(argc != 3)
	{
		cout << "Usage : " << argv[0] << " <IP> <port>\n";
		exit(1);
	}

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		ErrorHandling("WSAStartup() error");

	fp = fopen("receive.dat", "wb");
	hSocket = socket(PF_INET, SOCK_STREAM, 0);

	memset(&servAdr, 0, sizeof(servAdr));
	servAddr.sin_family = AF_INET;
	servAddr.sin_addr.s_addr = inet_addr(argv[1]);
	servAddr.sin_port = htons(atoi(argv[2]));

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

	while ((readCnt = recv(hSocket, buf, BUF_SIZE, 0)) != 0)
		fwrite((void*)buf, 1, readCnt, fp);

	cout << "Received file data" << endl;
	send(hSocket, "Thank you", 10, 0);
	fclose(fp);
	closesocket(hSocket);
	WSACleanup();
	return 0;
}

void	ErrorHandling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

내용 확인문제

  1. TCP에서의 스트림 형성이 의미하는 바가 무엇인지 설명해보자. 그리고 UDP에서도 스트림이 형성되었다고 할 수 있는 요소가 있는지 없는지 말해보고, 그 이유에 대해서도 설명해보자.

    스트림이 형성되었다는 것은 두 호스트가 소켓을 통해 연결되어 상호간에 데이터 송수신이 가능한 상태가 되었다는 것을 의미
    UDP에서는 두 소켓이 연결되이 않기때문에 스트림이 형성된다고 할 수 없음

  2. 리눅스에서의 close 함수 또는 윈도우에서의 closesocket 함수 호출은 일방적인 종료로써 상황에 따라서 문제가 되기도 한다. 그렇다면 일방적인 종료가 의미하는 바는 무엇이며, 어떠한 상황에서 문제가 되는지 설명해보자.

    입출력 스트림을 모두 닫아버리는 것을 일방적인 종료라고 하며, 데이터 송신은 끝났으나 수신할 것이 남아있는 경우 문제가 될 수 있음

  3. Half-close는 무엇인가? 그리고 출력 스트림에 대해서 Half-close를 진행한 호스트는 어떠한 상태에 놓이게되며, 출력 스트림의 Half-close 결과로 상대 호스트는 어떠한 메시지를 수신하게 되는가?

    Half-close란 입출력 스트림 중 한쪽만 닫아버리는 것을 뜻함
    출력 스트림에 대해서 Half-close를 진행할 경우 데이터 송신을 중지하며 더이상 출력 함수를 호출하지 못하지만, 출력버퍼에 남아있던 데이터는 그대로 전송
    또한 출력 스트림을 닫음으로써 EOF가 상대 호스트로 전달됨