나의 브을로오그으

#12. IO 멀티플렉싱(Multiplexing) 본문

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

#12. IO 멀티플렉싱(Multiplexing)

__jhp_+ 2022. 8. 27. 10:32

#1. IO 멀티플렉싱 기반의 서버

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

이전 Chapter 11(Pipe라인 기반의 서버 모델)에서는 다중접속 서버의 구현을 위해서 클라이언트의 연결요청이 있을 때 마다 새로운 프로세스를 생성하였다. 그러나 프로세스의 생성에는 꽤 많은 대가를 지불해야 하기 떄문에 많은 양의 연산과 메모리 공간도 필요하다. 또한 프로세스마다 별도의 메모리 공간을 유지하기 때문에 상호간에 데이터를 주고받으려면 다소 복잡한 방법을 택할 수밖에 없다. (IPC는 다소 복잡한 통신방식임)

"그렇다면 다수의 클라이언트에게 서비스를 제공할 수 있는 (프로세스 생성 없이) 대안이 없을까??"

있다! 바로 IO 멀티플렉싱 서버가 그것이다. 물론 이 방법이 모든 문제의 해결책은 아니다. 항상 상황에 맞는 서버 모델을 구현하도록 하자!!

 

멀티플렉싱의 이해

멀티플렉싱이라는 단어는 전자 및 통신공학에서 매우 흔하게 등장한다.

"하나의 통신채널을 통해서 둘 이상의 데이터(시그널)를 전송하는데 사용되는 기술"

"물리적 장치의 효율성을 높이기 위해서 최소한의 물리적인 요소만 사용해서 최대한의 데이터를 전달하기 위해 사용되는 기술"

 

[IO 멀티플렉싱 서버 모델의 또 다른 이해]

어느 교실에 학생이 열 명 있다. 그런데 이 책에서 나오는 등장 인물들처럼 이 아이들 역시 상상을 초월하는 인물들이다. 선생님은 한 분인데, 수업시간 내내 선생님에게 질문을 한다. 그래서 이 학교에서는 어쩔 수 없이, 이 반에 학생 한 명당 교사 한 명을 두었다. 따라서 이 교실에만 현재 교사의 수가 열 명이다.  이후로도 한 학생이 전학을 오면 교사도 한 명 늘리고, 두 명의 학생이 전학 오면 교사도 두 명을 늘렸다. 전학 온 녀석들도 모두 질문 에 살고 질문에 죽는 녀석들이기 때문이다. 지금 언급한 이야기에서 학생은 클라이언트에, 교사는 클라이언트와 데이터르 주고받는 서버 쪽 프로세스에 비유하면, 이 반의 운영방식은 멀티프로세스 기반이라 할 수 있다. 그런데 어느 날 이 반에 아주 무시무시한 능력을 소유하신 선생님께서 전근을 오셨다. 이분은 아이들의 무차별한 질문에 혼자 답을 다한다. 그 답변의 속도가 너무 빨라서 학생들은 대기하지 않고도 답변을 들을 수 있다. 그래서 학교에서는 교사의 효율적 활용을 위해 이분을 제외한 나머지 분들을 다른 반으로 이동시켰다. 때문에 이제 학생들은 질문에 앞서 손을 들어야 하고, 교사는 손을 든 학생의 질문을 확인하고선 답변을 하기 시작했다. 즉, 이 교실은 멀티플렉싱 기반으로 운영되기 시작한 것이다.

다소 엉뚱하지만, 이 이야기에서 IO 멀티플렉싱 기법을 이해할 수 있다. 선생님이 손을 든 아이가 있는지 확인하는 것처럼, IO 멀티플렉싱 서버에서는 프로세스가, 손을 든(데이터가 수신 된) 소켓이 있는지 확인을 한다. 그래서 손을 든 소켓을 통해서 전송된 데이터를 수신하게 된다.

 

 

#2. select 함수의 이해와 서버의 구현

select 함수를 이용하는 것이 멀티플렉싱 서버의 구현에 있어서 가장 대표적인 방법이다. 윈도우에서도 동일한 이름의 동일한 기능을 하므로 이식성도 좋다.

 

select 함수의 기능과 호출순서

select 함수를 사용하면 한곳에 여러 개의 파일 디스크립터를 모아놓고 동시에 이들을 관찰할 수 있다.

[관찰 항목]

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

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

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

 

[※ 참고 관찰항목 각각을 가리켜 '이벤트(event)'라고 한다.]

