이 글은 윤성우의 TCP/IP 소켓 프로그래밍을 참고하였습니다.

순서

I. select 함수의 기능과 호출 순서

II. 파일 디스크립터(fd)의 설정

III. select() 함수 분석

IV. 서버 구현


I. select 함수의 기능과 호출 순서

select함수의 특징은 한곳에 여러 파일 디스크립터들을 모아놓고 동시에 관찰할 수 있다.
관찰할 수 있는 "Event"들은 아래 항목들이 있다.

1. 수신한 데이터를 가지고 있는 소켓이 있는가? (read)
2. 블로킹되지 않고 데이터의 전송이 가능한 소켓이 있는가? (write)
3. 예외상황이 발생한 소켓은 무엇인가? (except)

그렇다면 이런 event들을 보기 위해서 어떤 흐름으로 select함수를 호출해야할까?
다음 그림과 같이 3단계로 진행된다.

그림1. select 함수 호출의 3단계

이것만 봐서는 이해가 안될 것이다.

이해하기 위해 다음과 같은 흐름을 생각해야한다.

1. select는 다중 파일 디스크립터를 확인한다.
여러 파일 디스크립터를 리스트처럼 모아두며 쭉 탐색할 것 같다. (fd는 2이상의 정수이므로)
리스트에 내가 어떤 파일디스크립터를 확인할지 set, delete 설정이 필요할 것 같다.
--> "파일 디스크립터의 설정"

2. 특정 fd를 모르고 탐색을 하면 처음부터 끝까지 탐색할 것이다.
리스트처럼 모아둔다면 무한정 탐색은 못할것이고... 제일 큰 fd가 몇인지 max를 알아야 할 것 같다.
--> "검사 범위 지정"

3. 파일 디스크립터의 변화를 언제까지 기다릴까?
--> "타임아웃 설정"

4. 1~3에서 설정한 것들로 select 함수를 호출한다.

5. 어느 fd에서 어떤 시그널이 왔을까?
어떤 fd에서 어떤 시그널이 왔는지 확인한 뒤 작업을 진행해야 한다.
--> 호출 결과 확인

위의 5가지 흐름을 꼭 생각하면서 select함수를 이해하자.


II. 파일 디스크립터의 설정

이번 단락에서 위 항목 중 1번(설정)이 포함되어있다.

select 함수는 여러 파일 디스크립터를 동시에 관찰할 수 있다고 하였다.
그러기 위해서 파일 디스크립터를 모아두어야 하고, 앞서 말한 3가지 이벤트로 분리도 해야한다!

이렇게 파일 디스크립터를 세 묶음으로 모을 때 사용되는 것이 fd_set형 변수이다.

그림2 : 자료형 fd_set의 구조

자료형 fd_set은 각 파일디스크립터의 위치에 있는 값이 비트단위로 이뤄진 배열이다.

그럼 이 fd_set을 설정해주는 매크로 함수들을 보자.

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


FD_ZERO(fd_set* fdset) : 인자로 전달된 주소의 fd_set형 변수의 모든 비트를 0으로 초기화

FD_SET(int fd, fd_set* fdset) : 매개변수 fdset으로 전달된 주소의 변수에
                                매개변수 fd로 전달된 파일 디스크립터 정보를 등록한다.
                                (fdset의 fd를 1로 세팅한다 = 등록)

FD_CLR(int fd, fd_set* fdset) : 매개변수 fdset으로 전달된 주소의 변수에
                                매개변수 fd로 전달된 파일 디스크립터 정보를 삭제한다.
                                (fdset의 fd를 0으로 세팅한다 = 삭제)
                                
FD_ISSET(int fd, fd_set* fdset) : 매개변수 fdset으로 전달된 주소의 변수에
                                  매개변수 fd로 전달된 파일 디스크립터 정보가 있으면 양수 반환
                                  (fdset의 fd의 변화를 확인함)

 

 


III. select 함수 분석

이번 단락에서 위 항목 중 2(범위) 3(Timeout), 4(호출), 5(결과)번이 포함되어있다.

#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 반환
    
    ㄴ. maxfd     : 검사 대상이 되는 파일 디스크립터의 수
    ㄴ. readset   : "수신된 데이터의 존재여부"를 볼 fd_set의 주소 전달
    ㄴ. writeset  : "블로킹 없는 데이터 전송의 가능여부"를 볼 fd_set의 주소 전달
    ㄴ. exceptset : "예외상황의 발생여부"를 볼 fd_set의 주소 전달
    ㄴ. timeout   : 무한정 블로킹을 방지할 타임아웃 설정 인자를 전달
    
    ㄴ. 반환값     : 오류시 -1, 타임 아웃시 0
                     정상 반환 시 1이상으로 변화가 발생 한 fd의 수를 반환한다.

read/write/except의 인자는 각각 fd가 설정된 묶음인 fd_set의 주소라는 것을 주의하고

이번 단락에서는 첫 번째 인자인 maxfd와 마지막 인자인 timeout과 정상 반환에 대해 분석해보자.


