实现目标

【1】创建TCP服务器和客户端,实现简易聊天程序;
【2】单一进程,通过I/O复用epoll函数实现;
【3】客户端/服务器任一结束,结束连接和对方进程。



epoll类函数

  epoll与select/poll是有本质上区别的,是为了处理大批量文件描述符而作了改进的poll,相比select/poll能够显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。epoll特点:

【1】监听文件描述符数目没有限制;
【2】无需遍历整个监听文件描述符集合,只需遍历那些被内核IO事件异步唤醒而加入ready队列的描述符集合;
【3】监听复杂度不随描述符增加(fd)而降低,即为O(1)。



函数原型

#include <sys/epoll.h>

int epoll_create(int size);
int epoll_create1(int flag);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events, int maxevents, int timeout,const sigset_t*sigmask);

  “epoll_create”用于创建一个 epoll 的描述符(句柄),形参“size”表示监听文件描述符最大数目。Linux2.6.8版本及更高以后,size值不需指定,大于0即可,因为内核可以动态的分配大小。

  “epoll_create”执行成功后,返回一个由系统分配的文件描述符(fd),所以在函数退出后,必须调用 “close”将该文件描述符关闭,否则一直占用系统文件描述符资源。



2)epoll_create1

“epoll_create”函数是在Linux 2.6.27中加入的,比较少用。



flag值

含义

0

与epoll_create功能一样

EPOLL_CLOEXEC

创建的epfd会设置FD_CLOEXEC模式

EPOLL_NONBLOCK

创建的epfd会设置为非阻塞



3)epoll_ctl

  “epoll_ctl”是epoll相关控制函数,可以用于注册、修改、删除监听事件。



参数

含义

epfd

epoll文件描述符,即epoll_create返回值

op

执行动作,如注册、修改、删除

fd

需监听的文件描述符

events

监听事件信息


(1)对于“op”参数,一般对应三个宏




含义

EPOLL_CTL_ADD

注册(添加)新的监听事件

EPOLL_CTL_MOD

修改已有监听事件

EPOLL_CTL_DEL

删除一个监听文件描述符


(2)“events”监听事件具体信息

struct epoll_event
{
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};

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

  对于“events”,我们需关心的是结构体中的“events”(具体事件),及“data”中的文件描述符“fd”。
(3)常用注册事件类型



事件

含义

可作为注册事件(events)

可作为返回事件(revents)

EPOLLIN

数据可读



EPOLLPRI

高优先级数据可读(比如TCP带外数据)



EPOLLOUT

数据可写



POLLWRNORM

普通数据可写



EPOLLRDHUP

TCP连接被对端关闭,或者关闭了写操作



EPOLLONESHOT

只监听一次事件,监听完这次事件后,如果还需要继续监听该事件,需要再次把这个s事件加入EPOLL队列



EPOLLLT

水平触发模式,默认模式



EPOLLET

边沿触发模式



EPOLLHUP

发生挂起



EPOLLERR

发生错误





4)epoll_wait

  “epoll_wait”等待事件发生。



参数

含义

epfd

epoll文件描述符,即epoll_create返回值

events

监听返回事件集合

maxevents

监听文件描述符数目,不能大于epoll_create 创建值

timeout

超时时间(毫秒),传入0立即返回;传入-1(INFTIM)为不超时阻塞



4)epoll_pwait

  “epoll_pwait”是Linux 2.6.19加入的,与“epoll_wait”不同的是,可以捕获额外的信号来唤醒epoll阻塞,sigmask表示要捕获的信号量。



工作模式

  epoll对文件描述符的操作有两种触发模式:LT(level trigger)和ET(edge trigger),LT模式是默认模式。

  LT模式: 有事件发生则报告,相同事件也会持续报告,直至系统清除;因此系统可以不用立即处理响应事件。

  ET模式: 只有事件发生变化时才会报告事件,相同的事件不会重复报告,因此,应用程序必须立即处理响应事件,否则导致事件“丢失”。

  从ET模块定义可以看出,ET模式可以减少了 epoll 事件被重复触发的次数,因此效率要比LT模式高。另外,epoll工作在 ET 模式的时候,必须使用非阻塞函数(如TCP非阻塞socket),避免因为一个文件描述符的阻塞操作而导致其他文件描述符事件无法被响应。



使用步骤

int  c_fd = -1, e_fd = -1,max_fd=-1
struct epoll_event e_event,wait_event;

e_fd = epoll_create(10);
e_event.data.fd = c_fd;
e_event.events = EPOLLIN | EPOLLRDHUP | EPOLLPRI;
epoll_ctl(e_fd, EPOLL_CTL_ADD, c_fd, &e_event);
max_fd = 1;
for(;;)
{
ret = epoll_wait(e_fd,&wait_event,max_fd+1,-1);
/* todo */
}

