Notice
Recent Posts
Recent Comments
Today
Total
04-27 17:32
Archives
관리 메뉴

Jeongchul Kim

BSD Socket - server & multi-client file transfer 본문

Linux

BSD Socket - server & multi-client file transfer

김 정출 2016. 11. 22. 02:29

BSD Socket

Project

프로젝트 개요

서버와 멀티 클라이언트 모델을 구현하고, 파일 입출력을 이용해 다중 파일을 주고 받는다.

pthread를 통해 쓰레드를 구현하며, 비동기 입출력과 입출력 다중화를 epoll과 select를 통해 구현한다.


클라이언트와 서버의 통신 구조

서버와 멀티 클라이언트가 연결되며, 클라이언트의 Thread를 통해 파일 입출력과 서버와의 송수신을 진행한다.

서버 동작

1. 시작 시간 기록

int duration;

struct timeval before, after;

gettimeofday(&before, NULL);


2. 연결 대기(bind-listen)

memset(&hints, 0, sizeof(struct addrinfo));

hints.ai_family = AF_UNSPEC;

hints.ai_socktype = SOCK_STREAM;

hints.ai_flags = AI_PASSIVE;

s = getaddrinfo(NULL, port , &hints, &result);

if(s != 0) {

perror("getaddrinfo");

goto leave;

}

for(rp = result; rp != NULL; rp = rp->ai_next) {

listenSock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);

if(listenSock == -1)

continue;

             

s = bind(listenSock, rp->ai_addr, rp->ai_addrlen);

if(s == 0)

break;

             

close(listenSock);

}

if(rp == NULL) {

perror( "Not Bind");

goto leave;

}

freeaddrinfo(result);

s = setNonBlocking(listenSock);

if(s == -1) {

perror("Non Blocking");

abort();

}

if ((listen(listenSock, SOMAXCONN)) < 0 ) {

perror("listen");

goto error;

}                                 




3. 모든 클라이언트와 연결될 때까지 대기

// accept

while(1) {

char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];

struct sockaddr in_addr;

socklen_t in_len;

in_len = sizeof in_addr;

if((acceptedSock = accept(listenSock, &in_addr, &in_len)) < 0) {

if((errno == EAGAIN) || (errno == EWOULDBLOCK))

break;

else{

perror("accept");

break;

}

}

s = getnameinfo (&in_addr, in_len, hbuf, sizeof hbuf, ... ,NI_NUMERICHOST | NI_NUMERICSERV);

if(s == 0)

printf("Accepted connection on desriptor %d host=%s, port=%s\n",acceptedSock,hbuf,sbuf);

clientSocket[num_client++] = acceptedSock;

setNonBlocking(acceptedSock); // set non-block mode

ev.events = EPOLLIN | EPOLLET;

ev.data.fd = acceptedSock;

if(epoll_ctl(epollfd, EPOLL_CTL_ADD, acceptedSock, &ev) == -1) {

perror("epoll_ctl :: accpted socket");

goto error;

}

if(num_client == MAX_CLIENT) {

if(boolCheckDollar) {

for(j = 0; j < num_client; j++) {

send(clientSocket[j], "$", 2, 0);

printf("send client %d $\n",j);

}

boolCheckDollar = 0;

}

}

}

   





4. ‘$’를 모든 클라이언트에 송신

if(num_client == MAX_CLIENT) {

if(boolCheckDollar) {

send(clientSocket[j], "$", 2, 0);

      printf("send client %d $\n",j);

  }

  boolCheckDollar = 0;

}


5. 모든 클라이언트로부터 ‘@’를 수신할 때까지 다음을 반복

5-1 각 클라이언트에서 수신된 메시지를 읽어 20개의 클라이언트에 모두 송신


for(j = 0; j < num_client; j++) {

recv(clientSocket[j], clientMsg, MAX_DATA, 0);

printf("%s",clientMsg);

int k;

for(k = 0; k < num_client; k++) {

send(clientSocket[k], clientMsg, strlen(clientMsg), 0);

}

if(!strcmp(clientMsg,"@")) {

printf("finish client %d\n,",j);

finishClient++;

}

}


6. 모든 클라이언트에 ‘%’를 송신

7. 모든 클라이언트와의 연결이 끊어진 것을 확인

8. 모든 연결 close


