
이번 Chapter에서는 다중접속 서버의 두 번째 구현방법인 멀티플렉싱 기반의 서버 구현에 대해 살펴보자.
이전까지는 다중접속 서버의 구현을 위해서 클라이언트의 연결요청이 있을 때마다 새로운 프로세스를 생성하였다. 이제는 실제 사용되는 방법이지만 문제가 전혀 없는 방법은 아니다.
프로세스의 생성에는 상당히 많은 대가를 지불해야 한다.
많은 양의 연산이 요구되며, 필요한 메모리 공간도 비교적 큰 편이다.
또한 프로세스마다 별도의 메모리 공간을 유지하기 때문에 상호간에 더이터를 주고받으려면 다소 복잡한 방법(IPC)을 택해야 한다.
그래서 이번 Chapter에서는 프로세스의 생성을 동반하지 않으면서 다수의 클라이언트에게 서비스를 제공할 수 있는 방법에 대해 알아보자.
- 하나의 통신채널을 통해서 둘 이상의 데이터(시그널)를 전송하는데 사용되는 기술
- 전화의 경우 말을 하는 순서가 일치하지 않기 때문에, 그리고 목소리의 주파수도 다르기 때문에 멀티 플렉싱이 가능하다.
- 데이터의 송수신도 실시간으로 0의 지연시간을 가지고 서비스 해야 하는 것이 아니기 때문에 멀티플렉싱이 가능하다.
- 하나의 프로세스가 다수의 소켓을 관리하는 방법에 있다.
- 멀티플렉싱
- 하나의 리소스를 둘 이상의 영역에서 공유
컵 전화기 시스템에 멀티플렉싱 기술을 도입해서 필요한 컵의 수와 실의 길이를 줄였듯이, 서버에 멀티플렉싱 기술을 도입해서 필요한 프로세스의 수를 줄일 수 있다.
서버 모델을 알아보자
멀티프로세스 서버 모델에 멀티플렉싱 긱술을 적용하면 다음과 같이 프로세스의 수가 줄어든다.
여기서 중요한 것은 기술했듯이, 접속해있는 클라이언트의 수에 상관없이, 서비스를 제공하는 프로세스의 수는 딱 하나라는 사실이다.
select 함수를 이용하는 것이 멀티플렉싱 서버의 구현에 있어서 가장 대표적인 방법이다.
그리고 윈도우에도 이와 동일한 이름으로 동일한 기능을 제공하는 함수가 있기 때문에 이식성에 있어서도 좋다.
select 함수를 사용하면 한곳에 여러 개의파일 디스크립터를 모아놓고 동시에 이들을 관찰할 수 있다.
1. 수신한 데이터를 지니고 있는 소켓이 존재하는가 ?
2. 블로킹되지 않고 데이터의 전송이 가능한 소켓은 무엇인가 ?
3. 예외상황이 발생한 소켓은 무엇인가 ?
- 관찰항목 각각을 가리켜 '이벤트(event)'라 한다.
-위에서 정리한 관찰항목 각각을 가리켜 이벤트라 하고, 관찰항목에 속하는 상황이 발생했을 때 '이벤트(event)가 발생했다'라고 표현한다.