- 위에서 정리한 관찰항목 각각을 가리켜 이벤트라 하고, 관찰항목에 속하는 상황이 발생했을 때, '이벤트(event)가 발생했다' 라고 표현한다. 이는 매우 일반적인 표현이기 때문에 이 표현에 익숙해질 필요가 있다.

 

[호출순서]

Step One.

  * 파일 디스크립터의 설정

  * 검사의 범위 지정

  * 타임아웃의 설정

 

Step Two.

  * select 함수의 호출

 

Step Three

  * 호출결과 확인

 

 

파일 디스크립터의 설정

select 함수를 사용하면 여러 개의 파일 디스크립터를 동시에 관찰할 수 있다고 하였다. 물론 파일 디스크립터의 관찰은 소켓의 관찰로 해석할 수 있다. 그렇다면 먼저 관찰하고자 하는 파일 디스크립터를 모아야 한다. 모을 때도 관찰항목(수신, 전송, 예외)에 따라서 구분해서 모아야 한다. 즉, 위에서 언급한 세가지 관찰 항목별로 구분해서 세 묶음으로 모아야 한다.

파일 디스크립터를 세 묶음으로 모을 때 사용하는 것이 fd_set형 변수이다. (비트단위로 이루어진 배열)

fd0 fd1 fgd2 fd3 ....
0 1 0 1 ...

위 표를 배열이라고 볼때 가장 왼쪽 비트는 파일 디스크립터(fd0)가 0(상태)을 나타낸다. 이 비트가 1로 설정되면 해당 파일 디스크립터가 관찰의 대상임을 의미한다. 여기서 관찰 대상은 fd1과 fd3 파일 디스크립터이다.

"그럼 파일 디스크립터의 숫자를 확인해서 fd_set형 변수에 직접 값을 등록해야 하나?"

물론 아니다! fd_set형 변수는 조작이 비트단위로 이루어지기 때문에 번거롭다. 그래서 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_ISSET(int fd, fd_set* fdset) 매개변수 fdset으로 전달된 주소의 변수에 매개변수 fd로 전달된 파일 디스크립터 정보가 있으면 양수를 반환한다.

위의 함수들 중에서 FD_ISSET은 select 함수의 호출결과를 확인하는 용도로 사용된다.

 

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

/* linux */
#include <sys/select.h>
#include <sys/time.h>

int select(
int maxfd, fd_set* readset, fd_set* writeset, fd_set* exceptset, const struct timeval* timeout);
-> 성공 시 0 이상, 실패 시 -1 반환

max_fd : 검사 대상이 되는 파일 디스크립터의 수.

readset : fd_set형 변수에 '수신된 데이터의 존재여부'에 관심 있는 파일 디스크립터 정보를 모두 등록해서 그 변수의 주소 값을 전달한다.

writeset : fd_set형 변수에 '블로킹 없는 데이터 전송의 가능여부'에 관심 있는 파일 디스크립터 정보를 모두 등록해서 그 변수의 주소 값을 전달한다.

excepteset : fd_set형 변수에 '예외상황의 발생여부'에 관심이 있는 파일 디스크립터 정보를 모두 등록해서 그 변수의 주소 값을 전달한다.

timeout : select 함수호출 이후에 무한정 블로킹 상태에 빠지지 않도록 타임아웃(time-out)을 설정하기 위한 인자를 전달한다.

반환 값 : 오류발생시에는 -1이 반환되고, 타임 아웃에 의한 반환 시에는 0이 반환된다. 그리고 관심대상으로 등록된 파일 디스크립터에 해당 관심에 관련된 변화가 발생하면 0보다 큰 값이 반환되는데, 이 값은 변화가 발생한 파일 디스크립터의 수를 의미한다.

 

select 함수는 세가지 관찰항목의 변화를 확인하는데 사용된다고 하지 않았는가? 바로 이 세가지 관창항목별로 fd_set형 변수를 선언해서 파일 디스크립터 정보를 등록하고, 이 변수의 주소 값을 위 함수의 두번째, 세 번째 그리고 네 번째 인자로 전달하게 된다. 그런데 이에 앞서 (select 함수의 호출에 앞서) 다음 두 가지를 먼저 결정해야 한다.

"파일 디스크립터의 관찰(검사) 범위는 어떻게 되지?"

"select 함수의 타임아웃 시간을 어떻게 할까?"

