Develop

[c] 다중연결 서버 만들기 #3 - poll() 사용

by hooni posted Apr 23, 2013
?

단축키

Prev이전 문서

Next다음 문서

ESC닫기

크게 작게 위로 아래로 댓글로 가기 인쇄
poll은 select 와 마찬가지로 다중입출력 을 구현하기 위한 방법으로 사용되며, 동시에 여러개의 클라이언트를 다루는 서버를 제작하기 위한방법으로 흔히 사용된다. 

select 의 경우 입출력 이벤트가 발생했을 때 넘겨주는 정보가 너무 적음으로써, 프로그래밍시 여기에 신경을 써줘야 하는데 poll 을 이용하면 이러한 제한을 극복할수 있다.
select 에 대한 자세한 내용은 select 를 통한 입출력 다중화 와 다중연결서버 만들기 #2 를 참조하기 바란다.

poll
다음은 poll의 함수원형이다.
int poll(struct poolfd *ufds, unsigned int nfds, int timeout);
poll이 여러개의 파일을 다루는 방법은 select 와 마찬가지로 파일지시자의 이벤트를 기다리다가 이벤트가 발생하면, poll 에서의 block 이 해제되고, 다음 루틴에서 어떤 파일지시자에 이벤트가 발생했는지 검사하는 방식을 사용하게 된다.
우선 poll 함수의 첫번째 인자인 pollfd 구조체에 대해서 알아보도록 하겠다. poolfd 구조체만 알만 poll 의 대부분을 다 이해한것이나 마찬가지이니, 주의 깊게 읽어 보기 바란다.
struct pollfd
{
	int fd;         // 관심있어하는 파일지시자
	short events;   // 발생된 이벤트
	short revents;  // 돌려받은 이벤트
};
pollfd 구조체는 3개의 멤버변수가 있는데, 이 구조체에 우리가 관심있어하는 파일지시자를 세팅하고(fd), 관심있어 하는 파일지시자가 어떤 이벤트가 발생하는걸 기다릴것인지(events)를 지정하게 된다. 그럼 poll 은 해당 fd 에 해당 events 가 발생하는지를 검사하게 되고, 해당 events 가 발생하면 revents 를 채워서 돌려주게 된다. 
revents 는 events 가 발생했을때 커널에서 이 events 에 어떻게 반응 했는지에 대한 반응 값이다.
후에 revent 값을 조사함으로써, 해당 파일지시자에 어떠한 event 가 최해지고 커널에서 그 event를 어떻게 처리했는지 (입력/출력이 제대로 이루어졌는지, 아니면 에러가 발생했는지)를 알아내서 적절한 조취(읽을 데이타가 있으면 읽거나 하는등의 일)를 취할수 있게 된다.

그럼 events 에 세팅할수 있는 events 에 대해서 알아보도록 하겠다. 이 값들은
    #define POLLIN      0x0001  // 읽을 데이타가 있다.
    #define POLLPRI     0x0002  // 긴급한 읽을 데이타가 있다.
    #define POLLOUT     0x0004  // 쓰기가 봉쇄(block)가 아니다. 
    #define POLLERR     0x0008  // 에러발생
    #define POLLHUP     0x0010  // 연결이 끊겼음
    #define POLLNVAL    0x0020  // 파일지시자가 열리지 않은것같은
                                // Invalid request (잘못된 요청)
2번째 인자인 nfds 는 pollfd 의 배열의 크기 즉 우리가 조사할 파일지시자의 크기(네트웍프로그래밍측면에서 보자면 받아들일수 있는 클라이언트의 크기) 로, 보통 프로그래밍 할때 그크기를 지정해준다. 
마지막 아규먼트인 timeout 는 select 의 time 와 같은 역할을 한다.
  • 값을 지정하지 않을경우 이벤트가 발생하기 전까지 영원히 기다린다.
  • 0일경우는 기다리지 않고 곧바로 다음 루틴을 진행하고자
  • 0보다 큰 양의 정수일 경우에는 해당 시간만큼을 기다리게 된다. 해당 시간내에 어떤 이벤트가 발생하면 즉시 되돌려 주며, 시간을 초과하게 될경우 0을 return 한다.

위의 3가지 아규먼트를 채워넣음으로써 poll을 사용할수 있다. poll 함수의 return 값은 int 형인데, 에러일경우 -1 이 리턴되고, 그렇지 않을경우 revent 가 발생한 pollfd 구조체의 숫자를 돌려주게 된다. 

이제 poll 버젼의 우편주소 프로그램의 서버를 작성해 보도록 하자. 

예제 : zipcode_poll.c
#include <sys/time.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/poll.h>

// 받아들일수 있는 클라이언트의 크기
#define OPEN_MAX    600 