if(finishClient == MAX_CLIENT) {

for(j = 0; j < num_client; j++) {

send(clientSocket[j], "%", 2, 0);

printf("send client %d %\n",j);

close(clientSocket[j]);

}

}


9. 종료 시간 기록

gettimeofday(&after, NULL);

duration = (after.tv_sec - before.tv_sec) * 1000000 + (after.tv_usec - before.tv_usec);


10. 걸린 시간을 출력하고 종료.

printf("Processing time = %d.%06d sec\n", duration / 1000000, duration % 1000000);

클라이언트 동작

1. 서버에 연결

serverAddr.sin_family = AF_INET;

serverAddr.sin_addr.s_addr = inet_addr(IP);

serverAddr.sin_port = htons(PORT);

if ((ret = connect(clientSock, (struct sockaddr*)&serverAddr, sizeof(serverAddr)))) {

perror("connect");

goto leave1;

}else {

printf("[CLIENT] Connected to %s\n", inet_ntoa(*(struct in_addr *)&serverAddr.sin_addr));

send(clientSock,"!",2,0);

}


2. 송신할 파일, 수신 데이터 저장할 파일 open


void *clientRead(void *arg) // client file read and send

{  

  FILE *fin;

  char fileBuff[BUFF];

  char dirPath[PATH];

  char strFileNo[2];

  int fileNo;

 

  strcpy(dirPath,"/home/pi/workspace/adv-sys-programming/file_");

 

  fileNo = *((int*)arg);

 

  sprintf(strFileNo, "%d", fileNo);

  strcat(dirPath, strFileNo);

 

  if((fin = fopen(dirPath, "r")) == NULL) {

      perror(dirPath);

      exit(0);

  }


void *clientWrite(void *arg) // client file write and recv

{

  FILE *fout;

  char fileBuff[BUFF];

  char dirPath[PATH];

  char strFileNo[2];

  int fileNo;

  strcpy(dirPath,"/home/pi/workspace/adv-sys-programming/client-file/file_");

  fileNo = *((int*)arg);

  sprintf(strFileNo, "%d", fileNo);

  strcat(dirPath, strFileNo);

  if((fout = fopen(dirPath, "a")) == NULL) {

      perror(dirPath);

      exit(0);

  }




3. ‘$’ 수신을 기다림

if (FD_ISSET(clientSock, &rfds)) {

if((ret = recv(clientSock, rdata, BUFF, 0)) < 0 ) {

printf("Connection closed by remote host\n");

exit(1);

}

//printf("%s\n",rdata);

if(!strcmp(rdata,"$")) {

;

}

}


4. 송신 가능하다면

4.1 송신할 파일의 Text 데이터를 한 줄 읽어 송신

4.2 더 이상 보낼 데이터가 없다면 ‘@’를 송신

if(!strcmp(rdata,"$")) {

threadIdRead = pthread_create(&clientThread[0], NULL, clientRead, (void*)&fileNo);

if(threadIdRead < 0) {

perror("thread create error");

exit(1);

}

pthread_join(clientThread[0], (void**)&status);

printf("return thread %d %d\n", fileNo, status);

}else if(!strcmp(rdata,"%")){

printf("close socekt\n");

close(ret);

}else {

threadIdWrite = pthread_create(&clientThread[1],NULL, clientWrite, (void*)&fileNo);

if(threadIdWrite < 0) {

perror("thread create error");

exit(1);

}

pthread_join(clientThread[1], (void**)&status);

              //printf("return thread %d %d\n", fileNo, status);             

}




while(!feof(fin)) {

fgets(fileBuff, BUFF, fin);

      //printf("%s\n",fileBuff);

      send(sock, fileBuff, strlen(fileBuff), 0);

  }

send(sock, "@", 2, 0);

  printf("send @ to server\n");

  fclose(fin);

}


5. 수신 가능하다면

5.1 수신된 메시지를 읽어 수신 데이터 파일에 저장

5.2 수신된 데이터가 ‘%’를 수신하여 7번으로


recv(sock, fileBuff, BUFF, 0);

  //printf("%s",fileBuff);

  fputs(fileBuff, fout);

  fclose(fout);

}




6. 4.5를 반복

7. 서버와 연결 끊고, 파일을 닫고


else if(!strcmp(rdata,"%")){

printf("close socekt\n");

close(ret);

}


8. 종료

결과물


결론

서버와 클라이언트와의 종료가 정상적으로 작동되지 않았다.

