'프로그래밍'에 해당되는 글 1건

  1. 2010.02.21 IO 멀티플렉싱 서버 구현
Network/테스트베드2010. 2. 21. 16:00
반응형

1. 개요

서버에서 여러명의 클라이언트와 동시에 메시지를 주고받기 위한 멀티 쓰레드는 각 쓰레드당 메모리와 CPU의 스택을 필요로 하기 때문에 서버의 성능이 저하될 수 있는 단점이 있다. 따라서 이러한 단점을 보완하고자 서버에서 각 클라이언트마다 쓰레드를 생성하는 것이 아니라 하나의 쓰레드만을 생성하여 여러 클라이언트와 통신을 할 수 있도록 나온 것이 IO 멀티 플렉싱이다.

본 문서에서는 윈도우 XP 환경에서 IO 멀티 플렉싱을 이용한 에코 서버 프로그래밍에 대해 설명한다. 2절에서는 IO 멀티플렉싱 관련 이론 및 함수에 대해 설명하고 3절에서는 프로그래밍한 각 소스코드를 분석하며 4절에서는 프로그램 실행 결과를 나타냄으로써 본 문서를 마친다.

 

2. IO 멀티플렉싱

그림 1과 같이 하나의 쓰레드를 이용하여 여러 클라이언트와 동시에 메시지를 주고받을 수 있는 IO 멀티플렉싱은 select( )를 통해 구현이 가능하다. select( )는 클라이언트가 메시지를 전송했던지 아니면 클라이언트가 서버로 접속을 했던지 아니면 서버에서 클라이언트로 보낼 메시지가 있다던가 하는 서버소켓이나 클라이언트 소켓에서의 변화가 발생했을 때, 변화가 발생한 소켓을 알 수 있도록 하는 함수이다. select( )를 이용하여 소켓의 변화를 알 수 있도록 하기 위해서는 그림 2와 같이 읽기셋, 쓰기셋, 예외셋 등과 같이 소켓을 모아놓은 집합인 소켓셋을 필요로 한다. 읽기셋은 서버에서 클라이언트로부터 메시지를 수신한다던지 혹은 클라이언트로부터의 접속을 받아들일 때 이용되고, 쓰기셋은 서버에서 클라이언트로 메시지를 송신하기 위해 이용되며 예외셋은 클라이언트로부터 긴급 상황 시 전송되는 OOB(out-of-band) 메시지를 수신했는지 알고자 할 때 이용된다.

 

2.1. 관련 함수

■ select( )

select( )는 지정한 소켓의 변화를 확인하고자 사용되는 함수로 소켓함수를 호출해야 할 시점을 알려주는 역할을 하며 기본형은 표 1과 같다. 즉, select( )는 각 소켓셋에 저장된 소켓에 변화가 생길 때까지 기다리고 있다가 소켓이 어떠한 동작을 하면 동작한 소켓을 제외한 나머지 소켓을 모두 제거한다. 그림 3과 같이 select( )가 실행되고 나서 소켓셋에 남아있는 소켓은 어떠한 동작이 이루어진 소켓이 된다. 표 1은 기본형 및 파라미터 등을 나타낸 것이다. 이 중 n은 리눅스에서만 사용되는 기능이며 윈도우에서는 리눅스와의 호환을 위해서 사용된다. 따라서 윈도우 기반에서 구현한 본 프로그래밍에서는 이 값을 NULL로 지정하여 사용하였다. 그 다음에는 각 소켓셋이 파라미터로 들어가며 마지막 파라미터인 timeout은 NULL로 설정하면 읽기셋, 쓰기셋, 예외셋에 삽입한 소켓 중에 변화가 생길 때까지 대기하고 있다가 변화가 발생한 소켓의 수를 리턴하게 된다. timeout을 양수로 설정한 경우에는 변화가 발생한 소켓이 생길 때까지 대기하고 있다가 설정한 시간이 되면 변화가 발생한 소켓이 없더라도 대기상태를 해제하게 된다. 이때 변화가 발생한 소켓이 없다면 0을 리턴하게 된다. 그리고 timeout 값이 0으로 설정되면 대기시간 없이 바로 리턴하게 된다.

 

