Chapter 15 - 소켓과 표준 입출력

15-1 : 표준 입출력 함수의 장점

표준 입출력 함수는 이식성(Portability)이 뛰어나며 버퍼링을 통한 성능 향상에 도움이 됨

  • 표준 입출력 함수를 사용시 소켓의 입출력 버퍼와는 별도의 추가적인 입출력 버퍼가 제공됨
  • 따라서 표준 입출력 함수 버퍼에 데이터가 먼저 전달되고, 이 데이터가 소켓의 출력버퍼로 이동된 후 마지막으로 상대방에게 전송됨
  • 전송해야할 데이터의 양이 많을수록 버퍼링의 유무에 따른 성능차가 발생함
    • 이는 데이터 전송 패킷에 헤더정보가 들어가기 때문
    • 즉, 버퍼링으로 한번에 전송하는 데이터의 양을 늘려 출력버퍼로의 데이터 이동 횟수를 줄이는 것이 효율적임


시스템 함수를 이용한 파일복사 프로그램 예시

// syscpy.cpp

#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#define	BUF_SIZE 3

using namespace std;

int main(int argc, char *argv[])
{
	int		fd1, fd2;
	int		len;
	char	buf[BUF_SIZE];

	fd1 = open("news.txt", O_RDONLY);
	fd2 = open("cpy.txt", O_WRONLY | O_CREAT | O_TRUNC);

	while ((len = read(fd1, buf, sizeof(buf))) > 0)
		write(fd2, buf, len);

	close(fd1);
	close(fd2);
	return 0;
}

표준 입출력 함수를 이용한 파일복사 프로그램 예시

// stdcpy.cpp

#include <iostream>
#include <unistd.h>
#define	BUF_SIZE 3

using namespace std;

int main(int argc, char *argv[])
{
	FILE	*fp1;
	FILE	*fp2;
	char	buf[BUF_SIZE];

	fp1 = fopen("news.txt", "r");
	fp2 = fopen("cpy.txt", "w");

	while (fgets(buf, BUF_SIZE, fp1) != NULL)
		fputs(buf, fp2);

	fclose(fp1);
	fclose(fp2);
	return 0;
}

단, 표준 입출력 함수 사용시에도 단점이 존재

  • 양방향 통신이 어려움
  • 상황에 따라 fflush() 함수이 빈번하게 호출될 수 있음
  • 파일 디스크립터를 FILE 구조체의 포인터로 변환해야함

15-2 : 표준 입출력 함수 사용하기

소켓의 파일 디스크립터를 FILE 포인터로 변환하는데는 fdopen() 함수를 사용

#include <stdio.h>

FILE *fdopen(int fildes, const char *mode);
// 성공시 변환된 FILE 구조체 포인터, 실패시 NULL 반환

fdopen() 함수 사용 예시

// desto.cpp

#include <iostream>
#include <fcntl.h>

int	main(void)
{
	FILE	*fp;
	int		fd = open("data.dat", O_WRONLY | O_CREAT | O_TRUNC);
	if (fd == -1)
	{
		fputs("file open error", stdout);
		return -1;
	}

	fp = fdopen(fd, "w");
	fputs("Network C programming \n", fp);
	fclose(fp);
	return 0;
}


fdopen()과 반대로 FILE 포인터를 파일 디스크립터로 변환하기 위해서는 fileno() 함수를 사용

#include <stdio.h>

int	fileno(FILE *stream);
// 성공시 변환된 파일 디스크립터, 실패시 -1 반환

fileno() 함수 사용 예시

// todes.cpp

#include <iostream>
#include <fcntl.h>

using namespace std;

int	main(void)
{
	FILE	*fp;
	int		fd = open("data.dat", O_WRONLY | O_CREAT | O_TRUNC);
	if (fd == -1)
	{
		fputs("file open error", stdout);
		return -1;
	}

	cout << "First file descriptor : " << fd << endl;
	fp = fdopen(fd, "w");
	fputs("TCP/IP SOCKET PROGRAMMING\n", fp);
	cout << "Second file descriptor : " << fileno(fp) << endl;
	fclose(fp);
	return 0;
}

15-3 : 소켓 기반에서의 표준 입출력 함수 사용

표준 입출력 함수를 활용한 에코 서버 예시

// echo_stdserv.cpp

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

using namespace std;

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

int		main(int argc, char *argv[])
{
	int					serv_sock, clnt_sock;
	struct sockaddr_in	serv_addr, clnt_addr;
	int					str_len, i;
	char				message[BUF_SIZE];
	socklen_t			clnt_addr_size;
	FILE				*readfp, *writefp;

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

	serv_sock = socket(PF_INET, SOCK_STREAM, 0);
	if (serv_sock == -1)
		error_handling("socket() error");
	
	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]));

	if (::bind(serv_sock, (sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
		error_handling("bind() error");

	if (::listen(serv_sock, 5) == -1)
		error_handling("listen() error");
	
	clnt_addr_size = sizeof(clnt_addr);

	for (i = 0; i < 5; i++)
	{
		clnt_sock = accept(serv_sock, (sockaddr*)&clnt_addr, &clnt_addr_size);
		if (clnt_sock == -1)
			error_handling("accept() error");
		else
			cout << "Connected client " << i + 1 << endl;

		readfp = fdopen(clnt_sock, "r");
		writefp = fdopen(clnt_sock, "w");
		while (!feof(readfp))
		{
			fgets(message, BUF_SIZE, readfp);
			fputs(message, writefp);
			fflush(writefp);
		}

		fclose(readfp);
		fclose(writefp);
	}

	close(serv_sock);
	return 0;
}

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

표준 입출력 함수를 활용한 에코 클라이언트 예시

// echo_stdclnt.cpp

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

using namespace std;

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

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

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

	sock = socket(PF_INET, SOCK_STREAM, 0);
	if (sock == -1)
		error_handling("socket() error");
	
	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]));

	if (::connect(sock, (sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
		error_handling("connect() error");
	else
		cout << "Connected.........\n";

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

	while (1)
	{
		cout << "Input message(Q to quit) : ";
		fgets(message, BUF_SIZE, stdin);
		
		if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
			break;

		fputs(message, writefp);
		fflush(writefp);
		fgets(message, BUF_SIZE, readfp);
		cout << "Message from server : " << message;
	}

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

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

내용 확인문제

  1. 표준 입출력 함수를 사용했을 때 얻게 되는 장점 두가지는 무엇인가? 그리고 두가지 장점을 얻게 되는 이유는 또 무엇인가?

    이식성이 뛰어나며 버퍼링을 통한 성능향상
    표준 입출력 함수 사용시 추가적인 입출력 버퍼가 제공되기 때문

  2. 표준 출력함수를 이용해서 데이터를 전송하는 경우에는 다음과 같이 생각하는 것은 옳지 않다.
    fputs 함수호출을 통해서 데이터를 전송하면, 함수가 호출되자마자 데이터의 전송이 시작될거야!”
    그렇다면 위와 같은 생각이 옳지 않은 이유는 무엇이고, 위와 같이 생각하기 위해서는 추가로 어떠한 과정이 필요한지 설명해보자.

    fputs() 함수를 통해 데이터 전송시 데이터는 먼저 표준 함수의 출력버퍼에 저장되고, 이후 소켓의 출력버퍼로 이동
    함수호출 이후 곧바로 데이터가 전송되길 바란다면 fflush() 함수를 사용해야함