백그라운드 실행 시 종료가 정상적으로 되지 않아 프로세스가 남게되어 문제가 생겼다.

반면 파일 입출력은 각기 달리 포그라운드로 실행하면 정상적으로 작동되었다.


Socket

What is socket?

> socket은 1982년 BSD(Berkeley Software Distribution) UNIX 4.1에서 처음 소개되었다.


> socket은 software로 작성된 통신 접속점으로, network application program은 socket을 통하여

통신망으로 data을 송수신하게 된다.


> internet에서 정보를 주고 받는 것은 매우 복잡한 mechanism에 기반한다. 이를 단순화 하기 위해서 OSI 7 계층을 만들고, internet application 개발에 활용하고자, socket을 구현하였다.

OSI 7 : https://ko.wikipedia.org/wiki/OSI_%EB%AA%A8%ED%98%95


> socket은 TCP/IP를 이용하는 창구 역할을 하며, application과 socket 사이의 interface를 socket interface라고 한다.


> socket은 인터넷 연결, 종료, data transfer, domain name translate, address translation 등과 관련된 주요 함수들을 제공한다.


> socket은 internet과 software의 endpoint 역할. 접점.


> PC에서는 TCP/IP가 수행되고 있으며, network driver는 LAN 카드와 같은 NIU(Network Interface Unit)를 구동하는 software를 말한다.


Socket Network Programming

> socket 함수를 이용하여, socket 객체를 만들어, data를 주고 받는 network programming이다.


> server / client model 개발을 진행해야 한다.

- server는 internet service를 제공한다.

- client는 service를 요청하기 위해 고객에게 제공되는 program


> network program은 communication으로 data를 교환한다. 이 때 사용하는 언어의 문법을 맞춰야하는데 application protocol이라고 한다. presentation layer 에서는 protocol로 HTTP, IRC, FTP가 있으며, Transport layer에서는 TCP, UDP를 선택해야 한다.


Socket Programming Process


1. socket 생성

소켓을 생성한다. software 간 communication 가능. socket은 kernel에서 관리하는 객체


2. binding

만들어진 socket은 연결이 되지 않은 상태이다. IP와 Port 번호를 제공해야 한다.


3. 연결 대기열 생성

client가 socket을 통해 접속 요청을 하면, 이 접속 요청은 연결 대기열로 들어간다. 대기열 buffer가 꽉찼다면 접속은 거부된다.


4. 연결 대기열에서 클라이언트 요청 가져오기

server는 연결 대기열에서 맨 앞에 있는 client 요청을 가져온다.


5. 클라이언트와 연결

연결이 이루어졌다면, 데이터 통신을 하고 데이터를 처리한다.


Server

socket 생성하기

#include <sys/types.h>

#include <sys/socket.h>

int socket(int domain, int type, int protocol);

int server_socketsd = socket(PF_INET,SOCK_STREAM,0);


> domain : socket이 사용될 주소 영역을 지정한다.

- PF_INET : IPv4 Internet Protocol

- PF_INET6 : IPv6 Internet Protocol

- PF_LOCAL : Local communication을 위한 UNIX Protocol

- PF_PACKET : Low level socket을 위한 interface
- TCP/IP는 AF_INET을 사용한다.

> type : communication에서 사용될 packet type을 지정해야 한다.

- SOCK_STREAM :  연결 지향

- SOCK_DGRAM : data gram 지향

> protocol : communication에 사용할 protocol을 지정하기 위해 사용

- type : SOCKET_STREAM / protocol : IPPROTO_TCP

- type : SOCK_DGRAM / protocol : IPPROTO_UDP

> Return

- file descriptor table의 index, -1은 error  


2. process를 socket에 Binding

socket 함수로 생성된 socket은 kernel에 존재하지만, 아직 process와 연결되지 않았다. bind()함수를 이용해 socket에 연결할 수 있으며, 이를 바인딩(binding)이라고 한다.  즉 bind는 process를 internet에서 유일한 개체로 인식시키기 위해서 IP와 Port를 할당한다.


#include <sys/types.h>

#include <sys/socket.h>

int bind(int sockfd, struct sockaddr *my_addr, socklen_t addrlen);

struct sockaddr_in serveraddr;

bzero(&serveraddr, sizeof(serveraddr));

serveraddr.sin_family = AF_INET;

serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);

serveraddr.sin_port = htons(atoi(argv[1]));

bind(server_sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));


