모든 내용은 [윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어] 를 기반으로 제 나름대로 이해하여 정리한 것입니다. 다소 부정확한 내용이 있을수도 있으니 이를 유념하고 봐주세요!
# 멀티플렉싱 기법 중 하나인 select는 오래 전에 개발됨.
- 이를 이용하면 아무리 프로그램의 성능을 최적화시켜도, 허용할 수 있는 동시접속자의 수가 백을 넘기 힘듦.
(물론 하드웨어 성능에 따라 많이 좌지우지 되겠지만)
- 따라서, 웹기반의 서버개발이 주를 이루는 오늘날의 개발환경에서는 적절치 않음.
- 따라서 이에 대한 대안으로 리눅스 영역에서 주로 활용되는 epoll에 대해 공부해보자.
# select 기반의 IO 멀티플렉싱이 느린 이유
1) select 함수호출 이후에 항상 등장하는, "모든 파일 디스크립터를 대상"으로 하는 반복문
2) select 함수를 호출할때마다 인자로 매번 전달해야 하는 관찰대상에 대한 정보들
=> 매번 운영체제에게 이 정보를 전달해야 함.
(1보다 2가 성능에 치명적인 약점이 될 수 있음, 코드의 개선을 통해서 덜 수 있는 유형의 부담이 아니기 때문)
# select 함수와 epoll 함수는 파일 디스크립터(즉, 소켓)의 변화를 관찰하는 함수인데,
소켓은 운영체제에 의해 관리되는 대상이므로, 두 함수 모두 절대적으로 운영체제에 의해 기능이 완성되는 함수임.
# select 함수의 단점을 해결하기 위해서는?
- "운영체제에게 관찰대상에 대한 정보를 딱 한번만 알려주고서,
관찰 대상의 범위, 또는 내용에 변경이 있을때 변경사항만 알려주도록" 해야함.
- 이러면, 매번 select 함수를 호출할 때마다 관찰대상에 대한 정보를 운영체제에게 전달할 필요가 없음.
- 단, 운영체제가 이를 지원할때만 가능(운영체제마다 지원여부와 방식의 차이가 존재)
- 리눅스에서는 이를 epoll, 윈도우에서는 IOCP 라고 함.
# select 함수의 장점도 있긴 있다!
- 개선된 IO 멀티플렉싱 모델은 운영체제별로 호환이 되지 않음.
- 반면, select 함수는 대부분의 운영체제에서 지원함.
- 다음 두가지 유형의 조건이 만족 또는 요구된다면,
리눅스라고 할지라도 epoll을 사용할 필요가 없음
1) 서버의 접속자 수가 많지 않다.
2) 다양한 운영체제에서 운영이 가능해야 한다.
# epoll의 장점
- 장점(select 함수의 단점과 반대)
1) 상태변화의 확인을 위한, 전체 파일 디스크립터를 대상으로 하는 반복문이 필요없음
2) select 함수에 대응하는 epoll_wait 함수 호출시, 관찰대상의 정보를 매번 전달할 필요가 없다.
# epoll 기반의 서버 구현에 구현에 필요한 함수와 구조체
1) epoll_create : epoll 파일 디스크립터 저장소(epoll 인스턴스) 생성
#include <sys/epoll.h>
int epoll_create(int size);
-> 성공시 epoll 파일 디스크립터, 실패시 -1반환
- size : epoll 인스턴스의 크기정보
- (관찰 대상인) 파일 디스크립터의 저장소 : "epoll 인스턴스"
- size는 운영체제에 전달하는 힌트에 지나지 않음.
즉, 운영체제는 이 값을 "참고만" 하여 epoll 인스턴스를 생성.
- 리눅스 2.6.8 이후부터는 size가 완전히 무시됨.
커널 내에서 epoll 인스턴스 크기를 유동적으로 변화시키기 때문.
- epoll 인스턴스는 소켓과 마찬가지로 운영체제가 관리함.
따라서, 소켓 생성시와 마찬가지로 파일 디스크립터를 반환하고, 소멸시 close 함수를 사용함.
2) epoll_ctl : 저장소(epoll 인스턴스)에 파일 디스크립터 등록 및 삭제
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
-> 성공시 0, 실패시 -1 반환
- epfd : 관찰대상을 등록할 epoll 인스턴스의 파일 디스크립터
- op : 관찰대상의 추가, 삭제 또는 변경여부 지정
- fd : 등록할 관찰대상의 파일 디스크립터
- event : 관찰대상의 관찰 이벤트 유형
* epoll_ctl의 두번째 인자(op)로 전달 가능한 상수와 그 의미
1) EPOLL_CTL_ADD : 파일 디스크립터를 epoll 인스턴스에 등록.
2) EPOLL_CTL_DEL : 파일 디스크립터를 epoll 인스턴스에서 삭제. 이때, 네번째 인자(event)에 NULL을 전달.
3) EPOLL_CTL_MOD : 등록된 파일 디스크립터의 이벤트 발생상황을 변경.
- 구조체 epoll_event는 이벤트가 발생한 파일 디스크립터를 묶는 용도로 사용되나,
파일 디스크립터를 epoll 인스턴스에 등록할 때, 이벤트 유형을 등록하는 용도로도 사용됨
e.g.
struct epoll_event event;
....
event.events=EPOLLIN; // 수신할 데이터가 존재하는 이벤트 발생시
event.data.fd=sockfd; // epoll 인스턴스에 sockfd 파일 디스크립터 등록을 위함임
epoll_ctl(efpd,EPOLL_CTL_ADD,sockfd,&event);
....
* epoll_event 구조체의 멤버인 events에 저장 가능한 상수와 이벤트의 유형
- EPOLLIN : 수신할 데이터가 존재하는 상황
- EPOLLOUT : 출력버퍼가 비워져서 당장 데이터를 전송할 수 있는 상황
- EPOLLPRI : OOB 데이터가 수신된 상황
- EPOLLRDHUP : 연결이 종료되거나 Half-close가 진행된 상황,
이는 엣지 트리거 방식에서 유용하게 사용될 수 있음
- EPOLLERR : 에러가 발생한 상황
- EPOLLET : 이벤트의 감지를 엣지 트리거 방식으로 동작시킴
- EPOLLONESHOT : 이벤트가 한번 감지되면, 해당 파일 디스크립터에서는 더이상 이벤트를 발생시키지 않음.
따라서 epoll_ctl 함수의 두번째 인자로 EPOLL_CTL_MOD를 전달해서 이벤트를 재설정해야함.
=> 위 상수들은 비트 OR 연산자(연산자 |)를 이용해서 둘 이상을 함께 등록할 수 있다.
3) epoll_wait : select 함수와 마찬가지로 파일 디스크립터의 변화를 대기
- select 방식에서는 관찰대상인 파일 디스크립터 저장을 위해 fd_set 변수를 직접 선언하였음.
- epoll 방식에서는 파일 디스크립터 저장을 운영체제가 담당하기에,
저장소의 생성을 운영체제에게 요청 해야함.(epoll_create)
- 관찰 대상인 파일디스크립터의 추가, 삭제 => select : FD_SET, FD_CLR, ... / epoll : epoll_ctl
- select <=> epoll_wait : 파일 디스크립터의 변화를 대기
- select 방식 : select 함수 호출시 전달한 fd_set형 변수의 변화를 통해 상태변화를 확인(이벤트 발생여부 확인)
- epoll 방식 : epoll_event 구조체를 기반으로 상태변화가 발생(이벤트가 발생)한 파일 디스크립터가 별도로 묶임
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_event 기반의 배열을 넉넉하게 선언후,
epoll_wait 함수 호출시 인자로 전달하면, 상태변화(이벤트)가 발생한 파일 디스크립터의 정보가
이 배열에 별도로 묶임 => select 함수 사용시처럼 전체 관심대상 파일 디스크립터를 대상으로 한 반복문이 불필요
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
// -> 성공시 이벤트가 발생한 파일 디스크립터 수, 실패시 -1반환
- epfd : 이벤트 발생의 관찰 영역인 epoll 인스턴스의 파일 디스크립터
- events : 이벤트가 발생한 파일 디스크립터가 채워질 버퍼의 주소값
- maxevents : 두번째 인자로 전달된 주소값의 버퍼에 등록 가능한 최대 이벤트 수 - 버퍼를 동적으로 할당해야함
- timeout : 1/1000초 단위의 대기시간. -1 전달시 이벤트가 발생할 때까지 무한대기
e.g.
int event_cnt;
struct epoll_event *ep_events;
....
ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE); // EPOLL_SIZE는 매크로 상수값
....
event_cnt=epoll_wait(epfd,ep_events,EPOLL_SIZE,-1);
....
# epoll 기반의 에코 서버
echo_epollserv.c
#define _XOPEN_SOURCE 700
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#define BUF_SIZE 100
#define EPOLL_SIZE 50
void error_handling(char *message);
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) // 실행파일 경로/PORT번호 를 입력으로 받아야 함
{
printf("Usage : %s <port> \n", argv[0]);
exit(EXIT_FAILURE);
}
serv_sock=socket(PF_INET,SOCK_STREAM,0); // TCP 소켓 생성
/* 서버 주소정보 초기화 */
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 인스턴스에 있는 파일 디스크립터 중
실제로 이벤트가 발생한 파일 디스크립터를 따로 모아놓는 동적배열
최대 EPOLL_SIZE 만큼 이벤트가 발생할 수 있음
*/
event.events=EPOLLIN; // 수신한 데이터가 있는 이벤트
event.data.fd=serv_sock; // 서버소켓이 대상
epoll_ctl(epfd,EPOLL_CTL_ADD,serv_sock,&event); // epoll 인스턴스에 이벤트 등록
while(true)
{
event_cnt=epoll_wait(epfd,ep_events,EPOLL_SIZE,-1);
/*
epoll 인스턴스에 있는 관심대상에서
이벤트가 발생할때까지 무한대기
*/
if(event_cnt==-1)
{
puts("epoll_wait() error");
break;
}
for(i=0;i<event_cnt;++i) // 이벤트 발생한 파일 디스크립터에 대해서만 반복문(select 기반과 다른 점)
{
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 // 클라이언트의 메시지를 실제로 수신하는 소켓에 대해(accept 함수호출로 생성된 소켓)
{
str_len=read(ep_events[i].data.fd,buf,BUF_SIZE); // 클라이언트로부터 데이터 수신
if(str_len==0) // close request!(EOF 수신)
{
epoll_ctl(epfd,EPOLL_CTL_DEL,ep_events[i].data.fd,NULL);
// 클라이언트가 종료했으므로 이 소켓 또한 관심대상에서 제외하고
close(ep_events[i].data.fd); // 이 소켓의 연결도 종료
printf("close client: %d \n",ep_events[i].data.fd);
}
else
write(ep_events[i].data.fd,buf,str_len); // 수신한 문자열을 다시 클라이언트로 에코
}
}
}
close(serv_sock); // 서버소켓 소멸
close(epfd); // epoll 인스턴스 소멸
return 0;
}
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(EXIT_FAILURE); // -1
}
# 레벨 트리거와 엣지 트리거 : 차이점은 "이벤트가 발생하는 시점"
1) 레벨 트리거 : 입력버퍼에 데이터가 남아있는 동안에 계속해서 이벤트가 등록됨.
2) 엣지 트리거 : 입력버퍼에 데이터가 수신된 상황에 딱 한번만 이벤트가 등록됨.
- "이벤트가 등록되었다"는 의미는 운영체제가 변화가 발생한 디스크립터로 등록했다는 의미.
즉, 해당 이벤트를 struct epoll_event 배열에 추가함.
- epoll은 기본적으로 레벨 트리거 방식으로 동작한다.
- select 모델은 레벨 트리거 방식으로 동작함.
# 레벨 트리거의 이벤트 특성 파악하기
echo_EPLTserv.c
#define _XOPEN_SOURCE 700
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#define BUF_SIZE 4
#define EPOLL_SIZE 50
void error_handling(char *message);
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) // 실행파일 경로/PORT번호 를 입력으로 받아야 함
{
printf("Usage : %s <port> \n", argv[0]);
exit(EXIT_FAILURE);
}
serv_sock=socket(PF_INET,SOCK_STREAM,0); // TCP 소켓 생성
/* 서버 주소정보 초기화 */
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 인스턴스에 있는 파일 디스크립터 중
실제로 이벤트가 발생한 파일 디스크립터를 따로 모아놓는 동적배열
최대 EPOLL_SIZE 만큼 이벤트가 발생할 수 있음
*/
event.events=EPOLLIN; // 수신한 데이터가 있는 이벤트
event.data.fd=serv_sock; // 서버소켓이 대상
epoll_ctl(epfd,EPOLL_CTL_ADD,serv_sock,&event); // epoll 인스턴스에 이벤트 등록
while(true)
{
event_cnt=epoll_wait(epfd,ep_events,EPOLL_SIZE,-1);
/*
epoll 인스턴스에 있는 관심대상에서
이벤트가 발생할때까지 무한대기
*/
if(event_cnt==-1)
{
puts("epoll_wait() error");
break;
}
puts("return epoll_wait");
for(i=0;i<event_cnt;++i) // 이벤트 발생한 파일 디스크립터에 대해서만 반복문(select 기반과 다른 점)
{
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 // 클라이언트의 메시지를 실제로 수신하는 소켓에 대해(accept 함수호출로 생성된 소켓)
{
str_len=read(ep_events[i].data.fd,buf,BUF_SIZE); // 클라이언트로부터 데이터 수신
if(str_len==0) // close request!(EOF 수신)
{
epoll_ctl(epfd,EPOLL_CTL_DEL,ep_events[i].data.fd,NULL);
// 클라이언트가 종료했으므로 이 소켓 또한 관심대상에서 제외하고
close(ep_events[i].data.fd); // 이 소켓의 연결도 종료
printf("close client: %d \n",ep_events[i].data.fd);
}
else
write(ep_events[i].data.fd,buf,str_len); // 수신한 문자열을 다시 클라이언트로 에코
}
}
}
close(serv_sock); // 서버소켓 소멸
close(epfd); // epoll 인스턴스 소멸
return 0;
}
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(EXIT_FAILURE); // -1
}
- read 함수호출 시 사용할 버퍼의 크기를 4바이트로 축소
=> 입력버퍼에 수신된 데이터를 한번에 읽어들이지 못하게 하기 위함임
- epoll_wait 함수의 호출횟수를 확인하기 위한 문장 삽입
# 엣지 트리거 기반의 서버 구현을 위해서 알아야 할 것 두가지
1) 변수 errno을 이용한 오류의 원인을 확인하는 방법
- 일반적으로 리눅스에서는 소켓 관련 함수는 -1을 반환함으로써 오류의 발생을 알림
=> 따라서, 오류의 원인을 정확히 알수는 없음
- 리눅스에서는 오류 발생시 추가적인 정보제공을 위해 다음의 변수를 전역으로 선언 : int errno;
- errno 변수 접근을 위해서는 헤더파일 errno.h를 포함해야 함
(이 헤더파일은 errno에 extern 선언이 존재함)
- read 함수는 입력버퍼가 비어서 더이상 읽어들일 필요가 없을때 -1을 반환하고,
이때 errno에는 상수 EAGAIN가 저장됨.
2) 넌-블로킹(Non-blocking) IO를 위한 소켓의 특성을 변경하는 방법
- 소켓을 넌-블로킹 모드로 변경하는 방법
#include <fcntl.h>
int fcntl(int filedes, int cmd, ...);
// -> 성공시 매개변수 cmd에 따른값, 실패시 -1반환
- 리눅스에서 파일의 특성을 변경 및 참조하는 함수임
- filedes : 특성 변경의 대상이 되는 파일의 파일 디스크립터
- cmd : 함수호출의 목적에 해당하는 정보
- 파일(소켓)을 넌-블로킹 모드로 변경을 위해서는
int flag=fcntl(fd,F_GETTL,0); // 파일 디스크립터에 이미 설정되어있는 특성정보를 int형으로 얻음
fcntl(fd,F_SETL,flag|O_NONBLOCK); // O_NONBLOCK 특성을 더하여 재설정
=> read & write 함수 호출시에도 데이터의 유무에 상관없이 블로킹이 되지 않는 파일(소켓)이 만들어짐
# 엣지 트리거 기반의 서버 구현
- 엣지 트리거 방식은 데이터가 수신되면 딱 한 번만 이벤트가 등록되므로,
입력과 관련한 이벤트 발생시, 입력버퍼에 저장된 데이터 전부를 읽어들여야 한다.(대부분의 경우)
- 따라서, 입력버퍼가 비어있는지를 확인하는 과정을 거쳐야 함. 그 방법으로
"read 함수가 -1을 반환하고, 변수 errno에 저장된 값이 EAGAIN이라면
더이상 읽어들일 데이터가 존재하지 않는 상황"
- 소켓을 넌-블로킹 모드로 만드는 이유는 엣지 트리거 방식 특성상
블로킹 방식으로 동작하는 read & write 함수의 호출은
서버를 오랜 시간 멈추는 상황으로까지 이어지게 할 수 있기 때문.
=> 엣지 트리거 방식에서는 반드시 넌-블로킹 모드 소켓을 기반으로 read & write 함수를 호출하자.
ehco_EPETserv.c
#define _XOPEN_SOURCE 700
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#define BUF_SIZE 4
#define EPOLL_SIZE 50
void setnonblockingmode(int fd);
void error_handling(char *message);
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) // 실행파일 경로/PORT번호 를 입력으로 받아야 함
{
printf("Usage : %s <port> \n", argv[0]);
exit(EXIT_FAILURE);
}
serv_sock=socket(PF_INET,SOCK_STREAM,0); // TCP 소켓 생성
/* 서버 주소정보 초기화 */
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 인스턴스에 있는 파일 디스크립터 중
실제로 이벤트가 발생한 파일 디스크립터를 따로 모아놓는 동적배열
최대 EPOLL_SIZE 만큼 이벤트가 발생할 수 있음
*/
setnonblockingmode(serv_sock);
event.events=EPOLLIN; // 수신한 데이터가 있는 이벤트
event.data.fd=serv_sock; // 서버소켓이 대상
epoll_ctl(epfd,EPOLL_CTL_ADD,serv_sock,&event); // epoll 인스턴스에 이벤트 등록
while(true)
{
event_cnt=epoll_wait(epfd,ep_events,EPOLL_SIZE,-1);
/*
epoll 인스턴스에 있는 관심대상에서
이벤트가 발생할때까지 무한대기
*/
if(event_cnt==-1)
{
puts("epoll_wait() error");
break;
}
puts("return epoll_wait");
for(i=0;i<event_cnt;++i) // 이벤트 발생한 파일 디스크립터에 대해서만 반복문(select 기반과 다른 점)
{
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);
printf("connected client: %d \n",clnt_sock);
}
else // 클라이언트의 메시지를 실제로 수신하는 소켓에 대해(accept 함수호출로 생성된 소켓)
{
while(true) // 이벤트 발생시 입력버퍼에 모든 데이터를 수신하기 위함임
{
str_len=read(ep_events[i].data.fd,buf,BUF_SIZE); // 클라이언트로부터 데이터 수신
if(str_len==0) // close request!(EOF 수신)
{
epoll_ctl(epfd,EPOLL_CTL_DEL,ep_events[i].data.fd,NULL);
// 클라이언트가 종료했으므로 이 소켓 또한 관심대상에서 제외하고
close(ep_events[i].data.fd); // 이 소켓의 연결도 종료
printf("close client: %d \n",ep_events[i].data.fd);
}
else if(str_len<0)
{
if(errno==EAGAIN) // 다 읽어들여 입력버퍼가 빈 경우
break;
}
else
write(ep_events[i].data.fd,buf,str_len); // 수신한 문자열을 다시 클라이언트로 에코
}
}
}
}
close(serv_sock); // 서버소켓 소멸
close(epfd); // epoll 인스턴스 소멸
return 0;
}
void setnonblockingmode(int fd)
{
int flag=fcntl(fd,F_GETFL,0);
fcntl(fd,F_SETFL,flag|O_NONBLOCK);
}
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(EXIT_FAILURE); // -1
}
# 엣지 트리거 방식의 장점
- 데이터의 수신과 데이터가 처리되는 시점을 분리할 수 있다.
: 입력버퍼에 데이터가 수신된 상황(이벤트가 등록된 상황)임에도 불구하고,
이를 읽어들이고 처리하는 시점을 서버가 결정할 수 있도록 하는 것은 서버 구현에 엄청난 유연성을 제공
- 레벨 트리거 방식으로 이것이 불가능한 것은 아니나, 사실상 불가능.
(처음 이벤트가 등록된 상황에서 처리하지 않으면, epoll_wait 함수를 호출할때마다
이벤트가 발생하고, 이로 인해 발생하는 이벤트의 수가 계속 누적되므로)
- 일반적으로 엣지 트리거가 빠르지만, 상황에 따라 다를 수 있음.
[출처] : 윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어
'Programming > 열혈 TCP, IP 소켓 프로그래밍(저자 윤성우)' 카테고리의 다른 글
Ch 18. 멀티쓰레드 기반의 서버구현 (0) | 2020.08.10 |
---|---|
Ch 17. 내용 확인문제 (0) | 2020.08.10 |
Ch 16. 내용 확인문제 (0) | 2020.08.10 |
Ch 16. 입출력 스트림의 분리에 대한 나머지 이야기 (0) | 2020.08.10 |
Ch 15. 내용 확인문제 (0) | 2020.08.10 |