나의 브을로오그으

#13. 다양한 입출력 함수들 본문

네트워크/열혈 TCP_IP 소켓프로그래밍

#13. 다양한 입출력 함수들

__jhp_+ 2022. 8. 31. 19:37

#1. send & recv 입출력 함수

윈도우 기반 예제에서는 send & recv함수를 사용했지만, 정작 마지막 인자에 0이외의 인자를 전달해본적이 없다. 즉 윈도우에서 사용한것 조차 제대로 사용하지 않은 것이다.

 

리눅스에서의 send & recv

#include <sys/socket.h>

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

sockfd  : 데이터 전송 대상과의 연결을 의미하는 소켓의 파일 디스크립터 전달

buf : 전송할 데이터를 저장하고 있는 버퍼의 주소 값 전달.

nbytes : 전송할 바이트 수 전달.

flags : 데이터 전송 시 적용할 다양한 옵션 정보 전달.

(위의 send 함수는 윈도우의 send함수와 이름만 차이가 있을뿐 인자의 의미, 순서까지 완전히 동일하다.)

 

#include <sys/socket.h>

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

sockfd : 데이터 수신 대상과의 연결을 의미하는 소켓의 파일 디스크립터 전달.

buf : 수신된 데이터를 저장할 버퍼의 주소 값 전달.

nbytes : 수신할 수 있는 최대 바이트 수 전달.

flags : 데이터 수신 시 적용할 다양한 옵션 정보 전달.

 

(send함수와 마찬가지로 recv함수도 window recv 함수와 별차이가 없다. 

그러면 이제 마지막 인자에 해당하는 옵션정보를 알아보자. 옵션정보는 OR연산자(|)를 통해서 둘 이상을 함께 전달할 수 있다.)

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

(해당 옵션은 운영체제마다 조금씩 다를 수 있다. 따라서 실제 개발 시에는 사용할 운영체제에 대한 정보가 필요하다.)

 

 

MSG_OOB: 긴급 메시지의 전송

옵션 MSG_OOB는 'Out-of-band data'라 불리는 긴급 메시지의 전송에 사용된다. 예를 들어서 변원에서 진료를 받기 위해 사람들이 대기 중에 있다고 가정해보자. 그런데 이 병원에 갑자기 응급 환자가 들어온다면?

"당연히 먼저 처리해 줘야한다." 응급 환자의 수가 적지 않을때는 대기 중에 있는 사람들의 양해가 필요할 것이다.

바로 이러한 문제때문에 병원에서는 읍급실을 운영하는것이 아니겠는가? 즉, 긴급으로 무언가를 처리하려면, 처리방법 및 경로가 달라져야 한다. 그럼 이제 간단한 예제를 통해 MSG_OOB옵션을 추가해서 데이터를 송수신해 보겠다.

 

[oob_send.c]

/* linux */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>

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