bind는 server에서 사용한다. INADDR_ANY 는 0.0.0.0을 의미하며, 모든 internet address로 부터 기다리겠다는 의미이며,

htons의 parameter로 들어가는 숫자가 port  번호이다.



3. 연결 대기열 만들기

이제 client는 server에 연결할 수 있는 상태가 되었으며, 연결 요청 시 연결 대기열 queue에 들어간다.

process는 이 연결 대기열 queue에 있는 요청 중 가장 앞에 있는 요청을 꺼내와서, client와의 communication을 위한

connect socket을 만든다.


연결 대기열 queue는 buffer의 역할을 한다.



#include <sys/socket.h>

int listen(int socket, int backlog);


listen(server_socksd, 5);

> socket : 연결 대기열을 가질 socket이다. 이 socket은 연결 대기열 queue에 client 연결 요청이 있는지 검사한다.

> backlog : 연결 대기열 queue의 크기이다.


> Return

- 0 이면 성공 / -1 이면 실패



4. 연결 대기열 요청 가져오기

연결 대기열 queue에 있는 클라이언트의 연결 요청을 accept 함수로 가져 온다.


#include <sys/types.h>

#include <sys/socket.h>

int accept(int socket, struct sockaddr *addr, socklen_t *addrlen);

client_socksd = accept(server_sockfd, (struct sockaddr *)&clientaddr, &client_len);


> socket : listen할 socket으로 연결 대기열 queue에서 client 요청을 가져온다.

> addr : 가져온 client의 주소 정보를 넘긴다.

> addrlen : addr의 길이이다.


> Return : accept이 성공적으로 수행되면, 0 보다 큰 socket 지정 번호를 반환한다.

socket 지정 번호는 client 와 연결된 socket으로 connection socket이라고 부르며 client와 communication 한다.


server는 연결 대기열 queue에 client의 연결을 기다리는 listen socket 과 connection socket 이 분리되어 다르다는 점을 이해해야 한다.



5. Data 송수신

send

write와 동일하며, send() 함수를 통해 데이터 보내는 것이 아닌, 보내라고 kernel에게 넘긴다.

유저 영역의 data가 kernel로 복사된다. error의 경우는 socket이 없거나, buffer가 꽉 찼을 경우이다.


ssize_t send(int socket, const void *buffer, size_t length, int flags);


> socket : data를 보낼 socket

> buffer : 보낼 data

> length : buffer의 size


> Return : 복사된 byte 수를 보낸다.

> Error

- EAGAIN , EWOULDBLOCK : NONBLOCK 모드시 기다리지 않고 나온다.



recv

연결된 socket의 수신 도착한 buffer의 length를 가져온다. 반드시 return 값을 체크해야 한다.


ssize_t recv(int socket, void *buffer, size_t length, int flags);


> socket : 읽어 올 socket을 가르킨다.

> buffer : 도착한 data를 담을 buffer를 가르킨다.

> length : buffer의 크기


> flags

- MSG_PEEK : 읽어오고, kernel에서 buffer를 안 지운다.


> Return : 성공할 경우, 읽어들인 메세지의 길이를 반환한다.

Client

1. Socket 생성하기

server와 마찬가지로 socket을 생성한다.


2. Server에 연결 요청하기

server로의 연결 요청은 connect 함수를 이용한다.


#include <sys/types.h>

#include <sys/socket.h>

int  connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);

connect(server_sockfd, (struct sockaddr *)&serveraddr, client_len);


sockfd에 socket을 지정하고 sockaddr에 지정된 IP와 Port 번호로 연결을 시도한다.


>  Return

-  성공하면 0을 반환 / 실패하면 -1

3. Server와 data 통신

send, recv 사용



I/O 통지모델의 할아버지 select

select는 싱글쓰레드로 다중 I/O를 처리하는 멀티플렉싱 통지모델의 가장 대표적인 방법이다. 해당 파일 디스크립터가 I/O를 할 준비가 되었는지 알 수 있다면, 그 파일 디스크립터가 할당받은 커널Buffer에 데이터를 복사해주기만 하면된다. 이런 목적하에 통지모델은 파일디스크립터의 상황을 파악할 수 있게 하는 기능을 할 수 있어야한다. select는 많은 파일 디스크립터들을 한꺼번에 관찰하는 FD_SET 구조체를 사용하여 빠르고 간편하게 유저에게 파일 디스크립터의 상황을 알려준다.


