프로세스의 생성은 상당히 많은 대가를 지불해야 하는 연산 과정을 거침
그만큼 생성 후에도 시스템의 자원을 많이 차지하게 됨
또한 모든 프로세스들은 서로 독립적인 메모리 공간을 할당 받아서 사용하기 때문에
프로세스간 통신을 하기 위해서는 다소 복잡한 방법을 선택할 수 밖에 없었음 (IPC)
하나의 프로세스로도 여러 클라이언트들과 데이터를 주고 받을 수 있다면 좋지 않을까?
→ ‘I/O 멀티플렉싱’
그러나 멀티플렉싱 방식이 무조건 멀티 프로세스 서버보다 좋은 것은 아님
→ 즉, 구현하고자 하는 서버의 특성에 따라 결정해서 사용해야 함
멀티플렉싱은 ‘여러 개를 묶어서 하나로 만드는 것’이라고 생각하면 이해가 쉬움
→ 그럼 멀티플렉싱 서버에서는 무엇을 하나로 만들었을까?
클라이언트 개수에 상관 없이 부모 프로세스 하나만 존재함
→ 클라이언트의 입/출력 연결을 하나로 묶어버림 (진짜 그렇다는 것은 아니고 개념적으로)
select 함수를 사용하게 되면 한 곳에 모아놓은 여러 개의 파일 디스크립터를 동시에 관찰할 수 있음
수신할 데이터를 지니고 있는 파일 디스크립터가 어떤 것들인지,
데이터를 전송할 경우 블로킹되지 않고 바로 전달 가능한 파일 디스크립터는 어떤 것들인지,
예외가 발생한 파일 디스크립터는 어떤 것들인지 등 관찰 가능
[select 함수의 기능과 호출 순서]
수신할 데이터를 지니고 있는 소켓이 존재하는가?
데이터를 전송할 경우 블로킹되지 않는 소켓은 무엇인가?
예외 상황이 발생한 소켓이 있는가?
→ 파일 디스크립터를 세 묶음으로 모아 놓기 위해 사용되는 것이 fd_set 데이터 타입의 자료형임
fd_set은 0과 1을 나타내는 비트들의 배열인데 1로 설정된 비트가 관찰 대상이 되는 파일 디스크립터
select 함수는 호출했을 때 관찰 대상들에게서 변화가 발생해야 리턴하며,
그렇지 않으면 변화가 발생될 때까지 무한정 블로킹 상태에 있게 됨
→ 그러나 타임아웃을 설정해 놓으면 관찰 대상자들에게서 변화가 없더라도 리턴되므로 무한 대기 상태에 빠지는 것을 피할 수 있음
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int n, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
// 성공 시 0 이상, 오류 발생 시 -1 리턴
// n : 검사 대상이 되는 파일 디스크립터의 수
// readfds : "입력 스트림에 변화가 발생했는지" 확인하고자 하는 소켓들의 정보를 전달함
// 여기서 입력 스트림에 변화가 발생했다는 것은 수신할 데이터가 있다는 뜻임
// writefds : "데이터 전송 시, 블로킹되지 않고 바로 전송이 가능한지" 확인하고자 하는 소켓들의 정보를 전달
// exceptfds : "예외가 발생했는지" 확인하고자 하는 소켓들의 정보를 전달
// timeout : 함수 호출 후, 무한 대기 상태에 빠지지 않도록 타임-아웃을 설정하기 위한 인자를 전달
// 리턴 값 : -1이 리턴되는 경우, 오류 발생을 의미함
// 또한 0이 리턴된 경우에는 타임아웃에 의해 리턴되었음을 의미함
// 마지막으로 리턴된 값이 0보다 큰 경우는 변경된 파일 디스크립터의 수를 의미함
// (참고) select 함수 호출 시 전달되는 파일 디스크립터의 정보는 소켓뿐 아니라 파일을 나타내는 경우에도 전달 가능
[fd_set 자료형 데이터 조작 함수]
함수 선언 | 기능 |
---|---|
FD_ZERO(fd_set* fdset); | fdset 포인터가 가리키는 변수의 모든 비트들을 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 함수 선언을 보면 readfds, writefds 그리고 exceptfds라는 이름으로 총 세 개의 fd_set 변수의 포인터를 요구함
select 함수는 여러 파일 디스크립터를 검사하고 그 결과를 전달해 줌
확인해야 하는 파일 디스크립터의 범위를 제한해 주면 보다 효율적으로 수행할 수 있음
→ 그래서 select 함수의 첫 번째 인자로 검사해야 하는 총 디스크립터의 개수를 넘겨주게 됨
일반적으로 디스크립터는 생성될 때마다 값이 1씩 증가하기 때문에 가장 큰 파일 디스크립터 값에 1을 더해서 인자로 전달하면 됨
(1을 더하는 이유는 디스크립터 값이 0부터 시작하기 때문)
→ 따라서 인자로 n이라는 값을 넘겨 주게 되면, select 함수는 검사하게 되는 파일 디스크립터의 범위를 0부터 n-1로 설정됨
(따라서 반드시 1을 더해줘야 함)
struct timeval
{
long tv_sec; // seconds
long tv_usec; // microseconds
}
// tv_sec이 3이고, tv_usec가 500000이면 타임아웃은 3.5초로 설정됨
// 설정한 timeval 구조체 변수의 포인터를 select 함수의 마지막 인자로 넘겨주게 되면
// 파일 디스크립터에 아무런 변화가 없더라도 3.5초가 지나면 무조건 리턴하게 됨
// 타임 아웃을 설정해 주지 않을 경우, NULL 포인터를 인자로 전달하면 됨
함수 호출이 정상적으로 리턴했다는 것은 파일 디스크립터에 변화가 있었거나 타임아웃이 발생한 경우 중 하나
→ 리턴 값에 따라 확인 가능함
select 함수 호출 후에 fd_set 변수를 살펴보면, 파일 디스크립터 3의 위치는 여전히 1로 설정되어 있고,
파일 디스크립터 1의 위치는 0으로 변경된 것을 알 수 있음
→ 파일 디스크립터 3으로부터 수신할 데이터가 존재한다는 의미
⇒ 즉, select 함수 호출이 끝나고 나서도, fd_set 변수에서 1로 설정되어 남아 있는 파일 디스크립터가 변화를 일으킨 파일 디스크립터임
// select.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/time.h>
#include <stdlib.h>
#define BUFSIZE 30
int main(int argc, char** argv)
{
fd_set reads, temps;
int result;
char message[BUFSIZE];
int str_len;
struct timeval timeout;
FD_ZERO(&reads); // fd_set 구조체 변수를 초기화
FD_SET(0, &reads); // standard input 설정
// (파일 디스크립터 0을 나타내는 위치를 1로 설정 -> 표준 입력에 변화가 있는지 관심을 두고 볼 거임)
/* 여기서 타임아웃 설정을 하면 안 됨
timeout.tv_sec = 5;
timeout.tv_usec = 100000;
*/
while(1)
{
temps = reads; // fd_set 변수를 임시 변수에 복사함
// 변화가 생긴 파일 디스크립터 위치를 제외한 나머지 위치의 비트들이 0으로 초기화되므로
// 원본 변수를 직접 select 함수의 인자로 전달해버리면 또 다시 변수를 설정해야 함
// 이를 막기 위해 원본은 보존하고 임시 변수에 원본을 복사해서 select() 호출
// 타임아웃 설정은 while문 안에서 해야 함 -> 매번 타임아웃을 재설정 할 수 있어야 하므로
timeout.tv_sec = 5;
timeout.tv_usec = 0;
result = select(1, &temps, 0, 0, &timeout);
if(result == -1)
{
puts("select 오류 발생");
exit(1);
}
else if(result == 0)
{
puts("시간이 초과되었습니다 : select ");
}
else
{
if(FD_ISSET(0, &temps))
{
str_len = read(0, message, BUFSIZE);
message[str_len] = 0;
fputs(message, stdout);
}
}
}
}
[실행 결과]
실행 후 아무 입력도 없이 5초 정도가 지나서 타임 아웃이 발생함 (”시간이 초과되었습니다 : select”)
그리고 나서 메시지를 전송하자 출력해 주고 있음
// echo_selserv.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define BUFSIZE 100
void error_handling(char* message);
int main(int argc, char** argv)
{
int serv_sock;
struct sockaddr_in serv_addr;
fd_set reads, temps;
int fd_max;
char message[BUFSIZE];
int str_len;
struct timeval timeout;
if(argc != 2)
{
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)))
error_handling("bind() error");
if(listen(serv_sock, 5) == -1)
error_handling("listen() error");
FD_ZERO(&reads);
FD_SET(serv_sock, &reads); // fd_set 변수에 서버 소켓의 파일 디스크립터를 설정해 줌
// 서버 소켓으로부터 수신할 데이터가 있는지 관심을 두겠다는 의미
fd_max = serv_sock;
while(1)
{
int fd, str_len;
int clnt_sock, clnt_len;
struct sockaddr_in clnt_addr;
temps = reads;
timeout.tv_sec = 5;
timeout.tv_usec = 0;
if(select(fd_max + 1, &temps, 0, 0, &timeout) == -1)
error_handling("select() error");
for(fd = 0; fd < fd_max + 1; fd++)
{
// select 함수 호출 후에 상태 변화가 있었던 파일 디스크립터를 찾아내기
if(FD_ISSET(fd, &temps))
{
if(fd == serv_sock) // 서버 소켓인지 확인하기 (서버 소켓에서 변화가 있었다 -> 연결 요청이 있었다를 의미)
{
clnt_len = sizeof(clnt_addr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_len); // 연결 요청 수락
FD_SET(clnt_sock, &reads); // 리턴되는 소켓을 reads 변수에 추가함 (모든 클라이언트의 입/출력을 하나로 멀티플렉싱 하기 위해)
if(fd_max < clnt_sock)
fd_max = clnt_sock;
printf("클라이언트 연결 : 파일 디스크립터 %d \n", clnt_sock);
}
else // 변화가 있던 파일 디스크립터가 서버 소켓이 아닌 경우 (클라이언트로부터 데이터가 전송된 경우 - 데이터 또는 연결 종료)
{
str_len = read(fd, message, BUFSIZE);
if(str_len == 0) // 연결 종료인지 확인
{
FD_CLR(fd, &reads); // reads 변수에서 제외
close(fd);
printf("클라이언트 종료 : 파일 디스크립터 %d \n", fd);
}
else
write(fd, message, str_len);
}
}
}
}
}
void error_handling(char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
[실행 결과]