기능

지정한 파일 디스크립터의 변화를 확인

기본형

int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

파라미터

n : 검사할 파일 디스크립터 개수

readfds : 수신된 메시지 혹은 클라이언트 접속을 감시해야 하는 소켓들을 모아놓은 읽기셋의 포인터

writefds : 메시지 전송할 수 있는 지 감시해야 하는 소켓들을 모아놓은 쓰기셋의 포인터

exceptfds : 오류가 발생했는지 감시해야 되는 소켓들을 모아놓은 예외셋의 포인터

timeout : 초와 1/1000 초 단위로 대기시간을 설정

리턴값

0 이상 : 조건을 만족하는 소켓의 개수

0 : 타임아웃

-1 : 에러

 

 

소켓셋은 아래와 같이 fd_set 구조체로 되어 있으며 내부 변수로는 fd_count와 fd_array가 있다. fd_count는 fd_array에 저장된 소켓의 개수를 나타내며 fd_array는 소켓 파일디스크립터가 저장되는 배열을 말한다.

 

typedef struct fd_set

{

u_int fd_count;

SOCKET fd_array[FD_SETSIZE];

} fd_set;

 

■ FD_ZERO( )

FD_ZERO( )는 소켓셋을 초기화할 때 사용되는 함수이다. 파라미터 fdset은 초기화하고자 하는 소켓셋의 포인터이며 해당 주소에 존재하는 소켓셋에 삽입되어 있던 소켓을 모두 삭제하는 기능을 한다. 표 2는 기본형 및 파라미터를 나타낸 것이다.

기능

소켓셋 fdset 초기화

기본형

void FD_ZERO(fd_set *fdset);

파라미터

fdset : 소켓셋의 주소

■ FD_SET( )

FD_SET( )는 소켓을 소켓셋에 삽입하고자 할 때 사용되는 함수이다. 파라미터 fd는 소켓셋에 집어넣을 소켓이고, fdset은 소켓을 삽입할 소켓셋의 주소이다. 표 3은 기본형 및 파라미터를 나타낸 것이다.

 

기능

소켓 fd를 소켓셋 fdset에 삽입

기본형

void FD_SET(int fd, fd_set *fdset);

파라미터

fd : 소켓셋에 삽입할 소켓 파일 디스크립터

fdset : 소켓셋의 주소

 

■ FD_ISSET( )

FD_ISSET( )는 소켓이 소켓셋에 존재하는 지 알고자 할 때 사용되는 함수이다. 즉, 소켓 fd가 소켓셋 fdset에 현재 존재하는지 알려주는 기능을 한다. 만약 소켓 fd가 소켓셋 fdset에 존재하면 0이 아닌 소켓값을 리턴하게 되고 소켓이 해당 소켓셋에 존재하지 않으면 0을 리턴하게 된다. 표 4는 기본형 및 파라미터를 나타낸 것이다.

 

기능

소켓 fd가 소켓셋 fdset에 현재 존재하는 지를 알려줌

기본형

int FD_ISSET(int fd, fd_set *fdset);

파라미터

fd : 소켓셋에 삽입할 소켓 파일 디스크립터

fdset : 소켓셋의 주소

리턴값

0 : 소켓이 소켓셋에 존재하지 않음

0 이상 : 소켓 fd가 소켓셋 fdset에 존재함

 

■ FD_CLR( )

FD_CLR은 소켓셋 fdset에서 소켓 fd를 삭제하고자 할 때 사용되는 함수이다. 표 5는 기본형 및 파라미터를 나타낸 것이다.

 

기능

소켓 fd를 소켓셋 fdset에서 삭제