이중 첫 번째, 파일 디스크립터의 관찰(검사) 범위는 select 함수의 첫 번째 매개변수와 관련이 있다. 사실 select 함수는 관찰의 대상이 되는 파일 디스크립터의 수를 첫 번째 인자로 요구하고 있다. 따라서 fd_set형 변수에 등록된 파일 디스크립터의 수를 확인할 필요가 있는데, 파일 디스크립터의 값은 생성될 때마다 1씩 증가하기 때문에 가장 큰 파일 디스크립터의 값에 1을 더해서 인자로 전달하면 된다. (파일 디스크립터는 0으로 시작)

그리고 두 번째, select 함수의 타임아웃 시간은 select 함수의 마지막 매개변수와 관련이 있는데, 매개변수 선언에서 보이는 자료형 timeval은 구조체 기반의 자료형이다.

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

원래 select 함수는 관찰중인 파일디스크립터에 변화가 생겨야 반환한다. 때문에 변화가 생기지 않으면 무한정 블로킹 상태에 머물게 된다.바로 이러한 상황을 막기 위해서 타임아웃을 지정하는 것이다. 위 구조체 변수를 선언해서 멤버 tv_sec에 초 단위 정보를, 멤버 tv_usec에 마이크로 초 단위 정보를 지정하고, 이 변수의 주소 값을 select 함수의 마지막 인자로 전달을 하면, 파일 디스크립터에 변화가 발생하지 않아도 지정된 시간이 지나면 함수가 반환을 한다. 단! 이렇게 해서 반환이 되는 경우, select 함수는 0을 반환한다. 때문에 반환 값을 통해서 반환의 원인을 알 수 있다. 그리고 타임아웃을 설정하고 싶지 않을 경우에는 NULL을 인자로 전달하면 된다.

 

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

[※참고 파일 디스크립터의 변화]

여기서 말하는 파일 디스크립터의 변화는 관심대상으로 등록된 파일 디스크립터에 해당 관심에 관련된 변화가 발생했음을 뜻한다. 즉, select 함수의 두 번째 인자를 통해서 '데이터 수신여부'의 관찰 대상에 포함된 파일 디스크립터로 수신된 데이터가 존재하는 경우가 파일 디스크립터에 변화가 발생한 경우이다.

 

 

[select.c]

