소켓프로그래밍#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개의 댓글

관련 채용 정보