多路转接技术是在不断更新进步的,一开始多路转接采用的是select方案,但是select方案存在的缺点比较多,所以在此基础上改进,产生了poll方案。poll是多路转接的另一种方案,它使用起来比select方案简单很多,也比较好用。
poll函数原型:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
select方案的最大缺点就在于它的输入和输出都是用同一个参数,比如我们将设置好的需要等待的文件描述符集合输入进去使用的是readfds这个参数,它将文件描述符集合中读事件就绪的文件描述符输出出来也是使用readfds这个参数。这就导致我们每一次都需要重置参数,所以这给我们使用select增加了很大的成本。并且fd_set是位图结构,所以就导致select函数可以检测的文件描述符数量是有上限的。
针对select方案的上述缺点,poll进行了改进:poll的参数struct pollfd *fds
其实是一个结构体数组,它里面的结构如下图所示:
其中fd表示要等待的文件描述符是什么,events表示我们要传递进去的等待事件是什么,revents表示内核给我们传递出来的事件。这个结构和select就有很大的差别,它把原来select的readfds拆分成了两个参数,将输入和输出进行了分离。第二个参数nfds_t nfds
其实就是一个整数,代表结构体数组的元素个数。第三个参数int timeout
代表等待时间,当该参数设置为-1时代表永久阻塞,当该参数设置为0时代表非阻塞,当该参数设置大于0时代表在规定时间内阻塞,超时之后返回0。
pollfd结构体中的events和revents可以设置不同的事件,它们常用的取值有:
我们可以编写一个poll方案的多路转接服务器,来演示一下poll函数接口的使用:
Sock.hpp:
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
class Sock
{
public:
static const int gbacklog = 20;
static int Socket()
{
int listenSock = socket(PF_INET, SOCK_STREAM, 0);
if (listenSock < 0)
{
exit(1);
}
int opt = 1;
setsockopt(listenSock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
return listenSock;
}
static void Bind(int socket, uint16_t port)
{
struct sockaddr_in local; // 用户栈
memset(&local, 0, sizeof local);
local.sin_family = PF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = INADDR_ANY;
// 2.2 本地socket信息,写入sock_对应的内核区域
if (bind(socket, (const struct sockaddr *)&local, sizeof local) < 0)
{
exit(2);
}
}
static void Listen(int socket)
{
if (listen(socket, gbacklog) < 0)
{
exit(3);
}
}
static int Accept(int socket, std::string *clientip, uint16_t *clientport)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int serviceSock = accept(socket, (struct sockaddr *)&peer, &len);
if (serviceSock < 0)
{
// 获取链接失败
return -1;
}
if(clientport) *clientport = ntohs(peer.sin_port);
if(clientip) *clientip = inet_ntoa(peer.sin_addr);
return serviceSock;
}
};
PollServer.cc:
#include
#include
#include "Sock.hpp"
#define NUM 1024
struct pollfd fdsArray[NUM]; // 保存历史上所有的合法fd
#define DFL -1
using namespace std;
static void showArray(struct pollfd arr[], int num)
{
cout << "当前合法sock list# ";
for (int i = 0; i < num; i++)
{
if (arr[i].fd == DFL)
continue;
else
cout << arr[i].fd << " ";
}
cout << endl;
}
static void usage(std::string process)
{
cerr << "\nUsage: " << process << " port\n"
<< endl;
}
// readfds: 现在包含就是已经就绪的sock
static void HandlerEvent(int listensock)
{
for (int i = 0; i < NUM; i++)
{
if (fdsArray[i].fd == DFL)
continue;
if (i == 0 && fdsArray[i].fd == listensock)
{
// 我们是如何得知哪些fd,上面的事件就绪呢?
if (fdsArray[i].revents & POLLIN)
{
// 具有了一个新链接
cout << "已经有一个新链接到来了,需要进行获取(读取/拷贝)了" << endl;
string clientip;
uint16_t clientport = 0;
int sock = Sock::Accept(listensock, &clientip, &clientport); // 不会阻塞
if (sock < 0)
return;
cout << "获取新连接成功: " << clientip << ":" << clientport << " | sock: " << sock << endl;
// read/write -- 不能,因为你read不知道底层数据是否就绪!!select知道!
// 想办法把新的fd托管给select?如何托管??
int i = 0;
for (; i < NUM; i++)
{
if (fdsArray[i].fd == DFL)
break;
}
if (i == NUM)
{
cerr << "我的服务器已经到了最大的上限了,无法在承载更多同时保持的连接了" << endl;
close(sock);
}
else
{
fdsArray[i].fd = sock; // 将sock添加到select中,进行进一步的监听就绪事件了!
fdsArray[i].events = POLLIN;
fdsArray[i].revents = 0;
showArray(fdsArray, NUM);
}
}
} // end if (i == 0 && fdsArray[i] == listensock)
else
{
// 处理普通sock的IO事件!
if(fdsArray[i].revents & POLLIN)
{
// 一定是一个合法的普通的IO类sock就绪了
// read/recv读取即可
// TODO bug
char buffer[1024];
ssize_t s = recv(fdsArray[i].fd, buffer, sizeof(buffer), 0); // 不会阻塞
if(s > 0)
{
buffer[s] = 0;
cout << "client[" << fdsArray[i].fd << "]# " << buffer << endl;
}
else if(s == 0)
{
cout << "client[" << fdsArray[i].fd << "] quit, server close " << fdsArray[i].fd << endl;
close(fdsArray[i].fd);
fdsArray[i].fd = DFL; // 去除对该文件描述符的select事件监听
fdsArray[i].events = 0;
fdsArray[i].revents = 0;
showArray(fdsArray, NUM);
}
else
{
cout << "client[" << fdsArray[i].fd << "] quit, server error " << fdsArray[i].fd << endl;
close(fdsArray[i].fd);
fdsArray[i].fd = DFL; // 去除对该文件描述符的select事件监听
fdsArray[i].events = 0;
fdsArray[i].revents = 0;
showArray(fdsArray, NUM);
}
}
}
}
}
// ./SelectServer 8080
// 只关心读事件
int main(int argc, char *argv[])
{
if (argc != 2)
{
usage(argv[0]);
exit(1);
}
// 是一种类型,位图类型,能定义变量,那么就一定有大小,就一定有上限
// fd_set fds; // fd_set是用位图表示多个fd的
// cout << sizeof(fds) * 8 << endl;
int listensock = Sock::Socket();
Sock::Bind(listensock, atoi(argv[1]));
Sock::Listen(listensock);
for (int i = 0; i < NUM; i++)
{
fdsArray[i].fd = DFL;
fdsArray[i].events = 0;
fdsArray[i].revents = 0;
}
fdsArray[0].fd = listensock;
fdsArray[0].events = POLLIN;
int timeout = -1;
while (true)
{
int n = poll(fdsArray, NUM, timeout);
switch (n)
{
case 0:
cout << "time out ... : " << (unsigned long)time(nullptr) << endl;
break;
case -1:
cerr << errno << " : " << strerror(errno) << endl;
break;
default:
HandlerEvent(listensock);
// 等待成功
// 1. 刚启动的时候,只有一个fd,listensock
// 2. server 运行的时候,sock才会慢慢变多
// 3. select 使用位图,采用输出输出型参数的方式,来进行 内核<->用户 信息的传递, 每一次调用select,都需要对历史数据和sock进行重新设置!!!
// 4. listensock,永远都要被设置进readfds中!
// 5. select 就绪的时候,可能是listen 就绪,也可能是普通的IO sock就绪啦!!
break;
}
}
return 0;
}
poll方案的优点:
poll方案的多路转接不同于select方案的多路转接使用三个位图来表示三个fdset的方式,poll使用一个pollfd的指针来实现,这个指针其实是一个结构体数组。
pollfd结构体里包含了要监视的event和发送的event,不再使用select的那种输入输出采用同一个参数的方式,因此poll函数接口使用起来比select要简单方便。
除此之外,poll方案并没有最大的数量限制,pollfd这个结构体数组的元素个数是由我们用户自己决定的,它不像select那样采用位图结构,规定了最大数量限制就是1024。但是poll方案数量过大之后性能也是会下降的。
poll方案的缺点:
poll函数和select函数一样,poll函数返回后都需要轮询检测pollfd来获取就绪的文件描述符,当pollfd中监听的文件描述符数目增多时,性能也会下降。
每次调用poll函数都需要把大量的pollfd结构从用户态拷贝到内核态,这也会因为pollfd结构数组中元素个数过多时导致性能下降。
同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的文件描述符数量的增长,其效率也会线性下降。