int main(int argc, char* argv[])
{
	int sock;
	struct sockaddr_in recv_adr;
	if (argc != 3) {
		printf("Usage : %s <IP> <port> \n", argv[0]);
		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, (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* msg)
{
	fputs(msg, stderr);
	fputc('\n', stderr);
	exit(1);
}

1. 데이터의 전송이 진행된 후 send()함수에서는 MSG_OOB 옵션을 줌으로써 긴급으로 데이터를 전송하고 있다. 일반적인 도착 순서는 123,4,567,890 의 순으로 전달되어야 하지만, 이 중에서 4와 890을 긴급으로 전송하였으므로 4, 890, 123, 567순으로 예상해 볼 수 있다. (실제로 실행결과 그럴까?)

 

[oob_recv.c]

/* linux */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>

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

int acpt_sock;
int recv_sock;

int main(int argc, char* argv[])
{
	struct sockaddr_in recv_adr, serv_adr;
	int str_len, state;
	socklen_t serv_adr_sz;
	struct sigaction act;
	char buf[BUF_SIZE];
	if (argc != 2) {
		printf("Usage : %s <port> \n", argv[0]);
		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)
	{
		if (str_len == -1)
			continue;
		buf[str_len] = 0;
		puts(buf);
	}

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


void error_handling(char* msg)
{
	fputs(msg, 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;
	printf("urgent message: %s \n", buf);
}

1. 시그널 SIGURG와 관련된 부분을 자세히 보자. MSG_OOB의 긴급 메시지를 수신하게되면, 운영체제는 SIGURG 시그널을 발생시켜서 프로세스가 등록한 시그널 핸들러가 호출되게 한다. 특히 시그널 핸들러 urg_handler함수가 호출되면 핸들러 함수 내부에서 긴급 메시지의 수신을 위한 recv 함수의 호출문장도 삽입되어 있다.

 

fcntl(recv_sock, F_SETOWN, getpid());

fcntl 함수는 파일 디스크립터의 컨트롤에 사용이 된다. 그런데 여기서 보인 위 문장은 다음의 의미를 갖는다.

"파일 디스크립터 recv_sock이 가리키는 소켓의 소유자(F_SETOWN)를 getpid 함수가 반환하는 ID의 프로세스로 변경시키겠다."

소켓의 소유자? 소켓은 운영체제가 생성 및 관리를 하기 때문에 엄밀히 따지면 소켓의 소유자는 운영체제이다. 다만 여기서 말하는 소유자는 이 소켓에서 발생하는 모든 일의 책임 주체를 의미한다. 

"파일 디스크립터 recv_sock이 가리키는 소켓에 의해 발생하는 SIGURG 시그널을 처리하는 프로세스를 getpid 함수가 반환하는 ID의 프로세스로 변경시키겠다."

위 문장에서 SIGURG 시그널 처리란, "SIGURG 시그널의 핸들러 함수 호출"을 의미한다. 그런데 하나의 소켓에 대한 파일 디스크립터를 여러 프로세스가 함께 소유할 수 있지 않은가? 예를 들어서 fork 함수호출을 통해서 자식 프로세스가 생성되고, 생성과 동시에 파일 디스크립터까지 복사되는 경우도 이에 해당한다. 이러한 상황에서 SIGURG 시그널 발생시 어느 프로세스의 핸들러 함수를 호출해야 하겠는가? 당연히 어떠한 핸들러 함수도 호출되지 않는다!! (만약 호출된다면 문제가 커진다.) 따라서 SIGURG 시그널을 핸들링 할 때에는 반드시 시그널 함수를 처리할 프로세스를 지정해 줘야 한다. 그리고 getpid는 이 함수를 호출한 프로세스의 ID를 반환하는 함수이다. 결국 위의 문장은 현재 실행중은 프로세스를 SIGURG 시그널의 처리 주체로 지정하는 것이다. 이 프로그램에서는 프로세스가 1개뿐이므로 당연히 저런식으로 파일컨트롤 함수를 호출하여 설정해준다.

 

[실행결과 : oob_send.c]

root@my_linux:/tcpip# gcc oob_send.c -o send
root@my_linux:/tcpip# ./send 127.0.0.1 9190

 

[실행결과: oob_recv.c]

root@my_linux:/tcpip# gcc oob_recv.c -o recv
root@my_linux:/tcpip# ./recv 9190
123
Urgent Message: 4
567
Urgent Message: 0
89

"MSG_OOB 옵션을 추가해서 데이터를 전달할 경우, 딱 1바이트만 반환하고, 별로 빠르지도 않은것 같다!!!"

결론은 그러하다.!! 실제로 MSG_OOB옵션을 추가한다고 해서 데이터가 빨리 전송되지도 않으며, 시그널 핸들러인 urg_handler에 의해 읽히는 데이터의 크기도 1바이트밖에 되지 않는다. 나머지는 MSG_OOBJ 옵션이 추가되지 않은 일반적인 입력함수의 호출을 통해서 읽히고 만다. 왜냐하면 TCP에는 진정한 의미의 'Out-of-band data'가 존재하지 않기 때문이다. 사실 MSG_OOB에서의 OOB는 Out-of-band를 의미한다.

"전혀 다른 통신 경로로 전송되는 데이터"

즉, 진정한 의미의 Out-of-band 형태로 데이터가 전송되려면 별도의 통신 경로가 확보되어서 고속으로 데이터가 전달되어야 한다. 하지만 TCP는 별도의 통신 경로를 제공하지 않고 있다. 다만 TCP에 존재하는 Urgent mode라는 것을 이용해서 데이터를 전송해줄 뿐이다.

 

Urgent mode의 동작원리

MSG_OOB옵션은 그러면 대체 어떤 효과를??

"MSG_OOB 옵션 적용 데이터의 경우 긴급으로 처리해야 할 데이터이므로 꾸물거리지마!" 라고 독촉하게 된다.

이게 MSG_OOB의 진정한 의미이다. (엥?) 이것이 전부이고 데이터의 전송에는 "전송순서가 그대로 유지된다"라는 TCP 전송특성 역시 그대로 유지된다. (이게 긴급 메시지?)

긴급 메시지가 맞다. 전송자가 데이터의 처리를 재촉하는 상황에서 보내지기 때문이다.

TCP의 긴급 메시지는 빠른 이동을 보장하지는 않는다. 대신 긴급 메시지 처리를 하도록 요구하게 된다.

실제로 oob_recv.c의 실행과정에서 긴급 메시지가 이벤트 핸들러를 통해 인지되지 않았는가? 이것이 바로 MSG_OOB 모드 데이터 전송의 실제 의미이다.

<Output Buffer>

offset 0 1 2 3 4 5 6
  8 9 0 ... ... ... ...

TCP 헤더를 보면 URG = 1, URG Pointer=3 과 같은 정보가 들어있는데, URG = 1은 긴급 메시지가 존재하는 패킷이다. 라는 의미이며 URG Pointer는 Urgent Pointer의 위치가 오프셋 3의 위치에 있다를 의미한다.

 

자! 이렇게 MSG_OOB 옵션이 지정되면 패킷 자체가 긴급 패킷이 되며 Urgent Pointer를 통해서 긴급 메시지의 위치도 표시가 된다. 그러나 다음 사실은 알수가 없다.

"긴급 메시지가 문자열 890이야, 아니면 90인가? 아니면 0인가?"
그런데 이는 별로 중요하지 않다. 어차피 데이터를 수신하는 상대방은 Urgent Pointer의 앞 부분에 위치한 1바이트를 제외한 나머지는 일반적인 입력함수의 호출을 통해서 읽히기 때문이다. 다시한번 말하지만 긴급 메시지는 메시지 처리를 재촉하는데 의미가 있는 것이지 제한된 형태의 메시지를 긴급으로 전송하는데 의미가 있는 것은 아니다.

 

[컴퓨터 공학에서 말하는 오프셋(offset)]

"기본이 되는 위치를 바탕으로 상대적 위치를 표현하는 것이 오프셋이다."

 

입력버퍼 검사하기

MSG_PEEK옵션은 MSG_DONTWAIT옵션과 함께 설정되어 입력버퍼에 수신 된 데이터가 존재하는지 확인하는 용도로 사용된다. MSG_PEEK 옵션을 주고 recv 함수를 호출하면 입력버퍼에 존재하는 데이터가 읽혀지더라도 입력버퍼에서 데이터가 지워지지 않는다. 때문에 MSG_DONTWAIT 옵션과 묶여서 블로킹 되지 않는, 데이터의 존재유무를 확인하기 위한 함수의 호출 구성에 사용된다.

 

[peek_recv.c]

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>

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

int main(int argc, char* argv[])
{
	int acpt_sock, recv_sock;
	struct sockaddr_in acpt_adr, recv_adr;
	int str_len, state;
	socklen_t recv_adr_sz;
	char buf[BUF_SIZE];
	if (argc != 2) {
		printf("Usage : %s <port> \n", argv[0]);
		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 (true)
	{
		str_len = recv(recv_sock, buf, sizeof(buf) - 1, MSG_PEEK | MSG_DONTWAIT);
		if (str_len > 0)
			break;
	}

	buf[str_len] = 0;
	printf("Buffering %d bytes: %s \n", str_len, buf);

	str_len = recv(recv_sock, buf, sizeof(buf) - 1, 0);
	buf[str_len] = 0;
	printf("Read again : %s \n", buf);
	close(acpt_sock);
	close(recv_sock);
	return 0;
}


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

1. recv 함수를 호출하면서 MSG_PEEK을 옵션으로 전달하고 있다. MSG_DONTWAIT옵션을 함께 전달한 이유는 데이터가 존재하지 않아도 블로킹 상태에 두지 않기 위해서이다.

2. recv 함수를 한번 더 호출하고 있다. 이번엔 아무런 옵션도 설정하지 않았다. 때문에 이번에 읽어 들인 데이터는 입력버퍼에서 지워진다.

 

[실행결과 : peek_recv.c]

root@my_linux:/tcpip# peek_recv.c -o recv
root@my_linux:/tcpip# ./recv 9190
Buffering 3 bytes: 123
Read again: 123

 

[실행결과 : peed_send.c]

root@my_linux:/tcpip# gcc peek_send.c -o send
root@my_linux:/tcpip# ./send 127.0.0.1 9190

실행결과를 통해서 한번밖에 전송되지 않은 데이터가 두 번 읽혀진 것을 확인할 수 있다. 첫 번째 recv함수호출 시 MSG_PEEK 옵션을 지정했기 때문이다. 이로써 MSG_PEEK 옵션의 기능을 정확히 확인하였다.

 

 

 

#2. readv & writev 입출력 함수

readv & writev 입출력 함수는 데이터 송수신의 효율성을 향상시키는데 도움이 되는 함수들이다.

 

readv & writev 함수의 사용

"데이터를 모아서 전송하고, 모아서 수신하는 기능의 함수"

즉, writev 함수를 사용하면 여러 버퍼에 나뉘어 저장되어 있는 데이터를 한번에 전송할 수 있고, 또 readv 함수를 사용하면 데이터를 여러 버퍼에 나눠서 수신할 수 있다. 때문에 적절한 상황에서 사용을 하면 입출력 함수호출의 수를 줄일 수 있다.

#include <sys/uio.h>

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

filedes : 데이터 전송의 목적지를 나타내는 소켓의 파일 디스크립터 전달, 단 소켓에만 제한된 함수가 아니기 때문에, read 함수처럼 파일이나 콘솔 대상의 파일 디스크립터도 전달 가능하다.

iov : 구조체 iovec 배열의 주소 값 전달, 구조체 iovec의 변수에는 전송할 데이터의 위치 및 크기 정보가 담긴다.

iovcnt : 두 번째 인자로 전달된 주소 값이 가리키는 배열의 길이정보 전달.

 

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

이렇듯 구조체 iovec은 전송할 데이터가 저장되어 있는 버퍼(char형 배열)의 주소 값과 실제 전송할 데이터의 크기 정보를 담기 위해 정의되었다.

 

 

[writev.c]

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

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[2].iov_base = 4;

	str_len = writev(1, vec, 2);
	puts("");
	printf("Write bytes: %d \n", str_len);
	return 0;
}

1. 전송할 데이터의 저장된 위치와 크기정보를 담는다.

2. writev 함수의 첫 번째 전달인자가 1이므로 콘솔로 출력이 이뤄진다.

 

[실행결과 : writev.c]

root@my_linux:/tcpip# gcc writev.c -o WV
root@my_linux:/tcpip# ./WV
ABC1234
Write bytes: 7

 

#include <sys/uio.h>

ssize_t readv(int filedes, const struct iovec* iov, int iovcnt);
-> 성공 시 수신된 바이트 수, 실패 시 -1 반환

filedes : 데이터를 수신할 파일(혹은 소켓)의 파일 디스크립터를 인자로 전달.

iov : 데이터를 저장할 위치와 크기 정보를 담고 있는 iovec 구조체 배열의 주소 값 전달.

iovcnt : 두 번째 인자로 전달된 주소 값이 가리키는 배열의 길이정보 전달.

 

 

[readv.c]

#include <stdio.h>
#include <sys/uio.h>
#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);
	printf("Read bytes: %d \n", str_len);
	printf("First message: %s \n", buf1);
	printf("Second message: %s \n", buf2);
	return 0;
}

1. 첫번째 데이터를 저장할 저장소를 지정하였는데 이때 길이를 5로 설정했기 때문에 읽어온 데이터의 크기와 상관없이 5바이트만 버퍼에 저장한다.

2. vec[0]의 버퍼에 5바이트만 저장된고, 나머지 데이터는 vec[1]에 등록된 버퍼에 저장된다. 특히 구조체 iovec의 멤버 iov_len에는 버퍼에 저장할 최대 바이트 크기 정보를 저장해야 한다. 

3. readv 함수의 첫 번째 전달인자가 0이기 때문에 콘솔로부터 데이터를 수신한다.

 

[실행결과 : readv.c]

root@my_linux:/tcpip# gcc readv.c -o rv
root@my_linux:/tcpip# ./rv
I like TCP/IP socket programming~
Read bytes: 34
First message: I lik
Second message: e TCP/IP socket programming~

 

readv & writev 함수의 적잘한 사용

이 함수가 과연 언제 필요할까? 이 함수를 사용하는 경우는 모든 경우이다. 예를 들어 전송할 데이터가 여러 버퍼에 나뉘어 있는 경우, 모든 데이터의 전송을 위해서는 여러 번의 write 함수호출이 요구되는데, 이를 딱 한번의 writev 함수호출로 대신할 수 있으니 당연히 효율적이다. 마찬가지로 입력버퍼에 수신된 데이터를 여러 저장소에 나눠서 읽어 들이고 싶은 경우에도 여러 번 read 함수를 호출하는 것 보다 딱 한번 readv 함수를 호출하는 것이 보다 효율적이다.

일단 간단하게 생각해봐도 C언어 차원에서 생각해봐도 함수의 호출횟수가 적으면, 그만큼 성능이 향상되므로 이득이다. 그러나 전송되는 패킷의 수를 줄일 수 있다는데 더 큰 의미가 있다. 

만약 3개의 버퍼에 저장된 데이터를 보내야 하고, 네이글 알고리즘이 off되어 있다면 3개의 패킷으로 보내게 될것이다. 그러나 writev를 사용하면 모든 데이터를 출력버퍼로 밀어 넣기 때문에 하나의 패킷만 생성되어서 전송될 확률이 높다.

그러면 여러 영역에 나뉘어 있는 데이터를 전송순서에 맞춰 하나의 큰 배열에 옮겨다 놓고 한번의 write 함수호출을 통해서 전송을 하면 어떻겠는가? 이렇게 해도 writev 함수를 호출한 것과 결과는 같을것이다! 그러나 writev & readv를 사용하는 것이 여러모로 편리하다.

 

 

#3. 윈도우 기반으로 구현하기

이제 리눅스에서 보인 oob_send.c, oob_recv.c를 윈도우 기반으로 변경해 보고자 한다. 그런데 여기서 한가지 고민이 있다.

"윈도우에는 리눅스에서 보인 형태의 시그널 핸들링이 존재하지 않는다."

oob_send.c와 oob_recv.c의 핵심은 MSG_OOB 옵션의 설정에 있다. 그런데 이옵션에 대한 이벤트 핸들링이 윈도우 기반에서는 불가능하기 때문에 다른 방법을 고민해야 한다. 그래서 우리는 select 함수를 통해 이 문제를 해결하고자 한다.

- 수신한 데이터를 지니고 있는 소켓이 존재하는가?

- 블로킹되지 않고 데이터의 전송이 가능한 소켓은 무엇인가?

- 예외상황이 발생한 소켓은 무엇인가?

 

 

이중 마지막 "예외상황이 발생한 소켓은 무엇인가?"와 관련해서는 별도의 언급이 없었다. 그런데 예외상황이라는 것은 일반적이지 않은 프로그램의 흐름을 의미하기 때문에 Out-of-band 데이터의 수신도 예외상황에 해당이 된다. 즉, select 함수의 이러한 특성을 활용하면 윈도우 기반 예제에서도 Out-of-band 데이터의 수신을 확인할 수 있다.

 

[oob_send_win.c]

#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>

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

#define BUF_SIZE		30
void ErrorHandling(const char* msg);

int main(int argc, char* argv[])
{
	WSADATA wsaData;
	SOCKET hSocket;
	SOCKADDR_IN sendAdr;
	if (argc != 3)
	{
		printf("Usage : %s <IP> <port> \n", argv[0]);
		exit(1);
	}

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

	hSocket = socket(PF_INET, SOCK_STREAM, 0);
	if (hSocket == INVALID_SOCKET) {
		ErrorHandling("socket() error");
	}
	memset(&sendAdr, 0, sizeof(sendAdr));
	sendAdr.sin_family = AF_INET;
	sendAdr.sin_addr.S_un.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(const char* msg)
{
	fputs(msg, stderr);
	fputc('\n', stderr);
	exit(1);
}

 

[oob_recv_win.c]

#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>

#pragma comment(lib, "ws2_32.lib")
#define BUF_SIZE		30
void ErrorHandling(const char* msg);

int main(int argc, char* argv[])
{
	WSADATA wsaData;
	SOCKET hAcptSock, hRecvSock;

	SOCKADDR_IN recvAdr;
	SOCKADDR_IN sendAdr;
	int sendAdrSize, strLen;
	char buf[BUF_SIZE];
	int result;


	fd_set read, except, readCopy, exceptCopy;
	struct timeval timeout;

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

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

	hAcptSock = socket(AF_INET, SOCK_STREAM, 0);
	if (hAcptSock == SOCKET_ERROR)
	{
		ErrorHandling("socket() error");
	}

	memset(&recvAdr, 0, sizeof(recvAdr));
	recvAdr.sin_family = AF_INET;
	recvAdr.sin_addr.S_un.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("listem() error");
	}

	sendAdrSize = sizeof(sendAdr);
	hRecvSock = accept(hAcptSock, (SOCKADDR*)&recvAdr, &sendAdrSize);
	if (hRecvSock == SOCKET_ERROR)
	{
		ErrorHandling("accept() error");
	}

	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;
				printf("Urgent message: %s \n", buf);
			}

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

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


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

1. 코드가 길어졌지만 select 함수를 호출해서 Out-of-band 데이터를 수신하는 부분을 제외하면 별 다른 내용은 없다.

writev & readv는 함수에 직접 대응하는 함수는 윈도우에 없다. 그러나 윈도우에서 제공하는 '중첩 입출력(Overlapped IO)'를 이용하면 이와 동일한 효과를 얻을 수 있다. 

 

 

[내용 확인문제]

01. 다음 중, 데이터 전송옵션인 MSB_OOB에 대해서 잘못 설명한 것을 모두 고르면?

a. MSG_OOB는 Out-of-band 데이터의 전송을 의미한다. 그리고 이는 다른 경로를 통한 고속의 데이터 전송이라는 의미를 갖는다.

b. MSG_OOB는 다른 경로를 통한 고속의 데이터 전송이라는 의미를 갖기 때문에, TCP상에서도 이 옵션을 이용해서 전송된 데이터는 상대 호스트로 먼저 전송된다.

c. MSG_OOB 옵션이 주어진 상태에서 상대 호스트로 데이터가 먼저 전송된 이후에는 일반 데이터와 동일한 형태와 순서로 읽혀진다. 즉, 전송이 빠를 뿐, 수신 측에서는 이를 인지하지 못한다.

d. MSG_OOB는 TCP의 기본 데이터 전송방식을 벗어나지 못한다. 즉, MSG_OOB 옵션이 지정되더라도 전송순서는 그대로 유지된다. 다만 이는 수신 측에 데이터 처리의 긴급을 요청하는 용도로 사용될 뿐이다.

답)

b. MSG_OOB는 Out-of-band를 통한 데이터 전송에 의미가 있기 때문에 상대 호스트로 먼저 전송되는지가 중요하지 않다. 따라서 먼저 전송 될수도 안될수도 있다.

c. b와 마찬가지로 MSG_OOB 긴급 전송은 빠르게 도착한다가 아니라 Out-of-band를 통해 전송한다는데에 의미가 있다. 따라서 전송이 빠르다고 말하기 어려우며, 수신 측에서 이를 인지하지 못한다도 틀린 말이다.

 

02. readv & writev 함수를 이용해서 데이터를 송수신 할 경우 어떠한 이점이 있는지 함수 호출의 횟수와 입출력 버퍼의 관점에서 각각 설명해 보자.

답)

readv & writev 함수는 여러 버퍼에 저장된 데이터를 한번에 보내거나, 한번에 나누어서 수신이 가능하다. 호출 시 하나의 패킷에 밀어넣어 보내기 때문에 하나의 패킷만 생성될 확률이 높다. 따라서 굉장히 유용하고 일반적인 함수 read & write 함수를 호출하는 것보다 상대적으로 빠르다.

 

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

답)

MSG_PEEK | MSG_DONTWAIT 옵션을 준다.

각 옵션의 의미는 MSG_PEEK는 입력버퍼에 수신된 데이터의 존재유무를 확인 할 때 쓰는 옵션이며, MSG_DONTWAIT는 Non-Blocking 으로 입출력 함수를 호출하고자 할 때 쓰는 옵션이다.

 

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

답)

윈도우에서는 시그널 핸들링을 지원하지 않는다. 따라서 멀티플렉싱 기술(select)을 이용하여 해당 소켓의 핸들을 관찰대상으로 등록한다. MSG_OOB 데이터의 경우 Out-of-band 데이터 수신에 해당하며 이는 일반적인 데이터 흐름이 아니므로 예외상황이 발생한 것이라고 취급한다. 따라서 select 함수를 통해 수신여부를 체크하여 데이터를 확인할 수 있다.