Chapter 17 - select보다 나은 epoll

17-1 : epoll의 이해와 활용

select() 기반 멀티플렉싱은 오래 전에 개발되어 최적화한다 해도 성능이 그리 좋지 못함

  • select() 함수 호출이후 항상 모든 파일 디스크립터를 대상으로 반복문이 실행됨
  • select() 함수를 호출할 때마다 관찰대상에 대한 정보들을 인자로 매번 전달해야하며, 이는 운영체제에 전달되기 때문에 부담이 따름

따라서 웹 기반 서버개발이 주를 이루는 오늘날 개발환경에서는 대안으로 epoll을 활용(리눅스에서, 윈도우에서는 IOCP)
epoll은 운영체제에게 관찰대상에 대한 정보를 한번만 알려주고, 변경 사항이 있을때만 알려줌
단, epoll이나 IOCP과 같은 개선된 IO 멀티플렉싱 모델은 운영체제별로 호환되지 않기 때문에 대부분의 운영체제에서 지원되는 select() 역시 서버의 접속자 수가 많지 않은 경우에는 쓰임새가 있음


epoll의 장점

  • 전체 파일 디스크립터들의 상태변화를 확인하기 위한 반복문이 필요하지 않음
  • select() 함수에 대응하는 epoll_wait() 함수 호출시 관찰대상의 정보를 매번 전달할 필요가 없음

epoll 방식에서는 관찰대상인 파일 디스크립터의 저장을 운영체제가 담당
이 때 파일 디스크립터의 저장을 위한 저장소의 생성에 epoll_creat() 함수를 사용

#include <sys/epoll.h>

int epoll_create(int size);
//	성공시 epoll 파일 디스크립터, 실패시 -1 반환

epoll_create() 함수 호출로 생성되는 파일 디스크립터의 저장소를 ‘epoll 인스턴스’라고 함
매개변수로는 epoll 인스턴스의 크기를 결정하는 정보로 사용되나, 정확히 해당 크기로 생성되는 것이 아닌 참고로만 사용됨(리눅스 커널 2.6.8 이후부터는 완전히 무시)
반환값인 파일 디스크립터는 epoll 인스턴스를 구분하는 목적으로 사용되며 소멸시에는 close() 함수의 호출이 필요


생성된 epoll 인스턴스에 관찰대상이 되는 파일 디스크립터를 등록하는데는 epoll_ctl() 함수를 사용

#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 성공시 0, 실패시 -1 반환

struct epoll_event
{
	__uint32_t		events;
	epoll_data_t	data;
}

typedef union epoll_data
{
	void		*ptr;
	int			fd;
	__uint32_t	u32;
	__uint64_t	u64;
} epoll_data_t;

두번째 인자에는 관찰대상을 어떻게 조작할 것인지를 지정

  • EPOLL_CTL_ADD : 파일 디스크립터를 epoll 인스턴스에 등록
  • EPOLL_CTL_DEL : 파일 디스크립터를 epoll 인스턴스에서 삭제
  • EPOLL_CTL_MOD : 등록된 파일 디스크립터의 이벤트 발생상황을 변경

네번째 인자는 epoll_event 구조체의 포인터로 이벤트가 발생한 파일 디스크립터를 묶는 용도로도 사용되고, 이벤트의 유형을 등록하는 용도로도 사용됨
이벤트 유형을 나타내는 상수들은 비트 OR 연산자로 다수 등록 가능

  • EPOLLIN : 수신할 데이터가 존재하는 상황
  • EPOLLOUT : 출력버퍼가 비워져서 당장 데이터를 전송할 수 있는 상황
  • EPOLLPRI : OOB 데이터가 수신된 상황
  • EPOLLRDHUP : 연결이 종료되거나 Half-clsoe가 진행된 상황, 엣지 트리거 방식에서 유용하게 사용
  • EPOLLERR : 에러가 발생한 상황
  • EPOLLET : 이벤트의 감지를 엣지 트리거 방식으로 동작
  • EPOLLONESHOT : 이벤트가 한번 감지되면 해당 파일 디스크립터는 더이상 이벤트를 발생시키지 않음 - 추후 EPOLL_CTL_MOD를 전달하여 이벤트를 재설정해야함