1. int maxfd

fd_set형 변수에 등록된 fd의 수를 확인할 필요가 있는데,
fd의 값은 fd가 생성될 때마다 1씩 증가하기 때문에 가장 큰 fd에 +1한 값을 인자로 넘겨주면 된다.

+1을 해주는 이유는 fd의 값이 0부터 시작하기 때문이다.


2. (const struct timeval*) timeout

timeval의 구조체는 다음과 같이 정의되어 있다.

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

이 번수를 따로 선언해서 초와, 마이크로초 단위 정보를 저장하고,
이 구조체 변수의 주소값을 인자로 전달한다.

타임아웃 하기 싫으면 NULL값을 인자로 전달하자.


3. 정상 반환시 (event발생)

우리는 select 함수가 대기 중 특정 fd의 변화가 생기면 해당 fd의 값을 반환한다고 알고있다.

그렇다면 이 변화를 어떻게 아는 것일까?

fd_set의 변수에 다음 그림처럼 변화가 생기게 된다.

그림3. select 호출 후 변화

기존 fd_set의 변수의 fd에 변화가 생겼다. 변화가 생긴 fd의 값이 1로 켜졌다!!
또 그 이외의 fd 값은 0으로 꺼졌다!

그림만 보면 이런 생각이 든다.
원본이 훼손되는 것 인가?
--> YES!!

그렇다면 변화 확인이 한 번만 유효한가??
--> YES!!

위 두가지 이유로 select를 반복문에 넣어주어야 하고,
select를 호출하기 전에는 항상 진짜 원본의 복사본으로 작업을 해야한다.

+) 각 fd의 변화를 보고 작업하기 위해서는
변화된 fd의 수를 반환하므로 그 수만큼 반복해서 if( FD_ISSET( i, &cpy_fd_set ) )으로 작업할 것.

 


IV. 서버 구현

코드가 길기에 서버 구현부는 생략하고 select를 위한 코드를 집중적으로 적어보았다.

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

#define BUF_SIZE 100

int main()
{
    int serv_sock, clnt_sock; // 정의만하고 서버구현 안할 것. 각 소켓의 디스크립터이다.
    struct sockaddr_in serv_addr, clnt_addr; // sockaddr_in 구조체 변수
    socklen_t adr_sz; // 클라이언트 구조체 길이를 담고있는 변수
    
    struct timeval timeout; // select timeout 인자 구조체
    fd_set reads, cpy_reads; // 핵심의 fd_set 변수, 매 select 마다 cpy_reads라는 복사본 사용.
    int fd_max, str_len, fd_num, i;
    
    1. socket()
    2. serv_addr 구조체 정의 및 bind()
    3. listen()
    
    // fd_set 정보 만들기 위한 작업
    FD_ZERO(&reads); // reads fd_set 초기화
    FD_SET (serv_sock, &reads); // 서버소켓의 fd를 reads라는 fd_set에 등록!
    fd_max = serv_sock; // fd가 하나이므로 이게 fd_max값이다.
    
    while(1)
    {
        cpy_reads = reads; // select시 원본 훼손되므로 복사본을 이용한다!
        timeout.tv_sec=5; // timeout 초 정의
        timeout.tv_usec=5000; // timeout 마이크로초 정의
        
        // select함수 호출!!
        // write, except 안하고 read만 확인할 것임.
        fd_num = select(fd_max+1, &cpy_reads, 0, 0, &timeout);
        
        if(fd_num == -1) break; //error
        else if(fd_num ==0) continue; // timeout이면 다시 대기해봄
        
        for(i=0; i<fd_max+1;i++) // 변화 확인된 개수만큼 쭉 돈다
        {
            if ( FD_ISSET(i, &cpy_read)) // i번째 fd에 변화가 있나~ FD_ISSET으로 확인
            {    
                if(i==serv_sock) // 서버의 fd면!(서버의 변화?) 연결 요청이 온 것이다!
                {
                    adr_sz=sizeof(clnt_adr);
                    clnt_sock = accept(~~); // 이 때 클라이언트를 accept해준다!!
                    FD_SET(clnt_sock, &reads); // 원본 fd_set에 클라이언트도 등록!!
                    if(fd_max < clnt_sock) fd_max=clnt_sock; 최대 fd길이 늘려줌 (추가되었으니)
                    printf("client Connect : %d\n",clnt_sock)
                } 
                else  // 서버가 아닌, 클라이언트 쪽이라면 
                { 
                str_len = read(i, buf, BUF_SIZE);

                if( str_len =0) // 통신이 종료될 때
                {
                    FD_CLR(i, &reads); // 원본fd_set에서 해당 클라이언트의 fd 삭제해줌
                    close(i); // 그러고 소켓통신 닫아줌
                    printf("Closed client: %d\n", i);
                } else
                  write(i, buf, str_len); // 정상수신이 되었다면 하고 싶은거 하기
                }
            }
        }
    }
    close(serv_sock);
    return 0;
}

 

+ Recent posts