[열혈TCP/IP] 12 IO 멀티플렉싱 Multiplexing

4 minute read

12 IO 멀티플렉싱 Multiplexing

멀티프로세스 서버의 단점과 대안

프로세스의 생성에는 상당히 많은 대가를 지불한다. 많은 양의 연산이 요구되고, 필요한 메모리 공간도 비교적 크다. 그리고 프로세스마다 별도의 메모리 공간을 유지하기 때문에 상호간에 데이터를 주고받으려면 다소 복잡한 방법을 택할 수밖에 없다. (Inter Process Communicatino ICP는 복잡한 통신방법이다) 그리고 이의 대안이 바로 멀티플렉싱이다.

멀티플렉싱이라는 단어의 이해

  • 하나의 통신채널을 통해서 둘 이상의 데이터(시그널)를 전송하는데 사용되는 기술
  • 물리적 장치의 효율성을 높이기 위해서 최소한의 물리적 요소만 사용해서 최대한의 데이터를 전달하기 위해 사용되는 기술

  • 멀티플렉싱 OFF : 종이컵-실 전화기 모델에서 각 사용자는 다른 사용자와 종이컵-실-종이컵 전화기를 한대씩 모두 연결한다.
  • 멀티플렉싱 ON : 종이컵-실 전화기 모델에서 각 사용자가 하나씩 종이컵을 가지고 실을 한 곳으로 모아서 연결한다.
  • 종이컵과 실이 절약되고, 여러 사람이 동시에 하나의 실로 대화하기 위해서 시분할/주파수분할의 개념이 도입도입되어야 한다.

멀티플렉싱의 개념을 서버에 적용하기

멀티플렉싱 기술을 서버에 적용하면 필요한 프로세스의 수를 줄일 수 있다. 클라이언트의 숫자와 상관없이 서버에서 서비스를 제공하는 프로세스의 수는 딱 하나이다. 이제 하나의 프로세스에서 둘 이상의 클라이언트에게 서비스를 제공하는 방법에 대해서 고민하면 된다.

12-2 select 함수의 이해와 서버의 구현

select()함수를 이용하는 것이 멀티플렉싱 서버의 구현에 있어서 가장 대표적인 방법이다.

select()함수의 기능과 호출순서

select()함수를 사용하면 한곳에 여러 개의 fd를 모아놓고 동시에 이들을 관찰할 수 있다. 이때 관찰할 수 있는 항목은 다음과 같다.

  • 수신한 데이터를 지니고 있는 소켓이 존재하는가?
  • 블로킹되지 않고 데이터의 전송이 가능한 소켓은 무엇인가?
  • 예외상황이 발생한 소켓은 무엇인가?

위 3가지 항목을 각각 event라고 한다. 관찰항목에 속하는 상황이 발생했을 때, 이벤트가 발생했다라고 표현한다. 이벤트라고 표현하는 것은 매우 일반적인 표현이니 꼭 알고 있어야 한다.

select 함수의 호출과정은 일반 함수와 비교했을 때 복잡하다.

  1. 1단계 :
    1. 파일 디스크립터의 설정
    2. 검사의 범위 지정
    3. 타입아웃의 설정
  2. 2단계 :
    1. select()함수의 호출
  3. 3단계 :
    1. 호출 결과 확인

파일 디스크립터의 설정

select 함수를 사용하면 여러 개의 fd를 동시에 관찰할 수 있다. 물론 fd의 관찰은 소켓의 관찰로 해석할 수 있다. 그렇다면 먼저 관찰하려는 fd를 모아야 한다. 모을 때도 관찰항목(수신,전송,예외)에 다라서 구분해서 모아야 한다. 즉, 1~3단계 항목별로 구분해서 세 묶음으로 모아야 한다.

fd를 세 묶음으로 모을 때 사용되는 것이 fd_set()형 변수이다. 이는 0과 1로 표현되는, 비트 단위로 이뤄진 배열이라고 생각하면 된다.

  • fd0 fd1 fd2 fd3 …
  • 0 1 0 1 …

위 그림의 배열에서 가장 왼쪽 비트는 fd0을 나타내는 위치이다. 이 비트가 1로설정되면 fd가 관찰의 대상임을 의미한다. 위 그림에서는 1과 3이 관찰의 대상이다. fd_set형 변수의 조작은 다음 메크로 함수의 도움을 받는다.

  • FD_ZERO(fd_set fdset) : 인자로 전달된 주소의 fd_set형 변수의 모든 비트를 0으로 초기화한다.
  • FD_SET(int fd, fd_set *fdset): 매개변수 fdset으로 전달된 주소에 매개변수 fd로 전달된 파일 디스크립터 정보를 등록한다.
  • FD_CLR (int fd, fd_set *fdset) : 매개변수 fdset으로 전달된 주소에 매개변수 fd로 전달된 파일 디스크립터 정보를 삭제한다.
  • FD_ISSUE fdset(int fd, fd_set *fdset) : 매개변수 fdset으로 전달된 주소에 매개변수 fd로 전달된 파일 디스크립터 정보가 있으면 양수를 반환한다. 이는 select 함수의 호출결과를 확인하는 용도로 사용된다.