epoll_ctl() 함수 동작 예시

struct epoll_event event;
...
event.events = EPOLLIN;
event.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event);


epoll에서 epoll_wait() 함수는 select() 함수에 해당되며, 가장 마지막에 호출됨

#include <sys/epoll.h>

int	epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
// 성공시 이벤트가 발생한 파일 디스크립터의 수, 실패시 -1 반환

epoll_wait() 함수 동작 예시

int	event_cnt;
struct epoll_event	*ep_events;
...
ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);
...
event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
...


epoll 기반의 에코 서버 예시

// echo_epollserv.cpp
// 클라이언트는 이전의 ehoc_client.cpp를 사용

#include <iostream>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>

using namespace std;

#define BUF_SIZE 100
#define EPOLL_SIZE 50
void	error_handling(char *buf);

int		main(int argc, char *argv[])
{
	int					serv_sock, clnt_sock;
	struct sockaddr_in	serv_adr, clnt_adr;
	socklen_t			adr_sz;
	int					str_len;
	char				buf[BUF_SIZE];

	struct epoll_event	*ep_events;
	struct epoll_event	event;
	int					epfd, event_cnt;

	if(argc != 2)
	{
		cout << "Usage : " << argv[0] << " <port>\n";
		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, (sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
		error_handling("bind() error");

	if (::listen(serv_sock, 5) == -1)
		error_handling("listen() error");

	epfd = epoll_create(EPOLL_SIZE);
	ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);

	event.events = EPOLLIN;
	event.data.fd = serv_sock;
	epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);

	while (1)
	{
		event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
		if (event_cnt == -1)
		{
			error_handling("epoll_wait() error");
			break;
		}

		for (int i = 0; i < event_cnt; i++)
		{
			if (ep_events[i].data.fd == serv_sock)
			{
				adr_sz = sizeof(clnt_adr);
				clnt_sock == accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
				event.events = EPOLLIN;
				event.data.fd = clnt_sock;
				epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
				cout << "connected client : " << clnt_sock << endl;
			}
			else
			{
				str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
				if (str_len == 0)	// close request!
				{
					epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
					close(ep_events[i].data.fd);
					cout << "closed client : " << ep_events[i].data.fd << endl;
				}
				else
					write(ep_events[i].data.fd, buf, str_len);	// echo
			}
		}
	}

	close(serv_sock);
	close(epfd);
	return 0;
}

void	error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

17-2 : 레벨 트리거(Level Trigger)와 엣지 트리거(Edge Trigger)

레벨 트리거 : 입력버퍼에 데이터가 남아있는 동안 계속해서 이벤트가 등록됨
엣지 트리거 : 입력버퍼로 데이터가 수신된 상황에 단 한번만 이벤트가 등록됨

레벨 트리거의 이벤트 등록방식 확인 예제

// echo_EPLTserv.cpp
// 클라이언트는 이전의 ehoc_client.cpp를 사용

#include <iostream>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>

using namespace std;

#define BUF_SIZE 4
#define EPOLL_SIZE 50
void	error_handling(char *buf);

