Chapter 16 - 입출력 스트림의 분리에 대한 나머지 이야기

16-1 : 입력 스트림과 출력 스트림의 분리

fork() 함수 호출로 입력과 출력에 사용되는 파일 디스크립터를 구분

  • 입력루틴(코드)과 출력루틴의 독립을 통한 구현의 편의성 증대
  • 입력에 상관없이 출력이 가능하게 함으로써 속도의 향상 기대

fopen() 함수 호출로 읽기모드의 FILE 포인터와 쓰기모드의 FILE 포인터를 구분

  • 읽기모드와 쓰기모드의 구분을 통한 구현의 편의성 증대
  • 입력버퍼와 출력버퍼를 구분함으로써 버퍼링 기능의 향상

스트림 분리 이후 EOF에 대한 문제점 예시

// sep_serv.cpp

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

using namespace std;

#define BUF_SIZE 1024

int		main(int argc, char *argv[])
{
	int					serv_sock, clnt_sock;
	struct sockaddr_in	serv_addr, clnt_addr;
	char				buf[BUF_SIZE] = {0,};
	socklen_t			clnt_addr_size;
	FILE				*readfp, *writefp;

	serv_sock = socket(PF_INET, SOCK_STREAM, 0);
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	serv_addr.sin_port = htons(atoi(argv[1]));

	::bind(serv_sock, (sockaddr*)&serv_addr, sizeof(serv_addr));
	::listen(serv_sock, 5);
	clnt_addr_size = sizeof(clnt_addr);
	clnt_sock = accept(serv_sock, (sockaddr*)&clnt_addr, &clnt_addr_size);

	readfp = fdopen(clnt_sock, "r");
	writefp = fdopen(clnt_sock, "w");

	fputs("FROM SERVER : Hi~ client? \n", writefp);
	fputs("I love all of the world \n", writefp);
	fputs("You are awesome! \n", writefp);

	fclose(writefp);
	fgets(buf, sizeof(buf), readfp);
	fputs(buf, stdout);
	fclose(readfp);

	close(serv_sock);
	return 0;
}
// sep_clnt.cpp

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

using namespace std;

#define BUF_SIZE 1024

int		main(int argc, char *argv[])
{
	int					sock;
	char				buf[BUF_SIZE];
	struct sockaddr_in	serv_addr;
	FILE				*readfp, *writefp;

	sock = socket(PF_INET, SOCK_STREAM, 0);
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
	serv_addr.sin_port = htons(atoi(argv[2]));

	::connect(sock, (sockaddr*)&serv_addr, sizeof(serv_addr));
	readfp = fdopen(sock, "r");
	writefp = fdopen(sock, "w");

	while (1)
	{
		if (fgets(buf, sizeof(buf), readfp) == NULL)
			break;
		fputs(buf, stdout);
		fflush(stdout);
	}
	
	fputs("FROM CLIENT : Thank you! \n", writefp);
	fflush(writefp);

	fclose(writefp);
	fclose(readfp);
	return 0;
}

fclose() 함수 호출로 쓰기모드 FILE 포인터만 닫아준다고 해도 Half-close 대신 완전종료가 이루어진다는 문제가 발생


16-2 : 파일 디스크립터의 복사와 Half-close

읽기모드와 쓰기모드의 FILE 포인터는 모두 하나의 파일 디스크립터를 기반으로 생성
따라서 둘 중 어느 포인터를 대상으로 fclose() 함수를 호출하더라도 파일 디스크립터가 종료되기 때문에 소켓의 완전종료로 이어짐
즉, 이를 해결하기 위해서는 FILE 포인터를 생성하기에 앞서 파일 디스크립터를 복사하여 각각 읽기모드와 쓰기모드 FILE 포인터를 만들어야함
단, 이 상태로 하나의 파일 디스크립터를 닫는다 해도 나머지 하나의 파일 디스크립터가 입출력 모두 가능한 상태이기 때문에 상대 호스트로 EOF가 전송되지 않기 때문에 추가로 Half-close를 진행하기 위한 방법을 거쳐야함


파일 디스크립터의 복사는 fork() 함수 호출로 이루어지는 프로세스의 복사와 달리 하나의 프로세스 내에 원본과 복사본이 모두 존재하게됨
다시말해 파일 디스크립터의 복사란 완전한 복사가 아닌 동일한 파일 또는 소켓의 접근을 위한 또 다른 파일 디스크립터의 생성을 뜻함
파일 디스크립터를 복사하는데는 dup() 또는 dup2() 함수를 사용

