Part1_IO 고급 소켓 입출력

·2023년 11월 29일
0

[recv & send 입/출력 함수]

recv와 send는 데이터 입/출력 시에 방법에 있어서 옵션을 줄 수 있도록 인자를 하나 더 받는 다는 것을 제외하면 read, write와 동일한 기능을 하는 함수임

#include <sys/types.h>
#include <sys/socket.h>

int recv(int sock, void* buf, int len, unsigned int flags);
int send(int sock, const void *buf, int len, unsigned int flags);

// 성공 시 송/수신한 바이트 수, 실패 시 -1 리턴
// sock : 입/출력의 대상이 되는 소켓의 파일 디스크립터
// buf : 입력 및 출력을 위한 버퍼의 포인터
// len : 전송할 데이터의 바이트 수, 혹은 수신할 수 있는 최대 바이트 수
// flags : 데이터 입/출력 시 그 방법에 있어서 옵션을 설정하는 용도로 사용됨
// 옵션을 설정할 필요가 없어서 0을 인자로 넘길 경우 read, wrtie와 동일한 기능의 함수가 됨
// 옵션은 Bitwise-OR('|')로 묶어서 두 개 이상이 함께 전달될 수 있음

[flags에 들어갈 수 있는 옵션의 종류와 그 의미]

flagsDescriptionrecvsend
MSG_DONTROUTE데이터 전송 시 라우팅 테이블을 참조하지 않는다는 뜻으로 로컬 네트워크상에서 목적지를 찾겠다는 의미O
MSC_DONTWAIT데이터 입/출력 함수 호출 시 블로킹되지 않고 바로 리턴할 것을 요구하는 경우에 사용. 이를 두고 넌-블로킹 I/O라 한다.OO
MSG_OOB데이터 전송 시, 긴급 데이터(out-of-band data) 전송을 위한 옵션OO
MSG_PEEK버퍼에 수신된 데이터가 있는지 확인하기 위한 용도로 사용되는 옵션O

[긴급한 데이터 전송]

MSG_OOB는 긴급 데이터(Out-of-band Data)를 전송할 때 사용됨

// oob_send.c

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

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

int main(int argc, char** argv)
{
	int sock;
	struct sockaddr_in recv_addr;

	if(argc != 3)
	{
		printf("Usage : %s <IP> <port>\n", argv[0]);
		exit(0);
	}
	sock = socket(PF_INET, SOCK_STREAM, 0);
	if(sock == -1)
		error_handling("socket() error");

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

	if(connect(sock, (struct sockaddr*)&recv_addr, sizeof(recv_addr)) == -1)
		error_handling("connect() error!");

	write(sock, "123", 3);
	send(sock, "4", 1, MSG_OOB); // 긴급 전송
	write(sock, "567", 3);
	send(sock, "890", 3, MSG_OOB); // 긴급 전송

	close(sock);
	return 0;
}

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

일반적인 방법으로 데이터가 전송된다면 123, 4, 567, 890의 순서로 도착할 것임
그러나 이중에서 4와 890이 긴급으로 전송되었으므로 도착 순서에 변화가 생김

// oob_recv.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>

#define BUFSIZE 30

void error_handling(char* message);
void urg_handler(int signo);

int recv_sock;
int send_sock;