int		main(int argc, char *argv[])
{
	int					serv_sock, clnt_sock;
	struct sockaddr_in	serv_adr, clnt_adr;
	socklen_t			adr_sz;
	int					str_len;
	char				buf[BUF_SIZE];

	struct epoll_event	*ep_events;
	struct epoll_event	event;
	int					epfd, event_cnt;

	if(argc != 2)
	{
		cout << "Usage : " << argv[0] << " <port>\n";
		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, (sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
		error_handling("bind() error");

	if (::listen(serv_sock, 5) == -1)
		error_handling("listen() error");

	epfd = epoll_create(EPOLL_SIZE);
	ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);

	event.events = EPOLLIN;
	event.data.fd = serv_sock;
	epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);

	while (1)
	{
		event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
		if (event_cnt == -1)
		{
			error_handling("epoll_wait() error");
			break;
		}

		cout << "return epoll_wait" << endl;
		for (int i = 0; i < event_cnt; i++)
		{
			if (ep_events[i].data.fd == serv_sock)
			{
				adr_sz = sizeof(clnt_adr);
				clnt_sock == accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
				event.events = EPOLLIN;
				event.data.fd = clnt_sock;
				epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
				cout << "connected client : " << clnt_sock << endl;
			}
			else
			{
				str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
				if (str_len == 0)	// close request!
				{
					epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
					close(ep_events[i].data.fd);
					cout << "closed client : " << ep_events[i].data.fd << endl;
				}
				else
					write(ep_events[i].data.fd, buf, str_len);	// echo
			}
		}
	}

	close(serv_sock);
	close(epfd);
	return 0;
}

void	error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

echo_epollserv.cpp에서 버퍼의 크기를 축소하고, while문 내에 epoll_wait() 함수의 호출횟수를 확인하기 위한 문장이 삽입됨


엣지 트리거의 이벤트 등록방식 확인 예제

// echo_EPLTserv.cpp 에서 아래의 일부분만 변경

...
			if (ep_events[i].data.fd == serv_sock)
			{
				adr_sz = sizeof(clnt_adr);
				clnt_sock == accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
				event.events = EPOLLIN | EPOLLET;
				...
			}
...

이벤트는 한번만 등록됨을 확인할 수 있으나, 클라이언트의 실행결과에 문제 발생


errno.h 헤더파일에 있는 errno 전역변수를 통해 오류발생시의 정보를 확인할 수 있음
read() 함수의 경우 입력버퍼가 비어있을 때 -1을 반환하면서 errnoENGAIN 상수를 저장
소켓을 논블로킹 모드로 변환하는데는 fcntl() 함수를 사용

  • 두 번째 인자로 F_GETFL을 사용하여 인자로 전달된 파일 디스크립터에 설정되어있는 특성정보를 획득
  • F_SETFL을 사용하여 특성정보를 변경
  • fcntl(fd, F_SETFL, flag | O_NONBLOCK); 형태로 사용


엣지 트리거 방식은 데이터 수신시 단 한번 이벤트가 등록되기 때문에 입력 이벤트 발생시 대부분 입력버퍼에 저장된 데이터 전부를 읽어들어야함
따라서 read() 함수의 반환값 및 errno를 확인하여 입력버퍼가 비어있는지를 확인
이 경우 입출력 함수 호출시 서버를 오랜 시간 멈추는 상황을 만들지 않기 위해 반드시 논블로킹 소켓을 기반으로 해야함

레벨 트리거의 이벤트 등록방식 확인 예제

// echo_EPETserv.cpp
// 클라이언트는 이전의 ehoc_client.cpp를 사용

#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>

using namespace std;

#define BUF_SIZE 4
#define EPOLL_SIZE 50
void	setnonblockingmode(int fd);
void	error_handling(char *buf);

