12-1. I/O 멀티플렉싱 기반의 서버

 프로세스 생성은 상당히 많은 대가를 지불해야 하는 연산과정을 거치며, 그만큼 생성 후에도 시스템의 자원을 많이 차지하게 된다. 또한 모든 프로세스들은 서로 독립적인 메모리 공간을 할당 받아서 사용하기 때문에 프로세스간 통신을 하기 위해서는 다소 복잡한 방법을 선택 할 수밖에 없다.(IPC를 복잡한 방법이라고 말하고 있다. 실제로 서로 통신을 해야 하는 프로세스의 수가 많아지면 프로그래밍 하기가 까다로워 진다.)

  "멀티플렉싱"이란 여러 개를 묶어서 하나로 만드는 것"이다. 하나로 만들었다는 것은 "그 하나를 공유한다"라는 의미가 되므로 같은 뜻이 된다.

 결론적으로, 멀티플렉싱 서버는 연결된 클라이언트 대 프로세스의 비율이 다 대 일 된다. 이러한 경우 하나의 프로세스는 입,출력을 담당하게된다. 하지만 클라이언트의 연결 요청의 처리까지도 하나로 묶어버린다.


    12-2. select 함수 사용하기

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

 1) select 함수의 기능과 호출 순서
 select 함수를 호출하기 전에 다음의 세가지 준비 단계가 필요하다
 
 첫 번째로 '디스크립터 설정'이다. : select 함수를 사용하게 되면, 여러 개의 파일 디스크립터를 동시에 관찰할 수 있다고 하였다. 파일 디스크립터를 관찰할 수 있다는 말은 결국 소켓을 관찰할 수 있다는 말과 같다.

 관찰할 수 있는 항목은 다음과 같다.
수신할 데이터를 지니고 있는 소켓이 존재하는가?
데이터를 전송할 경우 블로킹되지 않는 소켓은 무엇인가?
예외 상황이 발생한 소켓이 있는가?
 이렇게 관찰 항목이 셋이기 때문에 세 묶음으로 파일 디스크립터를 준비해 둬야 한다.

 파일 디스크립터를 세 묶음으로 모아놓기 위해서 사용되는 것이 fd_set 데이터 타입의 자료형이다. fd_set은 0과 1을 나타내는 비트들의 배열이라고 생각하면 된다.

 1로 설정된 비트가 관찰 대상이 되는 파일 디스크립터를 의미한다.

 두 번째로 '검사 범위 설정'이다. : select 함수는 열ㅓ 파일 디스크립터를 관찰하고, 그 결과를 전달해 준다. 그렇다면 여러 개의 디스크립터를 확인해야 하는데 이왕이면 확인해야 하는 범위를 설정해 주면, 보다 효율적으로 수핼할 수 있을 것이다.

 세 번째로 '타임아웃(timeout) 설정'이다 : 타임아웃을 설정한다는 것은 블록킹 상태를 빠져 나가기 위한 시간 설정을 의미한다.

 select 함수는 호출했을 관찰 대상들에게서 변화(관찰 항목에 부합되는 소켓의 변화를 의미한다)가 발생해야 리턴하며, 그렇지 않으면 변화가 발생될 때까지 무한정 블로킹 상태에 있게 된다. 그러나 타임아웃을 설정해 놓으면 관찰 대상들에게서 변화가 없더라도 설정 시간이 지나면 무조건 리턴한다. 따라서 무한 대기 상태에 빠지는 것을 피할 수 있다.

 다음은 select 함수의 선언이다.