int main(int argc, char** argv)
{
	struct sockaddr_in recv_addr;
	struct sockaddr_in send_addr;
	int send_addr_size, str_len, state;
	struct sigaction act;
	char buf[BUFSIZE];

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

	act.sa_handler = urg_handler;
	sigemptyset(&act.sa_mask);
	act.sa_flags = 0;

	recv_sock = socket(PF_INET, SOCK_STREAM, 0);
	if(recv_sock == -1)
		error_handling("socket() error");

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

	if(bind(recv_sock, (struct sockaddr*)&recv_addr, sizeof(recv_addr)) == -1)
		error_handling("bind() error");
	listen(recv_sock, 5);

	send_addr_size = sizeof(send_addr);
	send_sock = accept(recv_sock, (struct sockaddr*)&send_addr, &send_addr_size);

	// fcntl() : 파일 디스크립터가 가리키는 파일의 특성을 변경시키는 함수
	// 파일의 소유자(소켓의 소유자) 변경
	fcntl(send_sock, F_SETOWN, getpid()); 
	state = sigaction(SIGURG, &act, 0); // 핸들러 초기화
	if(state != 0)
		error_handling("sigaction() error");

	while((str_len = recv(send_sock, buf, sizeof(buf), 0)) != 0)
	{
		if(str_len == -1)
		{
			continue;
		}

		buf[str_len] = 0;
		puts(buf);
	}
	close(send_sock);
	return 0;
}

void urg_handler(int signo)
{
	int str_len;
	char buf[BUFSIZE];
	str_len = recv(send_sock, buf, sizeof(buf) - 1, MSG_OOB); // 긴급 데이터를 수신하기 위해 MSG_OOB를 넘겨줌
	buf[str_len] = 0;
	printf("긴급 메시지 전송 : %s \n", buf);
}

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

fcntl()는 파일 디스크립터가 가리키는 파일(소켓은 물론 포함된다)의 특성을 변경시키는 함수임
두 번째 인자 F_SETOWN의 의미는 파일의 소유자를 변경하겠다는 의미

clnt_sock이 가리키는 소켓의 소유자를 getpid 함수가 리턴하는 ID의 프로세스로 변경하겠다
clnt_sock이 가리키는 소켓이 연결되어 있는 호스트로부터 들어오는 SIGURG 시그널을 어느 프로세스에게 전달해 줄 것인가를 결정하는 것
하나의 소켓을 여러 프로세스가 함께 공유할 수도 있음(fork 함수를 통해 자식 프로세스를 생성하고, 생성과 동시에 파일 디스크립터까지 복사하는 경우 등)
그런 상황에서 SIGURG 시그널 발생 시 어느 프로세스에게 시그널이 전달될까?
모든 프로세스에게 시그널을 전달해 주지는 않음
→ 이런 문제가 생길 수 있기 때문에 SIGURG 시그널을 핸들링할 때는 반드시 시그널을 받아서 처리하는 프로세스를 지정해 주게 되어 있음

getpid()는 현재 이 함수를 호출하는 프로세스의 ID를 리턴하는 함수
따라서 현재 실행 중인 프로세스가 SIGURG 시그널을 처리하도록 설정해 주고 있는 것

[실행 결과]

MSG_OOB 옵션을 줘서 recv 함수를 호출하는 경우 한 바이트만 읽어들임
MSG_OOB 옵션이 설정되어 전송되는 데이터는 ‘긴급’으로 처리되는 메시지가 아님
→ 그럼 어떤 의미를 가질까?

MSG_OOB 옵션 설정 후 데이터를 전송할 경우, TCP 프로토콜의 긴급 모드를 사용해서 데이터를 전송하게 됨
긴급 모드 데이터 전송이랑 전송되는 TCP 패킷의 헤더에 “이 데이터는 긴급을 요하고 있습니다”라고 설정해 주는 것을 의미

긴급 데이터 전송 명령을 받자마자 Urgent Pointer를 설정함
Urgent Pointer의 위치는 긴급으로 전송하는 데이터의 마지막 바이트 다음 위치로 설정됨