int main (void){
    fd_set set;        //fd0 fd1 fd2 fd3
    FD_ZERO(&set);     // 0   0   0   0
    FD_SET(1, &set);   // 0   1   0   0
    FD_SET(2, &set);   // 0   1   1   0
    FD_CLR(2, &set);   // 0   1   0   0
}

검사(관찰)의 범위지정과 타임아웃의 설정

select 함수는 세가지 관찰황목의 변화를 확인하는데 사용된다. 이 세가지 관찰항목별로 fd_set형 변수를 선언해서 fd 정보를 등록하고, 이 변수의 주소 값을 select()의 인자로 전달한다. 그래서 select()함수의 호출 이전에 두 가지를 설정해야 한다.

  1. fd의 관찰 범위
  2. select 함수의 timeout 시간

fd의 값은 0부터 시작하고 하나씩 생길 때마다 1씩 증가하기 때문에 가장 큰 fd + 1를 첫 번째 인자로 전달한다.(페이지279쪽 하단) 타임아웃은 아래의 변수를 선언해서 전달한다.

struct timeval{
    long tv_sec;
    long tv_usec;
}

select 함수는 관찰중인 파일 디스크립터에 변화가 생겨야 반환을 한다. 때문에 변화가 생기지 않으면 무한정 블로킹 상태에 머물게 된다. 초단위와 마이크로 초 단위 정보를 지정하고 select를 호출할 때 전달하면 된다.

select 함수호출 이우의 결과 확인

select 함수의 반환값이 0이 아닌 양수가 반환이 되면, 그 수만큼 fd에 변화가 발생했음을 의미한다. 여기서의 변화는 등록된 fd에 해당 관심에 관련된 변화가 생겼음을 의미한다. 즉, select 함수의 두 번째 인자를 통해서 데이터 수신여부의 관찰대상에 포함된 fd로 수신된 데이터가 존재하는 경우가 fd에 변화가 발생한 경우이다.

fd_set을 select의 인자로 전달했는데, 변화가 발생한 fd에 해당하는 비트는 그대로 1로 남아있고 나머지는 0으로 바뀐 것을 가지고 fd에 변화가 발생했다고 판단할 수 있다.

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

#define BUF_SIZE 30

int main(int argc, char *argv[])
{
	fd_set reads, temps;
	int result, str_len;
	char buf[BUF_SIZE];
	struct timeval timeout;

	FD_ZERO(&reads);
	FD_SET(0, &reads); // 0 is standard input(console)

	/*
	timeout.tv_sec=5;
	timeout.tv_usec=5000;
	*/

	while(1)
	{
		temps=reads;
		timeout.tv_sec=5;
		timeout.tv_usec=0;
		result=select(1, &temps, 0, 0, &timeout);
		if(result==-1)
		{
			puts("select() error!");
			break;
		}
		else if(result==0)
		{
			puts("Time-out!");
		}
		else 
		{
			if(FD_ISSET(0, &temps)) 
			{
				str_len=read(0, buf, BUF_SIZE);
				buf[str_len]=0;
				printf("message from console: %s", buf);
			}
		}
	}
	return 0;
}

멀티플렉싱 서버의 구현

다음 예제는 멀티플렉싱 기반의 에코 서버이다.

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

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

int main(int argc, char *argv[])
{
	int serv_sock, clnt_sock;
	struct sockaddr_in serv_adr, clnt_adr;
	struct timeval timeout;
	fd_set reads, cpy_reads;

	socklen_t adr_sz;
	int fd_max, str_len, fd_num, i;
	char buf[BUF_SIZE];
	if(argc!=2) {
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}

	serv_sock=socket(PF_INET, SOCK_STREAM, 0);
	memset(&serv_adr, 0, sizeof(serv_adr));
	serv_adr.sin_family=AF_INET;
	serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
	serv_adr.sin_port=htons(atoi(argv[1]));
	
	if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)
		error_handling("bind() error");
	if(listen(serv_sock, 5)==-1)
		error_handling("listen() error");

	FD_ZERO(&reads);
	FD_SET(serv_sock, &reads);
	fd_max=serv_sock;

	while(1)
	{
		cpy_reads=reads;
		timeout.tv_sec=5;
		timeout.tv_usec=5000;

		if((fd_num=select(fd_max+1, &cpy_reads, 0, 0, &timeout))==-1)
			break;
		
		if(fd_num==0)
			continue;

		for(i=0; i<fd_max+1; i++)
		{
			if(FD_ISSET(i, &cpy_reads))
			{
				if(i==serv_sock)     // connection request!
				{
					adr_sz=sizeof(clnt_adr);
					clnt_sock=
						accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
					FD_SET(clnt_sock, &reads);
					if(fd_max<clnt_sock)
						fd_max=clnt_sock;
					printf("connected client: %d \n", clnt_sock);
				}
				else    // read message!
				{
					str_len=read(i, buf, BUF_SIZE);
					if(str_len==0)    // close request!
					{
						FD_CLR(i, &reads);
						close(i);
						printf("closed client: %d \n", i);
					}
					else
					{
						write(i, buf, str_len);    // echo!
					}
				}
			}
		}
	}
	close(serv_sock);
	return 0;
}

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

머티플렉싱 서버에서 select 이후 FD_ISSUE를 통해서 원하는 이벤트가 들어왔는지 판단하고 이를 파싱해서 사용하는 예제이다. 책 285페이지