Chapter 13 - 다양한 입출력 함수들

13-1 : send & recv 입출력 함수

리눅스에도 send(), recv() 함수가 존재함

#include <sys/socket.h>

ssize_t	send(int sockfd, const void *buf, size_t nbytes, int flags);
// 성공시 전송된 바이트 수, 실패시 -1 반환

ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags);
// 성공시 수신한 바이트 수(단, EOF 전송시 0), 실패시 -1 반환

윈도우의 send(), recv() 함수와 자료형 이름 외에는 차이가 없음

send(), recv() 함수 모두 마지막 매개변수로 데이터 송수신시 적용할 옵션정보가 전달됨
옵션정보는 비트로 사용되기 때문에 비트 OR 연산자를 이용해 둘 이상을 함께 전달할 수 있음

  • MSG_OOB : 긴급 데이터(Out-of-bnad data)의 전송을 위한 옵션
  • MSG_PEEK : 입력버퍼에 수신된 데이터의 존재유무 확인을 위한 옵션
  • MSG_DONTROUTE : 데이터 전송과정에서 라우팅(Routing) 테이블을 참조하지 않을 것을 요구하는 옵션 - 따라서 로컬(Local) 네트워크상에서 목적지를 찾을 때 사용되는 옵션
  • MSG_DONTWAIT : 입출력 함수 호출과정에서 블로킹 되지 않을 것을 요구하기 위한 옵션 - 즉, 논블로킹(Non-blocking) IO 요구에 사용되는 옵션
  • MSG_WAITALL : 요청한 바이트 수에 해당하는 데이터가 전부 수신될 때까지, 호출된 함수가 반환되는 것을 막기 위한 옵션

MSG_OOB 옵션 사용한 데이터 송신 예시

// oob_send.cpp

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

using namespace std;

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

int		main(int argc, char *argv[])
{
	int					sock;
	struct sockaddr_in	recv_adr;

	if(argc != 3)
	{
		cout << "Usage : " << argv[0] << " <IP> <port>\n";
		exit(1);
	}
	
	sock = socket(PF_INET, SOCK_STREAM, 0);
	memset(&recv_adr, 0, sizeof(recv_adr));
	recv_adr.sin_family = AF_INET;
	recv_adr.sin_addr.s_addr = inet_addr(argv[1]);
	recv_adr.sin_port = htons(atoi(argv[2]));

	if (connect(sock, (struct sockaddr*)&recv_adr, sizeof(recv_adr)) == -1)
		error_handling("connect() error!");
	
	write(sock, "123", strlen("123"));
	send(sock, "4", strlen("4"), MSG_OOB);
	write(sock, "567", strlen("567"));
	send(sock, "890", strlen("890"), MSG_OOB);
	close(sock);
	return 0;
}

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

MSG_OOB 옵션 사용한 데이터 수신 예시

// oob_recv.cpp

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>

using namespace std;

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

int		acpt_sock;
int		recv_sock;

int		main(int argc, char *argv[])
{
	int					str_len, state;
	struct sockaddr_in	recv_adr, serv_adr;
	socklen_t			serv_adr_sz;
	struct sigaction	act;
	char				buf[BUF_SIZE];

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

	act.sa_handler = urg_handler;
	sigemptyset(&act.sa_mask);
	act.sa_flags = 0;
	
	acpt_sock = socket(PF_INET, SOCK_STREAM, 0);
	memset(&recv_adr, 0, sizeof(recv_adr));
	recv_adr.sin_family = AF_INET;
	recv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
	recv_adr.sin_port = htons(atoi(argv[1]));

	if (::bind(acpt_sock, (struct sockaddr*)&recv_adr, sizeof(recv_adr)) == -1)
		error_handling("bind() error");
	::listen(acpt_sock, 5);
	
	serv_adr_sz = sizeof(serv_adr);
	recv_sock = accept(acpt_sock, (struct sockaddr*)&serv_adr, &serv_adr_sz);

	fcntl(recv_sock, F_SETOWN, getpid());
	state = sigaction(SIGURG, &act, 0);

	while ((str_len = recv(recv_sock, buf, sizeof(buf), 0)) != 0)
	{
		if (str_len == -1)
			continue;
		buf[str_len] = 0;
		cout << buf << endl;
	}
	close(recv_sock);
	close(acpt_sock);
	return 0;
}

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