/* linux */
#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)

	/*
	imteout.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 \n", buf);
			}
		}
	}
	return 0;
}

1. fd_set 변수를 초기화 하고 파일 디스크립터 0의 위치를 1로 설정해 준다. 표준입력에 변화가 있는지 관심을 두고 보겠다는 의미이다. (FD_ZERO, FD_SET)

 

2. fd_set형 변수 reads를 temp에 복사하고 있는데 이렇게 하는 이유가 있다. select 함수호출이 끝나면 변화가 생긴 파일의 파일디스크립터의 위치를 제외한 나머지 위치의 비트들은 0으로 초기화 된다. 따라서 원본의 유지를 위해서는 이렇게 복사의 과정을 거쳐야 한다. 이부분은 select 함수 사용법의 일부이니 반드시 기억할것!!

 

3. 주석 처리된 부분은 타임아웃 설정 코드이다. 그런데 해당 위치에서 타임아웃을 설정하면 안 된다. 왜냐하면 select 함수호출후에는 구조체 timeval의 멤버 tv_sec와 tv_usec에 저장된 값이 타임아웃이 발생하기 까지 남았던 시간으로 바뀌기 때문이다. 따라서 select 함수를 호출하기 전에 매번 timeval 구조체 변수의 초기화를 반복해야 한다. 그래서 반복문 안에서 timeout 값을 설정하도록 했다.

 

4. 콘솔로부터 입력된 데이터가 있다면 0보다 큰 수가 반환되며, 입력한 데이터가 없어서 타임아웃이 발생하는 경우에는 0을 반환한다.

 

5. select함수 반환값이 0보다 크다면 변화를 보인 파일 디스크립터가 표준입력이 맞는지 (FD_ISSET(0, &temps)) 확인하고, 맞으면 표준입력으로부터 데이터를 읽어서 콘솔로 데이터를 출력하고 있다.

 

 

[실행화면]

root@my_linux:/tcpip# gcc select.c -o select
root@my_linux:/tcpip# ./select
Hi~
message from console: Hi~
Hello~
message from console: Hello~
Time-out!
Time-out!
Good bye~
message from console: Good bye~

실행하고 나서 아무런 입력이 없으면(5초 정도) 타임아웃이 발생 할 수 있다.

 

 

멀티플렉싱 서버의 구현

[echo_selectserv.c]

/* linux */
#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		30
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_sock, 0, sizeof(serv_sock));
	serv_sock.sin_family = AF_INET;
	serv_sock.sin_addr.s_addr = htonl(INADDR_ANY);
	serv_sock.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;
		}
		else if (fd_num == 0)
		{
			continue;
		}

		for (i = 0; i < fd_max+1; ++i) {
			if (FD_ISSET(i, &cpy_reads)){
				if (i == serv_sock) {
					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)
					{
						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)
{
	puts(buf, stderr);
	putc('\n', stderr);
	exit(1);
}

1. select 함수의 두 번째 인자로 전달된 fd_set형 변수 reads에 서버 소켓을 등록하고 있다. 이로써 데이터의 수신여부를 관찰하는 관찰대상에 서버 소켓이 포함되었다. 참고로 클라이언트의 연결요청도 데이터의 전송을 통해서 이뤄진다. 따라서 이후에 서버 소켓으로 수신된 데이터가 존재한다는 것은 연결요청이 있었다는 뜻으로 해석해야 한다.

 

2. while문 안에 구성된 무한루프 내에서 select 함수가 호출되고 있다. select 함수의 세 번째, 네 번째 매개변수가 비어있다. 이렇듯 관찰의 목적에 맞게 인자를 전달하면 된다.

 

3. select 함수가 1이상 반환했을 때 실행되는 반복문이다. 1이상 반환되었으므로 FD_ISSET 함수를 호출하면서 상태변화가 있었던(수신된 데이터가 있는 소켓의) 파일 디스크립터를 찾는다.

 

4. 상태변화가 확인이 되면 제일먼저 서버 소켓에서 변화가 있었는지 확인한다. 그리고 서버 소켓의 상태변화가 맞으면 이어서 연결요청에 대한 수락의 과정을 진행한다. 특히 fd_set형 변수 reads에 클라이언트와 연결된 소켓의 파일 디스크립터 정보를 등록하는 부분도 유심히 볼 필요가 있다. (FD_SET(clnt_sock, &reads))

 

5. 이어서 등장하는 else문은 상태변화가 발생한 소켓이 서버 소켓이 아닌 경우에 실행된다. 즉, 수신할 데이터가 있는 경우에 실행된다. 단, 이 경우에도 수신한 데이터가 문자열 데이터인지, 아니면 연결종료를 의미하는 EOF인지 확인해야 한다.

 

[실행결과 : echo_selectserv.c]

root@my_linux:/tcpip# gcc echo_selectserv.c -o selserv
root@my_linux:/tcpip# ./selserv 9190
connected client: 4
connected client: 5
closed client: 4
closed client: 5

 

[실행결과 : echo_client.c one]

root@my_linux:/tcpip# gcc ehco_client.c -o client
root@my_linux:/tcpip# ./client 127.0.0.1 9190
Connected...............
Input Message(Q to quit): Hi~
Message from server: Hi~
Input Message(Q to quit):Good bye
Message from server: Good bye
Input Message(Q to quit):Q

 

[실행결과 : echo_client.c two]

root@my_linux:/tcpip# ./client 127.0.0.1 9190
Connected...............
Input Message(Q to quit): Nice to meet you~
Message from server: Nice to meet you~
Input Message(Q to quit): Bye~
Message from server: Bye~
Input Message(Q to quit):Q

 

 

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

윈도우 기반 select 함수의 호출

윈도우에서도 select 함수를 제공한다. 그리고 모든 인자는 리눅스 기반의 select 함수와 동일하다. 단, 윈도우가 제공하는 select 함수의 첫 번째 인자는 리눅스를 포함하는 유닉스 계열의 운영체제와의 상호 호환성을 위해 존재하는 것 일뿐, 사실 별다른 의미를 갖고 있지는 않다.

#include <Winsock2.h>
int select(
    int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, const struct timeval* timeout);
   -> 성공 시 0 이상, 실패 시 -1 반환

반환 값, 그리고 매개변수의 순서와 의미까지 앞서 설명한 리눅스의 select와 동일하다.

 

[timeval 구조체, fd_set 구조체]

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

typedef struct fd_set
{
    u_int   fd_count;
    SOCKET  fd_array[FD_SETSIZE];
}fd_set;

timeval 구조체의 경우 리눅으와 동일하나 fd_set의 경우에는 윈도우 기반으로 바꿀 시 주의해야 한다. 윈도우의 fd_set은 리눅스와 같이 비트의 배열로 구성되어 있지 않다.

 

윈도우의 fd_set은 저장된 소켓의 핸들 수를 기록하기 위한 멤버 fd_count와 소켓의 핸들 저장을 위한 멤버 fd_array로 이뤄져 있다. 리눅스의 파일 디스크립터는 0에서부터 시작해서 값이 하나씩 증가하는 구조를 지닌다. 따라서 현재 생성된 파일 디스크립터의 수와 마지막으로 생성된 파일 디스크립터의 수 사이에 어떠한 관계성을 찾아 낼 수 있다. 그러나 윈도우 기반의 소켓 핸들은 0에서부터 시작하지 않을 뿐 아니라 생성되는 핸들의 정수 값 사이에서도 규칙을 찾아낼 수 없다. 때문에 소켓의 핸들을 그대로 저장할 수 있는 배열과 저장된 배열의 수를 기록하기 위한 변수 하나가 필요한 것이다. 그러나 다행히도 fd_set형 변수의 조작을 위한 FD_XXX의 이름구조를 갖는 네 개의 매크로 함수는 이름, 기능 및 사용방법이 리눅스와 동일하다. 좋게 표현하면 호환성의 유지를 위한 MS의 베려?!

 

 

윈도우 기반의 멀티플렉싱 서버의 구현

[echo_selectserv_win.c]

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>

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

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

int main(int argc, char* argv[])
{
	WSADATA wsaData;
	SOCKET hServSock, hClntSock;
	SOCKADDR_IN servAdr, clntAdr;
	TIMEVAL timeout;
	fd_set reads, cpyReads;

	int adrSz;
	int strLen, fdNum, i;
	char buf[BUF_SIZE];

	if (argc != 2)
	{
		printf("Usage : %s <port> \n");
		exit(1);
	}
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		ErrorHandling("WSAStartUp error");

	hServSock = socket(PF_INET, SOCK_STREAM, 0);
	memset(&servAdr, 0, sizeof(servAdr));
	servAdr.sin_family = AF_INET;
	servAdr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
	servAdr.sin_port = htons(atoi(argv[1]));

	if (bind(hServSock, (SOCKADDR*)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)
		ErrorHandling("bind() error");
	if (listen(hServSock, 5) == SOCKET_ERROR)
		ErrorHandling("bind() error");

	FD_ZERO(&reads);
	FD_SET(hServSock, &reads);

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

		if ((fdNum = select(0, &cpyReads, 0, 0, &timeout)) == SOCKET_ERROR)
			break;

		if (fdNum == 0)
			continue;
			
		for (i = 0; i < reads.fd_count; ++i)
		{
			if (FD_ISSET(reads.fd_array[i], &cpyReads))
			{
				if (reads.fd_array[i] == hServSock)
				{
					adrSz = sizeof(clntAdr);
					hClntSock = accept(hServSock, (SOCKADDR*)&clntAdr, &adrSz);
					FD_SET(hClntSock, &reads);
					printf("connected clinet: %d \n", hClntSock);
				}
				else    // read message!
				{
					strLen = recv(reads.fd_array[i], buf, BUF_SIZE-1, 0);
					if (strLen == 0)
					{
						FD_CLR(reads.fd_array[i], &reads);
						closesocket(cpyReads.fd_array[i]);
						printf("close client: %d \n", cpyReads.fd_array[i]);
					}
					else
					{
						send(hClntSock, buf, strLen, 0); // echo!
					}
				}
			}
		}
	}


	return 0;
}


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

 

 

[내용 확인문제]

01. 멀티플렉싱 기술에 대한 일반적인 의미를 말하고, IO를 멀티플렉싱 한다는 것이 무엇을 의미하는지 설명해보자.

답)

멀티플렉싱은 하나의 통신채널을 통해서 둘 이상의 데이터(시그널)를 전송하는 기술 또는 물리적인 장치의 효율성을 높이기 위해서 최소한의 물리적인 요소만 사용해서 최대한의 데이터를 전달하기 위해 사용되는 기술이다. 그래서 IO 멀티플렉싱은 IO작업을 필요로 하는 소켓을 하나의 채널로 묶어서 최소한의 리소스와 프로세스를 이용해서 데이터를 송수신하는 기술이다.

 

02. 멀티프로세스 기반의 동시접속 서버의 단점은 무엇이며, 이를 멀티플렉싱 서버에서 어떻게 보완하는지 설명해 보자.

답)

