책정리/열혈 TCP,IP

12장 IO 멀티플렉싱(Multiplexing)

GONII 2015. 8. 5. 16:17
  • 멀티플렉싱 기반의 서버

    멀티프로세스 서버의 단점과 대안

    이전 Chapter에서는 다중접속 서버의 구현을 위해 클라이언트의 연결요청이 있을 때마다 새로운 프로세스를 생성하였다. 이는 실제 사용되는 방법이지만 문제가 전혀 없는 방법은 아니다. 프로세스의 생성에는 상당히 많은 대가를 지불해야 하기 때문이다. 많은 양의 연산이 요구되며, 필요한 메모리 공간도 비교적 큰 편이다. 또한 프로세스마다 별도의 메모리 공간을 유지하기 때문에 상호간에 데이터를 주고받으려면 다소 복잡한 방법을 택할 수밖에 없다.

    프로세스의 생성을 동반하지 않으면서 다수의 클라이언트에게 서비스를 제공할 수 있는 방법으로 IO멀티플렉싱 서버가 있다. 하지만 이 모델은 구현하고자 하는 서버의 특성에 따라서 구현방법이 달리 결정되어야 한다. 즉 이 방법이 모든 경우에 있어서 최선은 아니다.

    멀티플렉싱이라는 단어의 이해

    • 하나의 통신 채널을 통해서 둘 이상의 데이터(시그널)를 전송하는데 사용되는 기술
    • 물리적 장치의 효율성을 높이기 위해서 최소한의 물리적인 요소만 사용해서 최대한의 데이터를 전달하기 위해 사용되는 기술

    멀티플렉싱의 개념을 서버에 적용하기

    서버에 멀티플렉싱 기술을 도입해서 필요한 프로세스의 수를 줄일 수 있다.

    위 모델에 멀티플렉싱 기술을 적용하면 다음과 같이 프로세스의 수가 줄어든다.

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

    select 함수의 기능과 호출 순서

    select 함수를 사용하면 한곳에 여러 개의 파일 디스크립터를 모아놓고 동시에 이들을 관찰할 수 있다. 이때 관찰할 수 있는 항목은 다음과 같다

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

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

    파일 디스크립터의 설정

    select 함수를 사용하면 여러 개의 파일 디스크립터를 동시에 관찰할 수 있다고 하였다. 물론 파일 디스크립터의 관찰은 소켓의 관찰로 해석할 수 있다. 그렇다면 먼저 관찰하고자 하는 파일 디스크립터를 모아야 한다. 모을 때도 관찰항목에 따라서 구분해서 모아야 한다.

    파일 디스크립터를 세 묶음으로 모을 때 사용하는 것이 fd_set형 변수이다. 이는 다음 그림에서 보이듯이 0과 1로 표현되는, 비트단위로 이뤄진 배열이라고 생각하면 된다.

    이 비트가 1로 설정되면 해당 파일 디스크립터가 관찰의 대상임을 의미한다. fd_set형 변수에 값을 등록하거나 변경하는 등의 작업은 다음 매크로 함수들의 도움을 통해 이뤄진다.

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로 전달된 파일 디스크립터 정보가 있으면 양수를 반환

위의 함수들 중에서 FD_ISSET은 select 함수의 호출결과를 확인하는 용도로 사용된다.

검사(관찰)의 범위지정과 타임아웃의 설정

#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 반환

  • int maxfd

    검사 대상이 되는 파일 디스크립터의 수

  • fd_set* readset

    fd_set형 변수에 '수신된 데이터의 존재여부'에 관심 있는 파일 디스크립터 정보를 모두 등록해서 그 변수의 주소 값을 전달

  • fd_set* writeset

    fd_set형 변수에 '빌로킹 없는 데이터 전송의 가능여부'에 관심 있는 파일 디스크립터 정보를 모두 등록해서 그 변수의 주소 값을 전달

  • fd_set* exceptset

    fd_set형 변수에 '예외상황의 발생여부'에 관심이 있는 파일 디스크립터 정보를 모두 등록해서 그 변수의 주소 값을 전달

  • const struct timeval* timeout

    select 함수 호출 이후에 무한정 블로킹 상태에 빠지지 않도록 타임아웃(time-out)을 설정하기 위한 인자를 전달

  • 반환 값

    오류 발생시에는 -1 반환

    타임 아웃에 의한 반환 시에는 0이 반환

    관심 대상으로 등록된 파일 디스크립터에 해당 관심에 관련된 변화가 발생하면 0보다 큰 값이 반환되는데 이 값은 변화가 발생한 파일 디스크립터의 수를 의미한다