#include <unistd.h>

int dup(int fildes);
int dup2(int fildes, int fildes2);
// 성공시 복사된 파일 디스크립터, 실패시 -1 반환

dup() 함수 기능 확인 예제

// dup.cpp

#include <iostream>
#include <unistd.h>

using namespace std;

int	main(int argc, char *argv[])
{
	int		cfd1, cfd2;
	char	str1[] = "Hi~\n";
	char	str2[] = "It's nice day~\n";

	cfd1 = dup(1);
	cfd2 = dup2(cfd1, 7);

	cout << "fd1 = " << cfd1 << " , fd2 = " << cfd2 << endl;
	write(cfd1, str1, sizeof(str1));
	write(cfd2, str2, sizeof(str2));

	close(cfd1);
	close(cfd2);
	write(1, str1, sizeof(str1));
	close(1);
	write(1, str2, sizeof(str2));
	return 0;
}


파일 디스크립터 복사 후 스트림 분리 예제

// sep_serv2.cpp
// 클라이언트는 이전의 sep_clnt.cpp 사용

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

using namespace std;

#define BUF_SIZE 1024

int		main(int argc, char *argv[])
{
	int					serv_sock, clnt_sock;
	struct sockaddr_in	serv_addr, clnt_addr;
	char				buf[BUF_SIZE] = {0,};
	socklen_t			clnt_addr_size;
	FILE				*readfp, *writefp;

	serv_sock = socket(PF_INET, SOCK_STREAM, 0);
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	serv_addr.sin_port = htons(atoi(argv[1]));

	::bind(serv_sock, (sockaddr*)&serv_addr, sizeof(serv_addr));
	::listen(serv_sock, 5);
	clnt_addr_size = sizeof(clnt_addr);
	clnt_sock = accept(serv_sock, (sockaddr*)&clnt_addr, &clnt_addr_size);

	readfp = fdopen(clnt_sock, "r");
	writefp = fdopen(dup(clnt_sock), "w");

	fputs("FROM SERVER : Hi~ client? \n", writefp);
	fputs("I love all of the world \n", writefp);
	fputs("You are awesome! \n", writefp);
	fflush(writefp);

	shutdown(fileno(writefp), SHUT_WR);
	fclose(writefp);

	fgets(buf, sizeof(buf), readfp);
	fputs(buf, stdout);

	fclose(readfp);
	close(serv_sock);
	return 0;
}

내용 확인문제

  1. 다음 중 FILE 구조체의 포인터와 파일 디스크립터에 대한 설명으로 옳지 않은 것을 모두 고르면?
    • FILE 구조체의 포인터와 마찬가지로 파일 디스크립터도 입력용과 출력용이 나뉜다. (X)
    • 파일 디스크립터가 복사되면 동일한 정수 값의 파일 디스크립터가 하나 더 추가되어서 총 두 개의 파일 디스크립터로 데이터를 입출력 할 수 있게 된다. (X)
    • 소켓 생성시 반환된 파일 디스크립터를 이용하여 입출력을 진행하는데, 파일 디스크립터를 거치지않고 바로 소켓의 FILE 구조체 포인터를 반환받아서 입출력에 사용하는 것도 가능하다. (X)
    • 파일 디스크립터를 기반으로 FILE 구조체의 포인터를 생성하는 것은 가능하다. 그리고 이렇게 생성된 FILE 구조체의 포인터를 이용하면 소켓 기반의 데이터 입출력도 가능하다. (O)
    • 파일 디스크립터가 읽기모드이면, 이를 기반으로 생성되는 FILE 구조체의 포인터도 읽기모드가 되고, 파일 디스크립터가 쓰기모드이면, 이를 기반으로 생성되는 FILE 구조체의 포인터도 쓰기모드가 된다. (X)
  2. EOF의 전송과 관련해서 옳지 않은 것을 모두 고르면?
    • 파일 디스크립터가 종료되면 EOF가 전달된다. (O)
    • 파일 디스크립터를 완전히 종료하지 않아도 출력 스트림에 대해서 종료를 진행하면 EOF가 전달된다. (O)
    • 파일 디스크립터가 복사되고 나면, 복사된 파일 디스크립터까지 모두 종료되어야 EOF가 전달된다. (O)
    • 파일 디스크립터가 복사된 상황이라 할지라도 shudow() 함수호출을 통해서 Half-close를 진행하면 상대 호스트로 EOF가 전달된다. (O)