멀티서버
왜 멀티서버인가?
- 전체적인 서비스 제공시간이 늦어져도 모든 클라이언트에게 서비스를 제공하기 위해
- cpu의 연산을 필요로 하지 않는 데이터의 송수신 시간이 큰 비중을 차지하므로, 이를 효율적으로 쓰기 위해
멀티서버 구현
- 멀티프로세싱 : 클라이언트 수만큼 프로세스 생성
- 멀티쓰레딩 : 클라이언트 수만큼 쓰레드 생성
- 멀티플랙싱 : 입출력 대상을 묶어서 관리
i/o 멀티플랙싱
어떻게 한 개의 프로세스만을 가지고 여러개의 fd를 다룰 수 있는가?
이를 i/o 멀티플랙싱이라고 하며, i/o 멀티플랙싱을 위한 여러가지 함수가 있다.
c
select(), poll(), epoll(), kqueue(), ...
여기서는 가장 기초적인 함수인 select를 알아보자.
select 함수
작업할 준비가 된 파일에 대해서만 작업을 하고, 나머지 파일은 막아두면 된다.
c
int select(int maxfd, fd_set *readset, fd_set *writeset,
fd_set *exceptset, const struct timeval *timeout);
maxfd: 검사 대상이 되는 파일 디스크립터 수readset: '읽기'가 가능한지 확인writeset: '쓰기'가 가능한지 확인exceptset: 예외가 발생했는지 확인timeout: 무한정 블로킹 방지용 시간 설정- 반환 값 : 오류발생시 -1, 타임아웃 0, 0보다 큰 경우 = 변화가 발생한 fd 갯수
select 함수의 특징
- 시간을 설정해야 한다.
- select 함수는 변화가 발생할 때까지 계속 가만히 있는다.
- 변화가 생기지 않으면 무한정 그 상태에 머물기 때문에 timeout을 지정해야 한다.
- timeout이 생긴 경우 0을 반환값으로 준다.
- 변화가 생기는 경우에는 파일 디스크립터가 다음과 같이 변한다.
c
<변화 전 : fd 0번 ~ 8번까지 등록>
1 1 1 1 1 1 1 1 1
- fd 0번과 2번이 변화 -
<변화 후 : 변화한 fd만 계속 1이다.>
1 0 1 0 0 0 0 0 0
fd 0번이 변화했다는 것 = fd 0번으로 수신한 데이터가 존재.
- 이 경우 파일 디스크립터는 2을 반환한다. (변화가 있는 데이터가 2개라는 뜻)
- 특정 항목은 감시하지 않게 할 수 있다.
- 만약 select의 매개변수에 NULL 또는 0을 주면 해당 이벤트는 감시하지 않는다.
c
result = select(1, &temps, 0, 0, &timeout);
// write와 except를 감시하지 않는다.
- fd를 어디에 담는가?
- 그 많은 fd를 어디에 담을 것인가?
- 이를 위해서 나온 특수한 구조체가 바로
fd_set이다.
fd_set 구조체
이를 위해 나온 구조체가 바로 fd_set이다.
fd를 저장하기 위한 구조체이며, 내부는 0, 1로 이뤄진 배열로 되어 있다.
만약 여기에 특정 fd를 등록하면 해당 index의 값이 1로 세팅된다.
c
<처음 생성 및 초기화 시>
0, 0, 0, 0, 0, 0, 0, ...
<fd 1번 등록 시>
0, 1, 0, 0, 0, 0, 0, ...
fd_set 관련 함수
fd_set의 생성 및 초기화는 다음과 같이 한다.
c
fd_set set; // fd_set 생성
FD_ZERO(&set); // fd_set 초기화
fd_set에 2번 fd와 5번 fd를 등록한다고 해보자.
c
FD_SET(2, &set);
FD_SET(5, &set);
fd_set에서 3번 fd를 삭제할 수도 있다. (즉, 1 → 0으로 값을 바꾼다는 이야기)
c
FD_CLR(3, &set);
fd_set 에서 4번 fd가 1인지(즉 값이 등록 되었는지) 확인하고 싶다면 다음과 같이 사용
c
FD_ISSET(4, &set);
select로 만드는 멀티서버
c
#include "../../socket_header.h"
#include "../../socket_header.c"
#define BUF_SIZE2 100
int main(int ac, char **av)
{
int serv_sock, clnt_sock, res;
struct sockaddr_in serv_adr, clnt_adr;
struct timeval timeout;
fd_set reads, cpy_reads;
socklen_t adr_size;
int fd_max, str_len, fd_num;
char buf[BUF_SIZE2];
if (ac != 2)
{
printf("usage : %s\n", av[0]);
exit(0);
}
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(av[1]));
res = bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr));
if (res == -1)
error_handling("bind() error");
res = listen(serv_sock, 5);
if (res == -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;
// 현재 fd번호에 1개를 추가해야 전체 갯수가 나온다.
if ((fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout)) == -1)
break ;
if (fd_num == 0)
continue ;
// 여기서부턴 select의 반환값이 1 이상일때만 실행된다.
for (int i = 0; i < fd_max + 1; i++)
{
// cpy_reads 중 i번째 fd가 그대로 1인지 검사. (= 변화가 있는 fd 잡아내기)
if (FD_ISSET(i, &cpy_reads))
{
// i가 서버의 소켓이라면
if (i == serv_sock)
{
adr_size = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_size);
// reads에 클라이언트 소켓 등록.
FD_SET(clnt_sock, &reads);
// 만약 클라이언트 소켓이 fd_max보다 크다면 fd_max값 바꿔주기.
if (fd_max < clnt_sock)
fd_max = clnt_sock;
// 몇번 소켓인지 표기.
printf("connected client : %d\n", clnt_sock);
}
// i가 서버의 소켓이 아니라면 (즉 클라이언트 소켓이라면)
else
{
// 일단 read로 buf에 그 내용을 쓴다.
str_len = read(i, buf, BUF_SIZE2);
// 만약 EOF(길이가 0)이면 해당 클라이언트 소켓을 닫을 준비를 한다.
if (str_len == 0)
{
FD_CLR(i, &reads); // reads에서 해당 소켓 삭제
close(i); // 소켓 닫기
printf("closed client : %d\n", i);
}
// 아니면 buf를 출력한다.
else
write(i, buf, str_len);
}
}
}
}
// 서버 소켓을 닫는다.
close(serv_sock);
return 0;
}