提示:以下是本篇文章正文内容,下面案例可供参考
多路转接天然的是让我们可以依次等待多个文件描述符.
什么叫做文件描述符状态的变化 —>1.可读 2.可写 3.异常
系统提供select函数来实现多路复用输入/输出模型.
如下所示:
参数timeout取值:
fd_set是一种位图结构.**比特位的位置代表fd的编号,比特位的内容代表(就绪/未就绪)**的概念.
提供了一组fd_set的接口,来比较方便的操作位图.
void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位
int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd 的位是否为真
void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位
查看fd_set的大小
因为fd_set是位图结构,求出来的结果是128字节,所以对于bit为就是1024.
timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0.
错误值可能为:
makefile
SelectServer:SelectServer.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm SelectServer
Sock.hpp
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
class Sock
{
public:
static const int gBackLog = 20;
static int Socket()
{
// 1.创建socket
int _listenSock = socket(AF_INET, SOCK_STREAM, 0);
if (_listenSock < 0)
{
exit(1);
}
int opt = 1;
setsockopt(_listenSock,SOL_SOCKET,SO_REUSEADDR | SO_REUSEPORT, &opt,sizeof opt);
}
static void Bind(int socket, uint16_t _port)
{
// 2.bind绑定
// 2.1填充服务器
struct sockaddr_in local; // 用户栈
memset(&local, 0, sizeof local);
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
// 2.2本地socket信息,写入_sock对应的内核区域
if (bind(socket, (const sockaddr *)&local, sizeof local) < 0)
{
exit(2);
}
}
static void Listen(int socket)
{
// 3.监听socket,为何要监听呢?tcp是面向连接的!
if (listen(socket, gBackLog) < 0)
{
exit(3);
}
// 允许别人来连接你了
}
static int Accept(int socket,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;
}
};
SelectServer.cc
#include
#include
#include
#include "Sock.hpp"
using namespace std;
// 保存历史上所有的合法fd
#define NUM (sizeof(fd_set) * 8)
int fdsArray[NUM] = {0};
#define DFL -1
static void Usage(string process)
{
cerr << "\nUsage: " << process << "port\n"
<< endl;
}
static void showArray(int arr[], int num)
{
cout << "当前合法sock list # ";
for (int i = 0; i < num; ++i)
{
if (arr[i] == DFL)
continue;
else
cout << arr[i] << " ";
}
cout << endl;
}
// redfds : 现在包含的就是已经就绪的sock
static void HandlerEvent(int listensock, fd_set &readfds)
{
for (int i = 0; i < NUM; ++i)
{
if (fdsArray[i] == DFL)
continue;
if (i == 0 && fdsArray[i] == listensock)
{
// 如何得知那些fd上面的事件就绪了呢?
if (FD_ISSET(listensock, &readfds))
{
// 具有了一个新链接
cout << "已经有一个新链接到来了,需要进行获取了..." << endl;
string ip;
uint16_t port;
int sock = Sock::Accept(listensock, &ip, &port); // 这里不会阻塞
if (sock < 0)
return;
cout << "获取新链接成功 : " << ip << " : " << port << " sock: " << sock << endl;
// read/wirte --- 不能调用,因为你read不知道底层数据是否就绪!!!
// secelt知道! 想办法把新的fd托管给select
int i = 0;
for (; i < NUM; ++i)
{
if (fdsArray[i] == DFL)
break;
}
if (i == NUM)
{
cerr << "我的服务器已经到了最大的上限了,无法在承载更多的连接了..." << endl;
close(sock);
}
else
{
// 将sock添加到select中,进一步的监听就绪事件了!
fdsArray[i] = sock;
showArray(fdsArray, NUM);
}
}
}
else
{
// 处理普通sock的IO事件!
if (FD_ISSET(fdsArray[i], &readfds))
{
// 一定是一个合法的普通的IO类sock就绪了
// read/recv读取即可
char buffer[1024];
ssize_t s = recv(fdsArray[i], buffer, sizeof buffer, 0);
if (s > 0)
{
buffer[s] = 0;
cout << "client[" << fdsArray[i] << "]# " << buffer << endl;
}
else if (s == 0)
{
cout << "client[" << fdsArray[i] << "] quit,server close: " << fdsArray[i] << endl;
close(fdsArray[i]);
fdsArray[i] = DFL; // 取出对该文件描述符的select的事件监听
}
else
{
cout << "client[" << fdsArray[i] << "] quit,server close: " << fdsArray[i] << endl;
close(fdsArray[i]);
fdsArray[i] = DFL; // 取出对该文件描述符的select的事件监听
showArray(fdsArray, NUM);
}
}
}
}
}
// ./SelectServer 8080
// 只关心读事件
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(1);
}
// fd_set fds; // fa_set是用位图表示多个fd的.
// cout<
int listensock = Sock::Socket();
Sock::Bind(listensock, 8080);
Sock::Listen(listensock);
for (int i = 0; i < NUM; ++i)
fdsArray[i] = DFL;
fdsArray[0] = listensock;
while (true)
{
int maxFd = -1;
fd_set readfds;
FD_ZERO(&readfds);
for (int i = 0; i < NUM; ++i)
{
if (fdsArray[i] == DFL) // 过滤不合法的fd
continue;
FD_SET(fdsArray[i], &readfds); // 添加所有的合法的fd到readfds中,方便select统一进行就绪监听
if (maxFd < fdsArray[i]) // 更新最大值
maxFd = fdsArray[i];
}
struct timeval timeout = {5, 0};
// 如何看待监听socket, 获取新链接的, 本质是需要先三次握手!
// 前提是给我发送syn -? 建立连接的本质,其实也是IO,一个建立好的连接,我们成为读时间就绪!
// accept: 等+"数据拷贝"
// string ip;
// uint16_t port;
// int sock = Sock::Accept(listensock,&ip,&port);
// 编写多路转接代码时,必须先保证条件就绪了,才能调用IO类函数!
int n = select(maxFd + 1, &readfds, nullptr,
nullptr, &timeout);
switch (n)
{
case 0:
cout << "time out ..." << (unsigned long long)time(nullptr) << endl;
break;
case -1:
cerr << errno << " : " << strerror(errno) << endl;
break;
default:
// 等待成功
// 1.刚启动的时候,只有一个fd,listenscok
// 2.server 运行的时候,sock才会慢慢变多
// 3.select 使用位图,采用输入输出型参数的方式,来进行 内核<->用户 信息的传递.
// 每一次调用select,都需要对历史数据和sock进行重新设置!
// 4.listensock, 永远都要被设置进readfds中!
// 5.select就绪的时候,可能是listen就绪,也可能是普通的IO sock就绪!!
HandlerEvent(listensock, readfds);
break;
}
}
return 0;
}
select编码特征
优点:占用资源少,并且高效.对比之前的多线程,多进程.
缺点:每一次都要进行大量的重置工作,效率比较低.
每一次能够检测的fd数量是有上限的.
每一次都需要内核到用户,用户到内核传递位图参数,有较为大量的数据拷贝
select编码特别不方便,需要用户自己维护数组
select底层需要遍历的方式,检测所需要检测的fd
参数说明:
events和revents的取值
和select相同.
不同于select使用三个位图来表示三个fdset的方式,poll使用一个pollfd的指针实现.
poll监听的文件描述符数目增多时
makefile
PollServer:PollServer.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm PollServer
#include
#include
#include
#include "Sock.hpp"
using namespace std;
// 保存历史上所有的合法fd
#define NUM 1024
struct pollfd fdsArray[NUM] = {0};
#define DFL -1
// 意义:不光是网络sock,本地的文件描述符也可以被托管给多路转接,那么后面的文件操作,管道等也可以直接对接到多路转接!!!
static void Usage(string process)
{
cerr << "\nUsage: " << process << "port\n"
<< endl;
}
static void showArray(int arr[], int num)
{
cout << "当前合法sock list # ";
for (int i = 0; i < num; ++i)
{
if (arr[i] == DFL)
continue;
else
cout << arr[i] << " ";
}
cout << endl;
}
// redfds : 现在包含的就是已经就绪的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 ip;
uint16_t port;
int sock = Sock::Accept(listensock, &ip, &port); // 这里不会阻塞
if (sock < 0)
return;
cout << "获取新链接成功 : " << ip << " : " << port << " sock: " << sock << endl;
// read/wirte --- 不能调用,因为你read不知道底层数据是否就绪!!!
// secelt知道! 想办法把新的fd托管给select
int i = 0;
for (; i < NUM; ++i)
{
if (fdsArray[i].fd == DFL)
break;
}
if (i == NUM)
{
cerr << "我的服务器已经到了最大的上限了,无法在承载更多的连接了..." << endl;
close(sock);
}
else
{
// 将sock添加到select中,进一步的监听就绪事件了!
fdsArray[i].fd = sock;
fdsArray[i].events = POLLIN;
fdsArray[i].revents = 0;
}
}
}
else
{
// 处理普通sock的IO事件!
if (fdsArray[i].revents & POLLIN)
{
// 一定是一个合法的普通的IO类sock就绪了
// read/recv读取即可
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的事件监听
}
else
{
cout << "client[" << fdsArray[i].fd << "] quit,server error: " << fdsArray[i].fd << endl;
close(fdsArray[i].fd);
fdsArray[i].fd = DFL; // 取出对该文件描述符的select的事件监听
}
}
}
}
}
// ./SelectServer 8080
// 只关心读事件
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(1);
}
// fd_set fds; // fa_set是用位图表示多个fd的.
// cout<
int listensock = Sock::Socket();
Sock::Bind(listensock, 8080);
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 = 1000;
while (true)
{
int n = poll(fdsArray,NUM,timeout);
switch (n)
{
case 0:
cout << "time out ..." << (unsigned long long)time(nullptr) << endl;
break;
case -1:
cerr << errno << " : " << strerror(errno) << endl;
break;
default:
HandlerEvent(listensock);
break;
}
}
return 0;
}
Sock.hpp的代码和select当时的代码相同!!!
那么现在的多路转接还有什么问题?
按照man手册的说法:是为处理大批量句柄而作了改进的poll.
他是在2.5.44内核中引进的(epoll(4) is a nwe API introduced in Linux kernel 2.5.44).
他几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路IO就绪通知方法.
epoll有三个相关的系统调用.
无论有多个接口,核心工作:只负责等!
第二个参数的取值:
struct epoll_event的结构如下:
epoll_data_t的结构如下:
events可以是以下几个宏的集合:
收集在epoll监控的事件中已经发送的事件:
操作系统如何得知,网络中的数据到来了呢?
网卡先得到数据会向CPU发送硬件中断,调用OS预设的中断函数,负责从外设进行数据拷贝.
epoll函数针对特定的一个或者多个fd,设定对应的回调机制;当fd缓冲区有数据的时候,进行回调.
当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关.
....
/* 红黑树的根节点,这棵树中存储着所有添加到epoll中的需要监控的事件*/
struct rb_root rbr;
/* 双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
struct list_head rdlist;
....
struct epitem{
struct rb_node rbn;//红黑树节点
struct list_head rdllink;//双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所属的eventpoll对象
struct epoll_event event; //期待发生的事件类型
}
总结一下,epoll的使用过程就是三部曲:
注意!!!
网上有些博客说,epoll中使用了内存映射机制
这种说法是不准确的.我们定义的struct epoll_event是我们在用户空间中分配好的内存.势必还是需要将内核的数据拷贝到这个用户空间的内存中的.
epoll有两种工作方式-水平触发(LT)和边缘触发(ET).
加入有这样一个例子
epoll默认状态下就是LT工作模式.
如果我们在第一步将socket添加到epoll描述符的时候使用了EPOLLET表示,epoll进入ET工作模式.
select和poll其实也是工作在LT模式下,epoll既可以支持LT,也可以支持ET.
LT是epoll的默认行为.使用ET能够减少epoll触发的次数.但是代价就是强逼着程序猿一次响应就绪过程中就把所有的数据都处理完.
相当于一个文件描述符就绪以后,不会反复被提示就绪,看起来就比LT更高效一些.但是在LT情况下如果也能做到每次就绪的文件描述符都立即处理,不让这个就绪被重复提示的话,其实性能也是一样的.
另一方面,ET的代码复杂程度更高了!
使用ET模式的epoll,需要将文件描述设置为非阻塞.这个不是接口上的要求,而是"工程实践"上的要求.
假设这样的场景:服务器接收到一个10K的请求,会向客户端返回一个应答数据.如果客户端收不到应答,不会发送第二个10K请求.
如果服务器写的代码是阻塞式的read,并且一次只read1K的数据的话(read不能保证一次就把所有的数据都读出来,参考man手册的说明,可能信号被打断),剩下的9K数据就会待在缓冲区中.
此时由于epoll是ET模式,并不会认为文件描述符就绪.epoll_wait就不会再次返回.剩下的9K数据会一直在缓冲区中.直到下一次客户端再给服务器写数据.epoll_wait才能返回.
但是问题来了.
所以为了解决上述问题(阻塞read不一定能一下把完整的请求读完),于是就可以使用非阻塞轮询的方式来读缓冲区,保证一定能把完整的请求都读出来.
如果是LT没这个问题.只要缓冲区中的数据没读完,就能够让epoll_wait返回文件描述符读就绪.
epoll的高性能,是有一定的特定场景的.如果场景选择不适宜的话,epoll的性能可能适得其反.
例如,典型的一个需要处理上万个客户端的服务器,例如各种互联网APP的入口服务器,这样的服务器就很适合epoll.
如果只是系统内部,服务器和服务器之间进行通信,只有少数的几个连接,这种情况下用epoll就并不合适.具体要根据需求和场景特点来决定使用哪种IO模型.
makefile
main:main.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm main
log.hpp
/*
* @Author: hulu [email protected]
* @Date: 2022-11-28 16:18:12
* @LastEditors: hulu [email protected]
* @LastEditTime: 2022-12-05 11:47:11
* @FilePath: /udp/log.hpp
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3
const char* log_level[]={"DEBUG","NOTICE","WARINING","FATAL"};
#define LOGFIFE "ServerTcp.log"
class Log
{
public:
Log():_logFd((-1))
{}
~Log()
{
if(_logFd!=-1)
{
fsync(_logFd);//将操作系统中的数据尽快刷盘
close(_logFd);
}
}
void enable()
{
_logFd=open(LOGFIFE,O_WRONLY|O_APPEND|O_CREAT,0666);
assert(_logFd!=-1);
dup2(_logFd,0);
dup2(_logFd,1);
dup2(_logFd,2);
}
private:
int _logFd;
};
//logMessage(DEBUG,"%d",10);
void logMessage(int level,const char* format,...)
{
assert(level>=DEBUG);
assert(level<=FATAL);
char logInfo[1024];
char* name=getenv("USER");
va_list ap; //ap--->char*
va_start(ap,format);
vsnprintf(logInfo,sizeof(logInfo)-1,format,ap);
va_end(ap); //ap=NULL
FILE* out=(level==FATAL)?stderr:stdout;
fprintf(out,"%s | %u | %s | %s\n",\
log_level[level],(unsigned int)time(nullptr),\
name==nullptr?"unknow":name,logInfo);
fflush(out);//将C缓冲区中的数据刷新到OS
fsync(fileno(out)); // 将OS中的数据尽快刷盘
}
Sock.hpp
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
class Sock
{
public:
static const int gBackLog = 20;
static int Socket()
{
// 1.创建socket
int _listenSock = socket(AF_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 = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
// 2.2本地socket信息,写入_sock对应的内核区域
if (bind(socket, (const sockaddr *)&local, sizeof local) < 0)
{
exit(2);
}
}
static void Listen(int socket)
{
// 3.监听socket,为何要监听呢?tcp是面向连接的!
if (listen(socket, gBackLog) < 0)
{
exit(3);
}
// 允许别人来连接你了
}
static int Accept(int socket,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;
}
};
EpollServer.hpp
#pragma once
#include
#include
#include
#include
#include "log.hpp"
#include "Sock.hpp"
using namespace std;
#define NUM 1024
class EpollServer
{
public:
using func_t = function<int(int)>;
EpollServer(uint16_t port,func_t func)
: _port(port),_func(func)
{
}
~EpollServer()
{
if (_listenSock != -1)
close(_listenSock);
if (_epFd != -1)
close(_epFd);
}
void InitEpollServer()
{
_listenSock = Sock::Socket();
Sock::Bind(_listenSock, _port);
Sock::Listen(_listenSock);
// 这里直接使用原生接口
_epFd = epoll_create(NUM);
if (_epFd < 0)
{
logMessage(FATAL, "%d:%s", errno, strerror(errno));
exit(4);
}
logMessage(DEBUG, "创建监听套接字成功:%d", _listenSock);
logMessage(DEBUG, "创建epoll成功:%d", _epFd);
}
void Run()
{
// 1.先添加_listenSock
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = _listenSock;
int n = epoll_ctl(_epFd, EPOLL_CTL_ADD, _listenSock, &ev);
assert(n == 0);
(void)n;
struct epoll_event revs[NUM];
int timeout = 1000;
while (true)
{
// 关于n: 就绪的fd的个数,只需要进行将底层的就绪队列中节点,一次从0下标放入到revs中即可
int n = epoll_wait(_epFd, revs, NUM, timeout);
switch (n)
{
case 0:
cout << "time out ..." << (unsigned long long)time(nullptr) << endl;
break;
case -1:
cerr << errno << " : " << strerror(errno) << endl;
break;
default:
HandlerEvents(revs, n);
break;
}
}
}
void HandlerEvents(struct epoll_event revs[], int n)
{
for (int i = 0; i < n; ++i)
{
int sock = revs[i].data.fd;
uint32_t event = revs[i].events;
if (event & EPOLLIN) // 读就绪就绪
{
if (sock == _listenSock) // 监听socket就绪,获取新链接
{
string ip;
uint16_t port;
int sockfd = Sock::Accept(_listenSock, &ip, &port);
if (sockfd < 0)
{
logMessage(WARINING, "%d:%s", errno, strerror(errno));
continue;
}
// 托管给Epoll
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sockfd;
int n = epoll_ctl(_epFd, EPOLL_CTL_ADD, sockfd, &ev);
assert(n == 0);
(void)n;
}
else // 普通socket就绪,进行数据INPUT
{
int n = _func(sock);
if(n == 0 || n<0)
{
// 文件描述符先移除在关闭
int x =epoll_ctl(_epFd,EPOLL_CTL_DEL,sock,nullptr);
assert(x == 0);
(void)x;
logMessage(DEBUG,"client quit: %d",sock);
close(sock);
}
}
}
else
{
}
}
}
private:
int _listenSock = -1;
int _epFd = -1;
uint16_t _port;
func_t _func;
};
main.cc
#include "EpollServer.hpp"
#include
static void Usage(string process)
{
cerr << "\nUsage: " << process << "\tport\n"
<< endl;
}
int myfunc(int sock)
{
char buffer[NUM];
ssize_t s = recv(sock, buffer,sizeof(buffer)-1,0); // 不会被阻塞
if(s > 0)
{
buffer[s] = 0;
logMessage(DEBUG,"client[%d] #:%s",sock,buffer);
}
return s;
}
// ./SelectServer 8080
// 只关心读事件
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(1);
}
unique_ptr<EpollServer> epollServer(new EpollServer(atoi(argv[1]), myfunc));
epollServer->InitEpollServer();
epollServer->Run();
}
对于IO多路复用的三个函数就介绍到这里了,下一篇博客我们基于ET模式下的epoll服务器,也加Reactor模式.
(本章完!)