기본형

void FD_CLR(int fd, fd_set *fdset);

파라미터

fd : 소켓셋에 삽입할 소켓 파일 디스크립터

fdset : 소켓셋의 주소

 

2.2. 동작절차

select( )를 이용해서 여러 클라이언트와 메시지를 주고받기 위해서는 그림 4와 같은 절차를 반복하여 실행해야 한다. 첫 번째로 FD_ZERO()를 이용해 읽기셋, 쓰기셋, 예외셋 각각의 셋을 비워준다. 두 번째로 각각의 소켓셋에 소켓을 삽입한다. 여기서 만약 클라이언트로부터 메시지가 전송되었는지를 알고 싶은 소켓이 있다면 읽기 셋에 소켓을 삽입하고 메시지를 보낼 수 있는 준비가 되어있는 지 알고 싶은 소켓이 있다면 쓰기셋에 소켓을 삽입한다. 긴급한 메시지인 OOB 메시지가 전송되었는지를 알고 싶은 소켓이 있다면 예외셋에 소켓을 삽입한다. 세 번째로 select()를 호출해서 소켓에 변화가 생길 때까지 대기상태에 있다가 소켓에 변화가 생기면 변화가 발생한 소켓만 제외하고 나머지 소켓을 소켓셋에서 삭제한 후 변화가 발생한 소켓의 수를 리턴한다. 마지막 네 번째로 각각의 소켓셋에 남아있는 소켓을 알아내서 적절한 함수를 호출하게 되며 이후에는 다시 첫 번째 절차로 반복 실행된다.

 

 

그림 5는 하나의 쓰레드로 여러 명의 클라이언트와 메시지를 주고받을 수 있는 IO 멀티플렉싱 서버 프로그램의 순서도를 나타낸 것이다. 일단 서버이기에 먼저 socket()를 통해 서버소켓을 생성한다. 이후 bind()를 통해 서버 소켓에 주소 정보를 부여하고 listen()을 통해 클라이언트가 접속하기를 기다린다. 다음 소켓셋 중 읽기셋인 readset을 생성하여 클라이언트가 접속했는지 혹은 클라이언트가 메시지를 전송했는지를 감지할 수 있도록 한다. 또한, 임시 소켓셋인 tempset을 별도로 생성하여 변화가 생긴 소켓 이외에 나머지 모든 소켓을 tempset에 저장할 수 있도록 한다. 이는 select()에서 변화가 생긴 소켓이 존재하면 이외에 나머지 모든 소켓은 삭제되기 때문이다. tempset에 서버소켓을 삽입한 후 다시 readset에 tempset을 삽입한 다음 select()를 호출하여 소켓에 변화가 생길 때까지 대기하고 있는다. 이후 읽기셋에 변화가 생겼을 시 변화가 생긴 소켓을 찾아내기 위해 FD_ISSET()를 호출한다. 이때 변화가 생긴 소켓이 서버소켓일 경우에는 클라이언트에서 서버로 접속한 경우이이기 때문에 클라이언트의 접속을 허락하고 클라이언트와 메시지를 주고받는 클라이언트 소켓을 기존에 전체 소켓이 저장되어 있던 tempset에 저장한다. 또한, 변화가 생긴 소켓이 클라이언트 소켓일 경우에는 클라이언트가 메시지를 전송한 것이기 때문에 클라이언트가 전송한 메시지를 읽어 들이게 되고 read()와 send()를 호출하여 수신한 메시지를 다시 클라이언트로 알려주도록 한다. 여기서 만약 클라이언트가 전송한 메시지의 버퍼크기가 0일 경우에는 연결을 종료하겠다는 메시지이기 때문에 클라이언트의 접속을 종료하고 기존에 전체소켓이 저장되어 있던 tempset에서 해당 소켓을 삭제하게 된다.

 

3. 소스 분석

3.1. 서버