int main(int argc, char **argv)
{

    int server_sockfd, client_sockfd, sockfd;

    int i, maxi;
    int nread;
    int state = 0;

    socklen_t clilen;

    struct sockaddr_in clientaddr, serveraddr;

    char buf[255];
    char line[255];

    FILE *fp;

    struct pollfd client[OPEN_MAX];

    if (argc != 2)
    {
        printf("Usage : ./zipcode_poll [port]n");
        printf("예    : ./zipcode_poll 4444n");
        exit(0);
    }


    if ((fp = fopen("zipcode.txt", "r")) == NULL)
    {
        perror("file open error : ");
        exit(0);
    }

    if ((server_sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
        perror("socket error : ");
        exit(0);
    }
    bzero(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(atoi(argv[1]));

    state = bind(server_sockfd, (struct sockaddr *)&serveraddr, 
                sizeof(serveraddr));

    if (state == -1)
    {
        perror("bind error : ");
        exit(0);
    }
    state = listen(server_sockfd, 5);
    if (state == -1)
    {
        perror("listen error : ");
        exit(0);
    }

    // pollfd  구조체에 
    // 소켓지시자를 할당한다.  
    // 소켓에 쓰기 events (POLLIN)에 대해서 
    // 반응하도록 세팅한다. 
    client[0].fd = server_sockfd;
    client[0].events = POLLIN;

    // pollfd 구조체의 모든 fd 를 -1 로 초기화 한다.  
    // fd 가 -1 이면 파일지시자가 세팅되어있지 않다는 뜻이다. 
    for (i = 1; i < OPEN_MAX; i++)
    {
        client[i].fd = -1;
    }
    maxi = 0;

    // POLLING 시작
    for (;;)
    {
        nread = poll(client, maxi + i, 1000);

        // 만약 POLLIN 이벤트에 대해서 
        // 되돌려준 이벤트가(revents) POLLIN
        // 이라면 accept 한다. 
        if (client[0].revents & POLLIN)
        {
            clilen=sizeof(clientaddr);
            client_sockfd = accept(server_sockfd, 
                            (struct sockaddr *)&clientaddr, 
                            &clilen);
            for (i = 1; i < OPEN_MAX; i++)
            {
                if (client[i].fd < 0)
                {
                    client[i].fd = client_sockfd;
                    break;
                }
            }

            if (i == OPEN_MAX)
            {
                perror("too many clients : ");
                exit(0);
            }

            client[i].events = POLLIN;

            if (i > maxi)
            {
                maxi = i;
            }

            if (--nread <= 0)
                continue;
        }

        // 현재 파일지시자의 총갯수 만큼 루프를 돌면서 
        // 각 파일지시자에 POLLIN revent 가 발생했는지를 조사하고 
        // POLLIN이 발생했다면, 해당 파일지시자에서 데이타를 읽어들이고, 
        // 주소정보를 돌려준다. 
        // 만약 "quit" 를 읽었다면, 소켓연결을 끊는다. 
        for (i = 1; i <= maxi; i++)
        {
            if ((sockfd = client[i].fd) < 0)
                continue;
            if (client[i].revents & (POLLIN | POLLERR))
            {
                rewind(fp);
                memset(buf, 0x00, 255);
                if (read(sockfd, buf, 255) <= 0)
                {
                    close(client[i].fd);
                    client[i].fd = -1;
                }
                else
                {
                    if (strncmp(buf, "quit", 4) == 0)
                    {
                        write(sockfd, "byebyen", 7);
                        close(client[i].fd);
                        client[i].fd = -1;
                        break;
                    }
                    while(fgets(line, 255, fp) != NULL)
                    {
                        if (strstr(line, buf) != NULL)
                            write(sockfd, line, 255);
                        memset(line, 0x00, 255);
                    }
                }
            }
        }
    }
}

select 버젼인 다중연결서버 만들기 #2와 비교해서 보기 바란다. 
코딩 분위기가 select 와 매우 비슷하다는걸 알 수 있을것이다. 

pollfd 에 입력된 파일지시자의 event 에 입력event 가 발생하면, 커널은 입력event 에 대한 결과를 되돌려줄것이다. 
이결과는 입력 event 가 제대로 처리되었다면 POLLIN 을 되돌려 줄 것이고, 어딘가에서 에러가 발생했다면 POLLERR 을 되돌려주게 될 것이다. 
그러므로 우리는 revent 를 검사함으로써, 해당 파일지시자에 읽을 데이타가 있다는걸 알게 되고, 데이타를 읽어서 적당한 행동(여기에서는 주소를 돌려주는)을 할수 있다. 
위의 프로그램은 이러한 일련의 과정을 보여준다.
select 버젼과 별차이가 없으므로 select 버젼의 소스를 이해했다면 위의 소스를 이해하는데 별 어려움이 없을것이다. 

poll 은 보통 select 에 비해서 해당파일지시자에 대해서 보다 많은 정보를 되돌려줌으로, 보통 select 보다 선호되는 추세이다.
select 버젼과 마찬가지로 polling 중간에 파일 I/O 가 들어갈경우, 파일 I/O 작업에서의 block 때문에 짧은시간에 다수의 메시지를 처리할경우 문제가 될 소지가 있다. 
그러므로 되도록이면 polling 중간에 파일 I/O 가 일어나지 않도록 해주어야 한다.
위의 소스의 경우도 주소정보를 미리 메모리 상에 올려놓고 쓰는게 더욱 좋은 방법이 될것이다.