TCP 헤더에 URG가 설정됨(URG = 1) → 즉, 긴급을 요함
Urgent Pointer의 위치는 3(offset 3)이라는 정보를 담고 있음
URG가 설정된 긴급 TCP 패킷을 받은 호스트의 운영체제는 제일 먼저 SIGURG 시그널을 발생시킴
호스트는 SIGURG 시그널의 발생을 감지하고 시그널 핸들러를 작동 시킴
시그널 핸들러 내에서는 긴급 데이터를 수신하기 위해서 recv 함수를 호출하게 되는데,
세 번째 전달 인자로 MSG_OOB를 넘겨주어야 함
그러면 Urgent Pointer의 위치 바로 전에 있는 1바이트만 리턴하고 나머지 데이터는 일반적인 recv 함수 호출을 통해서 수신하게 됨
긴급 데이터를 전송했다고 해서 이미 떠나 버린 패킷보다 빠르게 전달되지는 않음
출력 버퍼가 꽉 찼거나 하는 경우에는 패킷 전송이 불가능한데 이런 경우라도 버퍼가 비워지길 기다리지 않고 바로 전송될 수 있도록 특혜를 주고, 전송 시 전송을 늦추는 요인이 되는 Nagle 알고리즘과 같은 것들을 무시해줌
→ 생각한 것 만큼 빠르게 도달하지는 않지만 일반적인 데이터들보다는 빠르게 전달됨

[Offset]

기본이 되는 주소를 바탕으로, 상대적 주소를 만드는 것

실제 주소가 존재하고 있고, 실제 주소 번지 2를 기준으로 새로운 주소를 설정해 주고 있음
실제 주소는 1부터 시작하기도 하고 0부터 시작하기도 하지만 offset 주소는 반드시 0부터 시작함

[입력 버퍼 검사하기]

MSG_PEEK 옵션은 MSG_DONTWAIT 옵션과 함께 설정되어 입력 버퍼에 수신된 데이터가 있는지 확인하는 목적으로 사용되는 옵션임
MSG_PEEK 옵션을 주고, recv 함수를 호출했을 때 데이터가 버퍼에 존재한다면 버퍼로부터 데이터를 읽어들임
함수를 호출하는 시점에서는 MSG_PEEK 옵션을 주거나 안 주거나 차이가 없음
그러나 호출 이후에는 차이점이 생김
일반적으로 recv 함수를 호출해서 데이터를 읽어 들이게 되면, 읽어 들인 데이터는 버퍼에서 사라져야 함
그러나 MSG_PEEK 옵션을 주어서 데이터를 읽어 오게 되면, 읽고 나서도 데이터가 그대로 버퍼에 남음
→ 잘못 사용하게 되면 오히려 문제가 발생할 수 있음

// peek_send.c

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

void error_handling(char* message);

int main(int argc, char** argv)
{
	int sock;
	struct sockaddr_in recv_addr;

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

	sock = socket(PF_INET, SOCK_STREAM, 0);
	if(sock == -1)
		error_handling("socket() error");

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

	if(connect(sock, (struct sockaddr*)&recv_addr, sizeof(recv_addr)) == -1)
		error_handling("connect() error!");

	write(sock, "123", 3);

	close(sock);
	return 0;
}

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

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

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

int main(int argc, char** argv)
{
	int recv_sock;
	int send_sock;

	struct sockaddr_in recv_addr;
	struct sockaddr_in send_addr;
	int send_addr_size, str_len, state;

	char buf[BUFSIZE];

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

	recv_sock = socket(PF_INET, SOCK_STREAM, 0);
	if(recv_sock == -1)
		error_handling("socket() error");

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

	if(bind(recv_sock, (struct sockaddr*)&recv_addr, sizeof(recv_addr)) == -1)
		error_handling("bind() error");

	listen(recv_sock, 5);
	send_addr_size = sizeof(send_addr);
	send_sock = accept(recv_sock, (struct sockaddr*)&send_addr, &send_addr_size);

	sleep(1);
	str_len = recv(send_sock, buf, sizeof(buf) - 1, MSG_PEEK|MSG_DONTWAIT);
	buf[str_len] = 0;
	printf("총 %d 바이트 존재합니다 : %s\n", str_len, buf);

	str_len = recv(send_sock, buf, sizeof(buf) - 1, 0);
	buf[str_len] = 0;
	printf("읽어온 결과 입니다. : %s\n", buf);

	close(send_sock);
	return 0;
}

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