select 함수는 세가지 관찰항목의 변화를 확인하는데, 이 세가지 관찰항목별로 fd_set형 변수를 선언해서 파일 디스크립터 정보를 등록하고, 이 변수의 주소 값을 위 함수의 두 번째, 세 번째 그리고 네 번째 인자로 전달하게 된다. 그런데 이에 앞서 다음 두 가지를 먼저 결정해야 한다.

  • 파일 디스크립터의 관찰(검사) 범위는 어떻게 되는지?
  • select 함수의 타임아웃 시간을 어떻게 할지?

이 중 첫 번째 파일 디스크립터의 관찰(검사) 범위는 select 함수의 첫번째 매개변수와 관련이 있다. 따라서 fd_set형 변수에 등록된 파일 디스크립터의 수를 확인할 필요가 있는데 파일 디스킯터의 값은 생성될 때마다 1씩 증가하기 때문에 가장 큰 파일 디스크립터의 값에 1을 더해서 전달하면 된다.

두 번째 select 함수의 타임아웃 시간은 select 함수의 마지막 매개변수와 관련이 있는데 매개 변수 선언에 보이는 자료형 timeval 구조체 기반의 자료형으로 다음과 같이 정의되어 있다.

struct timeval

{

long tv_sec; // second

long tv_usec; // microseconds

}

select 함수는 관찰중인 파일 디스크립터에 변화가 생겨야 반환을 한다. 때문에 변화가 생기지 않으면 무한정 블로킹 상태에 머물게 된다. 바로 이러한 상황을 막기 위해서 타임아웃을 지정하는 것이다. 타임아웃을 설정하고 싶지 않을 경우 NULL을 인자로 전달하면 된다.

select 함수호출 이후의 결과확인

0이 아닌 양수가 반환이 되면 그 수만큼 파일 디스크립터에 변화가 발생했음을 의미한다.

select 함수가 양의 정수를 반환한 경우, 변화가 발생한 파일 디스크립터는 어떻게 알아낼 수 있을까? select 함수의 두 번재, 세 번째 그리고 네 번째 인자로 전달된 fd_set형 변수에 다음 그림에서 보이는 변화가 발생하기 때문에 어렵지 않게 알아낼 수 있다.

select 함수 호출이 완료되고 나면 select 함수의 인자로 전달된 fd_set형 변수에 변화가 생긴다. 1로 설정된 모든 비트가 다 0으로 변경되지만 변화가 발생한 파일 디스크립터에 해당하는 비트만 그대로 1로 남아있게 된다. 때문에 1로 남아있는 위치의 파일 디스크립터에서 변화가 발생했다고 판단할 수 있다.

최종! select 함수를 호출하는 예제의 확인

  • select.c

#include <stdio.h>

#include <unistd.h>

#include <sys/time.h>

#include <sys/select.h>

#define BUFSIZE 30

   

int main(int argc, char* argv[])

{

fd_set reads, temps;

int result, strLen;

char buf[BUFSIZE];

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 )

{

puts("select() error");

break;

}

else if( result == 0 )

{

puts("timeout!");

}

else

{

if(FD_ISSET(0, &temps))

{

strLen = read(0, buf, BUFSIZE);

buf[strLen] = 0;

printf("message from consol: %s", buf);

}

}

}

return 0;

}

멀티플렉싱 서버의 구현

  • echo_selectserv.c

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <unistd.h>

#include <arpa/inet.h>

#include <sys/socket.h>

#include <sys/select.h>

   

#define BUFSIZE 100

void errorHandling(char* buf);

   

int main(int argc, char* argv[])

