이 글은 윤성우의 TCP/IP 소켓 프로그래밍 책을 참고하였습니다.
순서
I. 레벨 트리거와 엣지 트리거 개념
II. 소켓의 Non-Blocking Mode
III. 엣지 트리거로 서버 구현
IV. 레벨 트리거와 엣지트리거의 비교
I. 레벨 트리거와 엣지 트리거 개념
epoll함수를 사용하기 위해서 꼭 이해해야 하는 개념이다.
두 차이는 다음과 같다.
레벨 트리거 : 입력 버퍼에 데이터가 남아있는 동안에 계속해서 이벤트가 새로 등록된다.
엣지 트리거 : 데이터가 수신된 상황에서 딱! 한 번만 이벤트로 등록된다.
만약 수신되는 버퍼의 크기가 작다고 하면
레벨트리거는 데이터를 모두 수신 못하므로 지속해서 이벤트가 등록된다.
엣지 트리거에서는 그럼에도 불구하고 단 한 번만 이벤트가 등록된다.
우리가 epoll 코드를 작성하게 된다면 기본적으로 레벨 트리거 방식으로 작동한다.
따라서 옵션을 추가해줘야하는데, 바로 이때 event 상수 중 EPOLLET이 사용되는 것이다.
따라서 userevent. events = EPOLLIN | EPOLLET; 로 정의해주게 된다.
엣지 트리거에는 문제점이 있다.
우리는 목표한 데이터가 다 올 때까지 read함수를 반복 호출해야 하고
readbuf가 비어있는 것을 확인하고 데이터가 다 왔다는 시그널을 날려야 한다.
하지만, read 함수는 기본적으로 readbuf가 비어있으면 무한 대기상태(Blocking) 상태를 가진다.
따라서 이 문제점을 해결하기 위해 소켓을 Non-Blocking 상태로 변경시켜주는 작업이 필요하다.
이를 다음 단락에서 확인해보자.
II. 소켓의 Non-blocking Mode
여기서는 두 가지를 설명할 것이다.
1. 변수 errno를 이용한 오류의 원인을 확인하는 방법
2. Non-blocking IO를 위한 소켓의 특성 변경하는 방법
2 같은 경우는 위에서 설명했고,
1의 경우에는 readbuf가 비어있을 때 어떤 에러를 처리해야 하는지 알아야만
이후 시그널을 보낼 수 있기 때문이다.
1. 변수 errno
일반적으로 리눅스에서는 -1을 오류의 반환 값으로 설정해둔다.
따라서 오류가 발생한 것은 알 수 있으나 정확히 어떤 원인인지는 모르기 때문에
리눅스에서는 int errno;라는 전역 변수를 이용해 추가적인 정보를 제공해준다.
이 변수는 <errno.h>를 헤더로 포함해야 한다.
그 이유는 이 헤더에 errno의 extern선언이 존재하기 때문이다.
우리가 알고자 하는 read함수에 대한 에러를 설명하면, 다음과 같다.
"read함수는 readbuf가 비어서 더 이상 읽을 데이터가 없으면 -1을 반환하고, errno변수에는 EAGAIN이 저장된다"
2. Non-blocking IO 소켓으로의 변경
리눅스에서 파일의 특성을 변경 및 참조하기 위해서는 다음 함수를 사용한다.
#include <fcntl.h> // 파일 컨트롤
int fcntl(int filedes, int cmd, . . .);
--> 성공 시 매개변수 cmd에 따른 값, 실패 시 -1 반환
ㄴ. filedes : 특성 변경의 대상이 되는 파일의 fd 전달
ㄴ. cmd : 함수호출의 목적에 해당하는 정보 전달.
Non-blocking으로의 변경방법
int flag = fcntl(fd, F_GETFL,0); // 기존에 설정되어 있던 특성정보를 가져옴
fcntl(fd, F_SETFL, flag | O_NONBLOCK); //넌 블로킹으로 특성 재 설정함.
인자2. int cmd
이 인자에는 현재 두 가지를 사용하였다.
F_GETFL : 첫 번째 인자로 전달된 fd에 설정되어 있는 특성정보를 int형으로 얻어옴.
F_SETFL : 첫 번째 인자로 전달된 fd에 3번째 가변 인자로 전달된 특성정보를 설정함.
Non-blocking 입출력을 의미하는 매크로 : O_NONBLOCK (아웃풋 넌-블럭이라는 뜻인 것 같다.)
III. 엣지 트리거로 서버 구현
앞선 레벨 트리거 코드에서 큰 차이점은 "*******" 주석처리된 부분인데,
이 부분이 엣지 트리거를 위한 구현이라고 보면 된다.
<hide/>
// 헤더 두 개만 추가로 더 선언하면 된다
#include <fcntl.h>
#include <errno.h>
// 버퍼가 4로 매우 작다
#define BUF_SIZE 4
#define EPOLL_SIZE 50
// 논블락킹으로 해주는 함수 만들었음.
void setnonblockingmode(int fd)
{
int flag = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flag|O_NONBLOCK);
}
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);
ep_events_buf = malloc(sizeof(struct epoll_event)* EPOLL_SIZE); // 버퍼 동적할당
setnonblockingmode(serv_sock); // 여기서 Non-blocking 모드 설정**************************************
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를 등록해주는 과정
// 여기서 소켓 설정 변경***************************************************************
setnonblockingmode(clnt_sock) // Non-blocking
userevent.events=EPOLLIN | EPOLLET; // EPOLLET설정해 엣지트리거설정
userevent.data.fd = clnt_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &userevent);
printf("connected client : %d\n",clnt_sock);
}
// 서버가 아니라면 -> 클라이언트의 변화
else
{
while(1) // 여기서 차이점이 생긴다 ****************************************************
{
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);
break;
}
// 만약 -1이고 errno를 통해 readbuf 빈거 확인했으면 다 받아서 break!!****************
else if(str_len<0) {
if(errno==EAGAIN) break;
}
else
{
write(ep_events_buf[i].data.fd, buf, str_len);
//정상 수신되면 원하는 작업들~~
}
}
}
}
}
close(serv_sock);
close(epfd);
return 0;
}
'Programming > Network(C++)' 카테고리의 다른 글
[Network][Thread] 멀티스레드(2)_스레드의 생성 및 실행(pthread)(Linux) (0) | 2020.12.20 |
---|---|
[Network][Thread] 멀티스레드(1)_스레드의 이해 (0) | 2020.12.20 |
[Network][TCP/IP] 멀티 플렉싱(4)_epoll 함수로 구현(Only Linux) (0) | 2020.12.19 |
[Network][TCP/IP] 멀티플렉싱(3)_select 함수로 구현(Window) (0) | 2020.12.19 |