소켓
네트워크 상에 연결되어 있는 컴퓨터 간에 데이터를 주고받기 위해 필요한 추상화된 장치이다.
유저 공간에 존재하는 프로세스(서버,클라이언트)는 커널 공간에 의해 생성된 소켓을 통해 데이터를 송수신 할 수 있다
소켓은 로컬 IP주소와 Port번호, 원격 IP주소와 Port 번호, 그리고 수신버퍼와 송신버퍼가 존재한다.
소켓 연결과 생성과정
- 서버
- 클라이언트로부터의 연결요청도 하나의 데이터 전송이다. 따라서 연결 요청을 받아들이기 위해 하나의 소켓이 필요하고 이 소켓을 가리켜
서버소켓
또는 리스닝 소켓
이라고 한다. listen 함수의 호출은 소켓을 리스닝 소켓으로 만든다.
- accept 함수의 결과로 서버소켓을 통해 클라이언트로부터 연결요청을 받으면, 연결요청 정보를 참조하여 클라이언트 소켓과의 통신을 위한 별도의 소켓을 하나 더 추가로 생성한다. 그리고 이렇게 생성된 소켓을 대상으로 데이터의 송수신이 진행된다.
- 클라이언트
- 소켓을 생성하고 연결 요청을 위해서 connect 함수를 호출하는 것이 전부이다
- 서버의 listen 함수 호출 이후에야 connect 함수 호출이 유효하다
서버 소켓의 역활
클라이언트에서 서버로 요청이 들어오면 서버는 클라이언트별로 소켓을 생성하여 요청을 처리한다(1대1 연결)
C언어로 구현한 간단한 서버&클라이언트 구조
echo-server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUFFER_SIZE 1024
void error_handling(char *message);
int main(int argc, char*argv[])
{
int serv_sock;
int clnt_sock;
struct sockaddr_in serv_addr;
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size;
char message[BUFFER_SIZE];
int str_len;
if(argc != 2)
{
printf("Usage : %s <port> \n",argv[0]);
exit(1);
}
// 1. 소켓을 생성한다
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if(serv_sock == -1)
error_handling("socket() error");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET; //
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
// 2. 소켓에 IP와 Port 번호를 할당한다
if(bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("bind() error");
// 3. 서버 소켓을 연결요청 가능 상태로 변경한다 (socket -> listening socket)
// 5개의 연결요청 대기큐를 생성하여 클라이언트의 요청을 하나씩 처리한다
if(listen(serv_sock, 5) == -1)
error_handling("listen() error");
clnt_addr_size = sizeof(clnt_addr);
for(int i=0; i<5; i++)
{
// 4. 클라이언트의 접속 요청을 수락한다 (클라이언트와 연결된 새로운 socket이 생성된다)
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
if(clnt_sock == -1)
error_handling("accept() error");
else
printf("Connected client %d \n", i+1);
// 5. 클라이언트와 송수신 한다
while ((str_len=read(clnt_sock, message, BUFFER_SIZE)) != 0)
write(clnt_sock, message, str_len);
// 6. 송수신이 끝난 클라이언트 소켓을 닫는다
close(clnt_sock);
}
// 7. 서버 소켓을 닫는다
close(serv_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc("\n", stderr);
exit(1);
}
echo_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void error_handling(char *message);
int main(int argc, char *argv[]) {
// 파일 디스크립터를 위한 변수
int sock;
char message[BUF_SIZE];
int str_len;
struct sockaddr_in serv_adr;
if (argc != 3) {
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
// 1. socket 하나를 생성한다.
sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock == -1)
error_handling("socket() error");
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
serv_adr.sin_port = htons(atoi(argv[2]));
// 2. socket을 이용해 server의 server socket(listen socket)에 연결을 요청한다.
if (connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
error_handling("connect() error!");
else
puts("Connected...........");
while(1) {
fputs("Input message(Q to quit): ", stdout);
fgets(message, BUF_SIZE, stdin);
if (!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
break;
// 3. 연결된 socket을 통해 server로부터 데이터를 송수신한다.
write(sock, message, strlen(message));
str_len = read(sock, message, BUF_SIZE-1);
message[str_len] = 0;
printf("Message from server: %s", message);
}
close(sock);
return 0;
}
void error_handling(char *message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
문제점
- for문을 이용하여 클라이언트의 요청을 처리하는 위 코드는 동시에 여러 명의 클라이언트의 요청을 처리할 수없고 하나의 클라이언트와 연결이 끊어질 때까지 다른 클라이언트들은 기다려야 하는 문제가 발생한다.
해결방안
위의 문제점을 해결하기 위한 해결방안으로 크게 3가지가 있다
- 멀티프로세스 기반 서버 - 클라이언트의 요청마다 프로세스를 생성하여 처리한다
- 멀티플렉싱 기반 서버 - 입출력 대상을 묶어서 관리하는 방식으로 처리한다
- 멀티스레드 기반 서버 - 클라이언트의 요청마다 스레드를 생성하여 처리한다
멀티 프로세스 기반 서버
멀티프로세스 기반의 서버는 클라이언트의 요청마다 프로세스를 생성하는 방식으로 서비스를 제공한다
- 부모 프로세스는
리스닝 소켓
으로 accept 함수를 통해 연결요청을 수락한다
- 이때 얻게 되는 소켓의 파일 디스크립터를 자식 프로세스를 생성해서 넘겨준다
- 자식 프로세스는 전달받은 파일 디스크립터를 바탕으로 서비스를 제공한다
echo_multi_process_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char *message);
void read_childproc(int sig);
int main(int argc, char *argv[]) {
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
pid_t pid;
struct sigaction act;
socklen_t adr_sz;
int str_len, state;
char buf[BUF_SIZE];
if (argc != 2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
act.sa_handler = read_childproc;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
state = sigaction(SIGCHLD, &act, 0);
// 1. socket 하나를 생성한다.
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]));
// 2. socket에 IP와 Port 번호를 할당한다.
if (bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr)) == -1)
error_handling("bind() error");
// 3. 생성한 socket을 server socket(listen socket)으로 등록한다.
if (listen(serv_sock, 5) == -1)
error_handling("listen() error");
while(1) {
adr_sz = sizeof(clnt_adr);
// 4. 부모 프로세스는 리스닝 소켓으로 accept 함수 호출을 통해서 연결요청을 수락한다.
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
if (clnt_sock == -1)
continue;
else
puts("new client connected...");
// 5. 이때 얻게 되는 소켓의 파일 디스크립터(클라이언트와 연결된 연결 소켓)를 자식 프로세스를 생성해 넘겨준다.
pid = fork();
if (pid == -1) {
close(clnt_sock);
continue;
}
if (pid == 0) {
close(serv_sock);
// 6. 자식 프로세스는 전달받은 파일 디스크립터를 바탕으로 서비스를 제공한다.
while((str_len = read(clnt_sock, buf, BUF_SIZE)) != 0)
write(clnt_sock, buf, str_len);
close(clnt_sock);
puts("client disconnected...");
return 0;
}
else
close(clnt_sock);
}
close(serv_sock);
return 0;
}
void read_childproc(int sig) {
pid_t pid;
int status;
pid = waitpid(-1, &status, WNOHANG);
printf("removed proc id: %d \n", pid);
}
void error_handling(char *message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
- 장점
- 프로그램의 흐름이 단순하기 때문에 이해하기 쉽다
- 안정적인 실행이 가능하다. 독립적인 메모리 공간을 가지므로 다른 프로세스끼리 영향을 끼치지 않는다
- 단점
- 프로세스의 생성에 많은 비용이 발생한다
- fork()를 이용하여 부모 프로세스를 복사하므로 한 소켓에 대한 파일 디스크립터도 복사된다. 따라서 하나의 소켓에 두 개의 파일 디스크립터가 존재하므로 두 개의 파일 디스크립터를 종료해야 해당 소켓을 제거할 수 있다
- 서로 다른 독립적인 메모리 공간을 가지므로 프로세스간 정보 교환이 어렵다
멀티 스레드 기반 서버
멀티스레드 기반의 서버는 클라이언트의 요청마다 스레드를 생성하여 서비스를 제공한다
- 메인 스레드는
리스닝 소켓
으로 accept 함수를 통해서 연결요청을 수락한다
- 이때 얻게 되는 소켓의 파일 디스크립터를 워커 스레드를 생성하여 넘겨준다
- 워커 스레드는 전달받은 파일 디스크립터를 바탕으로 서비스를 제공한다
echo_multi_thread_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <pthread.h>
#define BUF_SIZE 30
void * handle_clnt(void * arg);
void error_handling(char * msg);
int main(int argc, char *argv[]) {
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
pthread_t t_id;
socklen_t adr_sz;
int str_len, state;
char buf[BUF_SIZE];
if (argc != 2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
// 1. socket 하나를 생성한다.
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]));
// 2. socket에 IP와 Port 번호를 할당한다.
if (bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr)) == -1)
error_handling("bind() error");
// 3. 생성한 socket을 server socket(listen socket)으로 등록한다.
if (listen(serv_sock, 5) == -1)
error_handling("listen() error");
while(1) {
adr_sz = sizeof(clnt_adr);
// 4. 메인 스레드는 리스닝 소켓으로 accept 함수 호출을 통해서 연결요청을 수락한다.
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
if (clnt_sock == -1)
continue;
puts("new client connected...");
// 5. 클라이언트와 연결된 소켓의 파일 디스크립터를 워커 스레드를 생성해 넘겨준다.
pthread_create(&t_id, NULL, handle_clnt, (void*)&clnt_sock);
pthread_detach(t_id);
}
close(serv_sock);
return 0;
}
void * handle_clnt(void * arg) {
int clnt_sock=*((int*)arg);
int str_len=0, i;
char buf[BUF_SIZE];
// 6. 워커 스레드는 전달받은 파일 디스크립터를 바탕으로 서비스를 제공한다.
while((str_len = read(clnt_sock, buf, BUF_SIZE)) != 0)
write(clnt_sock, buf, str_len);
close(clnt_sock);
return NULL;
}
void error_handling(char * msg) {
fputs(msg, stderr);
fputc('\n', stderr);
exit(1);
}
- 장점
- 프로세스를 생성하는 비용보다 스레드를 생성하는 비용이 적다
- 스레드간 공유한느 메모리를 갖기 때문에, 스레드간 정보 교환이 쉽다
- 단점
- 하나의 프로세스에 다수의 스레드가 존재하므로 하나의 스레드에 문제가 생긴다면 프로세스에 영향을 미쳐 의도치 않은 문제가 발생할 수 있다
멀티 플렉싱 기반 서버
멀티 플렉싱 기반 서버는 입출력 대상을 묶어서 관리하는 방식으로 처리한다.
입출력 다중화
하나의 프로세스 혹은 스레드가 입력과 출력을 모두 다루는 기술을 말한다. 커널에서는 하나의 스레드가 여러 개의 소켓을 핸들링 할 수 있는 select
, poll
, epoll
과 같은 시스템 콜을 제공한다
한 개의 프로세스 혹은 스레드가 하나의 클라이언트에 대한 입출력만 처리할 수 있었던 이유는 입출력 함수가 blocked 되기 때문이다. 입출력 데이터가 준비될 때까지 무한정 봉쇄되어 여러 클라이언트가 입출력을 처리할 수 없기 때문이다.
그러나 멀티플렉싱 기법을 사용하면 여전히 입출력 함수는 blocked되지만, 입출력 함수를 호출하기 전에 어떤 소켓에서 입출력이 준비되었는지 알 수 있다.
멀티 스레드 기반 서버와 차이점
- 각 클라이언트 마다 별도의 스레드를 생성하는 것이 아닌 하나의 스레드에서 다수의 클라이언트에 연결된 소켓(파일 디스크립터)을 괸리하고 소켓에 이벤트(read/write)가 발생할 경우에만 별도의 스레드를 만들어 해당 이벤트를 처리하도록 구현할 수 있다
1. select
fd_set이라는 구조체로 파일 디스크립터들을 묶어서 관리하며 이벤트(입력,출력,에러)가 발생한 경우
이벤트가 발생한 파일 디스크립터 갯수를 리턴한다
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
- nfds: 검사 대상이 되는 파일 디스크립터의 수
- readfs: 읽기 이벤트를 검사할 파일 디스크립터의 목록
- writefds: 쓰기 이벤트를 검사할 파일 디스크립터의 목록
- exceptfds: 예외 이벤트를 검사할 파일 디스크립터의 목록
- timeout: 이벤트를 기다릴 시간 제한
- 반환 값: 이벤트가 발생한 파일의 갯수
echo_select_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/select.h>
#define BUF_SIZE 100
void error_handling(char *buf);
int main(int argc, char *argv[]) {
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
struct timeval timeout;
// 파일 상태 테이블 선언
fd_set reads, cpy_reads;
socklen_t adr_sz;
int fd_max, str_len, fd_num, i;
char buf[BUF_SIZE];
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");
FD_ZERO(&reads); // fd_set 테이블을 초기화한다.
FD_SET(serv_sock, &reads); // 서버 소켓(리스닝 소켓)의 이벤트 검사를 위해 fd_set 테이블에 추가한다.
fd_max = serv_sock;
while(1) {
cpy_reads = reads;
timeout.tv_sec = 5;
timeout.tv_usec = 5000;
// result
// -1: 오류 발생
// 0: 타임 아웃
// 1 이상 : 등록된 파일 디스크립터에 해당 이벤트가 발생하면 이벤트가 발생한 파일 디스크립터의 수를 반환한다.
if ((fd_num = select(fd_max+1, &cpy_reads, 0, 0, &timeout)) == -1)
break;
if (fd_num == 0)
continue;
for (i=0; i<fd_max+1; i++) {
if (FD_ISSET(i, &cpy_reads)) { // fd_set 테이블을 검사한다.
// 서버 소켓(리스닝 소켓)에 이벤트(연결 요청) 발생
if (i == serv_sock) { // connection request!
adr_sz = sizeof(clnt_adr);
clnt_sock= accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
FD_SET(clnt_sock, &reads); // fd_set 테이블에 클라이언트 소켓 디스크립터를 추가한다.
if (fd_max < clnt_sock)
fd_max = clnt_sock;
printf("connected client: %d \n", clnt_sock);
}
// 클라이언트와 연결된 소켓에 이벤트 발생
else { // read message!
str_len = read(i, buf, BUF_SIZE);
if (str_len == 0) { // close request!
FD_CLR(i, &reads); // fd_set 테이블에서 파일 디스크립터를 삭제한다.
close(i);
printf("closed client: %d \n", i);
} else {
write(i, buf, str_len); // echo!
}
}
}
}
}
close(serv_sock);
return 0;
}
void error_handling(char *buf) {
fputs(buf, stderr);
fputc('\n', stderr);
exit(1);
}
- 장점
- 단일 프로세스(스레드)에서 여러 파일의 입출력 처리가 가능하다
- 대부분 OS를 지원하여 이식성이 좋다 (POSIX 기준)
- 단점
- select 함수를 호출할 때 마다 관찰대상에 대한 정보를 운영체제에 전달해야 한다 -> 비용이 크다
- 어떤 파일 디스크립터에서 이벤트가 발생했는지 알 수 없으므로 fd_set에 존재하는 모든 파일 디스크립터를 검사해야 한다
- fd 개수에 제한이 있다 (1,024개)
- select 호출 때마다 데이터를 복사해야 한다 (select 함수를 호출한 후 이벤트를 처리할 때 fd_set 테이블 변경이 필요하기 때문에 미리 복사가 필요하다)
POSIX란?
POSIX(Portable Operating System Interface)는 이식 가능 운영 체제 인터페이스의 약자로, 서로 다른 UNIX OS의 공통 API를 정리하여 이식성이 높은 유닉스 응용 프로그램을 개발하기 위한 목적으로 IEEE가 책정한 애플리케이션 인터페이스 규격입니다.
2. epoll
select 함수의 단점을 보완하여 처음에 커널에 관찰대상에 대한 정보를 알려주고, 관찰 대상의 범위 또는 내용에 변경이 있을때만 변경사항을 알려주는 방식
리눅스에서는 epoll
, 윈도우에서는 IOCP
, FreeBSD(맥)에서는 Kqueue
로 운영체제별로 구현체가 다르다.
int epoll_create(int size); //size는 epoll_fd의 크기정보를 전달한다.
// 반환 값 : 실패 시 -1, 일반적으로 epoll_fd의 값을 리턴
int epoll_ctl(int epoll_fd, // epoll_fd
int operate_enum, // 어떤 변경을 할지 결정하는 enum값 int enroll_fd, // 등록할 fd
struct epoll_event* event // 관찰 대상의 관찰 이벤트 유형 ); // 반환 값 : 실패 시 -1, 성공시 0
int epoll_wait(int epoll_fd, // epoll_fd struct epoll_event* event, // event 버퍼의 주소 int maxevents, // 버퍼에 들어갈 수 있는 구조체 최대 개수 int timeout // select의 timeout과 동일 단위는 1/1000
);
// 성공시 이벤트 발생한 파일 디스크립터 개수 반환, 실패시 -1 반환
epoll_create : epoll 파일 디스크립터 저장소 생성
epoll_ctl : 저장소에 파일 디스크립터 등록 및 삭제
epoll_wait : select 함수와 마찬가지로 파일 디스크립터의 변화를 대기한다.
epoll_create를 통해 생성된 epoll 인스턴스에 관찰대상을 저장 및 삭제하는 함수가 epoll_ctl이고, epoll 인스턴스에 등록된 파일 디스크립터를 대상으로 이벤트의 발생 유무를 확인하는 함수가 epoll_wait이다.
echo_epoll.server.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
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");
// 커널이 관리하는 epoll 인스턴스라 불리는 파일 디스크립터의 저장소 생성
// 성공 시 epoll 파일 디스크립터, 실패시 -1 반환
epfd = epoll_create(EPOLL_SIZE);
ep_events = malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
event.events = EPOLLIN;
event.data.fd = serv_sock;
// 파일 디스크립터(serv_sock)를 epoll 인스턴스에 등록한다. (관찰대상의 관찰 이벤트 유형은 EPOLLIN)
epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);
while(1) {
// 성공 시 이벤트가 발생한 파일 디스크립터이의 수, 실패 시 -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;
// 파일 디스크립터(clnt_sock)를 epoll 인스턴스에 등록한다. (관찰대상의 관찰 이벤트 유형은 EPOLLIN)
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);
}
- 장점
- select 방식과는 달리 이벤트가 발생한 경우 전체 파일디스크립터를 검사할 필요가 없다
- select 함수에 대응하는 epoll_wait 함수 호출시, 커널에서 상태정보를 유지하기 때문에 관찰대상의 정보를 매번 전달할 필요가 없다
- 단점
- 운영체제에 따라 구현체가 다르므로 이식성이 좋지 않다
ref. 멀티 플렉싱 서버로 가기까지
ref. 열혈 TCP/IP 프로그래밍