아래 13~28번째 줄까지는 socket() 호출을 통해 서버소켓을 생성한 후 bind() 호출을 통해 주소 정보를 설정하고 listen()를 통해 클라이언트의 접속을 대기하는 초기 단계의 소스코드이다. 28번째 줄까지의 소스코드를 살펴보면 일반적인 소켓프로그래밍에서의 소스코드와 다르지 않은 것을 알 수 있다.

 

013 int serverSocket=socket(PF_INET,SOCK_STREAM,0);

014 struct sockaddr_in serverAddress;

015 memset(&serverAddress, 0, sizeof(serverAddress));

016 //serverAddress 의 주소 체계 설정

017 serverAddress.sin_family=AF_INET;

018 //serverAddress 에 서버 IP 대입

019 serverAddress.sin_addr.s_addr=htonl(INADDR_ANY);

020 //serverAddress 에 포트 대입

021 serverAddress.sin_port=htons(PORT);

022 //serverSocket 에 서버의 주소 정보 설정

023 bind(serverSocket, (struct sockaddr*) &serverAddress, sizeof(serverAddress));

024 printf("bind() complete.\n\n");

026 /*클라이언트가 접속하기를 기다리는 대기모드로 전환*/

027 listen(serverSocket,5);

028 printf("listen() complete.\n\n");

 

30 ~ 41번째 줄까지는 소켓셋을 생성하고 소켓셋을 초기화한 후에 소켓을 소켓셋에 집어넣는 단계까지 나타내고 있다. 본 프로그래밍에서 작성하고자 하는 것은 클라이언트가 접속해서 메시지를 전송하면 클라이언트가 전송한 메시지를 해당 클라이언트에게 다시 보내는 프로그램이기 때문에 읽기셋이 필요하다. 따라서 listen() 호출 이후에 클라이언트로부터 접속을 받아들일 읽기셋인 readset 생성하였다. 그리고 select()는 변화가 생긴 소켓 이외에 나머지 소켓을 읽기셋에서 삭제하기 때문에 이 소켓들을 저장할 소켓셋인 tempset을 생성하고 서버소켓을 tempset으로 대입 및 저장한다.

 

/*클라이언트로 부터 전송된 메시지가 있는지를 알아내는 읽기셋*/

031 fd_set readSet;

/*서버소켓과 서버에 접속하는 모든 클라이언트 소켓을 저장하고 있는 셋으로 tempSet 에 대입된 소켓들을 readSet 에 복사할것임*/

034 fd_set tempSet;

/*소켓 셋 초기화 */

037 FD_ZERO(&tempSet);

038 FD_ZERO(&readSet);

/* 클라이언트가 접속하기를 기다리는 서버소켓을 tempSet에 삽입*/

040 FD_SET(serverSocket, &tempSet);

041 int clientSocket; //클라이언트와 메시지를 주고 받는 클라이언트 소켓 생성

 

42 ~ 55번째 줄까지는 select()에 소켓셋을 대입하고 소켓에 변화가 생길 때까지 대기하기 위한 코드를 작성한 것이다. 서버소켓과 서버에 접속한 모든 클라이언트 소켓 등 모든 소켓을 포함하고 있는 tempset을 readset에 대입하면 tempset에 삽입된 소켓 정보가 readset으로 복사된다. 따라서 읽기 셋인 readset은 서버소켓과 모든 클라이언트 소켓이 삽입되어 있으므로 모든 소켓에 메시지가 전달된 정보를 알 수 있다. 이후 select()를 호출해서 readset에 삽입된 소켓에 변화가 생길 때까지 대기하게 된다.

 

