Computer Science 기본 지식/소켓 프로그래밍

[TCP/IP 소켓 프로그래밍] (8) 다중 접속 서버 - 멀티 플렉싱 기반 select 서버

로파이 2021. 4. 1. 17:17

출처 : 열혈 TCP/IP 소켓 프로그래밍 윤성우 저

 

멀티 프로세스 기반 서버의 단점

프로세스를 복사할 때마다 비용이 들고 독립적인 메모리로 상호간 데이터를 주고 받기 위해서는 Pipe라는 운영체제 자원을 사용해야한다.

다수의 프로세스를 생성하지 않으면서 클라이언트에게 서비스를 할 수 있는 방법 중 "멀티 플렉싱" 기술을 사용하면 다중 사용자에게 하나의 프로세스에서 서비스를 제공할 수 있다.

 

멀티 플렉싱

하나의 통신 채널을 통해 둘 이상의 데이터를 전송하는데 사용하는 기술

 

select 함수의 기능과 호출 순서

멀티 플렉싱 기술에서 여러 소켓/파일 디스크립터를 동시에 관찰하기 위해 사용하는 함수, select가 있다. select 함수는 

1. 수신한 데이터를 지니고 있는 소켓이 있는가

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

3. 예외사항이 발생한 소켓이 무엇인가

위 3가지 항목을 관찰 대상으로부터 정보를 얻어온다.

 

호출 순서

 

이미지 출처 : TCP/IP 소켓프로그래밍 (윤성우 저)

- 1. 파일 디스크립터 설정

select 함수를 호출하기 전 준비해야할 것이 있다. 먼저 관찰할 파일 디스크립터를 설정하는 것이다.

파일 디스크립터를 비트 셋으로 보고 어떤 offset에 해당하는 비트가 파일 디스크립터에 대한 수신, 전송, 예외에 대한 정보를 담고 있다.

 

fd_set 자료형 : 비트 셋을 표현하는 이진 수를 저장하는 배열

 

fd_set의 내용 설정, 확인과 관련된 매크로

  • FD_ZERO(fd_set*): 모든 비트를 0으로 초기화한다.
  • FD_SET(int, fd_set*): 해당 디스크립터를 관찰 대상으로 표기한다.
  • FD_CLR(int, fd_set*): 해당 디스크립터 정보를 삭제한다.
  • FD_ISSET(int, fd_set*): 해당 디스크립터에 대한 정보가 있는지 확인한다.

 

- 2. 검사의 범위 지정과 타임아웃 설정

int select(int maxfd, fd_set* readset, fd_set *writeset, fd_set *exceptset, const struct timeval * timeout);
  • 성공 시 0 이상, 실패시 -1 반환, 0은 타임 아웃
  • maxfd: 검사 대상의 파일 디스크립터 수
  • readset: "수신된 데이터의 존재 여부"를 체크하기 위해 전달하는 비트 셋
  • writeset: "블로킹 없는 데이터 전송의 가능 여부"에 관심 있는 비트 셋
  • exceptset: "예외 발생여부"에 관심 있는 비트 셋
  • timeout: select 함수 호출이 무한정 블로킹 상태에 있지 않도록 타임 아웃을 설정
  • 반환 값이 0이상 일 경우, 변화가 발생한 디스크립터의 수가 반환된다.

파일 디스크립터 수는 Linux에서 생성될때마다 1 씩 증가하므로 가장 큰 파일 디스크립터에 1을 더하면 된다.

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

select 함수는 관찰중인 파일 디스크립터에 변화가 생겨야 반환을 하기 때문에 무한 블로킹 상태에 빠질 수 있다. 타임 아웃을 위한 타이머를 설정하여 일정 시기가 지나면 select 함수를 빠져나온다.

 

멀티 플렉싱 기반 Select 에코 서버 구현

 

1. 서버 소켓 생성과 서버 소켓 핸들의 수신 데이터 여부를 관찰 대상으로 지정한다.

int main(int argc, char *argv[])
{
    struct sockaddr_in serv_adr = {};
    int 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(PORT);

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

    // server socket read ready
    fd_set reads, cpy_reads;
    FD_ZERO(&reads);
    FD_SET(serv_sock, &reads);

서버 소켓으로 클라이언트 연결 요청이 있는지 확인하기 위해 서버 소켓의 변화를 관찰 한다.

 

2. 타임 아웃 시간 설정과 수신에 대한 전체 변화를 select 함수를 통해 관찰한다.

관찰 대상 디스크립터: 매번 관찰 대상을 설정하기 위해 복사하여 설정한다.

타임 아웃 시간: select 함수 종료시 마지막 시간으로 설정되니 while 문 안에서 재초기화해야한다.

    char buf[BUF_SIZE];
    int fd_max = serv_sock, fd_num;
    struct timeval timeout;
    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) // if failed
            break;
        
        if(fd_num == 0) // time out
        {
            printf("Time-out\n");
            continue;
        }

 

3. 클라이언트 연결 요청 수락

변화가 있는 소켓이 있다면 for문으로 진입하여 각 소켓의 변화를 처리한다.

서버 소켓이 변하였다면 클라이언트 소켓을 연결하고 해당 클라이언트 소켓 디스크립터의 수신 여부를 관측 대상에 등록한다.

	for(int i=0;i<fd_max+1;++i)
        {
            if(FD_ISSET(i, &cpy_reads))
            {
                // server socket selected
                if(i==serv_sock) // connection requested
                {
                    struct sockaddr_in clnt_adr = {};
                    socklen_t adr_sz = sizeof(clnt_adr);
                    int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);

                    // client socket ready ready
                    FD_SET(clnt_sock, &reads);

                    if(clnt_sock > fd_max)
                        fd_max = clnt_sock;

                    printf("connected client: %d\n", clnt_sock);
                }

 

4. 클라이언트로부터 데이터 수신

관찰 대상으로 등록한 클라이언트 소켓으로 부터 변화가 있다면 데이터를 읽고 에코 메세지를 보낸다.

         	// client socket selected
                else 
                {
                    // read message
                    int str_len = read(i, buf, BUF_SIZE);
                    if(str_len == 0) // EOF
                    {
                        FD_CLR(i, &reads);
                        close(i);
                        printf("closed client %d \n", i);
                    }
                    else
                    {
                        write(i, buf, str_len); // echo
                    }
                }
            }
        }
    }
    close(serv_sock);
    return 0;
}