[ft_irc] 2. poll() 함수를 이용한 IO multiplexing

aqualung·2023년 6월 25일
0

🖥 입출력 다중화

Web서버, IRC서버 같은 서버들은 다수의 클라이언트의 동시 접속을 지원해야 한다.
동시에 여러 사용자가 접속할 때 이러한 문제를 예상해 볼 수 있다.

예를 들어 A와 B라는 클라이언트가 서버에 접속해 있다고 하자.

A의 요청을 처리하고 B의 요청을 '순서대로'처리한다면
B는 A의 요청이 모두 처리될 때까지 대기 상태에 들어간다. 이를 blocking된 상태라고 한다.

이러한 문제 때문에 우리는 클라이언트의 요청을 non-blocking하게 처리할 필요가 있다.

이 문제를 해결하는 방법은 다양하다. 클라이언트의 연결을 수락할 때마다 프로세스나 스레드를 생성하여 병렬로 처리하는 방법도 있지만 여기서는 IO multiplexing 이라는 입출력 다중화 방법을 사용할 것이다.

1. poll()

poll()함수는 위에 언급한 문제를 해결할 수 있는 함수 중 하나이다.
비슷한 기능을 하는 함수로는 select(), kqueue(), epoll() 등이 있다.

이러한 함수들은 등록된 소켓들을 순회하며 event가 발생한 소켓만을 반환해준다.

#include <poll.h>
int poll(struct pollfd fds[], nfds_t nfds, int timeout);

parameter

  1. fds[] - pollfd 구조체의 배열

    struct pollfd {
      int     fd; // 클라이언트의 소켓 fd
      short   events; // 감지할 이벤트
      short   revents; //발생한 이벤트
    };

    events에 어떤 이벤트의 발생을 기다릴 것인지 등록해두면 poll()함수는 등록한 이벤트가 발생했을 때 revents에 발생한 이벤트를 기록해준다.

  2. nfds - 첫번째 인자 fds[]의 크기

  3. timeout - 지정한 시간 내에 이벤트를 감지하지 못했다면 다음 줄로 넘어간다.(ms단위)

2. echo 서버 - IO Multi plexing

이전 글의 echo 서버를 수정한 것이다.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#include <poll.h>

#define BUF_SIZE 512

void error_handling(char* message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

int main(int argc, char* argv[])
{
    int serv_sock;
    int clnt_sock;

    // sockaddr은 주소정보를 담는 기본형태이다.
    // 주소체계에 따라 sockaddr_in, sockaddr_un, sockaddr_in6 등을 sockaddr로 형변환해서 사용하면 편리하다.
    struct sockaddr_in serv_addr;
    struct sockaddr_in clnt_addr;
    socklen_t clnt_addr_size;

    // TCP연결, IPv4 도메인을 위한 소켓 생성
    serv_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (serv_sock == -1)
        error_handling("socket error");
    
    //서버의 주소 정보 설정
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET; // 주소패밀리
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 서버의 ip주소
    serv_addr.sin_port = htons(atoi(argv[1])); // 서버 프로그램의 포트
    //htonl, htos - 빅엔디안 타입으로 변환

    // 소켓과 주소정보를 결합
    if (bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
        error_handling("bind error");
    
    // 연결요청 대기
    if (listen(serv_sock, 5) == -1)
        error_handling("listen error");

    // pollfd 배열 생성 및 초기화
    struct pollfd clients[512];
    for (int i = 0; i < 512; ++i)
        clients[i].fd = -1;

    // 배열의 첫번째에 서버 소켓을 등록
    clients[0].fd = serv_sock;
    clients[0].events = POLLIN;

    int max_i = 0; // pollfd에 등록되어 있는 가장 높은 인덱스
    int i;

    while (1)
    {
        if (poll(clients, max_i + 1, 5000) <= 0)
            continue;
        
        // 연결 수락
        if (clients[0].revents & POLLIN)
        {
            clnt_addr_size = sizeof(clnt_addr);
            clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);

            for (i = 1; i < 512; ++i)
            {
                if (clients[i].fd < 0)
                {
                    clients[i].fd = clnt_sock;
                    clients[i].events = POLLIN;
                    break;
                }
            }

            if (i == 512)
                error_handling("too many clients");

            if (i > max_i)
                max_i = i;
            printf("fd %d connected\n", clnt_sock);
            continue;
        }

        // 클라이언트 요청 처리
        // 등록된 모든 pollfd들을 검사한다.
        for (i = 1; i <= max_i; ++i)
        {
            if (clients[i].fd < 0)
                continue;
            // i번째 소켓에 POLLIN 이벤트가 발생
            if (clients[i].revents & POLLIN)
            {
                char* buf[BUF_SIZE];
                memset(buf, 0, BUF_SIZE);

				// 이벤트가 발생한 소켓을 읽어들임
                if (read(clients[i].fd, buf, BUF_SIZE) <= 0)
                // 0 또는 -1을 반환했을 때 연결 종료
                {
                    close(clients[i].fd);
                    printf("%d : client disconnected\n", clients[i].fd);
                    clients[i].fd = -1;

                    if (i == max_i)
                    {
                        for (; clients[max_i].fd < 0; --max_i)
                        {}
                    }
                }
                else //echo
                    write(clients[i].fd, buf, strlen((const char*)buf));
            }
        }

    }

    close(serv_sock);

    return 0;
}

socket() -> bind() -> listen()의 과정은 이전과 같지만
클라이언트를 accept()하고 데이터를 read()하는 제어흐름이 다르다.

우선 클라이언트가 보낸 데이터를 읽고 싶기 때문에 pollfdeventsPOLLIN으로 지정해준다.
POLLIN은 읽어들일 수 있는 데이터가 있을 때 reventsPOLLIN을 반환해준다.

poll()함수는 이벤트가 발생한 pollfd에만 revents를 기록해주기 때문에
pollfd들을 돌며 revents값을 검사하여 적절한 처리를 해주면 된다.

0개의 댓글

관련 채용 정보