🎮
IO 멀티플렉싱 기반의 서버(select)
January 19, 2024
IO 멀티플렉싱 기반의 서버
멀티플렉싱 : 하나의 통신채널을 통해서 둘 이상의 데이터를 전송하는데 사용되는 기술
여기서는 하나의 프로세스로 서비스를 제공하는 것을 말함.
select 함수의 이해와 서버의 구현
select
함수를 이용하는 것이 멀티플렉싱 서버의 구현에 있어서 가장 대표적인 방법이다. 윈도우에도 이와 동일한 이름으로 동일한 기능을 제공하는 함수가 있어 이식성도 좋다.
select 함수의 기능과 호출 순서
select
함수를 사용하면 한곳에 여러 개의 파일 디스크립터를 모아놓고 동시에 이들을 관찰할 수 있다.
이때 관찰할 수 있는 항목은 다음과 같다.
- 수신한 데이터를 지니고 있는 소켓이 존재하는가?
- 블로킹되지 않고 데이터의 전송이 가능한 소켓은 무엇인가?
- 예외상황이 발생한 소켓은 무엇인가?
💡관찰항목 각각을 가리켜 ‘이벤트(event)‘라 한다.
위에서 정리한 관찰항목 각각을 가리켜 이벤트라 하고, 관찰항목에 속하는 상황이 발생했을 때, ‘이벤트(event)가 발생했다’ 라고 표현한다. 이는 매우 일반적인 표현이기 때문에 익숙해질 필요가 있다.
select
함수의 호출 과정
순서 | 세부 순서 |
---|---|
Step one |
파일 디스크립터의 설정 검사의 범위 지정 타임아웃의 설정 |
Step two | select 함수의 호출 |
Step three | 호출결과 확인 |
관찰하고 싶은 파일 디스크립터의 모음을 fd_set형 변수로 나타낸다.
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로 전달된 파일 디스크립터 정보가 있으면 양수를 반환한다.
검사의 범위지정과 타임아웃의 설정
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 반환
maxfd
: 검사 대상이 되는 파일 디스크립터의 수readset
: fd_set형 변수에 ‘수신된 데이터의 존재여부’에 관심 있는 파일 디스크립터 정보를 모두 등록해서 그 변수의 주소 값을 전달한다.writeset
: fd_set형 변수에 ‘블로킹 없는 데이터 전송의 가능여부’에 관심 있는 파일 디스크립터 정보를 모두 등록해서 그 변수의 주소 값을 전달한다.exceptset
: fd_set형 변수에 ‘예외상황의 발생여부’에 관심이 있는 파일 디스크립터 정보를 모두 등록해서 그 변수의 주소 값을 전달한다.timeout
: select 함수호출 이후에 무한정 블로킹 상태에 빠지지 않도록 타임아웃을 설정한다.반환 값
: 오류 발생시에는 -1, 타임아웃시에는 0, 변화가 발생했을경우, 변화가 발생한 파일 디스크립터의 수 반환.
timeval
struct timeval
{
long tv_sec; // seconds
long tv_usec; // microseconds
}
- 타임아웃을 설정하고 싶지 않은 경우,
select
의 인자에 NULL을 넣는다. select
호출 후에는 타임아웃시까지 남은 시간으로 멤버변수가 변하기 때문에, 초기화를 꼭 해주어야 한다.
select 함수가 양의 정수를 반환한 경우, 변화가 없는 fd는 해당 fd_set에서 0으로 초기화 되고, 변화가 있는 경우는 여전히 1이므로 이를 통해 이벤트 여부를 확인할 수 있다.
예제 코드
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string>
int main(int argc, char *argv[])
{
fd_set reads, temp;
int result;
char buf[100];
FD_ZERO(&reads);
FD_SET(0, &reads);
timeval tv;
while(1)
{
temp = reads;
tv.tv_sec = 4;
tv.tv_usec = 300;
result = select(1, &temp, 0, 0, &tv);
if(result < 0)
printf("select error\n");
else if(result == 0)
printf("time out\n");
else
{
int str_len = read(0, buf, sizeof(buf)-1);
buf[str_len] = 0;
printf("message: %s \n", buf);
memset(buf, 0, sizeof(buf));
}
}
return 0;
}
멀티플렉싱 서버의 구현
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/select.h>
#define BUF_SIZE 30
void error_handling(char *message);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
struct timeval timeout;
fd_set reads, cpy_reads;
socklen_t adr_sz;
int fd_max, str_len, fd_num, i;
char buf[BUF_SIZE];
if(argc != 2)
{
printf("Usage : %s <port> \n", argv[0]);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
error_handling("bind() error");
if(listen(serv_sock, 5) == -1)
error_handling("listen() error");
FD_ZERO(&reads);
FD_SET(serv_sock, &reads);
fd_max = serv_sock;
while(1)
{
cpy_reads = reads;
timeout.tv_sec = 5;
timeout.tv_usec = 5000;
if((fd_num = select(fd_max+1, &cpy_reads, 0, 0, &timeout)) == -1)
break;
if(fd_num == 0)
continue;
for(i = 0 ; i < fd_max+1 ; i++)
{
if(FD_ISSET(i, &cpy_reads))
{
if(i == serv_sock) // connection request!
{
adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
FD_SET(clnt_sock, &reads);
if(fd_max < clnt_sock)
fd_max = clnt_sock;
printf("connected client: %d \n", clnt_sock);
}
else // read message
{
str_len = read(i, buf, BUF_SIZE);
if(str_len == 0) // close request!
{
FD_CLR(i, &reads);
close(i);
printf("closed client: %d \n", i);
}
else
{
write(i, buf, str_len); //echo
}
}
}
}
}
close(serv_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
윈도우 기반 멀티플렉싱 서버 구현
#include <sinwock2.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timeval * timeout);
// 성공 시 0 이상, 실패 시 -1 반환
typedef struct timeval
{
long tv_sec; // seconds
long tv_usec; // microseconds
} TIMEVAL;
typedef struct fd_set
{
u_int fd_count;
SOCKET fd_array[FD_SETSIZE];
} fd_set;
윈도우는 리눅스의 디스크립터와 다르게 핸들에 순차적인 번호가 부여되지 않아서 fd_count가 따로 필요하다.
예제 코드
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#define BUF_SIZE 30
void ErrorHandling(char *message);
int main(int argc, char *argv[])
{
WSADATA wsaData;
SOCKET hServSock, hClntSock;
SOCKADDR_IN servAdr, clntAdr;
TIMEVAL timeout;
fd_set reads, cpyReads;
int adrSz;
int strLen, fdNum, i;
char buf[BUF_SIZE];
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(&servAdr, 0, sizeof(servAdr));
servAdr.sin_family = AF_INET;
servAdr.sin_addr.s_addr = htonl(INADDR_ANY);
servAdr.sin_port = htons(atoi(argv[1]));
if(bind(hServSock, (struct sockaddr*)&servAdr, sizeof(servAdr)) == -1)
ErrorHandling("bind() error");
if(listen(hServSock, 5) == -1)
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) // winsock에서는 nfds 무시
break;
if(fdNum == 0)
continue;
for(i = 0 ; i < reads.fd_count ; i++)
{
if(FD_ISSET(reads.fd_array[i], &cpyReads))
{
if(reads.fd_array[i] == hServSock) // connection request!
{
adrSz = sizeof(clntAdr);
hClntSock = accept(hServSock, (struct sockaddr*)&clntAdr, &adrSz);
FD_SET(hClntSock, &reads);
printf("connected client: %d \n", hClntSock);
}
else // read message
{
strLen =recv(reads.fd_array[i], buf, BUF_SIZE-1, 0);
if(strLen == 0) // close request!
{
FD_CLR(reads.fd_array[i], &reads);
closesocket(cpyReads.fd_array[i]);
printf("closed client: %d \n", cpyReads.fd_array[i]);
}
else
{
send(reads.fd_array[i], buf, strLen, 0); // echo
}
}
}
}
}
closesocket(hServSock);
WSACleanup();
return 0;
}
void ErrorHandling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}