{

int servSock, clntSock;

struct sockaddr_in servAddr, clntAddr;

struct timeval timeout;

fd_set reads, cpyReads;

   

socklen_t addrSz;

int fdMax, strLen, fdNum, i;

char buf[BUFSIZE];

if( argc != 2 )

{

printf("Usage : %s <port>\n", argv[0]);

exit(1);

}

   

servSock = socket(PF_INET, SOCK_STREAM, 0);

memset(&servAddr, 0, sizeof(servAddr));

servAddr.sin_family = AF_INET;

servAddr.sin_addr.s_addr = htonl(INADDR_ANY);

servAddr.sin_port = htons(atoi(argv[1]));

   

if( bind(servSock, (struct sockaddr*)&servAddr, sizeof(servAddr)) == -1 )

errorHandling("bind() error");

if( listen(sockScok, 5) == -1 )

errorHandling("listen() error");

   

FD_ZERO(&reads);

FD_SET(servSock, &reads);

fdMax = servSock;

   

while(1)

{

cpyReads = reads;

timeout.tv_sec = 5;

timeout.tv_usec = 5000;

   

if((fdNum = select(fdMax+1, &cpyReads, 0, 0, &timeout)) == -1 )

break;

   

if( fdNum == 0 )

continue;

   

for( i = 0 ; i < fdMax+1 ; i++ )

{

if( FD_ISSET(i, &cpyReads) )

{

if( i == servSock )

{

addrSz = sizeof(clntAddr);

clntSock = accept(servSock, (struct sockaddr*)&clntAddr, &addrSz);

FD_SET(clntSock, &reads);

   

if( fdMax < clntSock )

fdMax = clntSock;

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

}

else

{

strLen = read(i, buf, BUFSIZE);

// close request

if( strLen == 0 )

{

FD_CLR(i, &reads);

close(i);

printf("close client : %d\n", i);

}

else

{

// echo

write(i, buf, strLen);

}

}

}

}

}

close(servSock);

return 0;

}

  • 윈도우 기반으로 구현하기

    윈도우 기반 select 함수의 호출

    윈도우에서도 select 함수를 제공한다. 그리고 모든 인자는 리눅스 기반의 select 함수와 동일하다.

#include <winsock2.h>

int select( int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, cont struct timeval* timeout);
// 성공 : 0 이상 반환

// 실패 : -1 반환

윈도우의 fd_set은 리눅스와 같이 비트의 배열로 구성되어 있지 않다.

typedef struct fd_set

{

u_int fd_count;

SOCKET fd_array[FD_SETSIZE];

} fd_set;

윈도우의 fd_set은 저장된 소켓의 핸들 수를 기록하기 위한 멤버 fd_count와 소켓의 핸들 저장을 위한 멤버 fd_array로 이뤄져있다.

윈도우 기반 멀티플렉싱 서버의 구현

  • echo_selectserv_win.c

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <winsock2.h>

#pragma comment(lib, "ws2_32.lib")

   

#define BUFSIZE 1024

void errorHandling(char* msg);

   

int main(int argc, char* argv[])

{

WSADATA wsaData;

SOCKET hServSock, hClntSock;

SOCKADDR_IN servAddr, clntAddr;

TIMEVAL timeout;

fd_set reads, cpyReads;

   

int addrSz;

int strLen, fdNum, i;

char buf[BUFSIZE];

   

if( argc != 2 )

{

printf("Usage : %s<PORT>\n", argv[0]);

exit(1);

}

if( WSAStartup(MAKEWORD(2,2), &wsaData) != 0 )

errorHandling("WSAStartup() error");

   

hServSock = socket(PF_INET, SOCK_STREAM, 0);

memset(&servAddr, 0, sizeof(servAddr));

servAddr.sin_family = AF_INET;

servAddr.sin_addr.s_addr = htonl(INADDR_ANY);

servAddr.sin_port = htons(argv[1]);

   

if( bind(hServSock, (SOCKADDR*)&servAddr, sizeof(servAddr)) == 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( i = 0 ; i < reads.fd_count ; i++ )

{

   

if( FD_ISSET(reads.fd_array[i], &cpyReads) )

{

// connection request

if( reads.fd_array[i] == hServSock )

{

addrSz = sizeof(clntAddr);

hClntSock = accept(hServSock, (SOCKADDR*)&clntAddr, &addrSz);

FD_SET(hClntSock, &reads);

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

}

// read message

else

{

strLen = recv(reads.fd_array[i], buf, BUFSIZE-1, 0);

// close request

if( strLen == 0 )

{

FD_CLR(reads.fd_array[i], &reads);

closesocket(cpyReads.fd_array[i]);

printf("closed client: %d\n", cpyReads.fd_array[i]);

}

else

{

// echo

send(reads.fd_array[i], buf, strLen, 0);

}

}

}

}

}

closesocket(hServSock);

WSACleanup();

return 0;

}

   

void errorHandling(char* msg)

{

fputs(msg, stderr);

fputc('\n', stderr);

exit(1);

}

 

반응형