본문으로 바로가기
반응형

select 함수의 이해와 서버의 구현

select 함수는 멀티플렉싱 서버 구현의 대표적인 방법이다.

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

 

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

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

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

 

그런데 select 함수는 사용방법에 있어서 일반적인 함수들과 많은 차이를 보인다.

보다 정확히 표현하면 사용하기가 만만치 않다. 그래도 select 함수가 멀티플렉싱 서버의 전부라고해도 과언이아니기때문에 차근차근 살펴보도록 하자.

위 그림은 select 함수를 호출해서 결과를 얻기까지의 과정을 간략히 정리한 것이다.

그림에서는 select 함수의 호출에 앞서 준비가 필요하고, 또 호출 이후에도 결과의 확인을 위한 별도의 과정이 존재함을 보이고 있다. 그럼 이제 하나씩 그림에서 보이는 순서대로 살펴보도록 하자.

 

 

파일 디스크립터의 설정

select 함수를 사용하면 여러 개의 파일 디스크립터를 동시에 관찬할 수 있다고 하였다. 물론 파일 디스크립터의 관찰은 소켓의 관찰로 해석할 수 있다. 그렇다면 먼저 관찰하고자 하는 파일디스크립터를 모아야한다.

모을 때도 관찰항목(수신, 전송, 예외)에 따라 구분해서 모아야한다. 즉, 바로 위에서 언급한 3가지 관찰 항목별로 구분해서 세 묶음으로 모아야한다.

 

파일 디스크립터를 세 묶음으로 모을 때 사용하는 것이 fd_set형 변수이다. 이는 다음 그림에서 보이듯이 0과 1로 표현되는, 비트 단위로 이뤄진 배열이라고 생각하면 된다.

위 그림의 배열에서 가장 왼쪽 비트는 파일 디스크립터 0을 나타낸다.(나타내는 위치이다). 이 비트가 1로 설정되면 해당 파일 디스크립터가 관찰의 대상임을 의미한다. 그렇다면 위 그림에서는 어떤 파일 디스크립터가 관찰대상으로 지정되어 있는가? 파일디스크립터 1과 3이 관찰대상으로 지정되어있다.

 

그럼 파일 디스크립터의 숫자를 확인해서 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 함수의 호출결과를 확인하는 용도로 사용된다. 그럼 간단히 위 함수들의 기능을 다음그림을 통해서해 정리해보겠다.

 

 

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

그림 12-5에서 보인 Step On의 '파일디스크립터 설정' 이외의 나머지 두 가지를 설명하겠다.

그런데 이에 앞서 select 함수를 먼저 보이고 간단히 설명하고자 한다.

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

int select(int maxfd, fd_set *readset, fd_set *sriteset, fd_set *exceptest, const struct timeval * timeout);

- maxfd : 검사 대상이 되는 파일 디스크립터 수

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

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

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

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

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

 

select 함수는 세가지 관찰항목의 변화를 확인하는데 사용된다. 바로 이 세가지 관찰항목별로 fd_set형 변수를 선언해서 파일 디스크립터 정보를 등록하고, 이 변수 주소 값을 위 함수의 두번째, 세번째, 네번째 인자로 전달하게 된다.

 

그런데 이에 앞서(select 함수의 호출에 앞서) 다음 2가지를 먼저 결정해야한다.

 

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

"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 함수호출 이후의 결과확인

12-5의 step one에 해당하는 select 함수 호출 이전의 작업에 대해 모두 알아보았다.

그리고 select 함수에 대해서도 설명하였다. 그러나 이에 못지않게 중요한 것이 함수 호출의 결과를 확인하는 방법이다.

select 함수의 반환 값에 대해서는 위에서 한번 정리하였다.

0이 아닌 양수가 반환되면, 그 수만큼 파일 디스크립터에 변화가 발생했음을 의미한다.

 

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

 

그렇다면 select 함수가 양의 정수를 반환한 경우, 변화가 발생한 파일 디스크립터는 어떻게 알아낼 수 있을까?

select 함수의 두번째, 세번째 그리고 네번째 인자로 전달된 fd_set형 변수에 다음 그림에서 보이는 변화가 발생하기 때문에 어렵지 않게 알아낼 수 있다.

위 그림에도 보이듯이 select 함수호출이 완료되고 나면, select 함수의 인자로 전달된 fd_set형 변수에는 변화가 생긴다. 1로 설정된 모든 비트가 다 0으로 변경되지만, 변화가 발생한 파일 디스크립터에 해당하는 비트만 그대로 1로 남아있게 된다. 때문에 여전히 1로 남아있는 위치의 파일 디스크립터에서 변화가 발생했다고 판단할 수 있다.

 

 

select 함수를 호출하는 예제

select 함수와 관련된 이론이 대략 끝났으니 이제 예제를 통해 모두를 통합할 차례이다.

다음의 간단한 예제를 통해 지금까지 봤던 내용을 정리해보자.

// select.c
#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;
}

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

반면 키보드로 문자열을 입력하면 해당 문자열이 재출력되는 것도 확인할 수 있을 것이다.

 

 

멀티플렉싱 서버의 구현

지금까지 select 함수의 사용법을 바탕으로 멀티플렉싱 서버를 구현할 차례이다. 

이번 예제를 통해 select 함수가 서버 구현에 어떻게 반영되는지 확인해보자. 예제는 멀티플렉싱 기반의 에코 서버이다.

// echo_selectserv.c
#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* message);

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)  // 연결요청
            {
               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* message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

실행결과를 확인하기위해 echo_client.c를 활용했지만, 다른 에코 클라이언트와도 잘 동작할 것이다.

반응형