본문 바로가기
개인공부/윤성우의 열혈 TCP&IP 소켓 프로그래밍

Chapter 12 - IO 멀티플렉싱(Multiplexing)

by 하고싶은건많은놈 2023. 2. 24.

12-1 : IO 멀티플렉싱 기반의 서버

프로세스의 생성에는 많은 연산 및 메모리가 필요함
또한 별도의 메모리를 점유하기 때문에 상호간의 데이터 전송에 불편

멀티플렉싱 : 하나의 통신채널을 통해 둘 이상의 데이터를 전송하는데 사용되는 기술
서버에 멀티플렉싱 기술을 도입하여 필요한 프로세스의 수를 줄일 수 있음


12-2 : select 함수의 이해와 서버의 구현

select() 함수를 사용하여 멀티플렉싱 서버를 구현할 수 있음

  • 여러개의 파일 디스크립터를 동시에 관찰 가능
  • 정확히는 소켓의 관찰항목인 이벤트(event)를 관찰
    • 수신한 데이터를 지니고 있는 소켓의 존재여부
    • 블로킹되지 않고 데이터의 전송이 가능한 소켓 탐색
    • 예외상황이 발생한 소켓 탐색
  1. 파일 디스크립터 설정
    관찰하고자 하는 파일 디스크립터들을 이벤트(수신, 전송, 예외)에 따라 구분해서 모아야 함
    이 때 비트단위 배열인 fd_set형 변수를 사용
    fd_set형 변수에 값을 등록하는 과정은 매크로 함수를 사용하여 이루어짐

    • FD_ZERO(fd_set *fdset) : 모든 비트를 0으로 초기화
    • FD_SET(int fd, fd_set *fdset) : 매개변수 fd로 전달된 파일 디스크립터 정보를 등록
    • FD_CLR(int fd, fd_set *fdset) : 매개변수 fd로 전달된 파일 디스크립터 정보를 삭제
    • FD_ISSET(int fd, fd_set *fdset) : 매개변수 fd로 전달된 파일 디스크립터 정보가 있으면 양수 반환
  2. 관찰 범위지정 및 타임아웃 설정

     #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 반환
    
     struct timeval
     {
         long    tv_sec        // seconds
         long    tv_usec;    // microseconds
     }

    첫번째 매개변수인 maxfd로 파일 디스크립터의 관찰범위를 설정
    파일 디스크립터의 값이 0부터 시작하기 때문에 일반적으로 가장 큰 파일 디스크립터의 값에 1을 더한 수를 전달
    마지막 매개변수인 timeout으로 타임아웃 시간을 설정
    타임아웃 설정시 지정된 시간이 지나면 select() 함수는 0을 반환
    타임아웃을 설정하지 않기 위해 NULL을 인자로 전달하면 파일 디스크립터에 변화가 생길때까지 무한정 블로킹 상태에 머뭄

  3. select() 함수 호출

  4. 호출결과 확인
    select() 함수 호출이 끝나면 인자로 전달된 fd_set형 변수에서 1로 설정되어있던 비트들 중 변화가 발생한 파일 디스크립터에 해당하는 비트만 제외하고 0으로 변경됨

select() 함수를 사용한 예제

// echo_storeserver.cpp

#include <iostream>
#include <unistd.h>
#include <sys/time.h>
#include <sys/select.h>
#define    BUF_SIZE 30

