发现一个好文章,因为最近在弄一个taf的网络库,发现原来的网络库只支持linux的epoll IO复用,代码没有地办在mac os和windows上用IDE调试,linux上的vim和emacs玩的不溜,就想找一个库来代替epoll在mac上调试,因为poll在windows系统上有bug,我打算用下边的select来修改原来的库,就在网上找到大神的这个文章,这里转来收藏和分享。
一、IO多路复用
所谓IO多路复用,就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
Linux支持IO多路复用的系统调用有select、poll、epoll,这些调用都是内核级别的。但select、poll、epoll本质上都是同步I/O,先是block住等待就绪的socket,再是block住将数据从内核拷贝到用户内存。
当然select、poll、epoll之间也是有区别的,如下表:
\ | select | poll | epoll |
---|---|---|---|
操作方式 | 遍历 | 遍历 | 回调 |
底层实现 | 数组 | 链表 | 哈希表 |
IO效率 | 每次调用都进行线性遍历,时间复杂度为O(n) | 每次调用都进行线性遍历,时间复杂度为O(n) | 事件通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到rdllist里面。时间复杂度O(1) |
最大连接数 | 1024(x86)或 2048(x64) | 无上限 | 无上限 |
fd拷贝 | 每次调用select,都需要把fd集合从用户态拷贝到内核态 | 每次调用poll,都需要把fd集合从用户态拷贝到内核态 | 调用epoll_ctl时拷贝进内核并保存,之后每次epoll_wait不拷贝 |
二、select示例
2.1 流程图
注:摘自IBM iSeries 信息中心。
2.2 相关函数
#include
#include
int select(int max_fd, fd_set *readset, fd_set *writeset, fd_set *exceptset, struct timeval *timeout)
- 该select()函数返回就绪描述符的数目,超时返回0,出错返回-1
- 第一个参数max_fd指待测试的fd个数,它的值是待测试的最大文件描述符加1,文件描述符从0开始到max_fd-1都将被测试。
- 中间三个参数readset、writeset和exceptset指定要让内核测试读、写和异常条件的fd集合,如果不需要测试可以设置为NULL。操作fd_set有四个宏:
//清空集合
void FD_ZERO(fd_set *fdset)
//将一个给定的文件描述符加入集合之中
void FD_SET(int fd, fd_set *fdset)
//将一个给定的文件描述符从集合中删除
void FD_CLR(int fd, fd_set *fdset)
//判断指定描述符是否在集合中
int FD_ISSET(int fd, fd_set *fdset)
- timeout是指 select 的等待时长,如果这段时间内所监听的 socket 没有事件就绪,超时返回0
2.3 示例程序
这里写一个程序,Client向Server发送消息,Server接收消息并原样发送给Client,Client再把消息输出到终端。
服务器端代码:
/*************************************************************************
> File Name: server.cpp
> Author: SongLee
> E-mail: [email protected]
> Created Time: 2016年04月28日 星期四 22时02分43秒
> Personal Blog: http://songlee24.github.io/
************************************************************************/
#include // sockaddr_in
#include // socket
#include // socket
#include
#include
#include // select
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define BUFFER_SIZE 1024
struct PACKET_HEAD
{
int length;
};
class Server
{
private:
struct sockaddr_in server_addr;
socklen_t server_addr_len;
int listen_fd; // 监听的fd
int max_fd; // 最大的fd
fd_set master_set; // 所有fd集合,包括监听fd和客户端fd
fd_set working_set; // 工作集合
struct timeval timeout;
public:
Server(int port);
~Server();
void Bind();
void Listen(int queue_len = 20);
void Accept();
void Run();
void Recv(int nums);
};
Server::Server(int port)
{
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htons(INADDR_ANY);
server_addr.sin_port = htons(port);
// create socket to listen
listen_fd = socket(PF_INET, SOCK_STREAM, 0);
if(listen_fd < 0)
{
cout << "Create Socket Failed!";
exit(1);
}
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
}
Server::~Server()
{
for(int fd=0; fd<=max_fd; ++fd)
{
if(FD_ISSET(fd, &master_set))
{
close(fd);
}
}
}
void Server::Bind()
{
if(-1 == (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr))))
{
cout << "Server Bind Failed!";
exit(1);
}
cout << "Bind Successfully.\n";
}
void Server::Listen(int queue_len)
{
if(-1 == listen(listen_fd, queue_len))
{
cout << "Server Listen Failed!";
exit(1);
}
cout << "Listen Successfully.\n";
}
void Server::Accept()
{
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int new_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);
if(new_fd < 0)
{
cout << "Server Accept Failed!";
exit(1);
}
cout << "new connection was accepted.\n";
// 将新建立的连接的fd加入master_set
FD_SET(new_fd, &master_set);
if(new_fd > max_fd)
{
max_fd = new_fd;
}
}
void Server::Run()
{
max_fd = listen_fd; // 初始化max_fd
FD_ZERO(&master_set);
FD_SET(listen_fd, &master_set); // 添加监听fd
while(1)
{
FD_ZERO(&working_set);
memcpy(&working_set, &master_set, sizeof(master_set));
timeout.tv_sec = 30;
timeout.tv_usec = 0;
int nums = select(max_fd+1, &working_set, NULL, NULL, &timeout);
if(nums < 0)
{
cout << "select() error!";
exit(1);
}
if(nums == 0)
{
//cout << "select() is timeout!";
continue;
}
if(FD_ISSET(listen_fd, &working_set))
Accept(); // 有新的客户端请求
else
Recv(nums); // 接收客户端的消息
}
}
void Server::Recv(int nums)
{
for(int fd=0; fd<=max_fd; ++fd)
{
if(FD_ISSET(fd, &working_set))
{
bool close_conn = false; // 标记当前连接是否断开了
PACKET_HEAD head;
recv(fd, &head, sizeof(head), 0); // 先接受包头,即数据总长度
char* buffer = new char[head.length];
bzero(buffer, head.length);
int total = 0;
while(total < head.length)
{
int len = recv(fd, buffer + total, head.length - total, 0);
if(len < 0)
{
cout << "recv() error!";
close_conn = true;
break;
}
total = total + len;
}
if(total == head.length) // 将收到的消息原样发回给客户端
{
int ret1 = send(fd, &head, sizeof(head), 0);
int ret2 = send(fd, buffer, head.length, 0);
if(ret1 < 0 || ret2 < 0)
{
cout << "send() error!";
close_conn = true;
}
}
delete buffer;
if(close_conn) // 当前这个连接有问题,关闭它
{
close(fd);
FD_CLR(fd, &master_set);
if(fd == max_fd) // 需要更新max_fd;
{
while(FD_ISSET(max_fd, &master_set) == false)
--max_fd;
}
}
}
}
}
int main()
{
Server server(15000);
server.Bind();
server.Listen();
server.Run();
return 0;
}
客户端代码:
/*************************************************************************
> File Name: client.cpp
> Author: SongLee
> E-mail: [email protected]
> Created Time: 2016年04月28日 星期四 23时10分15秒
> Personal Blog: http://songlee24.github.io/
************************************************************************/
#include // sockaddr_in
#include // socket
#include // socket
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define BUFFER_SIZE 1024
struct PACKET_HEAD
{
int length;
};
class Client
{
private:
struct sockaddr_in server_addr;
socklen_t server_addr_len;
int fd;
public:
Client(string ip, int port);
~Client();
void Connect();
void Send(string str);
string Recv();
};
Client::Client(string ip, int port)
{
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
if(inet_pton(AF_INET, ip.c_str(), &server_addr.sin_addr) == 0)
{
cout << "Server IP Address Error!";
exit(1);
}
server_addr.sin_port = htons(port);
server_addr_len = sizeof(server_addr);
// create socket
fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd < 0)
{
cout << "Create Socket Failed!";
exit(1);
}
}
Client::~Client()
{
close(fd);
}
void Client::Connect()
{
cout << "Connecting......" << endl;
if(connect(fd, (struct sockaddr*)&server_addr, server_addr_len) < 0)
{
cout << "Can not Connect to Server IP!";
exit(1);
}
cout << "Connect to Server successfully." << endl;
}
void Client::Send(string str)
{
PACKET_HEAD head;
head.length = str.size()+1; // 注意这里需要+1
int ret1 = send(fd, &head, sizeof(head), 0);
int ret2 = send(fd, str.c_str(), head.length, 0);
if(ret1 < 0 || ret2 < 0)
{
cout << "Send Message Failed!";
exit(1);
}
}
string Client::Recv()
{
PACKET_HEAD head;
recv(fd, &head, sizeof(head), 0);
char* buffer = new char[head.length];
bzero(buffer, head.length);
int total = 0;
while(total < head.length)
{
int len = recv(fd, buffer + total, head.length - total, 0);
if(len < 0)
{
cout << "recv() error!";
break;
}
total = total + len;
}
string result(buffer);
delete buffer;
return result;
}
int main()
{
Client client("127.0.0.1", 15000);
client.Connect();
while(1)
{
string msg;
getline(cin, msg);
if(msg == "exit")
break;
client.Send(msg);
cout << client.Recv() << endl;
}
return 0;
}
对上述程序的一些说明:
- 监听socket也由select来轮询,不需要单独的线程;
- working_set每次都要重新设置,因为select调用后它所检测的集合working_set会被修改;
- 接收很长一段数据时,需要循环多次recv。但是recv函数会阻塞,可以通过自定义包头(保存数据长度)。
三、poll示例
3.1 基本原理
poll
本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。
它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:
- 大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。
- poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
从上面看,select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。
3.2 相关函数
原型:
#include
int poll(struct pollfd fds[], nfds_t nfds, int timeout);
参数描述:
- 该poll()函数返回fds集合中就绪的读、写,或出错的描述符数量,返回0表示超时,返回-1表示出错;
- fds是一个struct pollfd类型的数组,用于存放需要检测其状态的socket描述符,并且调用poll函数之后fds数组不会被清空;
- nfds:记录数组fds中描述符的总数量;
- timeout:调用poll函数阻塞的超时时间,单位毫秒;
其中pollfd结构体定义如下:
typedef struct pollfd {
int fd; /* 需要被检测或选择的文件描述符*/
short events; /* 对文件描述符fd上感兴趣的事件 */
short revents; /* 文件描述符fd上当前实际发生的事件*/
} pollfd_t;
一个pollfd结构体表示一个被监视的文件描述符,通过传递fds[]指示 poll() 监视多个文件描述符,其中:
- 结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域。
- 结构体的revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域。
events域中请求的任何事件都可能在revents域中返回。合法的事件如下:
常量 | 说明 |
---|---|
POLLIN | 普通或优先级带数据可读 |
POLLRDNORM | 普通数据可读 |
POLLRDBAND | 优先级带数据可读 |
POLLPRI | 高优先级数据可读 |
POLLOUT | 普通数据可写 |
POLLWRNORM | 普通数据可写 |
POLLWRBAND | 优先级带数据可写 |
POLLERR | 发生错误 |
POLLHUP | 发生挂起 |
POLLNVAL | 描述字不是一个打开的文件 |
当需要监听多个事件时,使用POLLIN | POLLRDNORM
设置 events 域;当poll调用之后检测某事件是否就绪时,fds[i].revents & POLLIN
进行判断。
3.3 示例程序
这里写一个程序,Client向Server发送消息,Server接收消息并原样发送给Client,Client再把消息输出到终端。
#include // sockaddr_in
#include // socket
#include // socket
#include
#include
#include // poll
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define BUFFER_SIZE 1024
#define MAX_FD 1000
struct PACKET_HEAD
{
int length;
};
class Server
{
private:
struct sockaddr_in server_addr;
socklen_t server_addr_len;
int listen_fd; // 监听的fd
struct pollfd fds[MAX_FD]; // fd数组,大小为1000
int nfds;
public:
Server(int port);
~Server();
void Bind();
void Listen(int queue_len = 20);
void Accept();
void Run();
void Recv();
};
Server::Server(int port)
{
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htons(INADDR_ANY);
server_addr.sin_port = htons(port);
// create socket to listen
listen_fd = socket(PF_INET, SOCK_STREAM, 0);
if(listen_fd < 0)
{
cout << "Create Socket Failed!";
exit(1);
}
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
}
Server::~Server()
{
for(int i=0; i=0)
{
close(fds[i].fd);
}
}
}
void Server::Bind()
{
if(-1 == (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr))))
{
cout << "Server Bind Failed!";
exit(1);
}
cout << "Bind Successfully.\n";
}
void Server::Listen(int queue_len)
{
if(-1 == listen(listen_fd, queue_len))
{
cout << "Server Listen Failed!";
exit(1);
}
cout << "Listen Successfully.\n";
}
void Server::Accept()
{
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int new_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);
if(new_fd < 0)
{
cout << "Server Accept Failed!";
exit(1);
}
cout << "new connection was accepted.\n";
// 将新建立的连接的fd加入fds[]
int i;
for(i=1; i nfds ? i : nfds; // 更新连接数
}
void Server::Run()
{
fds[0].fd = listen_fd; // 添加监听描述符
fds[0].events = POLLIN;
nfds = 0;
for(int i=1; i
客户端与select的例子一样,这里不再列出了。
四、epoll示例
4.1 基本原理
epoll是在2.6内核中提出的,相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
epoll使用“事件”的就绪通知方式,通过epoll_ctl
注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait
便可以收到通知。epoll的优点在于:
- 没有最大并发连接的限制,能打开的fd上限远大于1024(1G的内存上能监听约10万个端口)
- 采用回调的方式,效率提升。只有活跃可用的fd才会调用callback函数,也就是说 epoll 只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,epoll的效率就会远远高于select和poll。
- 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。
epoll对文件描述符的操作有两种模式:LT(level trigger,水平触发)和ET(edge trigger,边缘触发)。二者的区别如下:
水平触发:默认工作模式,即当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序可以不立即处理该事件;下次调用epoll_wait时,会再次通知此事件。
边缘触发:当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次通知此事件。(直到你做了某些操作导致该描述符变成未就绪状态了,也就是说边缘触发只在状态由未就绪变为就绪时通知一次)
边缘触发(ET模式)在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞socket,以避免由于一个文件描述符的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
4.2 相关函数
epoll操作过程涉及三个函数:
#include
int epoll_create(int size);
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);
1) epoll_create函数用来创建一个epoll句柄,参数size用来告诉内核要监听的数目会有多少个。(现在size参数已经不再需要了,内核会动态调整分配的空间大小,但这个参数必须大于0,所以一般设置成1即可)
- 成功时返回一个文件描述符,表示epoll句柄(最后也需要close关闭)
- 失败时返回-1
2)epoll_ctl函数用于注册要监听的事件类型,它有四个参数:
- 第一个参数 epfd 表示epoll句柄,即epoll_create()的返回值;
- 第二个参数表示对fd的操作类型,
- EPOLL_CTL_ADD(注册新的fd到epfd中)
- EPOLL_CTL_MOD(修改已注册的fd的监听事件)
- EPOLL_CTL_DEL(从epfd中删除一个fd)
- 第三个参数是需要监听的fd
- 第四个参数是告诉内核需要监听什么事件
其中struct epoll_event结构体定义如下:
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 可以是以下几个宏的集合:
- EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
- EPOLLOUT:表示对应的文件描述符可以写;
- EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
- EPOLLERR:表示对应的文件描述符发生错误;
- EPOLLHUP:表示对应的文件描述符被挂断;
- EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
- EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
3)epoll_wait
函数等待事件的就绪,成功时返回就绪的事件数目,调用失败时返回 -1,等待超时返回 0。它也有四个参数:
- 第一个参数 epfd 即epoll句柄;
- 第二个参数 events 用来从内核得到就绪事件的集合;
- 第三个参数 maxevents 告诉内核这个 events 有多大;
- 第四个参数 timeout 表示等待时的超时时间,以毫秒为单位。
4.3 示例程序
这里写一个程序,Client向Server发送消息,Server接收消息并原样发送给Client,Client再把消息输出到终端。
#include // sockaddr_in
#include // socket
#include // socket
#include
#include
#include // epoll
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define BUFFER_SIZE 1024
#define EPOLLSIZE 100
struct PACKET_HEAD
{
int length;
};
class Server
{
private:
struct sockaddr_in server_addr;
socklen_t server_addr_len;
int listen_fd; // 监听的fd
int epfd; // epoll fd
struct epoll_event events[EPOLLSIZE]; // epoll_wait返回的就绪事件
public:
Server(int port);
~Server();
void Bind();
void Listen(int queue_len = 20);
void Accept();
void Run();
void Recv(int fd);
};
Server::Server(int port)
{
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htons(INADDR_ANY);
server_addr.sin_port = htons(port);
// create socket to listen
listen_fd = socket(PF_INET, SOCK_STREAM, 0);
if(listen_fd < 0)
{
cout << "Create Socket Failed!";
exit(1);
}
int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
}
Server::~Server()
{
close(epfd);
}
void Server::Bind()
{
if(-1 == (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr))))
{
cout << "Server Bind Failed!";
exit(1);
}
cout << "Bind Successfully.\n";
}
void Server::Listen(int queue_len)
{
if(-1 == listen(listen_fd, queue_len))
{
cout << "Server Listen Failed!";
exit(1);
}
cout << "Listen Successfully.\n";
}
void Server::Accept()
{
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int new_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);
if(new_fd < 0)
{
cout << "Server Accept Failed!";
exit(1);
}
cout << "new connection was accepted.\n";
// 在epfd中注册新建立的连接
struct epoll_event event;
event.data.fd = new_fd;
event.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, new_fd, &event);
}
void Server::Run()
{
epfd = epoll_create(1); // 创建epoll句柄
struct epoll_event event;
event.data.fd = listen_fd;
event.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &event); // 注册listen_fd
while(1)
{
int nums = epoll_wait(epfd, events, EPOLLSIZE, -1);
if(nums < 0)
{
cout << "poll() error!";
exit(1);
}
if(nums == 0)
{
continue;
}
for(int i=0; i
注意:
默认情况下,epoll采用 LT 模式;若要采用 ET 模式,调用epoll_ctl的时候在 events 中添加EPOLLET。
对于监听的sockfd,最好使用水平触发模式,边缘触发模式会导致高并发情况下,有的客户端会连接不上。
对于读写的connfd,水平触发模式下,阻塞和非阻塞效果都一样,不过为了防止特殊情况,还是建议设置非阻塞。
对于读写的connfd,边缘触发模式下,必须使用非阻塞fd,并要一次性全部读写完数据(否则会干扰其他事件)。
转自:https://blog.csdn.net/lisonglisonglisong/article/details/51328062
我建了一个QQ群《游戏动漫》,大家可以一起来交流技术:213571088