042 while(1){

045 readSet=tempSet;

/*읽기 셋인 readSet 에 변화가 발생할때 까지 대기하도록 설정함 select 함수는 readSet 에 삽입된 소켓에 변화가 생길때 까지 대기하고 있다가 변화가 발생하면 변화가 발생한 소켓의 수를 리턴함. 만약 select 함수 실행중에 에러가 발생하면 -1 을 리턴함*/

051 printf("Waiting for change in readset.\n\n");

052 if((num = select(0,&readSet,NULL,NULL,NULL))==SOCKET_ERROR){

053 printf("select() error.\n\n");

054 return 0;

055 }

 

56 ~ 101번째 줄까지는 읽기셋인 readset에 변화가 생긴 소켓을 찾아내서 처리하는 부분을 작성한 것이다. select()에서 대기하고 있다가 소켓에 변화가 발생하면 대기 상태가 해제되며 select()는 대기 상태를 해제 하면서 읽기셋에 변화가 발생한 소켓만 남겨놓고 나머지 소켓을 모두 제거한다. 이후 61번째 줄과 같이 for문을 이용해 읽기셋에서 변화가 생긴 소켓을 알아내는 단계를 거친다. 그래서 해당 소켓이 서버소켓일 경우에는 클라이언트로부터 접속한 소켓이기 때문에 accept() 호출을 통해 접속을 허락한 후 해당 소켓을 tempset에 저장 시킨다. 해당 소켓이 클라이언트 소켓일 경우에는 클라이언트가 전송한 메시지의 크기가 0인 경우는 종료한다는 것을 의미하기 때문에 전체 소켓셋에서 해당 소켓을 삭제하고 closesocket()을 통해 종료하게 된다. 만약 클라이언트가 메시지를 전송한 경우에는 recv() 및 send()를 통해 받은 메시지를 다시 클라이언트로 전송하게 된다.

 

059 printf("readset is changed. The number of the changed socket is %d. \nselect()'s wait state released.\n\n", num);

061 for(int index=0;index<tempSet.fd_count ;index++){

062 int fd=tempSet.fd_array[index];

/*fd가 readSet 에 존재하면 양수를 리턴하고 존재하지 않으면 0을 리턴함. fd 가 0이 아니라는 뜻은 즉 fd 가 readSet 에 존재하는 소켓. 즉 fd 가 변화가 발생한 소켓이라는 뜻 */

066 if(FD_ISSET(fd,&readSet)!=0){

067 printf("The changed socket number : %d\n",fd);

/*변화가 발생한 소켓이 서버 소켓일 경우 서버소켓은 클라이언트와 메시지를 주고받는 소켓이 아니라 클라이언트가 접속했는지를 감시하는 소켓이기 때문에 서버소켓에 변화가 발생했다는 뜻은 클라이언트가 접속했다는 뜻 */

072 if(fd==serverSocket){

/*클라이언트의 접속을 허락함*/

074 int clientSocket=accept(serverSocket,NULL,NULL);

075 printf("Client connected.\n\n");

/*모든 소켓을 다 포함하고 있는 tempSet 에 클라이언트와 메시지를 주고 받을수 있는 클라이언트 소켓을 삽입함*/

077 FD_SET(clientSocket,&tempSet);

078 printf("Client socket is inserted to tempSet.\n\n");

/*클라이언트 소켓의 파일디스크립터를 fdmax에 대입함*/

080 }else{

/*변화가 발생한 소켓이 서버소켓이 아닌경우 클라이언트 소켓이므로 클라이언트가 보낸 메시지를 읽어들인다*/

083 char fromClient[BUFFERSIZE];

084 int strlen=recv(fd,fromClient,BUFFERSIZE,0);

/*클라이언트가 종료메시지를 전송한 경우에는 해당 클라이언트와의 연결을 종료한다.*/

086 if(strlen==0){

/*tempSet 에서 소켓을 삭제한다*/

088 FD_CLR(fd,&tempSet);

/*클라이언트와 연결을 종료한다*/

090 closesocket(fd);

091 printf("Connection with client is over.\n\n");

092 }else{

/*종료메시지가 아닌경우에는 클라이언트가 전송한 메시지를 다시 해당 클라이언트에게 전송한다.*/

094 send(fd,fromClient,strlen,0);

095 }

096 }

097 }

099 }

100 }

