소켓프로그래밍#9 : IO 멀티플렉싱

kkado·2022년 6월 5일
1

네트워크프로그래밍

목록 보기
10/16

멀티...flex

가 아니고 multi plex

멀티플렉싱(Multiplexing) 이란

이전에 멀티 프로세스 기반의 서버를 알아보았는데 사실 프로세스를 여러 개 만드는 것은 좋지 않다.

프로세스를 생성하는 것 자체가 메모리를 많이 차지하게 되고, 또 프로세스 간의 통신이 필요한 상황에는 서버의 구현이 더 복잡해진다. 이러한 멀티 프로세스 서버의 대안들 중 하나가 바로 IO 멀티플렉싱이다.

하나의 프로세스가 다수의 클라이언트에게 서비스를 할 수 있도록 하는 것이다. 이러기 위해서는 하나의 프로세스가 여러 개의 소켓을 컨트롤 할 수 있어야 한다.

select 함수의 이해와 서버의 구현

select 함수를 이용하면, 파일 디스크립터(이하 fd)를 배열에 저장해 놓고 동시에 관찰할 수 있다. 그리고 다음과 같은 질문을 던진다.

  • 수신한 데이터를 지니고 있는 소켓이 존재하는가?
  • 블로킹되지 않고 데이터의 전송이 가능한 소켓은 무엇인가?
  • 예외상황이 발생한 소켓은 무엇인가?

그리고 select 함수의 호출 방법 및 순서는 다음과 같다.

fd 설정

select 함수는 fd를 하나의 배열로 관리한다고 하였다.
fd_set 형 변수에 select 함수에 전달할 fd의 정보를 하나로 묶어 놓는다.

그리고 다음과 같은 컨트롤 함수들을 이용하여 변수를 컨트롤 할 수 있다.

  • FD_ZERO(fd_set *fdset) : 인자로 전달된 주소의 fd_set형 변수의 모든 비트를 0으로 초기화한다.
  • FD_SET(int fd, fd_set *fdset) : 매개변수 fdset으로 전달된 주소의 변수에 매개변수 fd로 전달된 파일 디스크립터 정보를 등록한다.
  • FD_CLR(int fd, fd_set *fdset) : 매개변수 fdset으로 전달된 주소의 변수에서 매개변수 fd로 전달된 파일 디스크립터 정보를 삭제한다.
  • FD_ISSET(int fd, fd_set *fdset) : 매개변수 fdset으로 전달된 주소의 변수에 매개변수 fd로 전달된 파일 디스크립터 정보가 있다면 양수를 반환한다.

select 함수

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

int select(int maxfd, fd_set *readset, fd_set *writeset, 
			fd_set *exceptset, const struct timveval *timeout);

파라미터가 좀 많다.

  • maxfd : 검사 대상이 되는 fd의 수
  • readset : 수신된 데이터의 존재여부에 관심 있는 fd 정보를 fd_set형 변수에 등록해서, 그 변수의 주소값
  • writeset : 블로킹 없는 데이터 전송의 가능여부에 관심 있는 fd 정보를 fd_set형 변수에 등록해서, 그 변수의 주소값
  • exceptset : 예외상황의 발생여부에 관심 있는 fd 정보를 fd_set형 변수에 등록해서, 그 변수의 주소값
  • timeout : select 함수 호출 이후에 무한정 블로킹 상태에 빠지지 않도록 time-out을 설정하기 위한 인자
  • return 값 : 오류 발생 시 -1, timeout에 인한 반환 시 0, 관심 대상으로 등록된 fd에 변화가 발생하면 0보다 큰 값

select 함수호출 이후에는 변화가 발생한 소켓의 디스크립터만 1로 설정되고 나머지는 0으로 초기화 된다.

멀티플렉싱 서버 구현

1단계

FD_ZERO(&reads);
FD_SET(sockfd, &reads);
fd_max = sockfd;

기본적인 소켓 생성, 주소 설정, bind, listen 등의 과정은 생략. reads는 fd_set 형 변수

reads를 모두 0으로 초기화 하고 sockfd까지 reads에 추가한다. (연결 요청이 들어왔을 때 accept 해야 하므로 서버의 소켓도 변화를 감시해야 함)

2단계

while(1)
{
	cpy_reads = reads;
    timeout.tv_sec = 5;
    timeout.tv_usec = 0;
    
    fd_num = select(fd_max+1, &cpy_reads, 0, 0, &timeout);
    
    if(fd_num == 0)
    {
    	continue;
    }
}

select 함수를 호출할 때마다 timeout을 새롭게 명시해줘야 한다.
select의 3, 4번째 인자에 0을 전달하면, 수신된 데이터가 있는지에 대해서만 관찰하는 select 함수가 된 것이다.

3단계

for(int i=0;i<fd_max+1;i++)
{
	if(FD_ISSET(i, &cpy_reads))
    {
    	if(i == sockfd)
    	{
        	int adrsz = sizeof(cliaddr);
            cSockfd = accept(sockfd, (struct sockaddr*)&cliaddr, &adrsz);
            FD_SET(cSockfd, &reads);
            
            if(fd_max < cSockfd)
            	fd_max = cSockfd;
                
            printf("Connected client : %d\n, cSockfd);
    	}
        else
        {
        // read message 등 코드 실행 부분
        	FD_CLR(i, &reads);
            close(i);
            printf("Closed Client : %d\n, i);
        }
    }
}

for문으로 fd_max까지 확인하면서 변화가 생긴 fd가 어디인지 살펴본다.
만약 sockfd에서 생겼다면 새로운 연결 요청이 들어온 것이므로 accept 해 주고
만약 sockfd가 아닌 다른 fd에서 생겼다면 클라이언트와 연결된 소켓이라는 뜻이므로 데이터를 읽어들이는 등의 과정을 수행하고, 닫아야 하는 상황이 생긴다면 FD_CLR 함수와 close를 통해 삭제 해준다.

주의해야 할 점

select 에 사용할 fd_set 변수는 원본은 그냥 두고 복사본을 집어넣어 사용한다.

fd_set은 비트 단위의 리스트이다. FD_SET에 의해 소켓이 등록되면 해당 소켓에 매칭되는 비트는 1로 저장된다. 그리고 select 함수는 1로 저장된 것들만 감시한다.

select 함수가 실행되면, 그 0이든 1이든 하는 결과들을 2번째 인자로 넘겨주었던 fd_set 변수에 저장하는데, 데이터가 들어온 소켓은 1, 아니면 0으로 저장하고 나서 그것을 FD_ISSET 함수를 통해 보는 것이다.

만약 이 과정에서 값이 바뀐 fd_set 변수를 그대로 다음 select에 사용하게 되면 0으로 되어 있는 소켓들은 select 함수가 감시하지 않게 된다.

그래서 감지 목록을 유지하기 위해서 반드시 사용하기 전에 fd_set 함수를 복사해놓은 다른 변수를 만들고, 그 복사본 변수를 사용해야 한다.

select 사용하기 전에 timeval 시간을 재설정 해야 한다.

위 코드에서 timeout에 사용되는 timeval을 5초로 잡았다.
이 때, select에서 5초 중 2초만 기다렸다가 감시대상을 확인하고 리턴 하면, timeval 변수에는 5초에서 2초를 뺀 3초를 timeval 변수에 저장한다.

그러므로, 재설정 없이 계속 같은 timeval 변수를 select에 사용할 경우 시간은 점점 차감되어 나중에는 0이 되어 버릴 것이다.

profile
베이비 게임 개발자

0개의 댓글