recv 함수를 호출하면서 옵션으로 MSG_PEEK을 전달하고 있음
MSG_DONTWAIT 옵션을 함께 주는 이유는 보통 MSG_PEEK은 수신된 데이터가 있는지 확인하는 목적으로 사용되기 때문에 데이터가 존재하지 않을 경우에는 바로 리턴하기 위해서임

→ MSG_DONTWAIT 옵션은 블로킹 상태로 두지 않고 바로 리턴하게 해줌

sleep(1)은 MSG_DONTWAIT이 데이터가 존재하지 않아도 바로 리턴해버리므로 peek_send.c가 데이터를 전송하기도 전에, recv 함수가 호출되고 바로 리턴되는 것을 막기 위해 넣음
recv 함수를 한 번 더 호출해 줄 때 아무런 옵션도 설정하지 않음

만약 이전에 MSG_PEEK 옵션을 주고 호출했던 recv 함수가 버퍼에 잇는 데이터를 읽어 들이면서 버퍼에서 데이터를 삭제했다면, 읽어 들일 데이터가 없어서 블로킹 될 것임

[실행 결과]

결과를 보면 한번밖에 전송되지 않은 메시지가 두 번 읽혀지고 있음
첫 번째 함수 호출 시 MSG_PEEK 옵션을 설정했기 때문에, 버퍼의 데이터가 읽혀지고 나서도 남아 있음

[readv & writev 입/출력 함수]

readv와 writev 함수도 효율성을 향상시키는데 도움이 되는 함수들임

[readv & writev 함수 사용하기]

데이터를 모아서 전송하고 데이터를 나누어 수신하는 함수

  • writev : 여러 버퍼에 저장되어 있는 데이터를 한 번에 전송하게 됨
  • readv : 데이터를 여러 버퍼에 나누어서 수신할 수 있게 됨
#include <sys/uio.h>

int writev(int fd, const struct iovec* vector, int count);

// 성공 시 전송한 바이트 수, 실패 시 -1 리턴
// fd : 데이터 전송의 목적지를 나타내는 소켓의 파일 디스크립터를 전달함
// 소켓 뿐만 아니라 read, write 함수처럼 파일이나 콘솔을 입/출력 대상으로 할 수도 있음
// vector : 일반적으로 iovec 구조체 배열의 이름을 인자로 전달하는데, iovec 구조체에는 전송하고자
// 하는 데이터에 대한 정보가 담겨짐
// count : 데이터를 전송하기 위해서 참고할 iovec 구조체 변수의 수를 지정함
// 만약에 3이 인자로 전달되면, vector가 가리키는 iovec 구조체 변수를 시작으로 총 3개의 iovec 변수를
// 참고하여 데이터를 전송하게 됨

[iovec 구조체]

struct iovec
{
	ptr_t iov_base;
	size_t iov_len;
};

// iov_base : 전송할 데이터의 시작 주소를 가리킴
// iov_len : iov_base가 가리키는 위치를 시작으로 전송하고자 하는 바이트 수를 대입함
// 뒤에 나오는 readv 함수에서는 수신하고자 하는 최대 바이트 수를 나타내는 변수로 그 의미가 달라짐

writev의 첫 번째 인자 1은 파일 디스크립터를 나타내므로 콘솔에 출력을 하겠다는 의미임
ptr은 전송할 데이터 정보를 지니고 있는 iovec 배열을 가리키는 포인터임
세 번째 인자가 2이기 때문에 ptr이 가리키는 주소를 시작으로 해서 총 2 개의 iovec 변수를 참조하여
그 변수가 가리키고 있는 데이터를 전송한다는 의미
첫 번째 iovec 변수의 iov_base는 “ABC” 문자열을 가리키고 있으며 iov_len이 3이므로 “ABC”문자열 전부를 전송함
두 번째 iovec 변수의 iov_base는 “EBAD” 문자열을 가리키고 있으며 iov_len이 4이므로 역시 “EBAD” 문자열 전부를 전송함