select (성공 시 0 이상, 오류 발생 시 -1 리턴) (Language : c)
  1. #include <sys/time.h>
  2. #include <sys/types.h>
  3. #include <unistd.h>
  4.  
  5. int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
 * n : 검사 대상이 되는 파일 디스크립터의 수
 * readfds : "입력 스트림에 변화가 발생했는지" 확인하고자 하는 소켓들의 정보를 전달한다. ㅇ기서 입력 스트립에 변화가 발생했다는 것은 수신할 데이터가 있다는 뜻이다.
 * writefds : "데이터 전송 시, 블로킹되지 않고 바로 전송이 가능한지" 확인하고자 하는 소켓들의 정보를 전달한다.
 * exceptfds : "예외가 발생했는지" 확인하고자 하는 소켓들의 정보를 전달한다.
 * timeout : 함수 호출 후, 무한 대기 상태에 빠지지 않도록 타임-아웃(time-out)을 설정하기 위한 인자를 전달한다.
 * 리턴 값 : -1이 리턴되는 경우, 오류 발생을 의미한다. 또한 0이 리턴된 경우에는 타임아웃에 의해 리턴되었음을 의미한다. 마지막으로 리턴된 값이 0보다 큰 경우는 변경된 파일 디스크립터의 수를 의미한다.

 2) 파일 디스크립터 설정 하기
 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 포인터가 가리키는 변수에 설정되어 있는 파일 디스크립터 중에서...) : write 함수와 같은 출력 함수가 블로킹되는 경우는, 출력 버퍼에 아직 전송하지 못한 데이터가 많이 남앙 씨어서, 데이터 전송을 바로 할 없는 경우에 발생한다. 따라서 출력 버퍼가 여유 있는 경우에는 블로킹되지 않고, 데이터를 전송할 수 있는 경우가 된다.

 파일 디스크립터가 가리키는 소켓에서 예외가 발생하였는가?(exceptfds 포인터가 가리키는 변수에 설정되어 있는 파일 디스크립터 중에서...) : 예외 상황 이라고 해서 여러분이 생각하는 그런 아주 일반적인 예외 상황을 말하는 것이 아니다. TCP 기반의 Out-of-band data 가 수신되었을 경우를 의미하는 것이다.

 3) 파일 디스크립터 범위 설정하기
 select 함수는 여러 파일 디스크립터를 검사하고, 그 결과를 전달해 준다고 하였다. 그렇다면 select 함수는 여러 개의 파일 디스크립터를 확인해야 하는데, 이왕이면 확인해야 하는 파일 디스크립터의 범위를 제한해 주면, 보다 효율적으로 수행할 수 있을 것이다.

 그래서 select 함수의 첫 번째 인자로 검사해야 하는 총 디스크립터의 개 수를 넘겨주게 된다. 그러나 일반적으로 디스크립터는 생성될 때마다 값이 1씩 증가하기 때문에 가장 큰 파일 디스크립터 값에 1을 더해서 인자로 전달하면 된다.

 1을 더하는 이유는 디스크립터 값이 0부터 시박하기 때문이다. 따라서 인자로 n이라는 값을 념겨 주게 되면, select 함수는 검사하게 되는 파일 디스크립터의 범위를 0부터 n-1로 설정된다. 따라서 반드시 1을 더해줘야 한다.

 4) 타임아웃(time out) 설정하기
 다음은 타임아웃을 설정하기 위한 timeval 구조체를 보여 주고 있다.

(Language : c)
  1. struct timeval
  2. {
  3.     long tv_sec;        /* seconds */
  4.     long tv_usec;      /* microseconds */
  5. }
 * tv_sec는 초 단위 설정을 위한 변수이고, tv_usec는 마이크로 초 단위 설정을 위한 변수이다.

  5) select 함수 호출 이후 결과 확인
 select 함수가 리턴되고 나서 무엇보다도 중요한 것은 결과를 얻는 것이다. 일단 함수 호출이 정상적으로 리턴했다는 것은 파일 디스크립터에 변화가 있었거나, 아니면 타임아웃이 발생했거나 둘 중에 하나이다.

 리턴 값이 -1인 경우는 오류발생을 의미한다. 또한 0이 리턴 된 경우에는 타임아웃에 의해 리턴되었음을 의미한다. 즉 0이 리턴된 경우 파일 디스크립터에 아무런 변화도 발생하지 않았다는 의미가 된다.

 그러나 리턴된 값이 0보다 큰 경우에는, 변화가 발생한 파일 디스크립터의 수를 의미하게 된다. 예를 들어 수신할 데이터가 존재하는 파일 디스크립터가 두 개 발생했다면, 2가 리턴될 것이다.

 변화가 발생한 파일 디스크립터의 수는 이렇게 간단히 알 수 있다.

 6) select 함수 호출 예제
 다음은 select 함수를 사용한 코드이다.

select.c (Language : c)
  1. /***************************************************************************
  2. *            select.c
  3. *
  4. *  Mon Jan 14 15:01:44 2008
  5. *  Copyright  2008  pchero21
  6. *  pchero21@gmail.com
  7. ****************************************************************************/
  8.  
  9. /*
  10. *  This program is free software; you can redistribute it and/or modify
  11. *  it under the terms of the GNU General Public License as published by
  12. *  the Free Software Foundation; either version 2 of the License, or
  13. *  (at your option) any later version.
  14. *
  15. *  This program is distributed in the hope that it will be useful,
  16. *  but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  18. *  GNU General Public License for more details.
  19. *
  20. *  You should have received a copy of the GNU General Public License
  21. *  along with this program; if not, write to the Free Software
  22. *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
  23. */
  24.  
  25. #include <stdio.h>
  26. #include <stdlib.h>
  27. #include <unistd.h>
  28. #include <sys/types.h>
  29. #include <sys/time.h>
  30.  
  31. #define BUFSIZE 30
  32.  
  33. int main(int argc, char **argv)
  34. {
  35.     fd_set reads, temps;
  36.     int result;
  37.  
  38.     char message[BUFSIZE];
  39.     int str_len;
  40.     struct timeval timeout;
  41.        
  42.     FD_ZERO(&reads);
  43.     FD_SET(0, &reads);      /* standard input 설정 */
  44.    
  45.    /*
  46.     timeout.tv_sec = 5;
  47.     timeout.tv_usec = 100000;
  48.     */
  49.                 /* 이곳에 설정할 경우 문제 발생 */
  50.     while(1) {
  51.         temps = reads;
  52.         timeout.tv_sec = 5;
  53.         timeout.tv_usec = 0;

  54.         result = select(1, &temps, 0, 0, &timeout);
  55.         if(result == -1) {    /* select 함수 오류 발생 */
  56.             puts("select 오류 발생!");
  57.             exit(1);
  58.         } else if(result == 0) {    /* time-out 에 의한 리턴 */
  59.             puts("시간이 초과되었습니다 : select");
  60.         } else {        /* 파일 디스크립터 변화에 의한 리턴 */
  61.             if(FD_ISSET(0, &temps)) {
  62.                 str_len = read(0, message, BUFSIZE);
  63.                 message[str_len] = 0;
  64.                 fputs(message, stdout);
  65.             }
  66.         }
  67.     }   /* while(1) end */
  68. }
  69.  

 실행 화면