实现代码

服务器端(server)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/prctl.h>
#include <arpa/inet.h>
#include <errno.h>
#include <resolv.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <stdbool.h>
#include <sys/epoll.h>

int main (int argc, char * argv[])
{
int s_fd = -1, c_fd = -1, e_fd = -1,pid;
socklen_t addr_len;
struct sockaddr_in s_addr, c_addr;
char buf[1024];
ssize_t size = 0;
struct epoll_event e_event; /* 监听事件 */
struct epoll_event wait_event[10]; /* 监听事件结果 */
int max_fd;
int ret = 0;

if(argc != 3)
{
printf("format error!\n");
printf("usage: server <address> <port>\n");
exit(1);
}

/* 服务器端地址 */
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(atoi(argv[2]));
if(!inet_aton(argv[1], (struct in_addr *) &s_addr.sin_addr.s_addr))
{
perror("invalid ip addr:");
exit(1);
}

/* 创建socket */
if((s_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
perror("socket create failed:");
exit(1);
}

/* 端口重用,调用close(socket)一般不会立即关闭socket,而经历“TIME_WAIT”的过程 */
int reuse = 0x01;
if(setsockopt(s_fd, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse, sizeof(int)) < 0)
{
perror("setsockopt error");
close(s_fd);
exit(1);
}

/* 绑定地址 */
if(bind(s_fd, (struct sockaddr*)&s_addr, sizeof(s_addr)) < 0)
{
perror("bind error");
close(s_fd);
exit(1);
}

/* 监听socket */
if(listen(s_fd, 5) < 0)
{
perror("listen error");
close(s_fd);
exit(1);
}
addr_len = sizeof(struct sockaddr);

/* epoll 监听参数 */
e_fd = epoll_create(10); /* 最大监听10个事件 */
if(e_fd < 0)
{
perror("epoll create error");
close(s_fd);
exit(1);
}
e_event.data.fd = STDIN_FILENO;
e_event.events = EPOLLIN;
ret = epoll_ctl(e_fd, EPOLL_CTL_ADD, STDIN_FILENO, &e_event);
if(ret < 0)
{
perror("epoll_ctl error");
close(s_fd);
close(e_fd);
exit(1);
}

e_event.data.fd = s_fd;
e_event.events = EPOLLIN;
ret = epoll_ctl(e_fd, EPOLL_CTL_ADD, s_fd, &e_event);
if(ret < 0)
{
perror("epoll_ctl error");
close(s_fd);
close(e_fd);
exit(1);
}
max_fd = 2;

for(;;)
{
ret = epoll_wait(e_fd,wait_event,max_fd+1,-1); /* 阻塞 */

if(ret < 0)
{
perror("epoll_wait error");
break;
}
else if(ret == 0)
{
continue;
}

for(size = 0; size < ret;size++)
{
if(((wait_event[size].events & EPOLLIN) == EPOLLIN) && (wait_event[size].data.fd == s_fd))
{
if(c_fd>0)
{
continue;
}
printf("waiting client connecting\r\n");
c_fd = accept(s_fd, (struct sockaddr*)&c_addr, (socklen_t *)&addr_len);

if(c_fd < 0)
{
perror("accept error");
break;
}
else
{
printf("connected with ip: %s and port: %d\n", inet_ntop(AF_INET,&c_addr.sin_addr, buf, 1024), ntohs(c_addr.sin_port));
}
e_event.data.fd = c_fd;
e_event.events = EPOLLIN | EPOLLRDHUP | EPOLLPRI;
ret = epoll_ctl(e_fd, EPOLL_CTL_ADD, c_fd, &e_event);
if(ret < 0)
{
perror("epoll_ctl error");
close(s_fd);
break;
}

max_fd = 3;
}

if((wait_event[size].events & EPOLLIN) == EPOLLIN && wait_event[size].data.fd == STDIN_FILENO)
{
fflush(stdout);
memset(buf, 0, sizeof(buf));
size = read(STDIN_FILENO, buf, sizeof(buf) - 1);
if(size > 0)
{
buf[size-1] = '\0';
}
else
{
perror("read stdin error");
goto quit;
}
if(!strncmp(buf, "quit", 4))
{
printf("close the connect!\n");
goto quit;
}
if(buf[0] == '\0')
{
printf("please enter message to send:\n");
continue;
}
size = write(c_fd, buf, strlen(buf));
if(size <= 0)
{
printf("message'%s' send failed!errno code is %d,errno message is '%s'\n",buf, errno, strerror(errno));
goto quit;
}
printf("please enter message to send:\n");
}

if((wait_event[size].events & EPOLLRDHUP) == EPOLLRDHUP && wait_event[size].data.fd == c_fd)
{/* disconnect */
printf("client disconnect 0!\n");
goto quit;
}

if(((wait_event[size].events & EPOLLIN) == EPOLLIN && wait_event[size].data.fd == c_fd))
{
memset(buf, 0, sizeof(buf));
size = read(c_fd, buf, sizeof(buf) - 1);
if(size > 0)
{
printf("message recv %dByte: \n%s\n",size,buf);
}
else if(size < 0)
{
printf("recv failed!errno code is %d,errno message is '%s'\n",errno, strerror(errno));
goto quit;
}
else
{
printf("client disconnect 1!\n");
goto quit;
}
}
}
}
quit:
close(s_fd);
close(c_fd);
close(e_fd);

return 0;
}

客户端(client)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/un.h>
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
#include <signal.h>
#include <sys/prctl.h>
#include <sys/epoll.h>

int main (int argc, char * argv[])
{
int c_fd = -1,e_fd = -1,pid;
int ret = 0;
struct sockaddr_in s_addr;
socklen_t addr_len;
char buf[1024];
ssize_t size;
struct epoll_event e_event; /* 监听事件 */
struct epoll_event wait_event[10]; /* 监听事件结果 */
int max_fd; /* 监听文件描述符中最大的文件号 */

if(argc != 3)
{
printf("format error!\n");
printf("usage: client <address> <port>\n");
exit(1);
}

/* 创建socket */
c_fd = socket(AF_INET, SOCK_STREAM, 0);
if(c_fd < 0)
{
perror("socket create failed");
return -1;
}

/* 服务器端地址 */
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(atoi(argv[2]));
if(!inet_aton(argv[1], (struct in_addr *) &s_addr.sin_addr.s_addr))
{
perror("invalid ip addr");
exit(1);
}

/* 连接服务器*/
addr_len = sizeof(s_addr);
ret = connect(c_fd, (struct sockaddr*)&s_addr, addr_len);
if(ret < 0)
{
perror("connect server failed");
exit(1);
}

/* epoll 监听参数 */
e_fd = epoll_create(10); /* 最大监听10个事件 */
if(e_fd < 0)
{
perror("epoll create error");
exit(1);
}
e_event.data.fd = STDIN_FILENO;
e_event.events = EPOLLIN;
ret = epoll_ctl(e_fd, EPOLL_CTL_ADD, STDIN_FILENO, &e_event);
if(ret < 0)
{
perror("epoll_ctl error");
close(e_fd);
exit(1);
}

e_event.data.fd = c_fd;
e_event.events = EPOLLIN | EPOLLRDHUP | EPOLLPRI;
ret = epoll_ctl(e_fd, EPOLL_CTL_ADD, c_fd, &e_event);
if(ret < 0)
{
perror("epoll_ctl error");
close(e_fd);
exit(1);
}
max_fd = 2;

for(;;)
{
ret = epoll_wait(e_fd,wait_event,max_fd+1,-1); /* 阻塞 */

if(ret < 0)
{
perror("epoll_wait error");
break;
}
else if(ret == 0)
{
continue;
}
//printf("epoll out %d\n",ret);
for(size = 0; size < ret;size++)
{
if((wait_event[size].events & EPOLLIN) == EPOLLIN && wait_event[size].data.fd == STDIN_FILENO)
{
fflush(stdout);
memset(buf, 0, sizeof(buf));
size = read(STDIN_FILENO, buf, sizeof(buf) - 1);
if(size > 0)
{
buf[size-1] = '\0';
}
else
{
perror("read stdin error");
goto quit;
}
if(!strncmp(buf, "quit", 4))
{
printf("close the connect!\n");
goto quit;
}
if(buf[0] == '\0')
{
printf("please enter message to send:\n");
continue;
}
size = write(c_fd, buf, strlen(buf));
if(size <= 0)
{
printf("message'%s' send failed!errno code is %d,errno message is '%s'\n",buf, errno, strerror(errno));
goto quit;
}
printf("please enter message to send:\n");
}

if((wait_event[size].events & EPOLLRDHUP) == EPOLLRDHUP && wait_event[size].data.fd == c_fd)
{/* disconnect */
printf("server disconnect 0!\n");
goto quit;
}

if(((wait_event[size].events & EPOLLIN) == EPOLLIN && wait_event[size].data.fd == c_fd))
{
memset(buf, 0, sizeof(buf));
size = read(c_fd, buf, sizeof(buf) - 1);
if(size > 0)
{
printf("message recv %dByte: \n%s\n",size,buf);
}
else if(size < 0)
{
printf("recv failed!errno code is %d,errno message is '%s'\n",errno, strerror(errno));
goto quit;
}
else
{
printf("server disconnect 1!\n");
goto quit;
}
}
}
}

quit:
close(c_fd);
close(e_fd);
return 0;
}

执行效果
【Linux 网络编程】TCP epoll聊天程序_#include