// writev.c

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

int main(int argc, char** argv)
{
	struct iovec vec[2];
	char MSG1[] = "Computer ";
	char MSG2[] = "Communications";
	int str_len;

	vec[0].iov_base = MSG1;
	vec[0].iov_len = strlen(MSG1);

	vec[1].iov_base = MSG2;
	vec[1].iov_len = strlen(MSG2);

	str_len = writev(1, vec, 2);
	printf("\n총 %d 바이트 출력\n", str_len);
	return 0;
}

[실행 결과]

#include <sys/uio.h>

int readv(int fd, const struct iovec* vector, int count);

// 성공 시 수신한 바이트 수, 실패 시 -1 리턴
// fd : 데이터를 수신할 파일(혹은 소켓)의 파일 디스크립터를 인자로 전달함
// vector : writev 함수에서 사용되던 용도와 비슷함
// 어디에다가 얼마만큼 데이터를 수신할 것인지에 대한 정보를 iovec 구조체 변수에다가 넣어서
// 그 배열의 이름을 인자로 전달함
// count : 데이터를 수신하기 위해서 참고할 iovec 구조체 변수의 수를 지정함
// 만약 3이 인자로 들어가면 vector가 가리키는 iovec 구조체 변수를 시작으로 총 3개의 iovec 변수를 참고하여
// 데이터를 수신하게 됨
// readv.c

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

int main(int argc, char** argv)
{
	struct iovec vec[2];
	char MSG1[10] = {0, };
	char MSG2[10] = {0, };
	int str_len;

	vec[0].iov_base = MSG1;
	vec[0].iov_len = 9;

	vec[0].iov_base = MSG2;
	vec[0].iov_len = 9;

	str_len = readv(0, vec, 2);
	printf("\n총 %d 바이트 입력\n", str_len);
	printf("첫 번째 메시지 : %s \n", MSG1);
	printf("두 번째 메시지 : %s \n", MSG2);

	return 0;
}

[실행 결과]

두 개의 배열에 나뉘어서 데이터가 저장됨

[readv & writev 함수의 적절한 사용]

전송해야 할 데이터가 여러 개의 배열에 나뉘어서 저장되어 있는 경우에는, 모두 전송하기 위해서 write 함수를
여러 번 호출해야 하는데 이것보다는 한 번의 writev 함수 호출이 더 효율적임
또한 입력 버퍼에 수신된 데이터를 여러 배열에 나누어서 읽어 들이고 싶은 경우에도 read 함수를 여러 번
호출하는 것 보다 readv 함수 한 번 호출하는 것이 더 효율적임

어떤 이유에서 효율적일까?

  • 함수 호출이 적으므로 성능 향상을 가져옴
  • 전송되는 패킷의 수를 줄일 수 있음

한 번에 전송하고 싶은 데이터가 있는데 세 곳의 배열에 나뉘어 저장되어 있다면 모두 다 전송하기 위해서는 세 번의 write 함수 호출을 해야함
Nagle 알고리즘이 off 되어 있었다면 몇 개의 패킷이 생성되어 전송될까?
Nagle 알고리즘을 적용하지 않는 경우 출력 버퍼에 데이터가 들어오자 마자 데이터를 블록화해서 전송하기 때문에 총 세 개의 패킷이 생성되어 전송됨
한꺼번에 블록화해서 전송해도 되는 데이터를 세 개의 패킷으로 나누어 보낸 것임

→ 네트워크 입장에서는 트래픽이 증가하게 되고, 수신하는 호스트 입장에서는 처리해야 하는 패킷의 수가 증가함
write 함수를 여러 번 호출하는 대신 writev 함수를 사용하게 되면 여러 배열에 저장되어 있던 데이터를
하나의 패킷에 모두 담아서 한 번에 전송할 수 있게 됨

0개의 댓글