FD_SET

FD_SET은 하나의 FD(파일 디스크립터)의 상태를 하나의 비트로 표현한다. 파일 디스크립터의 번호는 고유하기 때문에, 파일 디스크립터의 번호를 인덱스로하여 해당 비트가 어떤 값을 가지고 있느냐에 따라서 준비상황을 통지 받을 수 있는 것이다. 먼저 파일 디스크립터의 번호를 FD_SET에 등록하면 해당 비트의 값이 1로 저장된다. 그리고 I/O처리 준비가 되면 SELECT를 통해 해당 비트의 값을 갱신하고 프로세스는 변경된 값을 보고 커널 버퍼에 데이터를 복사하면 되는 것이다. 귀찮은 비트연산을 단순화하여 다음의 메크로를 제공한다.


FD_ZERO(fd_set* set);        //fdset을초기화
FD_SET(int fd, fd_set* set);  //fd를 set에 등록
FD_CLR(int fd, fd_set* set);  //fd를 set에서 삭제
FD_ISSET(int fd, fd_set* set);//fd가 준비되었는지 확인


select

select는 read/ write/ error 3가지 I/O에 대한 통지를 받는다. 또한 select에 timeout을 설정하여 대기시간을 설정할 수 있다. signature 는 다음과 같다.


int select( int maxfdNum, //파일 디스크립터의 관찰 범위 (0 ~ maxfdNum -1)
           fd_set *restrict readfds, //read I/O를 통지받을 FD_SET의 주소, 없으면 NULL
           fd_set *restrict writefds,//write I/O를 통지받을 FD_SET의 주소, 없으면 NULL
           fd_set *restrict errorfds,//error I/O를 통지받을 FD_SET의 주소, 없으면 NULL
           struct timeval *restrict timeout //null이면 변화가 있을 때까지 계속 Block,
                                            //아니면 주어진 시간만큼 대기후 timeout.
          );
//반환값 : 오류 발생시 -1, timeout에 의한 반환은 0, 정상 작동일때 변경된 파일 디스크립터 개수


FD의 개수가 계속해서 바뀔 수 있으므로, 전체 파일 디스크립터의 개수를 저장하는 변수가 필요하다. 그리고 인자로 넘긴 FD_SET의 값은 변경되므로, 관찰할 FD의 목록이 변하지 않는다면 select로 넘기는 FD_SET은 복사된 값을 넘기는 것이 현명하다. 서버에서 select를 실제 사용하는 예시는 다음과 같다.


...
struct timeval timeout;     //타임 아웃에 사용할 timeval 변수
fd_set reads, cpy_reads;    //read용 FD_SET과 그 사본을 저장할 변수
int fd_max = 0, fd_num = 0; //관찰 범위, 변경된 fd 개수
...
FD_ZERO(&reads);             //reads초기화
FD_SET(server_sock, &reads); //server_socket 등록
max_fd = server_socket;      //server_socket부터 관찰 범위에 추가

while(TRUE){
  cpy_reads = reads;   //FD_SET보존을 위한 복사
  timeout.tv_sec = 5;  //time out 값 설정
  timeout.tv_usec = 5000;

  fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout);  //FD_SET사본으로 select 호출

  if(fd_num == -1)
      break; //에러
  if(fd_num == 0)
      continue; //timeout

  for(int fd = 0; fd < fd_max + 1 ; ++fd)
  {
     if(FD_ISSET(fd, &cpy_reads)) //fd가 준비 완료
     {
        if(i == server_socket) //fd가 서버인 경우  
        {
           //accpet 처리 (FD_SET으로 등록할 것)
        }
        else //fd가 클라이언트 세션인 경우
        {
           //recv및 closesocket 처리 (FD_CLR로 삭제할 것)
        }      
     }
}
close(server_socket);
return 0;


길어보이지만 중요한 부분은 이미 모두 설명이 되었다. 클라이언트 접속/ 종료시 FD_SET을 관리하고, I/O 준비 완료된 fd는 해당 함수를 호출해주면 select를 충분히 사용할 수 있다고 생각한다.


select의 한계