void	urg_handler(int signo)
{
	int		str_len;
	char	buf[BUF_SIZE];

	str_len = recv(recv_sock, buf, sizeof(buf) - 1, MSG_OOB);
	buf[str_len] = 0;
	cout << "Urgent message : " << buf << endl;
}

fcntl() 함수를 통해 파일 디스크립터 recv_sock이 가리키는 소켓에 의해 발생하는 시그널을 처리하는 프로세스를 getpid() 함수가 반환하는 ID의 프로세스로 변경
이를 통해 현재 프로세스를 시그널의 처리 주체로 지정함

MSG_OOB 옵션으로 전송되는 것은 1바이트 뿐
이는 TCP에 Out-of-band 데이터, 즉 전혀 다른 통신 경로로 전송되는 데이터가 실질적으로 존재하지 않기 때문임
따라서 MSG_OOB 옵션은 수신 대상에게 데이터 처리를 독촉하는데에 의미가 있음

MSG_OOB 옵션 설정시 데이터의 출력버퍼에서 마지막 데이터 다음(오프셋 1 증가) 위치인 Urgent Pointer를 가리키면서 해당 정보를 상대 호스트에게 전달
결국 수신하는 상대방은 Urgent Pointer 앞 부분의 1바이트를 제외한 나머지는 일반적인 입력함수의 호출을 통해서 읽게됨


MSG_PEEK 옵션을 사용한 데이터 송신 예시

// peek_send.cpp

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

using namespace std;

void	error_handling(char *message);

int		main(int argc, char *argv[])
{
	int					sock;
	struct sockaddr_in	recv_adr;

	if(argc != 3)
	{
		cout << "Usage : " << argv[0] << " <IP> <port>\n";
		exit(1);
	}
	
	sock = socket(PF_INET, SOCK_STREAM, 0);
	memset(&recv_adr, 0, sizeof(recv_adr));
	recv_adr.sin_family = AF_INET;
	recv_adr.sin_addr.s_addr = inet_addr(argv[1]);
	recv_adr.sin_port = htons(atoi(argv[2]));

	if (connect(sock, (struct sockaddr*)&recv_adr, sizeof(recv_adr)) == -1)
		error_handling("connect() error!");
	
	write(sock, "123", strlen("123"));
	close(sock);
	return 0;
}

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

MSG_PEEK 옵션을 사용한 데이터 수신 예시

// peek_recv.cpp

#include <iostream>
#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					acpt_sock, recv_sock;
	int					str_len, state;
	struct sockaddr_in	acpt_adr, recv_adr;
	socklen_t			recv_adr_sz;
	char				buf[BUF_SIZE];

	if(argc != 2)
	{
		cout << "Usage : " << argv[0] << " <port>\n";
		exit(1);
	}
	
	acpt_sock = socket(PF_INET, SOCK_STREAM, 0);
	memset(&acpt_adr, 0, sizeof(acpt_adr));
	acpt_adr.sin_family = AF_INET;
	acpt_adr.sin_addr.s_addr = htonl(INADDR_ANY);
	acpt_adr.sin_port = htons(atoi(argv[1]));

	if (::bind(acpt_sock, (struct sockaddr*)&acpt_adr, sizeof(acpt_adr)) == -1)
		error_handling("bind() error");
	::listen(acpt_sock, 5);
	
	recv_adr_sz = sizeof(recv_adr);
	recv_sock = accept(acpt_sock, (struct sockaddr*)&recv_adr, &recv_adr_sz);

	while (1)
	{
		str_len = recv(recv_sock, buf, sizeof(buf) - 1, MSG_PEEK | MSG_DONTWAIT);
		if (str_len > 0)
			break;
	}

	buf[str_len] = 0;
	cout << "Buffering " << str_len << " bytes : " << buf << endl;

	str_len = recv(recv_sock, buf, sizeof(buf) - 1, 0);
	buf[str_len] = 0;
	cout << "Read again : " << buf << endl;

	close(acpt_sock);
	close(recv_sock);
	return 0;
}

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