사용자 삽입 이미지


    12-3. 멀티플렉싱 서버의 구현

echo_server.c (Language : c)
  1. /***************************************************************************
  2. *            echo_serverv.c
  3. *
  4. *  Mon Jan 14 15:23:45 2008
  5. *  Copyright  2008  pchero21
  6. *  pchero21@gmail.com
  7. ****************************************************************************/
  8.  
  9. /*
  10. *  This program is free software; you can redistribute it and/or modify
  11. *  it under the terms of the GNU General Public License as published by
  12. *  the Free Software Foundation; either version 2 of the License, or
  13. *  (at your option) any later version.
  14. *
  15. *  This program is distributed in the hope that it will be useful,
  16. *  but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  18. *  GNU General Public License for more details.
  19. *
  20. *  You should have received a copy of the GNU General Public License
  21. *  along with this program; if not, write to the Free Software
  22. *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
  23. */
  24.  
  25. #include <stdio.h>
  26. #include <stdlib.h>
  27. #include <string.h>
  28. #include <unistd.h>
  29. #include <sys/types.h>
  30. #include <sys/time.h>
  31. #include <sys/socket.h>
  32. #include <netinet/in.h>
  33.  
  34. #define BUFSIZE 100
  35.  
  36. void error_handling(char *message);
  37.  
  38. int main(int argc, char **argv)
  39. {
  40.     int serv_sock;
  41.     struct sockaddr_in serv_addr;
  42.        
  43.     fd_set reads, temps;
  44.     int fd_max;
  45.    
  46.     char message[BUFSIZE];
  47.     int str_len;
  48.     struct timeval timeout;
  49.        
  50.     if(argc != 2) {
  51.         printf("Usage : %s <port>\n", argv[0]);
  52.         exit(1);
  53.     }
  54.    
  55.     serv_sock = socket(PF_INET, SOCK_STREAM, 0);
  56.     serv_addr.sin_family = AF_INET;
  57.     serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  58.     serv_addr.sin_port = htons(atoi(argv[1]));
  59.  
  60.     if(bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)))
  61.         error_handling("bind() error");
  62.     if(listen(serv_sock, 5) == -1)
  63.         error_handling("listen() error");
  64.    
  65.     FD_ZERO(&reads);
  66.     FD_SET(serv_sock, &reads);
  67.     fd_max = serv_sock;
  68.    
  69.     while(1) {
  70.         int fd, str_len;
  71.         int clnt_sock, clnt_len;
  72.         struct sockaddr_in clnt_addr;
  73.            
  74.         temps = reads;
  75.         timeout.tv_sec = 5;
  76.         timeout.tv_usec = 0;
  77.        
  78.         if(select(fd_max + 1,  &temps, 0, 0, &timeout) == -1)
  79.             error_handling("select() error!");
  80.        
  81.         for(fd = 0; fd < fd_max + 1;fd++) {
  82.             if(FD_ISSET(fd, &temps)) {
  83.                 if(fd == serv_sock) {      /* 연결 요청인 경우 */
  84.                     clnt_len = sizeof(clnt_addr);
  85.                     clnt_sock = accept(serv_sock,  (struct sockaddr *)&clnt_addr, &clnt_len);
  86.                     FD_SET(clnt_sock, &reads);
  87.                     if(fd_max < clnt_sock)
  88.                         fd_max = clnt_sock;
  89.                     printf("클라이언트 연결 : 파일 디스크립터 %d \n", clnt_sock);
  90.                 } else {
  91.                     str_len = read(fd, message, BUFSIZE);
  92.                     if(str_len == 0) {    /* 연결 종료 요청인 경우 */
  93.                         FD_CLR(fd, &reads);
  94.                         close(fd);
  95.                         printf("클라이언트 종료 : 파일 디스크립터 %d\n", fd);
  96.                     } else {
  97.                         write(fd, message, str_len);
  98.                     }
  99.                 }
  100.             }   // if(FD_ISSET(fd, &temps)
  101.         }   // for(fd = 0; fd < fd_max + 1; fd++)
  102.     }   // while(1)
  103. }
  104.  
  105. void error_handling(char *message)
  106. {
  107.     fputs(message, stderr);
  108.     fputc('\n', stderr);
  109.     exit(1);
  110. }
  111.  


==========================================================

 select 함수를 이용한 멀티플렉싱의 설명은 미진한 부분이 많다.
 자세한 사항에 대해 알고 싶다면 전문 사이트를 이용하는 것을 추천한다.