멀티프로세스 기반의 동시접속 서버는 클라이언트의 요청 시 매번 새로운 프로세스를 생성해서 서비스를 제공하는 방식이다. 프로세스 생성은 비용이 높은 작업이어서 많은 양의 연산이 요구되며, 필요한 메모리 공간도 비교적 큰 편이다.  또한 프로세스마다 별도의 메모리 공간을 유지하기 때문에 상호간에 데이터를 주고받으려면 다소 복잡한 방법을 통해 통신을 해야한다.(IPC는 다소 복잡한 통신방식임) 그래서 멀티플렉싱 서버의 소켓은 파일 디스크립터를 하나로 묶어서 관리하기 때문에, 하나의 프로세스에서 모든것을 처리하게 된다. 따라서 프로세스 생성에 따른 부담이 없다.

 

03. 멀티플렉싱 기반의 서버 구현에서는 select 함수를 사용한다. 다음 중 select 함수의 사용방법에 대해서 잘못 설명한 것을 모두 고르면?

a. select 함수의 호출에 앞서 입출력의 괄찰 대상이 되는 파일 디스크립터를 모으는 과정이 필요하다.

b. select 함수의 호출을 통해서 한번 관찰의 대상으로 등록이 되면, 추가로 select 함수를 호출하면서 재 등록의 과정을 거칠 필요가 없다.