select가 모든 fd를 순회하면서 recv()를 호출하는 방법보다는 훨씬 잘 구현된 멀티플렉싱인 것은 자명하다. 하지만 만들어진지 오래되다보니 그 한계점이 뚜렷하다. 동작 환경에 따라 다르지만 일반적으로 검사할 수 있는 fd개수가 최대 1024개로 제한된다. 그리고 관찰 영역에 포함되는 모든 파일 디스크립터에 대해서 순회하면서 한번씩 FD_ISSET으로 체크하는 것도 불필요한 체크인것 처럼 보인다. 실제로 상태가 변화된 fd의 목록을 넘겨준다면 더 빠르게 작동할 수 있지 않을까?

그리고 관찰 대상에 대한 정보인 FD_SET을 계속해서 select문을 통해서 운영체제에게 전달하는 것도 큰 부하를 일으킨다. 처음 FD_SET을 만들었을 때, 그리고 관찰 대상이 새로 추가되거나 삭제되는 경우에만 운영체제에게 데이터를 전달한다면 이 부하를 많이 줄일 수 있을 것이라고 생각한다.


select의 정체성

select를 사용해서 I/O의 상황을 알기 위해서는 프로세스가 커널에게 직접 상황 체크를 요청해야한다. 프로세스가 커널의 상황을 지속적으로 확인하고 그에 맞는 대응을 하는 형태로 구성되기 때문에 프로세스와 커널이 서로 동기화된 상태에서 정보를 주고 받는 형태로 볼 수 있다. 따라서 select의 통지형태를 동기형 통지방식이라 부를 수 있다.

그리고 select 그 자체는 I/O를 담당하지 않지만, 통지하는 함수의 호출방식이 timeout에 따라 non-blocking 또는 blocking 형태가 된다. timeout을 설정하지 않으면, 관찰 대상이 변경되지 않는 이상 반환되지 않으므로 blocking 함수가 되고, timeout이 설정되면 주어진 시간이 지나면 시간이 다되었다는 정보를 반환하므로 non-blocking 함수가 된다.

epoll



select의 대체자 epoll

epoll은 select의 단점을 보완하여 리눅스환경에서 사용할 수 있도록 만든 I/O 통지 기법이다. 전체 파일 디스크립터에 대한 반복문을 사용하지 않고, 커널에게 정보를 요청하는 함수(select 같은)를 호출할 때마다 전체 관찰 대상에 대한 정보를 넘기지도 않는다.

계속해서 정보를 넘기지 않기 위해서 관찰 대상인 fd들의 정보를 담은 저장소를 직접 운영체제가 담당한다. 운영체제에게 관찰대상의 저장소를 만들어달라고 요청하면 그 저장소에 해당하는 파일 디스크립터(이하 epoll_fd)를 리턴해준다. 관찰 영역이 변경되면(관찰대상 추가 삭제) epoll_fd를 통해 변경을 요청할 수 있다. 그리고 관찰 대상의 변경사항을 체크할때도 epoll_fd를 통해 확인을 한다. 따라서 전체 파일디스크립터를 순회하면서 FD_ISSET을 하는 문제는 더이상 발생하지 않는다.

계속해서 정보를 넘기지 않기 위해서 관찰 대상인 fd들의 정보를 담은 저장소를 직접 운영체제가 담당한다. 운영체제에게 관찰대상의 저장소를 만들어달라고 요청하면 그 저장소에 해당하는 파일 디스크립터(이하 epoll_fd)를 리턴해준다. 관찰 영역이 변경되면(관찰대상 추가 삭제) epoll_fd를 통해 변경을 요청할 수 있다. 그리고 관찰 대상의 변경사항을 체크할때도 epoll_fd를 통해 확인을 한다. 따라서 전체 파일디스크립터를 순회하면서 FD_ISSET을 하는 문제는 더이상 발생하지 않는다.


epoll

위의 동작을 코드상에 구현하려면 3가지 요청이 필요하다. 우선 epoll_fd를 만들어 주는 epoll_create 함수. 운영체제에 의해 만들어진 fd로 다른 fd와 같이 소멸시 close를 통한 반환이 필요하다.


int epoll_create(int size); //size는 epoll_fd의 크기정보를 전달한다.
//반환 값 : 실패 시 -1, 일반적으로 epoll_fd의 값을 리턴


관찰 대상이 되는 파일 디스크립터들을 등록, 삭제하는데 사용되는 epoll_ctl


int epoll_ctl(int epoll_fd,             //epoll_fd
             int operate_enum,         //어떤 변경을 할지 결정하는 enum값
             int enroll_fd,            //등록할 fd
             struct epoll_event* event //관찰 대상의 관찰 이벤트 유형
             );