int		main(int argc, char *argv[])
{
	int					serv_sock, clnt_sock;
	struct sockaddr_in	serv_adr, clnt_adr;
	socklen_t			adr_sz;
	int					str_len;
	char				buf[BUF_SIZE];

	struct epoll_event	*ep_events;
	struct epoll_event	event;
	int					epfd, event_cnt;

	if(argc != 2)
	{
		cout << "Usage : " << argv[0] << " <port>\n";
		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, (sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
		error_handling("bind() error");

	if (::listen(serv_sock, 5) == -1)
		error_handling("listen() error");

	epfd = epoll_create(EPOLL_SIZE);
	ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);

	setnonblockingmode(serv_sock);
	event.events = EPOLLIN;
	event.data.fd = serv_sock;
	epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);

	while (1)
	{
		event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
		if (event_cnt == -1)
		{
			error_handling("epoll_wait() error");
			break;
		}

		cout << "return epoll_wait" << endl;
		for (int i = 0; i < event_cnt; i++)
		{
			if (ep_events[i].data.fd == serv_sock)
			{
				adr_sz = sizeof(clnt_adr);
				clnt_sock == accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
				setnonblockingmode(clnt_sock);
				event.events = EPOLLIN | EPOLLET;
				event.data.fd = clnt_sock;
				epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
				cout << "connected client : " << clnt_sock << endl;
			}
			else
			{
				while (1)
				{
					str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
					if (str_len == 0)	// close request!
					{
						epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
						close(ep_events[i].data.fd);
						cout << "closed client : " << ep_events[i].data.fd << endl;
						break;
					}
					else if (str_len < 0)
					{
						if (errno == EAGAIN)
							break;
					}
					else
						write(ep_events[i].data.fd, buf, str_len);	// echo
				}
			}
		}
	}

	close(serv_sock);
	close(epfd);
	return 0;
}

void	error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

void	setnonblockingmode(int fd)
{
	int	flag = fcntl(fd, F_GETFL, 0);
	fcntl(fd, F_SETFL, flag | O_NONBLOCK);
}

엣지 트리거는 데이터의 수신과 데이터가 처리되는 시점을 분리할 수 있다는 장점이 있음