c. 멀티플렉싱 서버는 한 순간에 하나의 클라이언트에게만 서비스가 가능하다. 때문에 서비스를 필요로 하는 클라이언트는 서버에 접속한 후 자신의 순서가 오기를 기다려야 한다.

d. select 기반의 멀티플렉싱 서버는 멀티프로세스 기반의 서버와 달리 하나의 프로세스만 필요로 한다. 때문에 프로세스의 생성으로 인한 서버의 부담이 없다.

답)

b. select 함수의 호출이 끝나면 변화가 생긴 파일 디스크립터의 위치를 제외한 나머지 위치의 비트들은 0으로 초기화가 된다. 따라서 재등록을 통해 관찰대상으로 등록을 해야 한다.

c. 멀티플렉싱 서버는 한 순간에 등록된 모든 클라이언트에게 시분할 방식으로 서비스를 한다. 따라서 클라이언트 측에서는 동시에 통신하는 것처럼 서비스가 제공된다. 따라서 대기시간이 거의 없다.

 

 

04. select 함수의 관찰대상에 서버 소켓(리스닝 소켓)도 포함을 시켜야 한다. 그렇다면 어떠한 부류에 포함을 시켜야 하며, 그 부류에 포함시키는 이유도 설명해보자.

답)

서버와 클라이언트 연결 요청도 데이터 송수신을 통해 이루어지므로, 실제 클라이언트에서 연결요청을 보내면 이를 서버 소켓이 받아서 내부적으로 소켓을 생성해서 해당 소켓과의 연결이 될 수 있도록 해야한다. 따라서 서버 소켓(리스닝 소켓)은 '수신한 데이터가 존재하는가'라는 데이터 수신여부에 따른 부류에 포함시켜야 한다. 

 

05. select 함수의 호출에 사용되는 자료형 fd_set의 정의형태는 윈도우와 리눅스에서 차이를 보인다. 그렇다면 어떻게 차이가 나는지 설명하고, 차이가 날수밖에 없는 이유에 대해서도 설명해보자.

답)

리눅스와 윈도우 모두 select 함수의 인자는 동일하다. 리눅스는 파일을 파일디스크립터를 통해 구분하며, 0에서부터 시작해서 값이 하나씩 증가하는 구조를 지닌다. 따라서 현재 생성된 파일 디스크립터의 수와 마지막으로 생성된 파일 디스크립터의 수 사이에 어떠한 관계성을 찾아낼 수 있다. 반면, 윈도우는 파일의 식별자로 핸들을 사용하기 때문에  소켓 핸들이 0에서부터 시작하지 않을 뿐 아니라 생성되는 핸들의 정수 값 사이에서도 규칙을 찾아낼 수 없다. 때문에 소켓의 핸들을 그대로 저장할 수 있는 배열과 저장된 배열의 수를 기록하기 위한 변수 하나가 필요한 것이다.