이 글은 윤성우의 TCP/IP 소켓 프로그래밍 책을 참고하였습니다.
순서
I. select의 단점
II. epoll 개념
III. epoll 관련 함수들 분석
1. epoll_create
2. epoll_ctl
3. epoll_wait
IV. 서버 구현
I. select 함수의 단점
앞서 select를 설명하며 특이한 부분이 있었다.
바로 select를 호출해서 시그널을 볼 때 fd_set의 원본이 변경된다는 것이었다.
때문에 우리는 select를 호출할때는 항상 복사본을 사용했었다.
하지만 이것은 select기반의 IO 멀티플렉싱이 느린 이유가 된다.
매번 운영체제에게 관찰 대상을 인자로 전달해야하기 때문이다.
따라서 이런 개념이 필요하다
" 운영체제에게 관찰대상 정보를 딱 한번 알려주고, 변화가 있을 때만 변경 사항만 알려주도록 하자!!"
--> epoll
그렇다면 select는 쓸모없는가??
--> NO!
select는
1. Window, Linux 모두 동일하게 사용 가능하고 (다양한 운영체제에서 사용)
2. 서버의 접속자 수가 많지 않을 때
사용하기 좋다.
II. epoll 개념
위와같이 진행하기 위해서는 막 되는 것이 아니라, 운영체제가 OK~ 해야한다.
그렇기 때문에 운영체제에 따라서 지원여부, 방식도 차이가 있음을 기억하자.
(리눅스 : epoll, 윈도우 IOCP)
epoll의 장점은 다음과 같다
1. 전체 파일 디스크립터를 대상으로하는 반복문이 필요 없다.
2. select 함수에 대응하는 epoll_wait 함수호출 시, 관찰 대상의 정보를 매번 전달할 필요 없다.
epoll의 함수들은 다음과 같다.
#include <sys/epoll.h>
1. epoll_create : epoll fd 저장소 생성
2. epoll_ctl : 저장소에 fd 등록 및 삭제
3. epoll_wait : select함수와 마찬가지로 fd의 변화 대기
select에서는 fd_set형 변수를 직접 선언했다면, epoll은 운영체제가 알아서 담당해준다.
따라서 저장소 생성을 운영체제에 요청하는 함수가 1번이다.
그렇다면 epoll은 어떤 구조체나 자료형으로 이벤트들을 파악하는 것일까?
그 구조체는 다음과 같다.
struct epoll_event
{
__unit32_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 기반의 배열을 넉넉하게 선언하게되고,
상태변화가 발생한 fd의 정보가 이 배열에 별도로 묶이게 된다.
따라서 select처럼 반복해서 전체 fd를 대상으로 구현하는 것이 불필요해진다.
III. epoll 관련 함수들 분석
1. epoll_create
fd의 저장소(epoll 인스턴스)를 생성한다
#include <sys/epoll.h>
int epoll_create(int size);
--> 성공 시 epoll 파일 디스크립터, 실패 시 -1 반환
ㄴ. size : epoll 인스턴스의 크기정보
* 리눅스 커널 2.6.8 이후부터 전달인자 없음.
fd를 저장소에 모아두는데, 이 저장소를 "epoll 인스턴스"라고 한다.
int size는 인스턴스의 크기를 전달한다.
그러나 리눅스 커널 2.6.8 이후로는 전달 인자가 없어도 된다.
2. epoll_ctl
epoll 인스턴스에 관찰대상이 되는 fd를 등록, 삭제한다.
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
--> 성공 시 0, 실패 시 -1 반환
ㄴ. epfd : 관찰대상을 등록할 epoll 인스턴스의 파일 디스크립터
(cpoll_create의 반환값)
ㄴ. op : 관찰대상의 추가, 삭제 또는 변경 여부를 지정한다.
ㄴ. fd : 등록할 관찰대상의 파일 디스크립터
ㄴ. event : 관찰대상의 관찰 이벤트 유형
인자1. int epfd
epoll_create를 통해 반환된 epoll 파일 디스크립터가 이에 해당된다.
인자2. int op
관찰대상의 추가, 삭제, 변경 여부를 지정한다. 이에 대한 상수정보는 다음과 같다.
EPOLL_CTL_ADD : fd를 epoll 인스턴스에 "등록"
EPOLL_CTL_DEL : fd를 epoll 인스턴스에서 "삭제"
EPOLL_CTL_MOD : 등록된 fd의 이벤트 발생상황 "변경"
인자3. int fd
등록할 파일 디스크립터
인자4. struct epoll_event* event
주의할 점은 여기서의 event는 "어떤 이벤트를 확인할 것인가"에 대한 이벤트 구조체 변수의 주소이다.
따라서 변화가 생긴 fd를 저장하는 event 구조체는 나중에 따로 선언해주어야 한다.
이 인자로 전달하기 전에 epoll_event 변수를 선언해서 두 멤버에 값을 넣어주어야 한다.
코드로 보자
#include <sys/epoll.h>
struct epoll_event myevent; // 변수선언
myevent.event = EPOLLIN; // event멤버에 원하는 것 등록
myevent.data.fd = fd; // 파일 디스크립터 설정
epoll_cnt(epfd, EPOLL_CTL_ADD, fd, &myevent); // 함수 선언모습
EPOLLIN : 수신할 데이터가 존재하는 상황
EPOLLOUT : 출력버퍼가 비워져서 당장 데이터 송신이 가능한 상황
EPOLLPRI : OBB 데이터가 수신된 상황
EPOLLRDHUP : 연결이 종료되거나 Half-close가 진행된 상황 -> 엣지트리거에 유용
EPOLLERR : 에러가 발생된 상황
위의 이벤트들은 event에 저장가능한 상수들이고, 비트 "or" 연산자를이용해 둘 이상을 함께 등록할 수 있다.
3. epoll_wait
파일 디스크립터의 변화를 대기하는 함수
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
--> 성공 시 발생한 fd의 수, 실패 시 -1 반환
ㄴ. epfd : 이벤트 발생의 관찰영역인 epoll 인스턴스의 파일 디스크럽터(epfd)
ㄴ. events : 이벤트가 발생한 fd가 채워질 버퍼의 주소 값
ㄴ. maxevents : 두 번째 인자로 전달된 주소 값의 버퍼에 등록 가능한 최대 이벤트 수
ㄴ. timeout : ms 단위(1/1000초)의 대기시간, -1 전달 시 무한대기
인자1. int epfd
epoll_create 함수로 반환된 epoll 인스턴스의 파일 디스크립터 (epfd)
인자2. struct epoll_event* events
앞서 epoll_ctl을 위한 event 변수는 정보를 등록해서 보고자 하는걸 세팅했었다.
여기서의 event는 epoll_event의 주소값으로 이벤트가 발생했을 때 그 fd가 담길 버퍼이다. 잘 구별하자!!
중요한 점은 epoll_event* 자체를 변수로 선언하고 동적할당을 해주어야 한다.
인자3. int maxevents
두 번째 인자인 events라는 버퍼에 최대 몇개의 이벤트를 담을건지 갯수를 전달한다.
인자4. int timeout
1ms단위의 타임아웃 시간을 전송한다. 특별한 점은 -1을 전달시 무한 대기한다.
코드로 보자
#include <sys/epoll.h>
#define EPOLL_SIZE 50
struct epoll_event* ep_events_buf; // 주소 변수 정의
ep_events_buf = malloc(sizeof(struct epoll_event)* EPOLL_SIZE); // 크기만큼 동적할당
event_cnt = epoll_wait(epfd, ep_events_buf, EPOLL_SIZE, -1); // 함수 선언 방법
IV. 서버 구현
기본 서버구현은 생략하고 epoll함수의 구현 위주로 구현한 코드이다.
<hide/>
#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
int main()
{
// 기본 소켓 프로그래밍 변수들
int serv_sock, clnt_sock;
struct sockaddr_in serv_addr, clnt_addr;
socklen_t addr_sz;
int str_len, i;
char buf[BUF_SIZE]
// epoll위한 변수들
struct epoll_event* ep_events_buf; // 변화를 담을 버퍼의 주소!
struct epoll_event userevent; // 등록하기 위한 변수!
int epfd, event_cnt;
1. socket();
2. bind();
3. listen();
// 여기서부터 epoll 시작
epfd = epoll_create(EPOLL_SIZE); // epoll 인스턴스 생성
ep_events_buf = malloc(sizeof(struct epoll_event)* EPOLL_SIZE); // 버퍼 동적할당
userevent.events = EPOLLIN; // 수신관련한 이벤트를 보고싶다.
userevent.data.fd = serv_sock; // 서버 fd를 등록할 것이다.
epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &userevent); // ADD로 등록, 서버fd, 원하는 이벤트
while(1)
{
// 무한 대기하며 epfd중에 변화가 있는 fd 확인. 확인 후 event 버퍼에 넣음.
event_cnt = epoll_wait(epfd, ep_events_buf, EPOLL_SIZE, -1);
if(event_cnt ==-1)
{
puts("wait() error!\n");
break;
}
// 확인된 변화의 갯수만큼 for문
for(i=0; i<event_cnt; i++)
{
// 만약 이벤트가 생겼고 그 fd가 서버라면 -> 연결요청임!
if(ep_events_buf[i].data.fd == serv_sock)
{
addr_sz = sizeof(clnt_addr);
clnt_sock = accept(serv_sock, (struct sockaddr*) &clnt_addr, &addr_sz) // 이때 accept!!
// 클라이언트의 fd를 등록해주는 과정
userevent.events=EPOLLIN;
userevent.data.fd = clnt_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &userevent);
printf("connected client : %d\n",clnt_sock);
}
// 서버가 아니라면 -> 클라이언트의 변화
else
{
str_len = read(ep_events_buf[i].data.fd, buf, BUF_SIZE); // 디스크럽터 접근방법 집중!
if( str_len ==0) // close socket
{
epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events_buf[i], NULL);
close(ep_events_buf[i].data.fd);
printf("Close client : %d\n", ep_events[i].data.fd);
}
else
{
write(ep_events_buf[i].data.fd, buf, str_len);
//정상 수신되면 원하는 작업들~~
}
}
}
}
close(serv_sock);
close(epfd);
return 0;
}
'Programming > Network(C++)' 카테고리의 다른 글
[Network][Thread] 멀티스레드(1)_스레드의 이해 (0) | 2020.12.20 |
---|---|
[Network][TCP/IP] 멀티 플렉싱(5)_레벨, 엣지 트리거(Only Linux) (0) | 2020.12.19 |
[Network][TCP/IP] 멀티플렉싱(3)_select 함수로 구현(Window) (0) | 2020.12.19 |
[Network][TCP/IP] 멀티플렉싱(2)_select 함수로 구현 (Linux) (3) | 2020.12.18 |