내용 확인문제

  1. select 함수를 기반으로 서버를 구현할 때 코드상에서 확인할 수 있는 단점 두가지는 무엇인가?

    select() 함수의 호출 이후 모든 파일 디스크립터를 대상으로 하는 반복문이 실행됨
    select() 함수를 호출할 때마다 매번 관찰대상에 대한 정보들을 인자로 전달해야함

  2. select 방식이나 epoll 방식이나, 관찰의 대상이 되는 파일 디스크립터의 정보를 함수호출을 통해서 운영체제에 전달해야한다. 그렇다면 이들 정보를 운영체제에게 전달하는 이유는 어디에 있는가?

    관찰이 대상이 되는 파일 디스크립터들이 소켓을 가리키고, 소켓은 운영체제에서 관리가 되기 때문

  3. select 방식과 epoll 방식의 가장 큰 차이점은 관찰의 대상이 되는 파일 디스크립터를 운영체제에게 전달하는 방식에 있다. 어떻게 차이가 나는지, 그리고 그러한 차이를 보이는 이유는 무엇인지 설명해보자.

    selectselect() 함수 호출시마다 파일 디스크립터의 정보를 전달해야하지만, epoll은 운영체제에서 직접 파일 디스크립터를 관리하기때문에 해당 저장소에 등록시 한번만 전달하면 됨

  4. select 방식을 개선시킨 것이 epoll 방식이긴 하지만 select 방식도 나름의 장점이 있다. 어떠한 상황에서 select 방식을 선택하는 것이 보다 현명한 선택이 될 수 있는가?

    서버의 접속자 수가 많지 않고, 여러 운영체제에서 운용해야 하는 경우

  5. epoll은 레벨 트리거 방식, 또는 엣지 트리거 방식으로 동작한다. 그렇다면 이 둘이 어떻게 차이가 나는지 이벤트의 발생시점을 입력버퍼 기준으로 설명해보자.

    레벨 트리거는 입력버퍼가 남아있을 경우 계속해서 이벤트 발생
    엣지 트리거는 입력버퍼에 데이터가 수신된 상황에 한번만 이벤트 발생

  6. 엣지 트리거 방식을 사용하면 데이터의 수신과 데이터의 처리시점을 분리할 수 있다고 하였다. 그 이유는 무엇이고, 이는 어떠한 장점이 있는가?

    엣지 트리거 방식을 사용할시 데이터 수신시에만 이벤트가 발생하기 때문에 데이터가 수신된 이후 원하는 시점에 데이터를 처리할 수 있음
    데이터의 수신과 처리 시점 분리시 서버 구현에 엄청난 유연성이 생김

  7. 서버에 접속한 모든 클라이언트들 사이에서 메시지를 주고받는 형태의 채팅 서버를 레벨 트리거 방식의 epoll 기반으로, 엣지 트리거 방식의 epoll 기반으로 각각 구현해보자(참고로 채팅 서버는 레벨 트리거로 구현하건, 엣지 트리거로 구현하건 크게 차이를 보이지 않는다). 물론 서버의 실행을 위해서는 채팅 클라이언트가 필요한데 이는 Chapter 18에서 소개하는 예제 chat_clnt.cpp를 그대로 활용하기로 하자(컴파일 방법은 Chapter 18에서 별도로 참조해야 한다). 비록 Chapter 18을 공부한 상태는 아니나, 단순히 예제를 활용하는 정도이니 어려움은 없을 것이다. 만약에 이것이 부담스럽다면 Chapter 18을 먼저 공부한 다음에 이 문제를 해결해도 된다.

     // conv_EPLTserv.cpp
     // 클라이언트는 ch18의 chat_clnt.cpp를 사용
    
     #include <iostream>
     #include <unistd.h>
     #include <arpa/inet.h>
     #include <sys/socket.h>
     #include <sys/epoll.h>
    
     using namespace std;
    
     #define BUF_SIZE 100
     #define MAX_CLNT 256
     #define EPOLL_SIZE 50
     void	error_handling(char *buf);
     void	setnonblockingmode(int fd);
    
     int		clnt_cnt = 0;
     int		clnt_socks[MAX_CLNT];
    
     int	main(int argc, char *argv[])
     {
         int					serv_sock, clnt_sock;
         struct sockaddr_in	serv_adr, clnt_adr;
         socklen_t			adr_sz;
         int					str_len;
         char				buf[BUF_SIZE];
    
         struct epoll_event	*ep_events;
         struct epoll_event	event;
         int					epfd, event_cnt;
    
         if(argc != 2)
         {
             cout << "Usage : " << argv[0] << " <port>\n";
             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, (sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
             error_handling("bind() error");
    
         if (::listen(serv_sock, 5) == -1)
             error_handling("listen() error");
    
         epfd = epoll_create(EPOLL_SIZE);
         ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);
    
         setnonblockingmode(serv_sock);
         event.events = EPOLLIN;
         event.data.fd = serv_sock;
         epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);
    
         while (1)
         {
             event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
             if (event_cnt == -1)
             {
                 error_handling("epoll_wait() error");
                 break;
             }
    
             for (int i = 0; i < event_cnt; i++)
             {
                 if (ep_events[i].data.fd == serv_sock)
                 {
                     adr_sz = sizeof(clnt_adr);
                     clnt_sock == accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
                     clnt_socks[clnt_cnt++] = clnt_sock;
    
                     event.events = EPOLLIN;
                     event.data.fd = clnt_sock;
                     epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
                     cout << "connected client : " << clnt_sock << endl;
                 }
                 else
                 {
                     str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
                     if (str_len == 0)
                     {
                         epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
                         close(ep_events[i].data.fd);
    
                         for (int i = 0; i < clnt_cnt; i++)
                         {
                             if (ep_events[i].data.fd == clnt_socks[i])
                             {
                                 while (i++ < clnt_cnt - 1)
                                     clnt_socks[i] = clnt_socks[i + 1];
                                 break;
                             }
                         }
                         clnt_cnt--;
                         cout << "closed client : " << ep_events[i].data.fd << endl;
                         break;
                     }
                     else if (str_len < 0)
                     {
                         if (errno == EAGAIN)
                             break;
                     }
                     else
                     {
                         for (int i = 0; i < clnt_cnt; i++)
                             write(clnt_socks[i], buf, str_len);
                     }
                 }
             }
         }
         close(serv_sock);
         close(epfd);
         return 0;
     }
    
     void	error_handling(char *message)
     {
         fputs(message, stderr);
         fputc('\n', stderr);
         exit(1);
     }
    
     void	setnonblockingmode(int fd)
     {
         int	flag = fcntl(fd, F_GETFL, 0);
         fcntl(fd, F_SETFL, flag | O_NONBLOCK);
     }
    
     // conv_EPETserv.cpp
     // 클라이언트는 ch18의 chat_clnt.cpp를 사용
    
     #include <iostream>
     #include <unistd.h>
     #include <arpa/inet.h>
     #include <sys/socket.h>
     #include <sys/epoll.h>
    
     using namespace std;
    
     #define BUF_SIZE 100
     #define MAX_CLNT 256
     #define EPOLL_SIZE 50
     void	error_handling(char *buf);
     void	setnonblockingmode(int fd);
    
     int		clnt_cnt = 0;
     int		clnt_socks[MAX_CLNT];
    
     int	main(int argc, char *argv[])
     {
         int					serv_sock, clnt_sock;
         struct sockaddr_in	serv_adr, clnt_adr;
         socklen_t			adr_sz;
         int					str_len;
         char				buf[BUF_SIZE];
    
         struct epoll_event	*ep_events;
         struct epoll_event	event;
         int					epfd, event_cnt;
    
         if(argc != 2)
         {
             cout << "Usage : " << argv[0] << " <port>\n";
             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, (sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
             error_handling("bind() error");
    
         if (::listen(serv_sock, 5) == -1)
             error_handling("listen() error");
    
         epfd = epoll_create(EPOLL_SIZE);
         ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);
    
         setnonblockingmode(serv_sock);
         event.events = EPOLLIN;
         event.data.fd = serv_sock;
         epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);
    
         while (1)
         {
             event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
             if (event_cnt == -1)
             {
                 error_handling("epoll_wait() error");
                 break;
             }
    
             for (int i = 0; i < event_cnt; i++)
             {
                 if (ep_events[i].data.fd == serv_sock)
                 {
                     adr_sz = sizeof(clnt_adr);
                     clnt_sock == accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
                     clnt_socks[clnt_cnt++] = clnt_sock;
                     setnonblockingmode(clnt_sock);
    
                     event.events = EPOLLIN | EPOLLET;
                     event.data.fd = clnt_sock;
                     epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
                     cout << "connected client : " << clnt_sock << endl;
                 }
                 else
                 {
                     while (1)
                     {
                         str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
                         if (str_len == 0)
                         {
                             epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
                             close(ep_events[i].data.fd);
    
                             for (int i = 0; i < clnt_cnt; i++)
                             {
                                 if (ep_events[i].data.fd == clnt_socks[i])
                                 {
                                     while (i++ < clnt_cnt - 1)
                                         clnt_socks[i] = clnt_socks[i + 1];
                                     break;
                                 }
                             }
                             clnt_cnt--;
                             cout << "closed client : " << ep_events[i].data.fd << endl;
                             break;
                         }
                         else if (str_len < 0)
                         {
                             if (errno == EAGAIN)
                                 break;
                         }
                         else
                         {
                             for (int i = 0; i < clnt_cnt; i++)
                                 write(clnt_socks[i], buf, str_len);
                         }
                     }
                 }
             }
         }
         close(serv_sock);
         close(epfd);
         return 0;
     }
    
     void	error_handling(char *message)
     {
         fputs(message, stderr);
         fputc('\n', stderr);
         exit(1);
     }
    
     void	setnonblockingmode(int fd)
     {
         int	flag = fcntl(fd, F_GETFL, 0);
         fcntl(fd, F_SETFL, flag | O_NONBLOCK);
     }