101 }

 

3.2. 클라이언트

다음은 클라이언트의 소스코드 중 16 ~ 36번째 줄까지 나타낸 것으로 일반적인 소켓 프로그래밍에서의 클라이언트 소스코드와 같은 것을 볼 수 있다. 28번째 줄에서 문자를 입력하게 되면 이를 서버로 전송하게 되고 서버에서는 이를 다시 클라이언트로 보내는 에코 서버/클라이언트 프로그래밍인 것을 알 수 있다.

 

016 /*서버에 접속해서 데이터를 입출력 하는 클라이언트 소켓생성*/

017 clientSocket=socket(PF_INET, SOCK_STREAM, 0);

018 printf("Client socket %d is made.\n", clientSocket);

019 /*서버의 주소 정보가 저장될 server_adress 를 0으로 초기화*/

020 memset(&server_address, 0, sizeof(server_address));

021 /*server_address 에 서버의 주소 정보 대입*/

022 server_address.sin_family=AF_INET;

023 server_address.sin_addr.s_addr=inet_addr("127.0.0.1");

024 server_address.sin_port=htons(PORT);

025 /*서버에 접속*/

026 connect(clientSocket, (struct sockaddr*)&server_address,

027 sizeof(server_address));

028 printf("Connected to server.\nPlease input the message to send to server : ");

029 gets(toServer);

030 /*toServer 에 저장된 메시지를 서버로 전송*/

031 send(clientSocket,toServer,sizeof(toServer),0);

032 printf("Message to server : %s\n",toServer);

033 /*서버에서 보내온 메시지를 fromServer 에 저장*/

034 recv(clientSocket, fromServer, sizeof(fromServer),0);

035 printf("Message from server: %s\n",fromServer);

036 closesocket(clientSocket);

 

4. 실행 결과

그림 6(a)는 멀티플렉싱 서버에서 처음 실행한 화면을 나타내고 있다. bind() 및 listen() 호출 완료된 이후에 읽기셋에서의 변화를 대기하고 있는 상태를 보여주고 있다. 이 상태에서 클라이언트에서 서버로 접속을 하게 되면 그림 6(b)와 같이 클라이언트에서 생성된 소켓 파일 디스크립터의 번호와 서버에 접속되었다는 메시지가 나오게 된다.

 

클라이언트 커맨드 창에서 그림 7과 같이 메시지를 입력하게 되면 서버로부터 다시 메시지가 되돌아오는 것을 볼 수 있다. 그림 8은 클라이언트가 접속한 이후의 서버 화면을 보여주고 있다. 그림에서 볼 수 있듯이 읽기셋이 변화한 것을 서버에서 감지하게 되면 select()의 대기상태는 해제가 된다. 클라이언트에서 서버로 접속한 경우는 서버소켓에서의 변화가 있는 것이기 때문에 그림과 같이 서버 소켓 파일 디스크립터 번호인 1952에서 변화가 발생했다는 것을 출력하면서 다시 대기상태에 들어가게 된다. 이후 클라이언트에서 서버로 메시지를 전송하게 되면 다시 select()의 대기상태는 해제가 되는데 이번에는 클라이언트에서 메시지를 전송한 경우이기에 클라이언트의 소켓 파일 디스크립터 번호인 1916을 출력하는 것을 볼 수 있다. 이후에 한 번 더 대기상태가 해제되면서 클라이언트 소켓 파일 디스크립터인 1916이 출력되는 것은 그림 7에서 볼 수 있듯이 클라이언트가 메시지를 한번 전송하고 나서 종료하며 접속 종료 메시지를 전송했기 때문이다.

 

반응형

'Network > 테스트베드' 카테고리의 다른 글

DHCPv6 서버 구축 및 설치  (0) 2009.08.01
Posted by pmj0403