Web서버, IRC서버 같은 서버들은 다수의 클라이언트의 동시 접속을 지원해야 한다.
동시에 여러 사용자가 접속할 때 이러한 문제를 예상해 볼 수 있다.
예를 들어 A와 B라는 클라이언트가 서버에 접속해 있다고 하자.
A의 요청을 처리하고 B의 요청을 '순서대로'처리한다면
B는 A의 요청이 모두 처리될 때까지 대기 상태에 들어간다. 이를 blocking
된 상태라고 한다.
이러한 문제 때문에 우리는 클라이언트의 요청을 non-blocking
하게 처리할 필요가 있다.
이 문제를 해결하는 방법은 다양하다. 클라이언트의 연결을 수락할 때마다 프로세스나 스레드를 생성하여 병렬로 처리하는 방법도 있지만 여기서는 IO multiplexing 이라는 입출력 다중화 방법을 사용할 것이다.
poll()
함수는 위에 언급한 문제를 해결할 수 있는 함수 중 하나이다.
비슷한 기능을 하는 함수로는 select()
, kqueue()
, epoll()
등이 있다.
이러한 함수들은 등록된 소켓
들을 순회하며 event
가 발생한 소켓
만을 반환해준다.
#include <poll.h>
int poll(struct pollfd fds[], nfds_t nfds, int timeout);
fds[]
- pollfd 구조체의 배열
struct pollfd {
int fd; // 클라이언트의 소켓 fd
short events; // 감지할 이벤트
short revents; //발생한 이벤트
};
events
에 어떤 이벤트의 발생을 기다릴 것인지 등록해두면 poll()
함수는 등록한 이벤트가 발생했을 때 revents
에 발생한 이벤트를 기록해준다.
nfds
- 첫번째 인자 fds[]
의 크기
timeout
- 지정한 시간 내에 이벤트를 감지하지 못했다면 다음 줄로 넘어간다.(ms단위)
이전 글의 echo 서버를 수정한 것이다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <poll.h>
#define BUF_SIZE 512
void error_handling(char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char* argv[])
{
int serv_sock;
int clnt_sock;
// sockaddr은 주소정보를 담는 기본형태이다.
// 주소체계에 따라 sockaddr_in, sockaddr_un, sockaddr_in6 등을 sockaddr로 형변환해서 사용하면 편리하다.
struct sockaddr_in serv_addr;
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size;
// TCP연결, IPv4 도메인을 위한 소켓 생성
serv_sock = socket(AF_INET, SOCK_STREAM, 0);
if (serv_sock == -1)
error_handling("socket error");
//서버의 주소 정보 설정
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET; // 주소패밀리
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 서버의 ip주소
serv_addr.sin_port = htons(atoi(argv[1])); // 서버 프로그램의 포트
//htonl, htos - 빅엔디안 타입으로 변환
// 소켓과 주소정보를 결합
if (bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("bind error");
// 연결요청 대기
if (listen(serv_sock, 5) == -1)
error_handling("listen error");
// pollfd 배열 생성 및 초기화
struct pollfd clients[512];
for (int i = 0; i < 512; ++i)
clients[i].fd = -1;
// 배열의 첫번째에 서버 소켓을 등록
clients[0].fd = serv_sock;
clients[0].events = POLLIN;
int max_i = 0; // pollfd에 등록되어 있는 가장 높은 인덱스
int i;
while (1)
{
if (poll(clients, max_i + 1, 5000) <= 0)
continue;
// 연결 수락
if (clients[0].revents & POLLIN)
{
clnt_addr_size = sizeof(clnt_addr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
for (i = 1; i < 512; ++i)
{
if (clients[i].fd < 0)
{
clients[i].fd = clnt_sock;
clients[i].events = POLLIN;
break;
}
}
if (i == 512)
error_handling("too many clients");
if (i > max_i)
max_i = i;
printf("fd %d connected\n", clnt_sock);
continue;
}
// 클라이언트 요청 처리
// 등록된 모든 pollfd들을 검사한다.
for (i = 1; i <= max_i; ++i)
{
if (clients[i].fd < 0)
continue;
// i번째 소켓에 POLLIN 이벤트가 발생
if (clients[i].revents & POLLIN)
{
char* buf[BUF_SIZE];
memset(buf, 0, BUF_SIZE);
// 이벤트가 발생한 소켓을 읽어들임
if (read(clients[i].fd, buf, BUF_SIZE) <= 0)
// 0 또는 -1을 반환했을 때 연결 종료
{
close(clients[i].fd);
printf("%d : client disconnected\n", clients[i].fd);
clients[i].fd = -1;
if (i == max_i)
{
for (; clients[max_i].fd < 0; --max_i)
{}
}
}
else //echo
write(clients[i].fd, buf, strlen((const char*)buf));
}
}
}
close(serv_sock);
return 0;
}
socket()
-> bind()
-> listen()
의 과정은 이전과 같지만
클라이언트를 accept()
하고 데이터를 read()
하는 제어흐름이 다르다.
우선 클라이언트가 보낸 데이터를 읽고 싶기 때문에 pollfd
의 events
를 POLLIN
으로 지정해준다.
POLLIN
은 읽어들일 수 있는 데이터가 있을 때 revents
에 POLLIN
을 반환해준다.
poll()
함수는 이벤트가 발생한 pollfd
에만 revents
를 기록해주기 때문에
pollfd
들을 돌며 revents
값을 검사하여 적절한 처리를 해주면 된다.