관리 메뉴

Value Creator의 IT(프로그래밍 / 전자제품)

Chapter 17 epoll 기반 IO 멀티플렉싱 서버 구현 본문

1. 프로그래밍/4) Network

Chapter 17 epoll 기반 IO 멀티플렉싱 서버 구현

valuecreatort 2019. 11. 7. 21:53
반응형

select 함수 기반 멀티플렉싱의 단점을 극복한다.

select 함수는 호출할 때마다 관련 인자의 정보를 전달해줘야 한다.

 

리눅스의 epoll 함수는 운영체제에서 멀티플렉싱을 지원한다.(참고 : 윈도우의 IOCP)

 

단, select함수는 호환성이 상대적으로 좋다(select 기반 코드는 윈도우와 리눅스간 상호 변경 쉬움)

 

select 방식 사용 조건

1. 서버 접속자 수 적다.

2. 다양한 운영체제에서 사용가능해야 한다.

 

 

epoll 기반의 에코 서버

echo_epollserv.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>

#define BUF_SIZE 100
#define EPOLL_SIZE 50 //epoll 사이즈 선언  
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, i;
	char buf[BUF_SIZE];

	struct epoll_event *ep_events; //epoll 구조체 선언 
	struct epoll_event event; //epoll 구조체 선언
	int epfd, event_cnt;

	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");

	epfd=epoll_create(EPOLL_SIZE); //epoll 파일 디스크립터 정의 
	ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE); //epoll event 메모리 할당 

	event.events=EPOLLIN; //리스닝 소켓을 epoll에 등록한다. EPOLLIN을 대상으로 등록한다. 
	event.data.fd=serv_sock;	//epoll 파일 디스크립터를 서버 소켓으로 설정 
	epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event); //epoll에 서버 소켓 파일 디스크립터 추가 

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

		for(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);
				printf("connected client: %d \n", clnt_sock);
			}
			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);
						printf("closed client: %d \n", ep_events[i].data.fd);
					}
					else //메세지 수신의 경우 에코처리한다. 
					{
						write(ep_events[i].data.fd, buf, str_len);    // echo!
					}
	
			}
		}
	}
	close(serv_sock);
	close(epfd);
	return 0;
}

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

 

 

<레벨 트리거와 엣지 트리거>

레벨 트리거 : 입력 버퍼에 데이터가 남아있으면 계속해서 이벤트를 발생시킨다.

엣지 트리거 : 입력 버퍼에 데이터가 들어오는 순간 딱 한번 이벤트를 발생시킨다.

--> 소켓은 기본적으로 '레벨 트리거' 방식으로 동작한다.

 

 

epoll 기반의 Level Trigger 서버

echo_EPLTserv.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>

#define BUF_SIZE 4 //데이터를 4바이트씩 받아서, 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, i;
	char buf[BUF_SIZE];

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

	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");

	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)
		{
			puts("epoll_wait() error");
			break;
		}

		puts("return epoll_wait"); //버퍼에 데이터가 남아있는 동안 반복적으로 실행된다. 
		for(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);
				printf("connected client: %d \n", clnt_sock);
			}
			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);
						printf("closed client: %d \n", ep_events[i].data.fd);
					}
					else
					{
						write(ep_events[i].data.fd, buf, str_len);    // echo!
					}
			}
		}
	}
	close(serv_sock);
	close(epfd);
	return 0;
}

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

 

 

 

 

 

 

 

 

epoll 기반의 Edge Trigger 서버

엣지 트리거 구현을 위해서는 아래 사항을 설정한다.

1. Non-blocking IO로 소켓 속성을 변경한다.

int flag=fcntl(fd, F_GETFL, 0);

fcntl(fd, F_SETFL, flag|O_NONBLOCK);

 

데이터 크기에 따라서 IO로 인한 delay가 발생할 수 있다. 따라서 버퍼를 충분히 크게 설정해야 한다.

Non-blocking 이기 때문에 입력 함수의 호출과 다른 작업을 병행할 수 있다.

 

 

2. 입력 버퍼 상태 확인

int errno;

데이터 입력시 데이터 수신이 완료되었는지 확인이 필요하다. 헤더파일 <error.h>을 포함하고, 변수 errno를 참조한다. errno에 EAGAIN이 저장되면 버퍼가 빈 상태이다.

 

 

 

 

echo_EPETserv.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>

#define BUF_SIZE 4 //여전히 버퍼의 크기는 4이다. 
#define EPOLL_SIZE 50
void setnonblockingmode(int fd); //non-block 모드 설정을 위한 함수 
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, i;
	char buf[BUF_SIZE];

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

	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");

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

	setnonblockingmode(serv_sock); //서버 소켓에 대해 non-block 모드 설정
	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)
		{
			puts("epoll_wait() error");
			break;
		}

		puts("return epoll_wait");
		for(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); //연결 요청을 수락해서 생성된 소켓도 비동기 IO 옵션 설정
				event.events=EPOLLIN|EPOLLET;
				event.data.fd=clnt_sock;
				epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
				printf("connected client: %d \n", clnt_sock);
			}
			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);
							printf("closed client: %d \n", ep_events[i].data.fd);
							break;
						}
						else if(str_len<0)
						{
							if(errno==EAGAIN) //errno가 EAGAIN이 될 때까지 while을 계속 돌면서 데이터를 수신한다.
								break;
						}
						else
						{
							write(ep_events[i].data.fd, buf, str_len);    // echo!
						}
				}
			}
		}
	}
	close(serv_sock);
	close(epfd);
	return 0;
}

void setnonblockingmode(int fd)
{
	int flag=fcntl(fd, F_GETFL, 0); //리스닝 소켓도 비동기 IO를 진행하도록 옵션 설정
	fcntl(fd, F_SETFL, flag|O_NONBLOCK);
}
void error_handling(char *buf)
{
	fputs(buf, stderr);
	fputc('\n', stderr);
	exit(1);
}

 

 

엣지 트리거 기반에서는 데이터 송수신 횟수와 이벤트 발생 횟수가 일치한다.

(ex 데이터를 4번 보냈으면 이벤트도 4번만 발생한다.)

레벨 트리거 기반에서는 데이터를 보내면 이벤트가 몇 번 발생할지 버퍼의 크기에 따라 다르다.

 

 

 

 

 

서버 역할이 단순하고 데이터 송수신 상황이 다양하지 않다면 -> 레벨 트리거가 유리

서버 측에서 컨트롤 요소가 많은 경우 -> 엣지 트리거가 유리

 

 

 

 

 

 

 

 

 

 

 

 

 

반응형
Comments