위 그림은 select 함수를 호출해서 결과를 얻기까지의 과정을 간략히 정리한 것이다.
그림에서는 select 함수의 호출에 앞서 뭔가 준비가 필요하고, 또 호출 이후에도 결과의 확인을 위한 별도의 과정이 존재함을 보이고 있다.
위에서 select 함수를 사용하여 여러 개의 파일 디스크립터를 동시에 관찰할 수 있다고 하였다.
즉, 파일 디스크립터의 관찰은 소켓을 관찰한다는 것을 의미한다.
우선 파일 디스크립터를 모아야 한다.
파일 디스크립터는 관찰항목(수신, 전송, 예외)에 따라서 구분해서 모아야 한다.
파일 디스크립터를 세 묶음으로 모을 때 사용되는 것이 fd_set형 변수이다.
fd_set형 변수는 0과 1로 표헌되는, 비트 단위로 이뤄진 배열이다.
비트가 1로 설정되면 해당 파일 디스크립터는 관찰의 대상이 된다.
fd_set형 변수에 값을 등록하거나 변경하는 등의 작업은 다음 매크로 함수들의 도움을 통해서 이뤄진다.
지금까지 step1의 '파일 디스크립터 설정'을 설명하였다.
이번에는 나머지를 설명하겠다.
먼저 함수를 보자.
#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 반환
/*
maxfd : 검사 대상이 되는 파일 디스크립터의 수
readset : fd_set형 변수에 '수신된 데이터의 존재 여부'에 관심 있는 파일 디스크립터 정보를 모두 등록해서
그 변수의 주소 값을 전달한다
writeset : fd_set형 변수에 '블로킹 없는 데이터 전송의 가능 여부'에 관심 있는 파일 디스크립터 정보를 모두
등록해서 그 변수의 주소 값을 전달한다.
exceptset : fd_set형 변수에 '예외상황의 발생 여부'에 관심이 있는 파일 디스크립터 정보를 모두 등록해서
그 변수의 주소값을 전달한다.
timeout : select 함수 호출 이후에 무한정 블로킹 상태에 빠지지 않도록 타임아웃(time-out)을 설정하기 위한
인자를 전달한다.
반환 값 : 오류발생시에는 -1이 반환되고, 타임 아웃에 의한 반환 시에는 0이 반환된다.
그리고 관심대상으로 등록된 파일 디스크립터에 해당 관심에 관련된 변화가 발생하면 0보다 큰 값이 반환되는데,
이 값은 변화가 발생한 파일 디스크립터의 수를 반환한다.
세 가지 관찰항목별로 fd_set형 변수를 선언해서 파일 디스크립터 정보를 등록하고, 이 변수의 주소 값을 위 함수의 두 번째, 세 번째, 네 번째 인자로 전달한다.
그리고 select 함수의 호출에 앞ㅇ서 다음 두 가지를 먼저 결정해야 한다.
이 중 첫 번째 파일 디스크립터의 관찰(검사) 범위는 select 함수의 첫 번째 매개변수와 관련이 있다.
select 함수는 관찰의 대상이 되는 파일 디스크립터의 수를 첫 번째 인자로 요구한다.
따라서 fd_set형 변수에 등록된 파일 디스크립터의 수를 확인할 필요가 있는데,
파일 디스크립터의 값은 생성될 때마다 1씩 증가하기 때문에 가장 큰 파일 디스크립터의 값에 1을 더해서 인자로 전달하면 된다.
1을 더하는 이유는 파일 디스크립터의 값이 0에서부터 시작하기 때문이다.
그리고 두 번째, select 함수의 타임아웃 시간은 select 함수의 마지막 배개변수와 관련이 있다.
매개 변수 선언에서 보이는 자료형 timeval은 구조체 기반의 자료형으로, 다음과 같이 정의되어있다.
struct timeval
{
long tv_sec; //seconds
long tv_usec; //microseconds
}
원래 select 함수는 관찰중인 파일 디스크립터에 변화가 생겨야 반환을 한다.
때문에 변화가 생기지 않으면 무한정 블로킹 상태에 머물게 된다.
이러한 상황을 막기 위해서 타임아웃을 지정한다.
위 구조체 변수를 선언하고 멤버 tv_sec에 초 단위 정보를, tv_usec에 마이크로 초 단위 정보를 지정한다.
그리고 이 변수의 주소 값을 select 함수의 마지막 인자로 전달을 하면, 파일 디스크립터에 변화가 발생하지 않아도 지정된 시간이 지나면 함수가 반환을 한다.
단, 이렇게 해서 반환이 되는 경우, select 함수는 0을 반환한다.
때문에 반환 값을 통해서 반환의 원인을 알 수 있다.
그리고 타임아웃을 설정하고 싶지 않은 경우 NULL을 인자로 전달하면 된다.
지금까지 설명한 것을 select 함수를 호출하여 확인하자.
다시 말하지만 반환 값이 0이 아닌 양수가 반환이 되면 그 수만큼 파일 디스크립터에 변화가 생겼음을 의미한다.
그렇다면 select 함수가 양의 정수를 반환한 경우, 변화가 발생한 파일 디스크립터는 어떻게 알아낼 수 있을까 ?
select 함수의 두 번째, 세 번째 그리고 네 번째 인자로 전달된 fd_set형 변수에 다음 그림에서 보이는 변화가 발생하기 때문에 어렵지 않게 알아낼 수 있다.

select 함수 호출 이 후
- 변화가 발생한 소켓의 파일 디스크립터만 1로 설정
-입력 받은 데이터가 존재
-출력이 가능한 상황 등
- 변화가 없는 파일 디스크립터는 0으로 초기화됨
즉, 변화가 발생한 파일 디스크립터에 해당하는 비트만 그대로 1로 남아있게 된다.
때문에 여전히 1로 남아있는 위치의 파일 디스크립터에서 변화가 발생했다고 판단할 수 있다.
#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
#define BUF_SIZE 30
int main(void){
fd_set reads, temps;
int result, str_len;
char buf[BUF_SIZE];
struct timeval timeout;
FD_ZERO(&reads);
FD_SET(0, &reads); // 0은 표준 입출력
/*
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;
}
우선 14, 15행에서 fd_set형 변수를 초기화하고 파일 디스크립터 0의 위치를 1로 설정해주었다.
즉, 표준입력에 변화가 있는지 관심을 두고 보겠다는 뜻이다.
그 다음으로 나오는 주석처리된 부분은 타임아웃 설정을 위한 코드이다.
이 위치에서는 타임아웃을 설정하면 안 된다.
왜냐하면 select 함수호출 후에는 구조체 timeval의 멤버 tv_sec와 tv_usec에 저장된 값이 타임아웃이 발생하기까지 남았던 시간으로 바뀌기 때문이다.
따라서 select 함수를 호출하기 전에 매번 timeval 구조체 변수의 초기화를 반복해야 한다.
while문에 24행에서 fd_set형 변수 reads의 내용을 변수 temps에 복사하고 있다.
이것은 select 함수 호출이 끝나면 변화가 생긴 파일 디스크립터의 위치를 제외한 나머지 위치의 비트들이 0으로 초기화되기 때문에 원본의 유지를 위해서 이렇게 복사의 과정을 거쳐야 한다.
그 다음 timeval 구조체 변수의 초기화 코드를 반복문 안에 삽입해서 select 함수가 호출되기 전에 매번 새롭게 값이 초기화되도록 해야 한다.
select 함수를 호출하여 콘솔로부터 입력된 데이터가 있다면 0보다 큰 수가 반환되며, 입력된 데이터가 없어서 타임아웃이 발생하는 경우에는 0이 반환된다.
39행 ~ 44행은 select 함수가 0보다 큰 수를 반환했을 때 실행되는 영역이다.
여기 코드에서는 변화를 보인 파일 디스크립터가 표준입력이 맞는지 확인하고, 맞으면 표준입력으로부터 데이터를 읽어서 콘솔로 데이터를, 출력하고 있다.
실행결과
실행하고 나서 아무런 입력이 없으면 5초 정도 지난 후 타임아웃이 발생함을 확인할 수 있다.
반면 키보드로 입력하면 해당 문자열이 재출력되는 것도 확인할 수 있다.
select 함수를 바탕으로 멀티플렉싱 서버를 구현해보자
여기서 client는 [소켓 #04]에서 작성한 echo_client 코드를 사용하겠다.
#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];
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)
{
printf("%d\n", i);
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
{
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);
}
}
}
}
}
close(serv_sock);
return 0;
}
void error_handling(char *buf)
{
fputs(buf, stderr);
fputc('\n', stderr);
exit(1);
}
실행 결과
참고 : 윤성우의 열혈 TCP/IP 소켓 프로그래밍
Git : https://github.com/im2sh/Socket_Programming/tree/main/lab10