MSG_PEEK 옵션 사용시 데이터가 읽혀지더라도 입력버퍼에서 지워지지 않음
따라서 MSG_DONTWAIT 옵션과 함께 사용하여 데이터의 존재유무를 확인하기 위한 블로킹 되지 않는 함수의 호출 구성에 사용


13-2 : readv & writev 입출력 함수

readv(), writev() 함수는 데이터를 모아서 송수신하는 기능을 함
즉, 여러 버퍼에 나뉘어져 있는 데이터를 한번에 전송하거나 데이터를 여러 버퍼에 나눠서 수신할 수 있기 때문에 입출력 함수호출의 수를 줄일 수 있음

#include <sys/uio.h>

ssize_t writev(int filedes, const struct iovec *iov, int iovcnt);
// 성공시 전송된 바이트 수, 실패시 -1 반환

ssize_t readv(int filedes, const struct iovec *iov, int iovcnt);

struct iovec
{
	void	*iov_base;	// 버퍼의 주소 정보
	size_t	iov_len;	// 버퍼의 크기 정보
}

구조체 iovec는 전송할 데이터가 저장되어있는 버퍼의 주소값 및 크기 정보를 정의

writev() 함수를 사용한 예시

// writev.cpp

#include <iostream>
#include <sys/uio.h>

using namespace std;

int main(int argc, char *argv[])
{
	struct iovec	vec[2];
	char			buf1[] = "ABCDEFG";
	char			buf2[] = "1234567";
	int				str_len;

	vec[0].iov_base = buf1;
	vec[0].iov_len = 3;
	vec[1].iov_base = buf2;
	vec[1].iov_len = 4;

	str_len = writev(1, vec, 2);
	cout << " " << endl;
	cout << "Write bytes : " << str_len << endl;
	return 0;
}

readv() 함수를 사용한 예시

// readv.cpp

#include <iostream>
#include <sys/uio.h>

using namespace std;
#define	BUF_SIZE 100

int main(int argc, char *argv[])
{
	struct iovec	vec[2];
	char			buf1[BUF_SIZE] = {0, };
	char			buf2[BUF_SIZE] = {0, };
	int				str_len;

	vec[0].iov_base = buf1;
	vec[0].iov_len = 5;
	vec[1].iov_base = buf2;
	vec[1].iov_len = BUF_SIZE;

	str_len = readv(0, vec, 2);
	cout << "Read bytes : " << str_len << endl;
	cout << "First message : " << buf1 << endl;
	cout << "Second message : " << buf2 << endl;
	return 0;
}

전송해야 할 데이터가 여러 개의 버퍼에 나뉘어져있거나, 수신된 데이터를 여러 저장소에 나눠서 읽어들이고 싶은 경우 경우 효율적
특히 전송되는 패킷의 수가 줄어들기 때문에 Nagle 알고리즘이 중지된 상황에서 활용하기가 더 좋음


13-3 : 윈도우 기반으로 구현하기

윈도우 기반에서 MSG_OOB 옵션을 사용한 예시
윈도우에는 시그널 핸들링이 존재하지 않기 때문에 select() 함수를 사용하여 구현

// oob_send_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;
	SOCKADDR_IN	sendAdr;

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

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

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

	if (connect(hSocket, (SOCKADDR*)&sendAdr, sizeof(sendAdr)) == SOCKET_ERROR)
		ErrorHandling("connect() error!");

	send(hSocket, "123", 3, 0);
	send(hSocket, "4", 1, MSG_OOB);
	send(hSocket, "567", 3, 0);
	send(hSocket, "890", 3, MSG_OOB);

	closesocket(hSocket);
	WSACleanup();
	return 0;
}