using namespace std;

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);

    /*
    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)
        {
            cout << "select() error!\n";
            break;
        }
        else if (result == 0)
            cout << "Time-out!\n";
        else
        {
            if (FD_ISSET(0, &temps))
            {
                str_len = read(0, buf, BUF_SIZE);
                buf[str_len] = 0;
                cout << "Message from console : " << buf;
            }
        }
    }
    return 0;
}

select() 함수의 호출 후에는 구조체 timeval의 멤버에 저장된 값이 타임아웃이 발생하기까지 남았던 시간으로 바뀜에 유의

select() 함수를 사용한 멀티플렉싱 서버 예시

// echo_selectserv.cpp
// 클라이언트는 이전의 ehoc_client.cpp를 사용
#include <iostream>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/time.h>
#include <sys/select.h>
#include <sys/socket.h>

using namespace std;

#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;
    char                buf[BUF_SIZE];

    if(argc != 2)
    {
        cout << "Usage : " << argv[0] << " <port>\n";
        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, (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 (int i = 0; i < fd_max + 1; i++)
        {
            if (FD_ISSET(i, &cpy_reads))
            {
                if (i == serv_sock)    // connection request
                {
                    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;
                    cout << "connected client : " << clnt_sock << endl;
                }
                else    // read message
                {
                    str_len = read(i, buf, BUF_SIZE);
                    if (str_len == 0)    // close request
                    {
                        FD_CLR(i, &reads);
                        close(i);
                        cout << "closed client : " << i << endl;
                    }
                    else
                        write(i, buf, str_len);    // echo
                }
            }
        }
    }
    close(serv_sock);
    return 0;
}

12-3 : 윈도우 기반으로 구현하기

윈도우에도 select() 함수가 존재

#include <winsock2.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timeval *timeout);
// 성공시 0이상, 실패시 -1 반환

typedef struct timeval
{
    long    tv_sec    // seconds
    long    tv_usec    // microseconds
} TIMEVAL;

typedef struct fd_set
{
    u_int    fd_count;
    SOCKET    fd_array[FD_SETSIZE];
} fd_set;

단, 첫번째 인자는 리눅스 포함 유닉스 계열 운영체제와의 상호 호환성만을 위해 존재함
윈도우의 timeval 구조체는 typedef 선언을 포함함
윈도우의 fd_set 비트의 배열이 아닌 소켓 핸들 수를 기록하는 fd_count와 소켓 핸들을 저장하는 fd_array 멤버로 이루어져있음

  • 이는 0부터 순차적으로 생성되는 리눅스의 파일 디스크립터와 달리 윈도우 기반의 소켓 핸들은 생성되는 핸들의 정수 값 사이의 규칙이 없기 때문
  • 따라서 소켓 핸들을 그대로 저장할 수 있는 배열이 필요
  • 그러나 fd_set 변수를 조작하는 매크로들은 기능 및 사용방법이 리눅스와 동일함

select() 함수를 사용한 윈도우 기반 멀티플렉싱 서버 예시

// echo_selectserv_win.cpp
// 클라이언트는 이전의 echo_client_win.cpp를 사용

#include <iostream>
#include <winsock2.h>

using namespace std;

#define BUF_SIZE 1024
void    ErrorHandling(char* message);

int main(int argc, char *argv[])
{
    WSADATA        wsaData;
    SOCKET        hServSock, hClntSock;
    SOCKADDR_IN    servAdr, clntAdr;
    timeval        timeout;
    fd_set        reads, cpyReads;

    int        adrSz;
    int        strLen, fdNum;
    char    buf[BUF_SIZE];

    if (argc != 2)
    {
        cout << "Usage : " << argv[0] << " <port>\n";
        exit(1);
    }

    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
        ErrorHandling("WSAStartup() error");

    hServSock = socket(PF_INET, SOCK_STREAM, 0);
    memset(&servAddr, 0, sizeof(servAdr));
    servAddr.sin_family = AF_INET;
    servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servAddr.sin_port = htons(atoi(argv[1]));

    if (::bind(hServSock, (SOCKADDR*)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)
        ErrorHandling("bind() error");
    if (::listen(hServSock, 5) == SOCKET_ERROR)
        ErrorHandling("listen() error");

    FD_ZERO(&reads);
    FD_SET(hServSock, &reads);

    while (1)
    {
        cpyReads = reads;
        timeout.tv_sec = 5;
        timeout.tv_usec = 5000;

        if ((fdNum = select(0, &cpyReads, 0, 0, &timeout)) == SOCKET_ERROR)
            break;

        if (fdNum == 0)
            continue;

        for (int i = 0; i < reads.fd_count; i++)
        {
            if (FD_ISSET(reads.fd_array[i], &cpyReads))
            {
                if (reads.fd_array[i] == hServSock)    // conncection request
                {
                    adrSz = sizeof(clntAdr);
                    hClntSock = accept(hServSock, (SOCKADDR*)&clntAdr, &adrSz);
                    FD_SET(hClntSOck, &reads);
                    cout << "connected clinet : " << hClntSock << endl;
                }
                else    // read message
                {
                    strLen = recv(reads.fd_array[i], &reads);
                    if (strLen == 0)    // close request
                    {
                        FD_CLR(reads.fd_aaray[i], &reads);
                        closesocket(cpyReads.fd_array[i]);
                        cout << "closed client : " << cpyReads.fds_array[i] << endl;
                    }
                    else
                        send(reads.fd_array[i], buf, strLen, 0);    // echo
                }
            }
        }
    }
    closesocket(hServSock);
    WSACleanup();
    return 0;
}

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

내용 확인문제

  1. 멀티플렉싱 기술에 대한 일반적인 의미를 말하고, IO를 멀티플렉싱 한다는 것이 무엇을 의미하는지 설명해보자.

멀티플렉싱이랑 하나의 통신채널을 둘 이상의 데이터를 전송하는데 사용함을 뜻하며, IO를 멀티플렉싱 한다는 것은 IO를 필요로하는 소켓을 하나로 묶어 최소한의 리소스 및 프로세스를 이용하여 데이터를 송수신함을 의미함


  1. 멀티프로세스 기반의 동시접속 서버의 단점은 무엇이며, 이를 멀티플렉싱 서버에서 어떻게 보완하는지 설명해보자.

멀티프로세스 기반 서버는 동시접속 수에 따라 프로세스 수가 증가하기 때문에 연산 및 메모리의 부하가 큼
멀티플렉싱 서버에서는 동시접속 수가 늘어나더라도 하나의 프로세스가 관리하도록 하여 이를 보완함


  1. 멀티플렉싱 기반 서버 구현에서는 select 함수를 사용한다. 다음 중 select 함수의 사용방법에 대해서 잘못 설명한 것을 모두 고르면?
  • select 함수의 호출에 앞서 입출력의 관찰 대상이 되는 파일 디스크립터를 모으는 과정이 필요하다. (O)
  • select 함수의 호출을 통해서 한번 관찰의 대상으로 등록이 되면, 추가로 select 함수를 호출하면서 재등록의 과정을 거칠 필요가 없다. (X)
  • 멀티플렉싱 서버는 한 순간에 하나의 클라이언트에게만 서비스가 가능하다. 때문에 서비스를 필요로 하는 클라이언트는 서버에 접속한 후 자신의 순서가 오기를 기다려야한다. (X)
  • select 기반의 멀티플렉싱 서버는 멀티프로세스 기반의 서버와 달리 하나의 프로세스만 필요로한다. 때문에 프로세스의 생성으로 인한 서버의 부담이 없다. (O)

  1. select 함수의 관찰대상에 서버 소켓(리스닝 소켓)도 포함을 시켜야한다. 그렇다면 어떠한 부류에 포함을 시켜야하며, 그 부류에 포함시키는 이유도 설명해보자.

클라이언트로부터의 접속요청도 일종의 데이터 전송이기 때문에 readset에 포함시켜야함


  1. select 함수의 호출에 사용되는 자료형 fd_set의 정의형태는 윈도우와 리눅스에서 차이를 보인다. 그렇다면 어떻게 차이가 나는지 설명하고, 차이가 날수밖에 없는 이유에 대해서도 설명해보자.

리눅스는 파일 디스크립터, 윈도우는 핸들을 사용하기 때문에 리눅스에서는 비트플래그 배열을 사용하고 윈도우에서는 핸들 배열을 멤버로 가짐

댓글