Part1_IO 멀티플렉싱

·2023년 11월 28일
0

[I/O 멀티플렉싱 기반의 서버]

프로세스의 생성은 상당히 많은 대가를 지불해야 하는 연산 과정을 거침
그만큼 생성 후에도 시스템의 자원을 많이 차지하게 됨
또한 모든 프로세스들은 서로 독립적인 메모리 공간을 할당 받아서 사용하기 때문에
프로세스간 통신을 하기 위해서는 다소 복잡한 방법을 선택할 수 밖에 없었음 (IPC)
하나의 프로세스로도 여러 클라이언트들과 데이터를 주고 받을 수 있다면 좋지 않을까?
→ ‘I/O 멀티플렉싱’

그러나 멀티플렉싱 방식이 무조건 멀티 프로세스 서버보다 좋은 것은 아님
→ 즉, 구현하고자 하는 서버의 특성에 따라 결정해서 사용해야 함

멀티플렉싱은 ‘여러 개를 묶어서 하나로 만드는 것’이라고 생각하면 이해가 쉬움
→ 그럼 멀티플렉싱 서버에서는 무엇을 하나로 만들었을까?

클라이언트 개수에 상관 없이 부모 프로세스 하나만 존재함
→ 클라이언트의 입/출력 연결을 하나로 묶어버림 (진짜 그렇다는 것은 아니고 개념적으로)

[select 함수 사용하기]

select 함수를 사용하게 되면 한 곳에 모아놓은 여러 개의 파일 디스크립터를 동시에 관찰할 수 있음
수신할 데이터를 지니고 있는 파일 디스크립터가 어떤 것들인지,
데이터를 전송할 경우 블로킹되지 않고 바로 전달 가능한 파일 디스크립터는 어떤 것들인지,
예외가 발생한 파일 디스크립터는 어떤 것들인지 등 관찰 가능

[select 함수의 기능과 호출 순서]

  • 디스크립터 설정 : 3가지 관찰 항목이 존재하기 때문에 세 묶음으로 파일 디스크립터를 준비해야 함
    • 수신할 데이터를 지니고 있는 소켓이 존재하는가?

    • 데이터를 전송할 경우 블로킹되지 않는 소켓은 무엇인가?

    • 예외 상황이 발생한 소켓이 있는가?

      → 파일 디스크립터를 세 묶음으로 모아 놓기 위해 사용되는 것이 fd_set 데이터 타입의 자료형임
      fd_set은 0과 1을 나타내는 비트들의 배열인데 1로 설정된 비트가 관찰 대상이 되는 파일 디스크립터

  • 검사 범위 설정 : 여러 개의 디스크립터 중 어떤 걸 확인할지
  • 타임 아웃 설정 : 블록킹 상태를 빠져 나가기 위한 시간 설정

select 함수는 호출했을 때 관찰 대상들에게서 변화가 발생해야 리턴하며,
그렇지 않으면 변화가 발생될 때까지 무한정 블로킹 상태에 있게 됨
→ 그러나 타임아웃을 설정해 놓으면 관찰 대상자들에게서 변화가 없더라도 리턴되므로 무한 대기 상태에 빠지는 것을 피할 수 있음

#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int n, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);

// 성공 시 0 이상, 오류 발생 시 -1 리턴
// n : 검사 대상이 되는 파일 디스크립터의 수
// readfds : "입력 스트림에 변화가 발생했는지" 확인하고자 하는 소켓들의 정보를 전달함
// 여기서 입력 스트림에 변화가 발생했다는 것은 수신할 데이터가 있다는 뜻임
// writefds : "데이터 전송 시, 블로킹되지 않고 바로 전송이 가능한지" 확인하고자 하는 소켓들의 정보를 전달
// exceptfds : "예외가 발생했는지" 확인하고자 하는 소켓들의 정보를 전달
// timeout : 함수 호출 후, 무한 대기 상태에 빠지지 않도록 타임-아웃을 설정하기 위한 인자를 전달
// 리턴 값 : -1이 리턴되는 경우, 오류 발생을 의미함
// 또한 0이 리턴된 경우에는 타임아웃에 의해 리턴되었음을 의미함
// 마지막으로 리턴된 값이 0보다 큰 경우는 변경된 파일 디스크립터의 수를 의미함
// (참고) select 함수 호출 시 전달되는 파일 디스크립터의 정보는 소켓뿐 아니라 파일을 나타내는 경우에도 전달 가능

[파일 디스크립터 설정하기]

[fd_set 자료형 데이터 조작 함수]