void	ErrorHandling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}
// oob_send_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		hAcptSock, hRecvSock;
	SOCKADDR_IN	sendAdr, recvAdr;
	int			sendAdrSize, strLen;
	char		buf[BUF_SIZE];
	int			result;
	
	fd_set			read, except, readCopy, exceptCopy;
	struct timeval	timeout;

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

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

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

	if (::bind(hAcptSock, (SOCKADDR*)&recvAdr, sizeof(recvAdr)) == SOCKET_ERROR)
		ErrorHandling("bind() error!");
	if (::listen(hAcptSock, 5) == SOCKET_ERROR)
		ErrorHandling("listen() error");

	sendAdrSize = sizeof(sendAdr);
	hRecvSock = accept(hAcptSock, (SOCKADDR*)&sendAdr, &sendAdrSize);
	FD_ZERO(&read);
	FD_ZERO(&except);
	FD_SET(hRecvSock, &read);
	FD_SET(hRecvSock, &except);

	while (1)
	{
		readCopy = read;
		exceptCopy = except;
		timeout.tv_sec = 5;
		timeout.tv_usec = 0;

		result = select(0, &readCopy, 0, &exceptCopy, &timeout);
		if (result > 0)
		{
			if (FD_ISSET(hRecvSock, &exceptCopy))
			{
				strLen = recv(hRecvSock, buf, BUF_SIZE - 1, MSG_OOB);
				buf[strLen] = 0;
				cout << "Urgent message : " << buf << endl;
			}

			if (FD_ISSET(hRecvSock, &readCopy))
			{
				strLen = recv(hRecvSock, buf, BUF_SIZE - 1, 0);
				if (strLen == 0)
				{
					break;
					closesocket(hRecvSock);
				}
				else
				{
					buf[strLen] = 0;
					cout << buf << endl;
				}
			}
		}
	}

	closesocket(hSocket);
	WSACleanup();
	return 0;
}

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


writev()readv() 함수는 윈도우에 정의되어있지 않으나, 중첩 입출력(Overlapped IO)를 이용하면 동일한 효과를 얻을 수 있음


내용 확인문제

  1. 다음 중, 데이터 전송옵션인 MSB_OOB에 대해서 잘못 설명한 것을 모두 고르면?
    • MSG_OOB는 Out-of-band 데이터의 전송을 의미한다. 그리고 이는 다른 경로를 통한 고속의 데이터 전송이라는 의미를 갖는다. (O)
    • MSG_OOB는 다른 경로를 통한 고속의 데이터 전송이라는 의미를 갖기 때문에, TCP상에서도 이 옵션을 이용해서 전송된 데이터는 상대 호스트로 먼저 전송된다. (X)
    • MSG_OOB 옵션이 주어진 상태에서 상대 호스트로 데이터가 먼저 전송된 이후에는 일반 데이터와 동일한 형태와 순서로 읽혀진다. 즉, 전송이 빠를 뿐, 수신 측에서는 이를 인지하지 못한다. (X)
    • MSG_OOB는 TCP의 기본 데이터 전송방식을 벗어나지 못한다. 즉, MSG_OOB 옵션이 지정되더라도 전송순서는 그대로 유지된다. 다만 이는 수신 측에 데이터 처리의 긴급을 요청하는 용도로 사용될 뿐이다. (O)
  2. readv & writev 함수를 이용해서 데이터를 송수신 할 경우 어떠한 이점이 있는지 함수 호출의 횟수와 입출력 버퍼의 관점에서 각각 설명해보자.

    입력 버퍼를 한번에 읽어들이고 출력 버퍼들에 한번에 쓰기 때문에 함수 호출의 횟수가 줄어듬

  3. recv 함수호출을 통해서 입력버퍼의 데이터 존재유무를 확인하고자 할 때(확인 후 바로 반환하고자 할 때), recv 함수의 마지막 전달인자인 데이터 전송의 옵션을 어떻게 구성해야 하는가? 그리고 각각의 옵션이 의미하는 바는 무엇인지도 설명해 보자.

    MSG_PEEK 옵션으로 입력버퍼의 데이터를 읽어들이는데, 이 때 MSG_DONTWAIT 옵션도 함께 사용하여 입력버퍼가 비어있더라도 블로킹이 되지 않도록 구성

  4. 리눅스에서는 MSG_OOB 데이터의 수신을 이벤트 핸들러의 등록을 통해서 확인이 가능하다. 그렇다면 윈도우에서는 어떻게 MSB_OOB 데이터의 수신을 확인할 수 있는지, 그 방법을 설명해보자.

    윈도우에서는 select() 함수에 exceptset을 등록하여 확인