应用程序通过I/O复用函数向内核注册一组I/O事件,内核通过I/O复用函数把就绪的事件通知给应用程序。常用的I/O复用函数是select、poll、epoll。I/O复用函数本身是阻塞的,因为它们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。
当多个文件描述符同时就绪时,如果不采取额外的措施,程序就只能按顺序依次处理其中的每-个文件描述符,这使得服务器程序看起来像是串行工作的。如果要实现并发,只能使用多进程或多线程等编程手段。
9.1 select系统调用
select实现多路复用的方式就是将已连接的socket都放到一个文件描述符集合,然后调用select函数将文件描述符集合拷贝到内核里,让内核来检查是否有网络事件产生,检查的方式就是遍历文件描述符集合,当检查到有事件发生后,将此socket标记为可读或可写,接着再把整个文件描述符集合拷贝到用户空间,然后在用户空间遍历这些文件描述符,找到可读或可写的socket,然后在对其进行处理。
因此,使用select需要进行2次遍历文件描述符集合,一次是在内核空间,一次是在用户空间,而且还会发生2次拷贝文件描述符集合,即先从用户空间拷贝到内核空间,由内核修改后,再拷贝到用户空间。
select使用固定长度的BitsMap,表示文件描述符集合,而且所支持的文件描述符的个数是有限制的,在Linux系统中,由内核的FD_SETSIZE限制,默认最大值为1024,只能监听0~1023的文件描述符。
9.1.1 fd_set结构体
select可以监视3中文件描述符集合,即:接收、传输、异常,每个文件描述符集合使用fd_set结构体存储。fd_set结构体使用一个bit位来描述文件描述符的状态,如果该位是1,则该文件描述符是监视对象。因此,可以认为fd_set是一个很大的字节数组(BitsMap)。fd_set能容纳的文件描述符的数量由FD_SETSIZE指定。
首先介绍下面4种对fd_set操作的宏:(fd是指待操作的文件描述符)
// 这里的fd是文件描述符
FD_ZERO(fd_set *set); // 将set清零
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是否在set中,不在则返回0
接下来,将介绍如何fd_set是如何存储fd的。
例:假设有文件描述符1、2、3,fd_set的长度只有1字节,即8bit,那么1个字节长的fd_set最大可以存放8个文件描述符(fd)。
//创建读文件描述符集合
fd_set set;
//先对set清零
FD_ZERO(&set);
int fd = 5;
//将fd加入set
FD_SET(fd, &set); 此时,set变成 0001 0000 (第5位是1)
在将fd = 1, fd = 2加入set, 执行FD_SET后,set变成 0001 0011
//执行select阻塞等待
select(5 + 1, &set, 0, 0, 0);
若任意fd发生可读事件,如fd=5发生可读事件,则set变成 0001 0000,也就是没有发生事件的fd=1,fd=2会被清零。因此,可以认为值仍为1的位置上的文件描述符发生了变化。此时的set会被拷贝到用户空间。
如果描述符fd=100怎么办呢?其实不难,fd_set并不是只有1个字节,可以有n个字节,可以拿n字节凑多个bit(也就是bitmap),如果fd是100,仍然可以执行 FD(100, &set),执行后,set的值为 1xxxxxxxxxxx............,其中“1”就是第99个bit(从0开始)。
9.1.2 select函数
#include <sys/select.h>
#include <sys/time.h>
int select(int maxfd, fd_set* readset, fd_set* writeset, fd_set* exceptset, const struct timeval* timeout);
参数解释:
maxfd:被监听的文件描述符总数。通常设为最大文件描述符编号加1。
readset:指向可读文件描述符集合,可为NULL
writeset:指向可写文件描述符集合,可为NULL
exceptset:指向异常文件描述符集合,可为NULL
timeout:用来设置select的超时时间。timeout成员都为0时,select立即返回;timeout=NULL,select一直阻塞,直到某个文件描述符就绪。
返回值:成功时返回就绪(可读、可写、异常)文件描述符的总数;如果在超时时间内没有任何文件描述符就绪,将返回0;失败时返回-1并设置errno;如果在select等待期间,程序即受到信号,则select立即返回-1并设置errno为EINTR。
struct timeval
{
long tv_sec; //秒数
long tv_usec; //微秒数
};
下面是用select实现客户端向服务端发送什么,客户端就会收到什么的功能:
server:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/select.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <string.h>
#define BUF_SIZE 100
void error_handing(char* buf)
{
fputs(buf, stderr);
fputc('\n', stderr);
exit(1);
}
int main()
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_addr, clnt_addr;
struct timeval timeout;
fd_set readset, cpy_readset;
socklen_t addr_sz;
int fd_max, str_len, fd_num, i;
char buf[BUF_SIZE] = {0};
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(12345);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if ( bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1 )
{
error_handing("bind() error");
}
if ( listen(serv_sock, 5) == -1 )
{
error_handing("listen() error");
}
FD_ZERO(&readset);
FD_SET(serv_sock, &readset);
fd_max = serv_sock;
while(1)
{
cpy_readset = readset;
timeout.tv_sec = 5;
timeout.tv_usec = 5000;
//出现错误,跳出循环
/*cpy_readset先被拷贝到内核,里面保存有发生事件的socket,然后再被拷贝出内核,供用户使用.
这也是为什么要执行cpy_readset = readset的原因
*/
if ((fd_num = select(fd_max+1, &cpy_readset, 0, 0, &timeout)) == -1)
break;
//没有socket可读或可写
if (fd_num == 0)
continue;
for(i = 0; i < fd_max + 1; i++)
{
if(FD_ISSET(i, &cpy_readset)) //i可读或可写
{
if(i == serv_sock) //表明监听socket检测到有连接请求
{
addr_sz = sizeof(clnt_addr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &addr_sz);
//将clnt_sock加入readset监听集合
FD_SET(clnt_sock, &readset);
//看看用不用更新fa_max
if(fd_max < clnt_sock)
fd_max = clnt_sock;
printf("connect client: %d \n", clnt_sock);
}
else{ //表明连接socket需要读或写
str_len = read(i, buf, BUF_SIZE);
if (str_len == 0) //客服端断开连接,需要将该客户端连接剔除
{
FD_CLR(i, &readset);
close(i);
printf("close client: %d \n", i);
}
else //向客户端发送数据
{
write(i, buf, str_len);
}
}
}
}
}
close(serv_sock);
return 0;
}
client:
#include <unistd.h>
#include <sys/socket.h>
#include <netdb.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <iostream>
using namespace std;
#define BUFFERSIZE 1024
int main()
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in servAddr;
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(12345);
servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
int ret = connect(sock, (struct sockaddr*)&servAddr, sizeof(servAddr));
if (ret == -1)
{
cout <<"connect error"<<endl;
}
while(1)
{
char sendbuff[BUFFERSIZE] = {0};
cout << "input send data:" <<endl;
cin >> sendbuff;
send(sock, sendbuff, sizeof(sendbuff), 0);
char recvbuff[BUFFERSIZE] = {0};
int ret = recv(sock, recvbuff, sizeof(recvbuff), 0);
if (ret)
{
cout<<"recv serv:"<<recvbuff<<endl;
}
else if (ret == 0)
{
//连接断开
cout << "link failure!" <<endl;
close(sock);
break;
}
}
return 0;
}
9.2 poll系统调用
poll系统调用和select类似,也是遍历文件描述符集合,检查其中是否有事件发生。
但是,poll不再使用BitsMap存储所关注的文件描述符,而是使用动态数组,以链表的形式来保存,所以poll不再受文件描述符个数限制,当然还会收到系统文件描述符限制。
poll和select的相同点就是,都使用线性结构存储进程关注的socket集合,因此都需要遍历文件描述符集合来找到可读或可写的socket,时间复杂度都为O(n),而且也需要再用户空间和内核空间之间拷贝文件描述符集合,这种方式随着并发数增加,性能的损耗会呈指数级增长。
#include <poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
参数解释:
fds:保存所有被监听的文件描述符及其对应的可读、可写和异常等事件
nfds:fds的大小。typedef unsigned long int nfds_t;
timeout:poll的超时值。单位是毫秒(1s = 1000ms)。当timeout为-1时,poll调用将永远阻塞,直到某个事件发生;当timeout为0时,poll调用将立即返回。
返回值:
poll系统调用的返回值与select相同:
poll成功时返回就绪(可读、可写和异常)文件描述符的总数。
如果在超时时间内没有任何文件描述符就绪。poll将返回0。
失败时返回-1并设置errno。如果在poll等待期间,程序接收到信号,则select立即返回-1,并设置errno为EINTR。
struct pollfd
{
int fd;//文件描述符
short events;//fd上需要监听的事件,可以按位或
short revents;//实际发生的事件,由内核填充
};
pollfd结构体的事件类型如下表所示:
poll工作过程总结下来就是:
- poll 将描述符和事件fds传给内核
- 内核实现轮询检测fds,时间效率相当O(n)
- 找到就绪描述符,内核将fds返回给用户空间
- 用户遍历fdsO(n),处理事件
下面使用poll实现服务端收到什么就向客户端发送什么的功能:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <poll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define BUF_SIZE 100
#define MAXFD 10
void error_handing(char* buf)
{
fputs(buf, stderr);
fputc('\n', stderr);
exit(1);
}
//将文件描述符fd及其事件信息加入到监听集合fds中
void add_fds(struct pollfd fds[], int fd, short events)
{
int i = 0;
for(; i < MAXFD; i++)
{
if (fds[i].fd == -1)
{
fds[i].fd = fd;
fds[i].events = events;
fds[i].revents = 0;
break;
}
}
}
//删除fds数组中文件描述符fd和事件信息
void del_fds(struct pollfd fds[], int fd)
{
int i = 0;
for(; i < MAXFD; i++)
{
if (fds[i].fd == fd)
{
fds[i].fd = -1;
fds[i].events = 0;
fds[i].revents = 0;
break;
}
}
}
//初始化fds数组
void init_fds(struct pollfd fds[])
{
int i = 0;
for (; i < MAXFD; i++)
{
fds[i].fd = -1;
fds[i].events = 0;
fds[i].revents = 0;
}
}
int main()
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_addr, clnt_addr;
fd_set readset, cpy_readset;
socklen_t addr_sz;
char buf[BUF_SIZE] = {0};
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
assert(serv_sock != -1);
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(12345);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if ( bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1 )
{
error_handing("bind() error");
}
if ( listen(serv_sock, 5) == -1 )
{
error_handing("listen() error");
}
struct pollfd fds[MAXFD]; //定义pollfd类型的结构体数组fds
init_fds(fds);
add_fds(fds, serv_sock, POLLIN); //将serv_sock加入fds
while(1)
{
int n = poll(fds, MAXFD, 5000);
if (n == -1) //失败
{
perror("poll error");
}
else if (n == 0) //超时
{
printf("time out\n");
}
else //有文件描述符就绪
{
int i;
for (i = 0; i < MAXFD; i++) //循环遍历监听集合
{
// printf("i = %d, revents = %d\n", i, fds[i].revents);
if (fds[i].fd == -1)
{
continue;
}
if (fds[i].revents & POLLIN) //写事件发生
{
/*
此时有两种情况,若fds[i].fd == serv_sock
说明监听队列中有连接待处理,使用accept建立一个连接;
否则,说明没有新连接产生,是有客户端发来了数据,直接使
用recv接收客端数据,并打印,就ok
*/
if (fds[i].fd == serv_sock)
{
//accept
struct sockaddr_in clnt_addr;
socklen_t len = sizeof(clnt_addr);
//接收一个套接字已建立的连接,得到连接套接字clnt_sock
int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &len);
if (clnt_sock < 0)
{
continue;
}
printf("accept clnt_sock = %d\n", clnt_sock);
add_fds(fds, clnt_sock, POLLIN);//将新的连接套接字加入fds数组
}
else
{
int str_len = read(fds[i].fd, buf, BUF_SIZE);//接受客户端发来的数据
if (str_len == 0)//说明客户端已经关闭
{
close(fds[i].fd);//先关闭文件描述符
del_fds(fds,fds[i].fd);//将此文件描述符在fds数组里删除
printf("one client over\n");
}
else
{
printf("recv(%d) = %s\n", fds[i].fd, buf);
write(fds[i].fd, buf, str_len);
}
}
}
}
}
}
close(serv_sock);
return 0;
}
client的实现可复用9.1.2 的client代码。
9.3 epoll系统调用
epoll通过两个方面,很好的解决了select/poll的问题。
(1)epoll在内核空间中使用红黑树来监控进程所有待检测的文件描述符(监听文件描述符和连接文件描述符),把需要监控的socket通过epoll_ctl()函数加入内核中的红黑树中(红黑树是个高效的数据结构,增删查一般时间复杂度是O(logn)),通过对这颗红黑树操作,就无需像select/poll那样每次操作时都要拷贝整个socket集合,这样就减少了内核空间和用户空间大量的数据拷贝和内存分配。
(2)epoll在内核空间中使用一个链表来保存就绪事件,当某个socket可读或可写时,通过回调函数内核会将其加入到这个就绪事件列表中,当用户调用epoll_wait()函数时,只会向用户空间返回发生事件的文件描述符的个数,不需要像select/poll那样遍历整个socket集合,大大提高了检测效率。
以上这2点可通过下图看出:
9.3.1 实现 epoll需要的3个函数:
(1)创建epoll文件描述符空间
#include <sys/epoll.h>
int epoll_create(int size);
参数解释:
size:现在并不起作用,只是提示内核事件表需要多大
返回值:
成功时返回epoll文件描述符,失败时返回-1
(2)向空间添加、修改、删除文件描述符
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
参数解释:
epfd:epoll_create()的返回值
op:指定对fd的操作类型
fd:被监听的文件描述符.实际作用是作为红黑树的key
event:指定fd的事件类型
返回值:
成功时返回0,失败时返回-1
struct epoll_event
{
__uint32_t events;
epoll_data_t data;
};
typedef union epoll_data
{
void* ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
}epoll_data_t;
op参数的取值如下:
EPOLL_CTL_ADD:添加文件描述符到监听集合
EPOLL_CTL_DEL:从监听集合中删除文件描述符
EPOLL_CTL_MOD:修改文件描述符对应的事件类型
epoll_event结构体的成员events的取值如下(可通过或运算同时传递多个值):
EPOLLIN:需要读取数据情况
EPOLLOUT:输出缓冲区为空,可以立即发送数据的情况
EPOLLPRI:收到OOB数据的情况
EPOLLRDHUP:断开连接或半关闭情况,常使用在边缘触发方式中
EPOLLERR:发生错误情况
EPOLLET:以边缘触发的方式得到事件通知
EPOLLONESHOT:水平触发,即发生一次事件后,相应的文件描述符不再收到事件通知
(3)等待文件描述符发生变化
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
参数解释:
epfd:epoll_create()的返回值
events:保存发生事件的文件描述符集合的数组
maxevents:数组的大小
timeout:超时时间(毫秒),传递-1时,会一直等待直到事件发生
返回值:
成功时返回发生事件的文件描述符的个数,失败时返回-1
注意:随便往epoll_fd中加入一个int整数是无效的,虽然fd也是int类型,但是fd是文件描述符,是系统分配的,随便加入的整数是在系统中找不到的。
下面的添加操作是无效的:
int a = 10;
struct epoll_event event;
event.events = EPOLLIN;
event.data.fd = a;
epoll_ctl(epfd, EPOLL_CTL_ADD, a, &event);
下面使用epoll实现回声服务端:(客服端的实现见poll小结的客户端)
#include <unistd.h>
#include <sys/socket.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <stdio.h>
#define BUF_SIZE 100
#define EPOLL_SIZE 50
void error_handling(char* buf)
{
fputs(buf, stderr);
fputc('\n', stderr);
exit(1);
}
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] = {0};
struct epoll_event ep_events[EPOLL_SIZE];
struct epoll_event event;
int epfd, event_cnt;
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(12345);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if ( bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1 )
{
error_handling("bind() error");
}
if ( listen(serv_sock, 5) == -1 )
{
error_handling("listen() error");
}
epfd = epoll_create(EPOLL_SIZE); //相当于创建一颗红黑树的根节点
//将serv_sock加入监听集合
event.events = EPOLLIN;
event.data.fd = serv_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event); //将fd节点加入红黑树
//轮询等待事件的发生
while(1)
{
event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
if (event_cnt == -1)
{
puts("epoll_wait error");
break;
}
//遍历所有就绪的socket
for (i = 0; i < event_cnt; i++)
{
//监听socket有事件发生,处理连接请求
if (ep_events[i].data.fd == serv_sock)
{
addr_sz = sizeof(clnt_addr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &addr_sz);
event.events = EPOLLIN;
event.data.fd = clnt_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
printf("connect client:%d\n", clnt_sock);
}
else
{
//连接socket有读事件发生
str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
if (str_len == 0) //客户端连接关闭
{
//将连接socket从监听集合中删除且关闭
epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
close(ep_events[i].data.fd);
printf("close client:%d\n", ep_events[i].data.fd);
}
else
{
//向客服端发送数据
write(ep_events[i].data.fd, buf, str_len);
}
}
}
}
//最后要关闭监听socket和epoll文件描述符
close(serv_sock);
close(epfd);
return 0;
}
9.3.2 边缘触发(ET)和水平触发(LT)
epoll 支持两种事件触发模式,分别是边缘触发(edge-triggered,ET)和水平触发(level-triggered,LT)。
- 边缘触发:当被监控的 Socket 描述符上有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读取完;
- 水平触发:当被监控的 Socket 上有可读事件发生时,服务器端不断地从 epoll_wait 中苏醒,直到内核缓冲区数据被 read 函数读完才结束,目的是告诉我们有数据需要读取;
如果使用边缘触发模式,I/O 事件发生时只会通知一次,而且我们不知道到底能读写多少数据,所以在收到通知后应尽可能地读写数据,以免错失读写的机会。因此,我们会循环从文件描述符读写数据,那么如果文件描述符是阻塞的,没有数据可读写时,进程会阻塞在读写函数那里,程序就没办法继续往下执行。所以,边缘触发模式一般和非阻塞 I/O 搭配使用,程序会一直执行 I/O 操作,直到系统调用(如 read
和 write
)返回错误-1,错误类型errno为 EAGAIN
或 EWOULDBLOCK
。
默认情况下,文件描述符的读写操作都是阻塞的。但文件描述符可以被设置为非阻塞的状态,此时,文件描述符的 I/O 操作结果会立即返回。如果文件描述符没有就绪,那么会返回错误;否则会根据 I/O 操作的执行情况返回相应的结果(部分完成或全部完成)。下面两行代码可将文件描述符改为非阻塞状态:
int flag = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flag|O_NONBLOCK);
下面是ET和LT模式的服务端实现: (注意:大数据块通常用LT,小数据块通常用ET,listen()函数监听的fd通常用LT。也有人总结成:ET+循环读=LT+一次性读)
#include <unistd.h>
#include <sys/socket.h>
#include <netdb.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/epoll.h>
#define MAX_EVENT_NUMBER 1024
#define BUFFER_SIZE 10
//将文件描述符设置为非阻塞
void setnonblocking(int fd)
{
int flag = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flag|O_NONBLOCK);
}
//将文件描述符fd添加到epoll的监听集合中
void addfd(int epollfd, int fd, bool enableET)
{
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN;
if (enableET)
{
event.events |= EPOLLET;
}
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
//将要添加的文件描述符设为非阻塞
setnonblocking(fd);
}
//1. LT模式的工作流程
void LT(epoll_event* events, int event_cnt, int epollfd, int listenfd)
{
char buf[BUFFER_SIZE];
for (int i = 0; i < event_cnt; i++)
{
int sockfd = events[i].data.fd;
if (sockfd == listenfd) //如果就绪的是监听文件描述符,接受连接
{
struct sockaddr_in clnt_addr;
socklen_t clnt_addrlen = sizeof(clnt_addr);
int connfd = accept(listenfd, (struct sockaddr*)&clnt_addr, &clnt_addrlen);
addfd(epollfd, connfd, false); //对连接socket要使用LT模式
}
else if (events[i].events & EPOLLIN) //如果就绪的是连接文件描述符.其实监听文件描述符对应的事件也是写事件
{
/*只要socket读缓存中还有未读出的数据,这段代码就被触发*/
printf("event trigger once\n");
memset(buf, '\0', BUFFER_SIZE);
int ret = recv(sockfd, buf, BUFFER_SIZE-1, 0);
if (ret <= 0)
{
close(sockfd);
continue;
}
printf("get %d bytes of content: %s\n", ret, buf);
send(sockfd, buf, ret, 0);
}
else
{
printf("something else happened \n");
}
}
}
//2。ET模式的工作流程
void ET(epoll_event* events, int event_cnt, int epollfd, int listenfd)
{
char buf[BUFFER_SIZE];
for (int i = 0; i < event_cnt; i++)
{
int sockfd = events[i].data.fd;
if (sockfd == listenfd) //如果就绪的是监听文件描述符,接受连接
{
struct sockaddr_in clnt_addr;
socklen_t clnt_addrlen = sizeof(clnt_addr);
int connfd = accept(listenfd, (struct sockaddr*)&clnt_addr, &clnt_addrlen);
addfd(epollfd, connfd, false); //对连接socket要使用LT模式
}
else if (events[i].events & EPOLLIN) //如果就绪的是连接文件描述符.其实监听文件描述符对应的事件也是写事件
{
/*这段代码不会被重复触发,所以我们循环读取数据,确保把socket读缓存中的所有数据读出*/
printf("event trigger once\n");
while(1)
{
memset(buf, '\0', BUFFER_SIZE);
int ret = recv(sockfd, buf, BUFFER_SIZE-1, 0);
if (ret < 0)
{
/*对于非阻塞I/O,下面的条件成立表示数据已经全部读取完毕。此后,
epoll就能再次触发sockfd上的EPOLLIN事件,以驱动下一次读操作
*/
if ((errno == EAGAIN) || (errno == EWOULDBLOCK))
{
printf("read finished!\n");
break;
}
close(sockfd);
continue;
}
else if (ret == 0)
{
//客户端关闭连接
close(sockfd);
}
else
{
printf("get %d bytes of content: %s\n", ret, buf);
send(sockfd, buf, ret, 0);
}
}
}
else
{
printf("something else happened \n");
}
}
}
int main()
{
struct sockaddr_in serv_addr;
socklen_t addr_sz;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(12345);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
int serv_sock = socket(PF_INET, SOCK_STREAM, 0);
assert(serv_sock >= 0);
int ret = bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
assert(ret != -1);
ret = listen(serv_sock, 5);
assert(ret != -1);
//创建epoll监听集合
struct epoll_event events[MAX_EVENT_NUMBER];
int epollfd = epoll_create(5);
assert(epollfd != -1);
//将监听文件描述符加入监听集合
addfd(epollfd, serv_sock, true);
while(1)
{
int event_cnt = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
if (event_cnt == -1)
{
puts("epoll_wait error");
break;
}
//LT(events, event_cnt, epollfd, serv_sock); //使用LT模式
ET(events, event_cnt, epollfd, serv_sock); //使用ET模式
}
close(serv_sock);
close(epollfd);
return 0;
}
客户端代码参见epoll的实现。
一般来说,边缘触发的效率比水平触发的效率要高,因为边缘触发可以减少 epoll_wait 的系统调用次数,系统调用也是有一定的开销的的,毕竟也存在上下文的切换。
select/poll 只有水平触发模式,epoll 默认的触发模式是水平触发。
使用 I/O 多路复用时,最好搭配非阻塞 I/O 一起使用。多路复用 API 返回的事件并不一定可读写的(例如异常),如果使用阻塞 I/O, 那么在调用 read/write 时则会发生程序阻塞,因此最好搭配非阻塞 I/O。
9.3.3 EPOLLONESHOT事件
假如一个socket使用ET模式,在并发程序中,一个线程读取完该socket上的数据后开始处理,而在处理过程中该socket上又有新数据可读,此时另外一个线程被唤醒来读取这些新数据,于是就出现两个线程同时操作一个socket的现象,显然我们不希望看到这样。我们希望该socket在任意时刻都只能被一个线程处理,这可通过EPOLLONESHOT事件实现。
如果一个文件描述符注册了EPOLLONESHOT事件,操作系统最多触发该文件描述符上注册的一个可读、可写、异常事件,且只触发一次,除非使用epoll_ctl函数重置该文件描述符上注册的EPOLLONESHOT事件。这样,当一个线程在处理某个socket时,其他线程是不可能有机会处理该socket的。
反之,注册了EPOLLONESHOT的socket一旦被某个线程处理完毕,该线程就应该立即重置该socket上的EPOLLONESHOT,使得该socket下一次就绪时,其他线程也能处理这个socket。
下面的代码展示EPOLLONESHOT的使用:
#include <unistd.h>
#include <sys/socket.h>
#include <netdb.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <pthread.h>
#define MAX_EVENT_NUMBER 1024
#define BUFFER_SIZE 1024
struct fds
{
int epollfd;
int sockfd;
};
//将文件描述符设置为非阻塞
void setnonblocking(int fd)
{
int flag = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flag|O_NONBLOCK);
}
//fd添加到epoll的监听集合中
void addfd(int epollfd, int fd, bool enableOneshot)
{
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLET;
if (enableOneshot)
{
event.events |= EPOLLONESHOT;
}
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
//将要添加的文件描述符设为非阻塞
setnonblocking(fd);
}
/*
重置fd上的事件,使得fd被处理完后,下次就绪时,别的线程也能处理该fd
*/
void resetOnshot(int epollfd, int fd)
{
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLET | EPOLLONESHOT;
epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);
}
//工作线程
void* worker(void* arg)
{
int sockfd = ((fds*)arg)->sockfd;
int epollfd = ((fds*)arg)->epollfd;
printf("start new thread to recieve data on fd : %d\n", sockfd);
char buf[BUFFER_SIZE] = {0};
//循环读取sockfd上的数据,知道遇到EAGAIN错误
while(1)
{
int ret = recv(sockfd, buf, BUFFER_SIZE-1, 0);
if (ret == 0) //对端关闭连接
{
close(sockfd);
printf("foreiner closed the connection \n");
break;
}
else if (ret < 0) //数据接收完毕
{
if (errno == EAGAIN)
{
resetOnshot(epollfd, sockfd);
printf("read later\n");
break;
}
}
else
{
printf("get content: %s\n", buf);
//休眠5s,模拟数据处理过程
sleep(5);
}
}
printf("finish thread receiving data on fd: %d\n", sockfd);
}
int main()
{
struct sockaddr_in serv_addr;
socklen_t addr_sz;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(12345);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
int serv_sock = socket(PF_INET, SOCK_STREAM, 0);
assert(serv_sock >= 0);
int ret = bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
assert(ret != -1);
ret = listen(serv_sock, 5);
assert(ret != -1);
//创建epoll监听集合
struct epoll_event events[MAX_EVENT_NUMBER];
int epollfd = epoll_create(5);
assert(epollfd != -1);
/*将监听文件描述符加入监听集合
注意:监听socket上不能注册EPOLLONESHOT事件,否则应用程序只能处理一个客户端连接,
因为后续的客户连接请求将不再触发监听socket上的EPOLLIN事件。
*/
addfd(epollfd, serv_sock, false);
while(1)
{
int event_cnt = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
if (event_cnt == -1)
{
puts("epoll_wait error");
break;
}
for (int i = 0; i < event_cnt; i++)
{
int sockfd = events[i].data.fd;
if (sockfd == serv_sock) //如果就绪的是监听文件描述符,接受连接
{
struct sockaddr_in clnt_addr;
socklen_t clnt_addrlen = sizeof(clnt_addr);
int connfd = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addrlen);
//对每个连接socket都注册EPOLLONESHOT
addfd(epollfd, connfd, true);
}
else if (events[i].events & EPOLLIN) //如果就绪的是连接文件描述符.其实监听文件描述符对应的事件也是写事件
{
//新启动一个线程为sockfd服务,用完后要回收
pthread_t thread;
fds fds_for_new_worker;
fds_for_new_worker.epollfd = epollfd;
fds_for_new_worker.sockfd = sockfd;
pthread_create(&thread, NULL, worker, (void*)&fds_for_new_worker);
}
else
{
printf("something else happened \n");
}
}
}
close(serv_sock);
close(epollfd);
return 0;
}
尽管一个socket在不同事件可能被不同的线程处理,但是同一时刻肯定只有一个线程在为它服务。这就保证了连接的完整性,避免了很多可能的竟态条件。
9.3.4 引出Reactor
从前面可以看出,epoll的代码模式一直都是下面这样:
event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
for (i = 0; i < event_cnt; i++)
{
if (ep_events[i].data.fd == listenfd){
int clntfd = accept(listenfd, (struct sockaddr*)&clnt_addr, &clnt_addrlen);
....
}else{
if (events[i].events & EPOLLIN){
...recv...
}
if (events[i].events & EPOLLOUT){
...send...
}
}
}
但是,这样会降低程序效率,而且也不整洁。由于listenfd对应的事件也属于写事件(EPOLLIN),因此可以设计一种结构体,保存fd和fd对应的处理函数。例如将处理listenfd的函数(accept)跟listenfd放在一个结构体中,处理clntfd的函数(recv/send)跟clntfd放在一个结构体中。
struct sockitem {
int sockfd;
int (*callback)(int fd, int events, void *arg);
}
int recv_cb(int fd, int events, void *arg) {
}
int accept_cb(int fd, int events, void *arg) {
int clientfd = accept();
struct sockitem *si = (struct sockitem*)malloc(sizeof(struct sockitem));
si->sockfd = clientfd;
si->callback = recv_cb;
//
epoll_ctl()
}
int main(int argc, char *argv[]) {
socket();
bind(...);
listen(listenfd, 5);
int epfd = epoll_create(1);
struct epoll_event ev, events[512] = {0};
ev.events = EPOLLIN;
ev.data.fd = listenfd; //int idx = 2000;
struct sockitem *si = (struct sockitem*)malloc(sizeof(struct sockitem));
si->sockfd = listenfd;
si->callback = accept_cb;
ev.data.ptr = si;
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
while(1){
int nready = epoll_wait(epfd, events, 512, -1);
for (i = 0;i < nready;i ++) {
if (events[i].events & EPOLLIN) {
struct sockitem *si = (struct sockitem*)events[i].data.ptr;
si->callback(events[i].data.fd, events[i].events, si);
}
if (events[i].events & EPOLLOUT) {
struct sockitem *si = (struct sockitem*)events[i].data.ptr;
si->callback(events[i].data.fd, events[i].events, si);
}
}
}
}
注意要点:
1、使用event.data.ptr指针保存结构体指针,当有事件的时候就可以获取到fd和对应的cbFun函数。
2、固定的释放流程要写成原子操作:
epoll_ctl(eventloop->epfd, EPOLL_CTL_DEL, fd, &ev);
close(fd);
free(si);
3、read之后要将fd的事件改成EPOLLOUT,fd的回调函数设置为write;write之后要将fd的事件改成EPOLLIN,fd的回调函数设置为read。
假如有大量的客户端请求连接服务器,那么就需要多次对epoll_wait的返回结果进行循环遍历。返回结果中既有clientfd又有listenfd,且clientfd占绝大多数。这显然会降低服务器处理连接请求的效率。因此,需要将clientfd和listenfd的处理分开,创建一个/多个线程专门处理listenfd,创建多个线程去处理clientfd。此时,单线程reactor和多线程reactor都被引出,即:
单线程reactor:一个线程处理listenfd和clientfd。(参考libevent/redis)
多线程reactor:A、多线程处理listenfd和clientfd。
B、一个线程处理listenfd,多线程处理clientfd。(memcached)
多进程reactor:前提是多进程处理的数据是需要进程共享的。(ngix)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <errno.h>
#include <sys/epoll.h>
/*
保存单个fd对应的信息。
当接受/发送的数据非常大,一次发不完,可以借助recvbuffer/sendbuffer来保存.
*/
struct sockitem { //
int sockfd;
int (*callback)(int fd, int events, void *arg);
char recvbuffer[1024]; //接受缓冲区
char sendbuffer[1024]; //发送缓冲区
};
//保存所有fd公用的变量
struct reactor {
int epfd;
struct epoll_event events[512];
};
struct reactor *eventloop = NULL;
int recv_cb(int fd, int events, void *arg);
int send_cb(int fd, int events, void *arg) {
struct sockitem *si = (struct sockitem*)arg;
send(fd, "hello\n", 6, 0); //
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;
//ev.data.fd = clientfd;
si->sockfd = fd;
si->callback = recv_cb;
ev.data.ptr = si;
epoll_ctl(eventloop->epfd, EPOLL_CTL_MOD, fd, &ev);
}
int recv_cb(int fd, int events, void *arg) {
//int clientfd = events[i].data.fd;
struct sockitem *si = (struct sockitem*)arg;
struct epoll_event ev;
char buffer[1024] = {0};
int ret = recv(fd, buffer, 1024, 0);
if (ret < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) { //
return -1;
} else {
}
ev.events = EPOLLIN;
//ev.data.fd = fd;
epoll_ctl(eventloop->epfd, EPOLL_CTL_DEL, fd, &ev);
close(fd);
free(si);
} else if (ret == 0) { //
//
printf("disconnect %d\n", fd);
ev.events = EPOLLIN;
//ev.data.fd = fd;
epoll_ctl(eventloop->epfd, EPOLL_CTL_DEL, fd, &ev);
close(fd);
free(si);
} else {
printf("Recv: %s, %d Bytes\n", buffer, ret);
struct epoll_event ev;
ev.events = EPOLLOUT | EPOLLET;
//ev.data.fd = clientfd;
si->sockfd = fd;
si->callback = send_cb;
ev.data.ptr = si;
epoll_ctl(eventloop->epfd, EPOLL_CTL_MOD, fd, &ev);
}
}
int accept_cb(int fd, int events, void *arg) {
struct sockaddr_in client_addr;
memset(&client_addr, 0, sizeof(struct sockaddr_in));
socklen_t client_len = sizeof(client_addr);
int clientfd = accept(fd, (struct sockaddr*)&client_addr, &client_len);
if (clientfd <= 0) return -1;
char str[INET_ADDRSTRLEN] = {0};
printf("recv from %s at port %d\n", inet_ntop(AF_INET, &client_addr.sin_addr, str, sizeof(str)),
ntohs(client_addr.sin_port));
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;
//ev.data.fd = clientfd;
struct sockitem *si = (struct sockitem*)malloc(sizeof(struct sockitem));
si->sockfd = clientfd;
si->callback = recv_cb;
ev.data.ptr = si;
epoll_ctl(eventloop->epfd, EPOLL_CTL_ADD, clientfd, &ev);
return clientfd;
}
void *worker_thread(void *arg) {
while (1) {
int nready = epoll_wait(eventloop->epfd, eventloop->events, 512, -1);
if (nready < -1) {
break;
}
// listen
//
int i = 0;
for (i = 0;i < nready;i ++) {
if (eventloop->events[i].events & EPOLLIN) {
//printf("sockitem\n");
struct sockitem *si = (struct sockitem*)eventloop->events[i].data.ptr;
si->callback(si->sockfd, eventloop->events[i].events, si);
}
if (eventloop->events[i].events & EPOLLOUT) {
struct sockitem *si = (struct sockitem*)eventloop->events[i].data.ptr;
si->callback(si->sockfd, eventloop->events[i].events, si);
}
}
}
}
int main(int argc, char *argv[]) {
if (argc < 2) {
return -1;
}
int port = atoi(argv[1]);
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
return -1;
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(struct sockaddr_in));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = INADDR_ANY;
if (bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0) {
return -2;
}
if (listen(sockfd, 5) < 0) {
return -3;
}
eventloop = (struct reactor*)malloc(sizeof(struct reactor));
// epoll opera
eventloop->epfd = epoll_create(1);
struct epoll_event ev;
ev.events = EPOLLIN;
//ev.data.fd = sockfd; //int idx = 2000;
struct sockitem *si = (struct sockitem*)malloc(sizeof(struct sockitem));
si->sockfd = sockfd;
si->callback = accept_cb;
ev.data.ptr = si;
epoll_ctl(eventloop->epfd, EPOLL_CTL_ADD, sockfd, &ev);
pthread_t id;
pthread_create(&id, NULL, worker_thread, NULL);
}
后续内容请见第9-1章。
9.4 三组I/O复用的区别总结
9.5 I/O复用的高级应用
9.5.1 非阻塞connect
connect()默认为阻塞接口,超时时间在几十秒至几分钟。将一个socket设为非阻塞之后调用connect(),connect会立即返回EINPROGRESS错误,表示操作正在进行中,但仍未完成,三次握手继续进行。之后,调用select、poll等函数检查连接是否成功。
处理非阻塞connect()的步骤:
第一步:创建socket,返回套接口描述符;
第二步:调用fcntl把套接口描述符设置成非阻塞;
第三步:调用connect开始建立连接;
第四步:判断连接是否成功建立;
A:如果connect返回0,表示连接建立成功(服务器可客户端在同一台机器上时就有可能发生这种情况);
B:调用select等待连接建立成功:
如果select返回0,则表示建立连接超时;我们返回超时错误给用户,同时关闭连接,防止三次握手继续进行下去;
如果select返回值大于0,则需要检查socket是否可读、可写。如果socket可读或可写,则调用getsockopt获取socket上待处理的错误(SO_ERROR);如果连接建立成功,errno值是0;如果连接建立失败,errno值不是0,如ECONNREFUSED和ETIMEDOUT。
下面是使用非阻塞connect的代码:
#include <unistd.h>
#include <sys/socket.h>
#include <netdb.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/epoll.h>
#define BUFFER_SIZE 1024
//将文件描述符设置为非阻塞
void setnonblocking(int fd)
{
int flag = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flag|O_NONBLOCK);
}
/*
超时连接函数,参数分别为服务器的port、超时时间。
成功返回连接socket,失败返回-1
*/
int nonBlockConnect(int port, int time)
{
struct sockaddr_in servAddr;
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(port);
servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
setnonblocking(sockfd);
int ret = connect(sockfd, (struct sockaddr*)&servAddr, sizeof(servAddr));
//连接成功
if (ret == 0)
{
printf("connect with server immediately!\n");
return sockfd;
}
//连接失败
else if (errno != EINPROGRESS)
{
printf("nonblock connect not supportQ!\n");
return -1;
}
//errno==EINPROGRESS,连接还在进行中
fd_set readfds;
fd_set writefds;
struct timeval timeout;
timeout.tv_sec = time;
timeout.tv_usec = 0;
ret = select(sockfd+1, NULL, &writefds, NULL, &timeout);
if (ret <= 0)
{
/*select超时或出错,立即返回-1*/
printf("connection time out\n");
close(sockfd);
return -1;
}
//sockfd上没有事件发生
if (!FD_ISSET(sockfd, &writefds))
{
printf("no events on sockfd found\n");
close(sockfd); //因为是测试,所以关不关毕无所谓
return -1;
}
//sockfd上有事件发生
int error = 0;
socklen_t length = sizeof(errno);
//调用getsockopt获取错误并清楚sockfd的错误
if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &length) < 0)
{
printf("get socket option failed\n");
close(sockfd);
return -1;
}
//连接错误
if (error != 0)
{
printf("connection fail after select, error: %d\n", error);
close(sockfd);
return -1;
}
//连接成功
printf("uconnection success after select, sockfd: %d\n", sockfd);
return sockfd;
}
int main()
{
int sockfd = nonBlockConnect(12345, 10);
if (sockfd < 0)
{
return false;
}
close(sockfd);
return 0;
}
9.5.2 聊天室程序
该聊天室陈程序能让所有用户同时在线群聊,分为客户端和服务端两个部分。
客户端:
使用poll同时监听用户输入和网络连接,并利用splice函数将用户输入内容直接定向到网络连接上并发送,实现零拷贝。
服务端:
使用poll同时管理监听socke和连接socket,并且使用牺牲空间换取时间的策略来提高服务器性能。服务端功能是接收客户端数据,并把该数据发给每一个登录到该服务器上的客户端。
下面是服务端代码:
#define _GNU_SOURCE 1
#include <unistd.h>
#include <sys/socket.h>
#include <netdb.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <poll.h>
#define BUFFER_SIZE 64
#define USER_LIMIT 5 //最大用户数量
#define FD_LIMIT 65535 //文件描述符数量限制
//客户端数据:客户端socket地址、待发送给客户端的数据的位置、从客户端接收的数据
struct clientData
{
sockaddr_in address;
char* writeBuf;
char reacvBuf[BUFFER_SIZE];
};
//将文件描述符设置为非阻塞
void setnonblocking(int fd)
{
int flag = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flag|O_NONBLOCK);
}
int main()
{
struct sockaddr_in serv_addr;
socklen_t addr_sz;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(12345);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
int serv_sock = socket(PF_INET, SOCK_STREAM, 0);
assert(serv_sock >= 0);
int ret = bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
assert(ret != -1);
ret = listen(serv_sock, 5);
assert(ret != -1);
/*创建users数组,分配65535个clientData对象。每个可能的socket连接都能获得一个这样的对象,
并且socket的值可以直接用来索引(作为数组的下标)socket连接对应的clientData对象,
这是将socket和客户端数据关联的简单而高效的方式
*/
clientData* users = new clientData[FD_LIMIT];
//虽然分配了足够多的clientData对象,但为了提高poll性能,仍然有必要限制用户的数量
pollfd fds[USER_LIMIT + 1];
int userCnt = 0;
//初始化fds数组
for (int i = 1; i <= USER_LIMIT; i++)
{
fds[i].fd = -1;
fds[i].events = 0;
}
//将监听socket加入fds,对应的事件是可读或错误
fds[0].fd = serv_sock;
fds[0].events = POLLIN | POLLERR;
fds[0].revents = 0;
while(1)
{
ret = poll(fds, userCnt + 1, -1);
if (ret < 0)
{
printf("poll failuer\n");
break;
}
for(int i = 0; i < userCnt + 1; i++)
{
//如果监听socket有连接请求事件发生,接收连接
if ((fds[i].fd == serv_sock) && (fds[i].revents & POLLIN))
{
struct sockaddr_in clnt_addr;
socklen_t clnt_addrLen = sizeof(clnt_addr);
int connfd = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addrLen);
if (connfd < 0) //接收连接失败
{
printf("errno is: %d\n", errno);
continue;
}
//如果请求太多,则关闭新到的连接
if (userCnt >= USER_LIMIT)
{
const char* info = "too many users\n";
printf("%s", info);
send(connfd, info, strlen(info), 0);
close(connfd);
continue;
}
/*对于新的连接,将connfd加入fds和users数组。
users[connfd]对应新连接connfd的客户端数据
*/
userCnt++;
users[connfd].address = clnt_addr;
setnonblocking(connfd);
fds[userCnt].fd = connfd;
fds[userCnt].events = POLLIN | POLLRDHUP | POLLERR;
fds[userCnt].revents = 0;
printf("comes a new user, now have %d users\n", userCnt);
}
//如果连接socket有错误事件发生
else if (fds[i].revents & POLLERR)
{
printf("get an error from %d\n", fds[i].fd);
char errors[100];
memset(errors, '\0', 100);
socklen_t length = sizeof(errors);
//获取并清除socket错误状态
if (getsockopt(fds[i].fd, SOL_SOCKET, SO_ERROR, &errors, &length) < 0)
{
printf("get socket option failed\n");
}
continue;
}
//客户端关闭连接,则服务端也关闭连接,userCnt减1
else if (fds[i].revents & POLLRDHUP)
{
//随便为users[fds[i].fd]赋一个值
users[fds[i].fd] = users[fds[userCnt].fd];
close(fds[i].fd);
i--;
userCnt--;
printf("a client left\n");
}
//连接socket上有读事件发生
else if (fds[i].revents & POLLIN)
{
int connfd = fds[i].fd;
memset(users[connfd].reacvBuf, '\0', BUFFER_SIZE);
ret = recv(connfd, users[connfd].reacvBuf, BUFFER_SIZE-1, 0);
printf("get %d bytes of client data %s from %d", ret, users[connfd].reacvBuf, connfd);
if(ret < 0)
{
/*对于非阻塞I/O,errno==EAGAIN表示数据已经全部读取完毕*/
//读出错,关闭连接
if (errno != EAGAIN)
{
close(connfd);
users[fds[i].fd] = users[fds[userCnt].fd];
fds[i] = fds[userCnt];
i--;
userCnt--;
}
}
else if (ret == 0)
{
//客户端关闭连接,前面已经做过检测处理
}
else //服务端成功接收数据
{
//如果接收到客户数据,则通知其他socket连接准备写数据
for (int j = 1; j <= userCnt; j++)
{
if (fds[j].fd == connfd)
{
continue;
}
fds[j].events |= ~POLLIN;
fds[j].events |= POLLOUT;
//将收到的数据作为发给每个客户端的数据
users[fds[j].fd].writeBuf = users[connfd].reacvBuf;
}
}
}
//连接socket上有写事件发生
else if (fds[i].revents & POLLOUT)
{
int connfd = fds[i].fd;
if (!users[connfd].writeBuf)
{
continue;
}
ret = send(connfd, users[connfd].writeBuf, strlen(users[connfd].writeBuf), 0);
users[connfd].writeBuf = NULL;
//发送完数据后需要将fds[i]的事件更改为可读事件
fds[i].events |= ~POLLOUT;
fds[i].events |= POLLIN;
}
}
}
delete []users;
close(serv_sock);
return 0;
}
下面是客户端代码:
#define _GNU_SOURCE 1
#include <unistd.h>
#include <sys/socket.h>
#include <netdb.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <poll.h>
#define BUFFER_SIZE 64
int main()
{
struct sockaddr_in servAddr;
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(12345);
servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
assert(sockfd >= 0);
if (connect(sockfd, (struct sockaddr*)&servAddr, sizeof(servAddr)) < 0)
{
printf("connection failed\n");
close(sockfd);
return 1;
}
struct pollfd fds[2];
/*注册文件描述符0(标准输入)和sockfd上的可读事件*/
fds[0].fd = 0;
fds[0].events = POLLIN;
fds[0].revents = 0;
fds[1].fd = sockfd;
fds[1].events = POLLIN | POLLRDHUP;
fds[1].revents = 0;
char readBuf[BUFFER_SIZE];
int pipefd[2];
int ret = pipe(pipefd);
assert(ret != -1);
while(1)
{
ret = poll(fds, 2, -1);
//创建poll失败
if (ret < 0)
{
printf("poll failure\n");
break;
}
//sockfd上发生对方关闭连接
if (fds[1].revents & POLLRDHUP)
{
printf("server close the connection\n");
break;
}
//sockfd上发生可读事件,读取服务端发来的数据
else if (fds[1].revents & POLLIN)
{
memset(readBuf, '\0', BUFFER_SIZE);
recv(fds[1].fd, readBuf, BUFFER_SIZE-1, 0);
printf("%s\n", readBuf);
}
if (fds[0].revents & POLLIN)
{
//使用splice将用户输入的数据直接写到sockfd上(零拷贝)
ret = splice(0, NULL, pipefd[1], NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);
ret = splice(pipefd[0], NULL, sockfd, NULL, 32768, SPLICE_F_MORE | SPLICE_F_MOVE);
}
}
close(sockfd);
return 0;
}