함수 선언기능
FD_ZERO(fd_set* fdset);fdset 포인터가 가리키는 변수의 모든 비트들을 0으로 초기화함
FD_SET(int fd, fd_set* fdset);fdset 포인터가 가리키는 변수에 fd로 전달되는 파일 디스크립터 정보를 설정함
FD_CLR(int fd, fd_set* fdset);fdset 포인터가 가리키는 변수에서 fd로 전달되는 파일 디스크립터 정보를 삭제함
FD_ISSET(int fd, fd_set* fdset);fdset 포인터가 가리키는 변수가 fd로 전달되는 파일 디스크립터 정보를 지니고 있는지 확인

select 함수 선언을 보면 readfds, writefds 그리고 exceptfds라는 이름으로 총 세 개의 fd_set 변수의 포인터를 요구함

  • 파일 디스크립터를 통해 수신할 데이터가 존재하는가?
    입력 버퍼로 데이터가 수신되어서 읽어 들일 데이터가 존재하는 경우
    (소켓을 통해서 들어오는 클라이언트 연결 요청 또한 수신할 데이터가 존재하는 경우에 해당함)
  • 파일 디스크립터를 통해서 데이터를 전송할 경우 블로킹되지는 않는가?
    출력 함수가 블로킹되는 경우는 출력 버퍼에 아직 전송하지 못한 데이터가 많이 남아 있어서 데이터 전송을 바로 할 수 없는 경우에 발생함
    → 출력 버퍼가 여유 있는 경우에는 블로킹이 발생되지 않음
  • 파일 디스크립터가 가리키는 소켓에서 예외가 발생하였는가?
    TCP 기반의 Out-of-band data가 수신된 경우를 의미함

[파일 디스크립터 범위 설정하기]

select 함수는 여러 파일 디스크립터를 검사하고 그 결과를 전달해 줌
확인해야 하는 파일 디스크립터의 범위를 제한해 주면 보다 효율적으로 수행할 수 있음
→ 그래서 select 함수의 첫 번째 인자로 검사해야 하는 총 디스크립터의 개수를 넘겨주게 됨

일반적으로 디스크립터는 생성될 때마다 값이 1씩 증가하기 때문에 가장 큰 파일 디스크립터 값에 1을 더해서 인자로 전달하면 됨
(1을 더하는 이유는 디스크립터 값이 0부터 시작하기 때문)
→ 따라서 인자로 n이라는 값을 넘겨 주게 되면, select 함수는 검사하게 되는 파일 디스크립터의 범위를 0부터 n-1로 설정됨
(따라서 반드시 1을 더해줘야 함)

[타임아웃 설정하기]

struct timeval
{
	long tv_sec;  // seconds
	long tv_usec; // microseconds
}

// tv_sec이 3이고, tv_usec가 500000이면 타임아웃은 3.5초로 설정됨
// 설정한 timeval 구조체 변수의 포인터를 select 함수의 마지막 인자로 넘겨주게 되면
// 파일 디스크립터에 아무런 변화가 없더라도 3.5초가 지나면 무조건 리턴하게 됨
// 타임 아웃을 설정해 주지 않을 경우, NULL 포인터를 인자로 전달하면 됨

[SELECT 함수 호출 이후 결과 확인]

함수 호출이 정상적으로 리턴했다는 것은 파일 디스크립터에 변화가 있었거나 타임아웃이 발생한 경우 중 하나
→ 리턴 값에 따라 확인 가능함

  • -1 : 오류 발생
  • 0 : 타임아웃에 의해 리턴되었음을 의미 (파일 디스크립터에 변화 없음)
  • 0보다 큰 값 : 변화가 발생한 파일 디스크립터의 수를 의미함 (수신할 데이터가 존재하는 파일 디스크립터가 2개 발생했다면 2가 리턴됨)

select 함수 호출 후에 fd_set 변수를 살펴보면, 파일 디스크립터 3의 위치는 여전히 1로 설정되어 있고,
파일 디스크립터 1의 위치는 0으로 변경된 것을 알 수 있음
→ 파일 디스크립터 3으로부터 수신할 데이터가 존재한다는 의미
⇒ 즉, select 함수 호출이 끝나고 나서도, fd_set 변수에서 1로 설정되어 남아 있는 파일 디스크립터가 변화를 일으킨 파일 디스크립터임

[SELECT 함수 호출 예제]

// select.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/time.h>
#include <stdlib.h>

#define BUFSIZE 30