//반환 값 : 실패 시 -1, 성공시 0


operate_enum 값은 EPOLL_CTL_ADD(새로운 fd를 등록). EPOLL_CTL_DEL (기존 fd 삭제), EPOLL_CTL_MOD (등록된 fd의 이벤트 발생상황을 변경) 으로 구성된다. 3번째가 조금 이해가 안될지도 모르지만 차차 알게 될 것이다. 문제는 앞으로 계속 사용될 epoll_event* 구조체의 정체이다.


struct epoll_event
{
 __uint32_t events;
 epoll_data_t data;
}

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

enum Events
{
  EPOLLIN,   //수신할 데이터가 있다.
  EPOLLOUT,  //송신 가능하다.
  EPOLLPRI,  //중요한 데이터(OOB)가 발생.
  EPOLLRDHUD,//연결 종료 or Half-close 발생
  EPOLLERR,  //에러 발생
  EPOLLET,   //엣지 트리거 방식으로 설정
  EPOLLONESHOT, //한번만 이벤트 받음
}


epoll_event는 파일 디스크립터와 event, 그리고 기타 정보를 묶어서 만든 구조체이다. 처음 fd를 설정하는 EPOLL_CTL_MOD에서도 epoll_event 구조체를 사용하여 초기화하며, select와 같은 역할을 하는 epoll_wait 함수에서도 비어있는 epoll_event의 배열을 넘겨서 반환값을 받는 구조체로 사용한다. 중간에 나오는 엣지 트리거에 대해서는 이후에 설명하도록 한다.

실제로 변경된 fd들의 집합을 요청하는 함수는 epoll_wait이다. select의 select와 같은 역할을 한다. 앞서 설명한 함수들은 이 함수를 위한 포석이다.


int epoll_wait( int epoll_fd,              //epoll_fd
               struct epoll_event* event, //event 버퍼의 주소
               int maxevents,             //버퍼에 들어갈 수 있는 구조체 최대 개수
               int timeout                //select의 timeout과 동일 단위는 1/1000
             );
//성공시 이벤트 발생한 파일 디스크립터 개수 반환, 실패시 -1 반환


두 번째 인자로 들어가는 포인터는 epoll_event 구조체의 배열을 넘긴다. 함수가 정상 반환시 배열에 이벤트가 발생한 fd와 이벤트의 종류가 묶여서 들어온다. 따라서 모든 fd에 대하여 순회하면서 체크할 필요가 없다. 이벤트가 있는 fd들이 배열에 담겨오고 그 개수를 알 수 있으니 꼭 필요한 event만 순회하면서 처리할 수 있다는 장점이 여기서 발생한다.


epoll 사용하기

epoll을 서버에서 사용한 실제 예


int epoll_fd = epoll_create(EPOLL_SIZE);
struct epoll_event* events = malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
struct epoll_event init_event;
init_event.events = EPOLLIN;
init_event.data.fd = server_socket;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_socket, &init_event);

while(TRUE)
{
  int event_count = epoll_wait(epoll_fd, events, EPOLL_SIZE, -1);
  if( event_count = -1 )
     break;
  for( int i = 0 ; i < event_count; ++i )
  {
     if(events[i].data.fd == server_socket) //서버 소켓에 이벤트
     {
        //accept 처리
        ...
        init_event.events = EPOLLIN;
        init_event.data.fd = new_client_socket;
        epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_client_socket, &init_event);
     }
     else //이벤트가 도착한 소켓들
     {
        //read, write, closesocket처리
     }
  }
}
closesocket(server_socket);
close(epoll_fd);
return 0;


epoll의 정체성

epoll은 select의 단점을 많이 개선한 형태의 통지방식이다. FD_SET을 운영체제가 직접 관리하는 것으로 많은 부분이 개선되었다. 하지만 그 본질적인 동작 구조는 select와 크게 다르지 않다. 프로세스가 커널에게 지속적으로 I/O 상황을 체크하여 동기화 하는 개념은 여전히 유효하다. 따라서 epoll의 통지모델 역시 동기형 통지모델이다.

그리고 timeout개념이 select와 동일한 방식으로 동작하기 때문에 timeout에 들어온 인자가 어떠냐에 따라 blocking이기도 하고 non-blocking이기도 하다. 따라서 epoll의 전체적인 개념모델은 select와 같다고 생각한다.


Comments