int main(int argc, char** argv)
{
	fd_set reads, temps;
	int result;

	char message[BUFSIZE];
	int str_len;
	struct timeval timeout;

	FD_ZERO(&reads);   // fd_set 구조체 변수를 초기화
	FD_SET(0, &reads); // standard input 설정 
										 // (파일 디스크립터 0을 나타내는 위치를 1로 설정 -> 표준 입력에 변화가 있는지 관심을 두고 볼 거임)

	/* 여기서 타임아웃 설정을 하면 안 됨
	timeout.tv_sec = 5;
	timeout.tv_usec = 100000;
	*/

	while(1)
	{
		temps = reads; // fd_set 변수를 임시 변수에 복사함
									 // 변화가 생긴 파일 디스크립터 위치를 제외한 나머지 위치의 비트들이 0으로 초기화되므로
								   // 원본 변수를 직접 select 함수의 인자로 전달해버리면 또 다시 변수를 설정해야 함
									 // 이를 막기 위해 원본은 보존하고 임시 변수에 원본을 복사해서 select() 호출
		
		// 타임아웃 설정은 while문 안에서 해야 함 -> 매번 타임아웃을 재설정 할 수 있어야 하므로
		timeout.tv_sec = 5;
		timeout.tv_usec = 0;

		result = select(1, &temps, 0, 0, &timeout);
		if(result == -1)
		{
			puts("select 오류 발생");
			exit(1);
		}
		else if(result == 0)
		{
			puts("시간이 초과되었습니다 : select ");
		}
		else
		{
			if(FD_ISSET(0, &temps))
			{
				str_len = read(0, message, BUFSIZE);
				message[str_len] = 0;
				fputs(message, stdout);
			}
		}
	}
}

[실행 결과]

실행 후 아무 입력도 없이 5초 정도가 지나서 타임 아웃이 발생함 (”시간이 초과되었습니다 : select”)
그리고 나서 메시지를 전송하자 출력해 주고 있음

[멀티플렉싱 서버의 구현]

// echo_selserv.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <netinet/in.h>

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

int main(int argc, char** argv)
{
	int serv_sock;
	struct sockaddr_in serv_addr;

	fd_set reads, temps;
	int fd_max;

	char message[BUFSIZE];
	int str_len;
	struct timeval timeout;

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

	serv_sock = socket(PF_INET, SOCK_STREAM, 0);
	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, (struct sockaddr*)&serv_addr, sizeof(serv_addr)))
		error_handling("bind() error");
	if(listen(serv_sock, 5) == -1)
		error_handling("listen() error");

	FD_ZERO(&reads);
	FD_SET(serv_sock, &reads); // fd_set 변수에 서버 소켓의 파일 디스크립터를 설정해 줌
														 // 서버 소켓으로부터 수신할 데이터가 있는지 관심을 두겠다는 의미
	fd_max = serv_sock;

	while(1) 
	{
		int fd, str_len;
		int clnt_sock, clnt_len;
		struct sockaddr_in clnt_addr;
		
		temps = reads;
		timeout.tv_sec = 5;
		timeout.tv_usec = 0;

		if(select(fd_max + 1, &temps, 0, 0, &timeout) == -1)
			error_handling("select() error");

		for(fd = 0; fd < fd_max + 1; fd++)
		{
			// select 함수 호출 후에 상태 변화가 있었던 파일 디스크립터를 찾아내기
			if(FD_ISSET(fd, &temps))
			{
				if(fd == serv_sock) // 서버 소켓인지 확인하기 (서버 소켓에서 변화가 있었다 -> 연결 요청이 있었다를 의미)
				{
					clnt_len = sizeof(clnt_addr);
					clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_len); // 연결 요청 수락
					FD_SET(clnt_sock, &reads); // 리턴되는 소켓을 reads 변수에 추가함 (모든 클라이언트의 입/출력을 하나로 멀티플렉싱 하기 위해)
					if(fd_max < clnt_sock)
						fd_max = clnt_sock;
					printf("클라이언트 연결 : 파일 디스크립터 %d \n", clnt_sock);
				}
				else // 변화가 있던 파일 디스크립터가 서버 소켓이 아닌 경우 (클라이언트로부터 데이터가 전송된 경우 - 데이터 또는 연결 종료)
				{
					str_len = read(fd, message, BUFSIZE);
					if(str_len == 0) // 연결 종료인지 확인
					{
						FD_CLR(fd, &reads); // reads 변수에서 제외
						close(fd);
						printf("클라이언트 종료 : 파일 디스크립터 %d \n", fd); 
					}
					else
						write(fd, message, str_len);
				}
			}
		}
	}
}

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

[실행 결과]

0개의 댓글