目录
多路转接
多路转接是编写大型应用时,必定要才用到的一种技术。早期的多路转接方案是select方案,select方案面向与/适用于中小型应用,目前使用的多路转接方案为epoll方案。
select
select概述
select是操作系统提供的进行多路转接的一个接口。select系统调用是用来让程序监视多个文件描述符状态变化的。我们使用文件描述符关心的就是可否通过该文件描述符进行读写和此时该文件描述符有没有异常状态,从而进行处理。select系统调用一次性可以传递多个文件描述符,调用该select系统调用时,进程会在该系统调用处等待,知道我们传递的多个fd中,有一个或者多个文件描述符的状态发生了变化,即一个或多个文件描述符需要等待的事件就绪了,此时,此进程从select系统调用出返回。
select的作用
因为:IO=等+拷贝。select系统调用负责IO中等待的工作,拷贝的工作由recv、send、read、write这样的输入输出拷贝接口完成。
select系统调用只负责进行对多个文件描述符进行等待,等待的过程中,只要有事件就绪了,就进行事件的派发。
select系统调用可以同时对多个文件描述符进行等待。
select系统调用介绍
Select服务器代码示例
// main.cpp
// select-server
#include <iostream>
#include <string>
#include "SelectServer.hpp"
using namespace std;
// ./select-server port
int main(int argc, char *argv[])
{
if (argc != 2)
{
cout << "正确用法: " << argv[0] << " port" << endl;
return 0;
}
// 拿到端口号
int16_t port = stoi(argv[1]);
SelectServer svr(port);
svr.Loop(); // 启动select-server
return 0;
}
// Select.hpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
using namespace std;
class SelectServer
{
public:
static const int N = sizeof(fd_set) * 8;
static const int defaultfd = -1;
// TCP工作
void GetTcpsock(int port)
{
// 构建网络信息
_serveraddr.sin_family = AF_INET;
_serveraddr.sin_addr.s_addr = inet_addr("0.0.0.0");
_serveraddr.sin_port = htons(port);
// 创建套接字
_listensockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_listensockfd < 0)
{
cout << "套接字创建失败" << endl;
exit(-1);
}
cout << "套接字创建成功-sockfd:" << _listensockfd << endl;
// 设置地址复用
int opt = 1;
setsockopt(_listensockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
// 绑定
int n = bind(_listensockfd, (struct sockaddr *)&_serveraddr, sizeof(_serveraddr));
if (n < 0)
{
cout << "绑定失败" << endl;
exit(-1);
}
cout << "绑定成功" << endl;
// 设置监听状态
static const int gbacklog = 8;
n = listen(_listensockfd, gbacklog);
if (n < 0)
{
cout << "监听状态设置失败" << endl;
}
cout << "监听状态设置成功" << endl;
}
SelectServer(int port)
{
GetTcpsock(port);
// // 构建网络信息
// _serveraddr.sin_family = AF_INET;
// _serveraddr.sin_addr.s_addr = inet_addr("0.0.0.0");
// _serveraddr.sin_port = htons(port);
// // 创建套接字
// _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
// if (_listensockfd < 0)
// {
// cout << "套接字创建失败" << endl;
// exit(-1);
// }
// cout << "套接字创建成功-sockfd:" << _listensockfd << endl;
// // 设置地址复用
// int opt = 1;
// setsockopt(_listensockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
// // 绑定
// int n = bind(_listensockfd, (struct sockaddr *)&_serveraddr, sizeof(_serveraddr));
// if (n < 0)
// {
// cout << "绑定失败" << endl;
// exit(-1);
// }
// cout << "绑定成功" << endl;
// // 设置监听状态
// static const int gbacklog = 8;
// n = listen(_listensockfd, gbacklog);
// if (n < 0)
// {
// cout << "监听状态设置失败" << endl;
// }
// cout << "监听状态设置成功" << endl;
// 初始化读rfds的辅助数组
for (int i = 0; i < N; i++)
{
_rfd_array[i] = defaultfd;
}
// 将监听套接字放到辅助数组中
_rfd_array[0] = _listensockfd;
// 打印_rfd_array数组
cout << RfdsToString() << endl;
}
// listsockfd-获取连接
void AcceptClient()
{
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int sockfd = accept(_listensockfd, (struct sockaddr *)&clientaddr, &len);
if (sockfd < 0)
{
return;
}
cout << "服务端获取了一个新连接:fd:" << sockfd << "---[ip:" << inet_ntoa(clientaddr.sin_addr) << " port:" << ntohs(clientaddr.sin_port) << "]" << endl;
// 将这个新获取的sockfd插入到_rfds_array中
int pos = 0;
for (; pos < N; pos++)
{
if (_rfd_array[pos] == defaultfd)
{
break;
}
}
if (pos == N)
{
cout << "此服务器已经满载了,此fd关闭 fd:" << sockfd << endl;
::close(sockfd);
// 打印_rfd_array数组
cout << RfdsToString() << endl;
return;
}
else
{
// 将此新fd插入到_rfd_array中
_rfd_array[pos] = sockfd;
cout << "fd:" << sockfd << "已被添加到_rfd_array中" << endl;
// 打印_rfd_array数组
cout << RfdsToString() << endl;
}
}
// sockfd-通信IO
void ServiceIO(int pos)
{
// 缓冲区
char inbuffer[1024];
ssize_t n = recv(_rfd_array[pos], inbuffer, sizeof(inbuffer), 0);
if (n > 0)
{
// 读取成功
inbuffer[n] = '\0';
string send_str = "[Server Echo]#";
send_str += inbuffer;
cout << "获取到信息:" << inbuffer << endl;
send(_rfd_array[pos], send_str.c_str(), send_str.size(), 0);
}
else if (n == 0)
{
// 客户端退出
::close(_rfd_array[pos]);
cout << "客户端退出,fd:" << _rfd_array[pos] << "已被关闭" << endl;
// 清空pos位置的有效的fd
_rfd_array[pos] = defaultfd;
// 打印_rfd_array数组
cout << RfdsToString() << endl;
}
else
{
// 读取失败
::close(_rfd_array[pos]);
cout << "读取失败,fd:" << _rfd_array[pos] << "已被关闭" << endl;
// 清空pos位置的有效的fd
_rfd_array[pos] = defaultfd;
// 打印_rfd_array数组
cout << RfdsToString() << endl;
}
}
// 处理已就绪的rfds
void HanderEvent(fd_set &rfds)
{
for (int i = 0; i < N; i++)
{
// 非法fd
if (_rfd_array[i] == defaultfd)
{
continue;
}
// 合法fd
// 判断有效fd
if (FD_ISSET(_rfd_array[i], &rfds))
{
// 有效的fd
// 判断是listensockfd还是sockfd
if (_listensockfd == _rfd_array[i])
{
// listensockfd
AcceptClient();
}
else
{
// sockfd
ServiceIO(i);
}
}
}
}
// 启动服务
void Loop()
{
while (true)
{
fd_set rfds; // 读文件描述符集合
FD_ZERO(&rfds); // 清空
int max_rfd = defaultfd;
for (int i = 0; i < N; i++)
{
if (_rfd_array[i] == defaultfd)
{
continue;
}
// 合法fd
FD_SET(_rfd_array[i], &rfds);
if (max_rfd < _rfd_array[i])
{
max_rfd = _rfd_array[i];
}
}
struct timeval timeout = {1, 0}; // 等待策略
int n = select(max_rfd + 1, &rfds, nullptr, nullptr, &timeout);
if (n < 0)
{
cout << "select函数错误" << endl;
}
else if (n == 0)
{
// cout << "当前没有rfd就绪 timeout:" << timeout.tv_sec << "." << timeout.tv_usec << endl;
}
else
{
cout << "当前已经有rfd就绪了 select返回值:" << n << endl;
// 处理
HanderEvent(rfds);
}
}
}
// 将所有的合法的fd输出到字符串中
string RfdsToString()
{
string fdstr = "当前_rfd_array数组fd集合### ";
for (int i = 0; i < N; i++)
{
if (_rfd_array[i] == defaultfd)
{
continue;
}
fdstr += to_string(_rfd_array[i]);
fdstr += " ";
}
return fdstr;
}
private:
struct sockaddr_in _serveraddr; // server网络信息
int _listensockfd; // 监听套接字
int _rfd_array[N]; // 读rfds的辅助数组
};
select优点
select服务器可以在单进程模式下,并行的等待多个文件描述符,将多个文件描述符的等待时间进行重叠,提高效率。使用多线程方案时,也可以让多线程等待各自要等待的文件描述符,此时,也可以以多线程方案处理多客户端的请求。这种做法也是可以的,但是适用于小型应用。因为一旦客户端很多,就需要创建很多个线程,这样的话是弊大于利的,但多路转接方案相比多线程就会好很多,因为多路转接只是用单个进程/线程就可以处理多个客户端的请求,并且等待时间还是并行的,效率也高,select系统调用可以以一个单进程接口的方式,等待多个文件描述符,只要select系统调用主动返回,就必定有文件描述符事件就绪,此时,此文件描述符在进行读写时,就不会被阻塞,可以直接进行IO过程中拷贝的操作了。中、大型应用使用的多路转接方案居多。
select缺点
1、同时等待的文件描述符个数有上限,这个有上限是由于select的设计本身使用的fd_set位图类型大小有上限决定的。这是select设计自身时存在的问题。虽然一个进程的文件描述符表中管理的文件描述符的个数有上限,一般为32或者64,这是进程设计的问题,与select设计fd_set位图类型有上限无关,这是操作系统设计此接口的问题。
Linux中,一个进程的文件描述符表的大小是可以动态的进行扩展的,在源代码中支持此操作。云服务器中,一个进程可以打开的文件描述符的上限是65535个,一般一台服务器单进程若能处理2-3万个链接,此服务器就已经很厉害了。所以说,云服务器中一个进程的文件描述符表所能管理的最多文件描述符的个数为65535个,就已经完全够用了。
nginx采用多进程版的多路转接方案,单个服务器每秒就可以处理2-3万个链接。Redis采用单进程的多路转接方案。单进程没有并发问题,可以很好的实现数据库中的原子操作。
2、select系统调用的输入输出参数混合,每次调用select系统调用时,都要对该输入输出参数进行重置。这就导致了代码中的循环遍历操作很多,所以select方案的实现通常借助辅助数组,因为数组在遍历过程中最高效。
3、因为select系统调用使用fd_set位图,是一个输入输出型参数,所以,就性存在用户和内核之间的数据拷贝。这个问题是所有多路转接都存在的问题,此问题无法避免。
4、select系统调用底层监视多个文件描述符时,操作系统会在内部进行遍历检测文件描述符集合中的文件描述符,至少有一个文件描述符就绪就返回,如果没有就绪的文件描述符,就按照策略执行即可。所以select系统调用底层也是遍历,select上层遍历也很多,一旦客户端很多,会大大增加遍历的成本,影响效率。所以:select系统调用第一个参数为最大文件描述符的值+1,此字段为select系统调用内部,底层遍历时的边界,能够稍微优化一下遍历操作。
大部分操作系统都支持、兼容select这种多路转接方案。因为select出现的比较早,所以,旧的操作系统、老的操作系统可能不支持新的多路转接方案,但是一定支持select方案。
poll
poll概述
poll也是多路转接方案的一种。poll的作用与select作用等价。poll也是等待多个文件描述符,关心多个文件描述符上面的事件,poll和select一样,只负责对多个文件描述符进行等待。有事件就绪就进行事件的派发。
poll相比于select来说,改进了select缺点中的:1、同时等待的文件描述符的格式有上限问题。2、输入输出参数混合,每次都要进行重新设定的问题。这两个缺点
poll可以同时等待的文件描述符的个数无上限。
poll可以将输入输出参数做了分离,调用poll之前,不用每次都对输入输出参数做重置。
poll接口介绍
poll服务器示例代码
// main.cpp
// select-server
#include <iostream>
#include <string>
#include "PollServer.hpp"
using namespace std;
// ./select-server port
int main(int argc, char *argv[])
{
if (argc != 2)
{
cout << "正确用法: " << argv[0] << " port" << endl;
return 0;
}
// 拿到端口号
int16_t port = stoi(argv[1]);
PollServer svr(port);
svr.Loop(); // 启动select-server
return 0;
}
//pollServer.hpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>
#include <unistd.h>
using namespace std;
class PollServer
{
public:
static const int N = sizeof(fd_set) * 8;
static const int defaultfd = -1;
// TCP工作
void GetTcpsock(int port)
{
// 构建网络信息
_serveraddr.sin_family = AF_INET;
_serveraddr.sin_addr.s_addr = inet_addr("0.0.0.0");
_serveraddr.sin_port = htons(port);
// 创建套接字
_listensockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_listensockfd < 0)
{
cout << "套接字创建失败" << endl;
exit(-1);
}
cout << "套接字创建成功-sockfd:" << _listensockfd << endl;
// 设置地址复用
int opt = 1;
setsockopt(_listensockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
// 绑定
int n = bind(_listensockfd, (struct sockaddr *)&_serveraddr, sizeof(_serveraddr));
if (n < 0)
{
cout << "绑定失败" << endl;
exit(-1);
}
cout << "绑定成功" << endl;
// 设置监听状态
static const int gbacklog = 8;
n = listen(_listensockfd, gbacklog);
if (n < 0)
{
cout << "监听状态设置失败" << endl;
}
cout << "监听状态设置成功" << endl;
}
PollServer(int port)
{
GetTcpsock(port);
_size = 2;
_rfd_array = (struct pollfd *)malloc(sizeof(struct pollfd) * _size);
if (_rfd_array == nullptr)
{
cout << "初始化数组失败" << endl;
exit(-1);
}
cout << "初始化数组成功,容量:" << _size << endl;
// 初始化读rfds的辅助数组
for (int i = 0; i < _size; i++)
{
_rfd_array[i].fd = defaultfd;
_rfd_array[i].events = 0;
_rfd_array[i].revents = 0;
}
// 将监听套接字放到辅助数组中
_rfd_array[0].fd = _listensockfd;
_rfd_array[0].events = POLLIN;
// 打印_rfd_array数组
cout << RfdsToString() << endl;
}
// listsockfd-获取连接
void AcceptClient()
{
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int sockfd = accept(_listensockfd, (struct sockaddr *)&clientaddr, &len);
if (sockfd < 0)
{
return;
}
cout << "服务端获取了一个新连接:fd:" << sockfd << "---[ip:" << inet_ntoa(clientaddr.sin_addr) << " port:" << ntohs(clientaddr.sin_port) << "]" << endl;
// 将这个新获取的sockfd插入到_rfds_array中
int pos = 0;
for (; pos < _size; pos++)
{
if (_rfd_array[pos].fd == defaultfd)
{
break;
}
}
if (pos == _size)
{
// 此刻就需要扩容了
_size = _size * 2;
_rfd_array = (struct pollfd *)realloc(_rfd_array, sizeof(struct pollfd) * _size);
if (_rfd_array == nullptr)
{
cout << "扩容失败" << endl;
cout << "此服务器已经满载了,此fd关闭 fd:" << sockfd << endl;
::close(sockfd);
return;
}
// 初始化扩容的空间
for (int i = pos; i < _size; i++)
{
_rfd_array[i].fd = defaultfd;
_rfd_array[i].events = 0;
_rfd_array[i].revents = 0;
}
cout << "扩容成功---容量:" << _size << endl;
// 添加
_rfd_array[pos].fd = sockfd;
_rfd_array[pos].events = POLLIN;
cout << "fd:" << sockfd << "已被添加到_rfd_array中" << endl;
// 打印_rfd_array数组
cout << RfdsToString() << endl;
}
else
{
// 将此新fd插入到_rfd_array中
_rfd_array[pos].fd = sockfd;
_rfd_array[pos].events = POLLIN;
cout << "fd:" << sockfd << "已被添加到_rfd_array中" << endl;
// 打印_rfd_array数组
cout << RfdsToString() << endl;
}
}
// sockfd-通信IO
void ServiceIO(int pos)
{
// 缓冲区
char inbuffer[1024];
ssize_t n = recv(_rfd_array[pos].fd, inbuffer, sizeof(inbuffer), 0);
if (n > 0)
{
// 读取成功
inbuffer[n] = '\0';
string send_str = "[Server Echo]#";
send_str += inbuffer;
cout << "获取到信息:" << inbuffer << endl;
send(_rfd_array[pos].fd, send_str.c_str(), send_str.size(), 0);
}
else if (n == 0)
{
// 客户端退出
::close(_rfd_array[pos].fd);
cout << "客户端退出,fd:" << _rfd_array[pos].fd << "已被关闭" << endl;
// 清空pos位置的有效的fd
_rfd_array[pos].fd = defaultfd;
_rfd_array[pos].events = 0;
// 打印_rfd_array数组
cout << RfdsToString() << endl;
}
else
{
// 读取失败
::close(_rfd_array[pos].fd);
cout << "读取失败,fd:" << _rfd_array[pos].fd << "已被关闭" << endl;
// 清空pos位置的有效的fd
_rfd_array[pos].fd = defaultfd;
_rfd_array[pos].events = 0;
// 打印_rfd_array数组
cout << RfdsToString() << endl;
}
}
// 处理已就绪的rfds
void HanderEvent()
{
for (int i = 0; i < _size; i++)
{
// 非法or合法
if (_rfd_array[i].fd == defaultfd)
{
continue;
}
// 合法fd
// 是否读就绪
if (_rfd_array[i].revents & POLLIN)
{
if (_rfd_array[i].fd == _listensockfd)
{
// listensockfd
AcceptClient();
}
else
{
// sockfd
ServiceIO(i);
}
}
// 是否读就绪
if (_rfd_array[i].revents & POLLOUT)
{
}
}
}
// 启动服务
void Loop()
{
while (true)
{
int timeout = 1000; // 等待策略
int n = poll(_rfd_array, _size, timeout);
if (n < 0)
{
cout << "poll函数错误" << endl;
}
else if (n == 0)
{
// cout << "当前没有rfd就绪 timeout:" << timeout.tv_sec << "." << timeout.tv_usec << endl;
}
else
{
cout << "当前已经有rfd就绪了 poll返回值:" << n << endl;
// 处理
HanderEvent();
}
}
}
// 将所有的合法的fd输出到字符串中
string RfdsToString()
{
string fdstr = "当前_rfd_array数组fd集合### ";
for (int i = 0; i < _size; i++)
{
if (_rfd_array[i].fd == defaultfd)
{
continue;
}
if (_rfd_array[i].fd > 0)
{
fdstr += to_string(_rfd_array[i].fd);
fdstr += " ";
}
}
fdstr += "------容量";
fdstr += to_string(_size);
return fdstr;
}
private:
struct sockaddr_in _serveraddr; // server网络信息
int _listensockfd; // 监听套接字
struct pollfd *_rfd_array; // 读rfds的辅助数组
int _size; // rfds的辅助数组最大容量
};
poll的缺点
1、poll也需要将数据从用户到内核、内核到用户的拷贝。用户到内核的拷贝次数少,新增文件描述符时拷贝一次即可。但每次poll返回时,都有内核到用户的数据拷贝。
2、poll底层也是使用循环遍历的方式,对文件描述符集合进行检测。这种做法还是效率不高。
epoll
epoll概述
按照man手册说法:epoll是为了处理大批量的句柄而做了改进的poll。
句柄:凡是可以进行标定资源的,都是句柄。比如:FILE*、文件描述符fd、套接字sockfd都可以标记资源,所以这些都是句柄。
从文件角度上来说:epoll是为了处理大量的文件描述符而改进的poll。
epoll与poll相差甚大,epoll是Linux 2.5.44内核中被引进的,epoll几乎具备了之前select、poll的一切优点,被公认为Linux 2.6下性能最好的多路转接IO就绪的通知方法。
epoll是目前效率最高的多路转接方案,Rides、nginx底层都使用的是epoll,很多软件在处理大量句柄时,都采用epoll方案。
epoll接口介绍
epoll原理
epoll底层图
epoll服务器代码示例
// main.cpp
#include "epollServer.hpp"
int main(int argc, char *argv[])
{
if (argc != 2)
{
cout << "正确用法:" << argv[0] << " port" << endl;
exit(-1);
}
int16_t port = stoi(argv[1]); // 端口号
epollServer svr(port);
svr.Loop(); // 启动
return 0;
}
// epollServer.hpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>
#include <unistd.h>
#include <sys/epoll.h>
using namespace std;
class epollServer
{
static const int maxsize = 64;
private:
// Tcp工作
void GetTcpSock()
{
// 创建一个TCP listen sock
_listensockfd = socket(AF_INET, SOCK_STREAM, 0);
if (_listensockfd < 0)
{
cout << "创建TCP listen sock 失败" << endl;
exit(-1);
}
cout << "创建TCP listen sock 成功! listensockfd:" << _listensockfd << endl;
// 设置地址复用
int opt = 1;
setsockopt(_listensockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
// 绑定
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr("0.0.0.0");
serveraddr.sin_port = htons(_port);
int n = bind(_listensockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
if (n < 0)
{
cout << "绑定失败" << endl;
exit(-1);
}
cout << "绑定成功" << endl;
// 设置listensockfd为监听状态
static const int gbacklog = 8;
n = listen(_listensockfd, gbacklog);
if (n < 0)
{
cout << "listen sock fd设置监听状态失败" << endl;
exit(-1);
}
cout << "listen sock fd设置监听状态成功" << endl;
}
public:
epollServer(int port)
: _port(port),
_listensockfd(-1),
_epfd(-1),
_timeout(1000)
{
// 获取TCP连接
GetTcpSock();
// 创建epoll模型
_epfd = epoll_create(128);
if (_epfd < 0)
{
cout << "epoll模型创建失败" << endl;
exit(-1);
}
cout << "epoll模型创建成功 epfd:" << _epfd << endl;
// 将listensockfd添加至epoll模型红黑树中
struct epoll_event ev;
ev.data.fd = _listensockfd;
ev.events = EPOLLIN;
int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, _listensockfd, &ev);
if (n < 0)
{
cout << "添加listensockfd添加至epoll模型红黑树中-失败" << endl;
exit(-1);
}
cout << "添加listensockfd添加至epoll模型红黑树中-成功" << endl;
}
// 获取连接操作
void ClientAccept()
{
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int newfd = accept(_listensockfd, (struct sockaddr *)&clientaddr, &len);
if (newfd < 0)
{
// 获取连接失败
return;
}
cout << "server获取了一个新连接, fd:" << newfd << " [ip:" << inet_ntoa(clientaddr.sin_addr) << " port:" << ntohs(clientaddr.sin_port) << "]" << endl;
// 将这个新事件添加到红黑树中
struct epoll_event ev;
ev.data.fd = newfd;
ev.events = EPOLLIN;
int n = epoll_ctl(_epfd, EPOLL_CTL_ADD, newfd, &ev);
if (n < 0)
{
cout << "将fd:" << newfd << "添加至红黑树中-失败" << endl;
close(newfd);
cout << "已关闭此新获取的连接, fd:" << newfd << endl;
}
else
{
cout << "将fd:" << newfd << "添加至红黑树中-成功" << endl;
}
}
// IO通信操作
void ServiceIO(int sockfd)
{
char inbuffer[1024]; // 接收缓冲区
ssize_t n = recv(sockfd, inbuffer, sizeof(inbuffer) - 1, 0);
if (n > 0)
{
// 读取成功
inbuffer[n] = '\0';
cout << "server收到来自client的信息:" << inbuffer << endl;
string outbuffer = "[Server Echo] ### ";
outbuffer += inbuffer;
int m = send(sockfd, outbuffer.c_str(), outbuffer.size(), 0);
if (m < 0)
{
cout << "server发送信息失败#" << inbuffer << endl;
}
cout << "server发送信息成功#" << inbuffer << endl;
}
else if (n == 0)
{
// 客户端退出
// 将客户端信息从红黑树中删除
epoll_ctl(_epfd, EPOLL_CTL_DEL, sockfd, nullptr);
cout << "客户端退出,已将fd#" << sockfd << " 从红黑树中删除" << endl;
// 关闭此文件描述符
close(sockfd);
cout << "已将fd#" << sockfd << " 文件描述符-关闭" << endl;
}
else
{
// 读取失败
// 将客户端信息从红黑树中删除
epoll_ctl(_epfd, EPOLL_CTL_DEL, sockfd, nullptr);
cout << "读取失败,已将fd#" << sockfd << " 从红黑树中删除" << endl;
// 关闭此文件描述符
close(sockfd);
cout << "已将fd#" << sockfd << " 文件描述符-关闭" << endl;
}
}
// 处理就绪事件
void HanderEvents(int num)
{
// 遍历缓冲区中前num个事件,即为本轮要处理的就绪事件
for (int i = 0; i < num; i++)
{
// 判断读事件就绪
if (_data[i].events & EPOLLIN > 0)
{
// 判断listensockfd、sockfd
if (_data[i].data.fd == _listensockfd)
{
// 获取连接操作
ClientAccept();
}
else
{
// IO通信操作
ServiceIO(_data[i].data.fd);
}
}
// 判断写事件就绪
if (_data[i].events & EPOLLOUT > 0)
{
}
}
}
// 启动
void Loop()
{
while (true)
{
int n = epoll_wait(_epfd, _data, maxsize, _timeout);
if (n < 0)
{
// epoll_wait函数出错
cout << "epoll_wait函数出错了" << endl;
}
else if (n == 0)
{
// epoll_wait超时了
cout << "epoll_wait函数超时了" << endl;
}
else
{
// epoll_wait有事件就绪
cout << "epoll_wait函数检测到" << n << "个事件已就绪" << endl;
// 处理就绪事件
HanderEvents(n);
}
}
}
~epollServer()
{
// 将缓冲区中的数据清空
for (int i = 0; i < maxsize; i++)
{
_data[i].data.fd = -1;
_data[i].events = 0;
}
cout << "缓冲区中的数据已被清空" << endl;
if (_listensockfd > 0)
{
close(_listensockfd);
cout << "listen sock fd:" << _listensockfd << "已被操作系统关闭" << endl;
}
if (_epfd > 0)
{
close(_epfd);
cout << "listen sock fd:" << _epfd << "已被操作系统关闭" << endl;
}
}
private:
int16_t _port; // 端口号
int _listensockfd; // listen sockfd
int _epfd; // epoll sockfd
struct epoll_event _data[maxsize]; // 缓冲区
int _timeout; // 等待策略
};
epoll作用
epoll也是只负责多路转接中IO中等待的部分。拷贝的工作还是调用recv、send、read、write这样的具体的输入输出拷贝函数来完成IO操作中拷贝的工作。
epoll工作模式
LT:水平触发-工作模式
ET:边缘触发-工作模式
LT:只要底层检测到有事件就绪,就会一直通知上层去处理此数据,知道上层将此数据处理完。才不会通知。
ET:当底层的就绪事件从无到有,从有到多,即底层就绪事件的个数发生变化时,才会通知一次上层,让上层处理此数据,且要求上层一次性将自己内核中TCP接收缓冲区中输入此文件描述符的数据全部获取完,若上层获取自己的数据获取的不完整时,则底层不再通知上层有上层自己的数据。所以在ET模式下,上层必须一次性将属于自己的数据从内核TCP接收缓冲区中全部获取完,否则底层就不会通知上层获取数据,上层也不敢贸然在底层不通知的情况下获取内核中的数据。
对于LT和ET来说:ET更加高效。因为ET只会通知一次事件就绪给上层,而LT会一直给上层通知很多重复的通知,知道上层处理完。ET模式省略了很多次给上层的通知。在通知策略上讲:ET是更高效的。
epoll工作方式
epoll 有 2 种工作方式-水平触发(LT)和边缘触发(ET)
假如有这样一个例子:
我们已经把一个 tcp socket 添加到 epoll 描述符。这个时候 socket 的另一端被写入了 2KB 的数据
调用 epoll_wait,并且它会返回. 说明它已经准备好读取操作,然后调用 read, 只读取了 1KB 的数据,继续调用 epoll_wait......
水平触发 Level Triggered 工作模式
epoll 默认状态下就是 LT 工作模式.
当 epoll 检测到 socket 上事件就绪的时候, 可以不立刻进行处理. 或者只处理一部分
如上面的例子, 由于只读了 1K 数据, 缓冲区中还剩 1K 数据, 在第二次调用epoll_wait 时, epoll_wait 仍然会立刻返回并通知 socket 读事件就绪.,直到缓冲区上所有的数据都被处理完, epoll_wait 才不会立刻返回.,支持阻塞读写和非阻塞读写
边缘触发 Edge Triggered 工作模式
如果我们在第 1 步将 socket 添加到 epoll 描述符的时候使用了 EPOLLET 标志, epoll 进入 ET 工作模式.
当 epoll 检测到 socket 上事件就绪时, 必须立刻处理.
如上面的例子, 虽然只读了 1K 的数据, 缓冲区还剩 1K 的数据, 在第二次调用epoll_wait 的时候, epoll_wait 不会再返回了.
也就是说, ET 模式下, 文件描述符上的事件就绪后, 只有一次处理机会.
ET 的性能比 LT 性能更高( epoll_wait 返回的次数少了很多). Nginx 默认采用
ET 模式使用 epoll.
只支持非阻塞的读写
select 和 poll 其实也是工作在 LT 模式下. epoll 既可以支持 LT, 也可以支持 ET
对比 LT 和 ET
LT 是 epoll 的默认行为.
使用 ET 能够减少 epoll 触发的次数. 但是代价就是强逼着程序猿一次响应就绪过程中就把所有的数据都处理完.相当于一个文件描述符就绪之后, 不会反复被提示就绪, 看起来就比 LT 更高效一些. 但是在 LT 情况下如果也能做到每次就绪的文件描述符都立刻处理, 不让这个就绪被重复提示的话, 其实性能也是一样的. 另一方面, ET 的代码复杂程度更高了
理解 ET 模式和非阻塞文件描述符
使用 ET 模式的 epoll, 需要将文件描述设置为非阻塞. 这个不是接口上的要求, 而是 "工程实践" 上的要求. 假设这样的场景: 服务器接收到一个 10k 的请求, 会向客户端返回一个应答数据. 如果客户端收不到应答, 不会发送第二个 10k 请求.

如果服务端写的代码是阻塞式的 read, 并且一次只 read 1k 数据的话(read 不能保证一次就把所有的数据都读出来, 参考 man 手册的说明, 可能被信号打断), 剩下的 9k 数据就会待在缓冲区中.

此时由于 epoll 是 ET 模式, 并不会认为文件描述符读就绪. epoll_wait 就不会再次返回. 剩下的 9k 数据会一直在缓冲区中. 直到下一次客户端再给服务器写数据epoll_wait 才能返回但是问题来了.
服务器只读到 1k 个数据, 要 10k 读完才会给客户端返回响应数据.
客户端要读到服务器的响应, 才会发送下一个请求
客户端发送了下一个请求, epoll_wait 才会返回, 才能去读缓冲区中剩余的数据.

所以, 为了解决上述问题(阻塞 read 不一定能一下把完整的请求读完), 于是就可以使用非阻塞轮训的方式来读缓冲区, 保证一定能把完整的请求都读出来。而如果是 LT 没这个问题. 只要缓冲区中的数据没读完, 就能够让 epoll_wait 返回文件描述符读就绪.
如果epoll工作模式为LT时,当读事件就绪时,此时,可以不读取完底层到来的数据,下次还可以再进行读取。
当epoll工作模式为LT时,假设底层读事件就绪,此文件描述符的读事件节点就会从红黑树中被关联至就绪队列中,epoll_wait接口返回此文件描述符的读事件就绪,通知给上层,若上层不把此节点中的数据取完,此文件描述符事件节点,一直在就绪队列中,不移除,只要上层将此文件描述符事件节点中的数据全部获取完,次文件描述符事件节点才会从就绪队列中移除。这就是LT工作模式。
当epoll工作模式为ET时,假设底层读事件就绪,此红黑树节点就会从红黑树中被关联至就绪队列中,epoll_wait接口返回一次此文件描述符读事件就绪,通知给上层,此时,只要上层读取了,不管是将此红黑树节点中接收缓冲区中的数据读取完了还是没有读取完,此红黑树节点都会从就绪队列中移除。这就是ET工作模式。ET就是这么设计的。
ET模式下,上层用户只有一次读取数据的机会,而且一定要保证将红黑树节点中接收缓冲区中的数据读取完整。
ET模式下,一旦就绪,必须把本轮数据全部读取完毕。
所以,epoll方案ET模式会倒逼程序员,必须将本轮数据全部读取完毕。
上层如何保证将底层本轮数据读取完毕?
上层如何保证将底层本轮数据读取完毕了?
循环读取此红黑树节点对应的TCP接收缓冲区中的数据,知道读取到的数据长度小于缓冲区长度,此时,就能够保证上层如何保证将底层本轮数据读取完毕了。
但是,如果使用循环读取的方式,若最后一次读取时,此红黑树节点对应的TCP接收缓冲区的数据大小为0或者小于最小读取阈值时,此次读取就会阻塞,epoll是单进程的,且阻塞了,这样,此epoll服务就会被阻塞甚至挂起。
所以,epoll服务使用ET模式时,必须将文件描述符设置为非阻塞状态。非阻塞状态下,读取到EAGAIN或者EWDBLOCK状态,表示全部读取完毕了。
ET模式高效原因
所以,ET模式高效的原因:因为在ET模式下,会倒逼程序员必须把本轮数据全部读取完毕,所以只能将文件描述符设置为非阻塞状态且循环读取数据,尽快将底层中TCP接收缓冲区中的数据读走。所以,ET模式下,一定会以最快的速度,把内核TCP接收缓冲区中的数据取走。这样,此主机操作系统就会给对方主机操作系统发送应答报文时,发送一个比较大的接收窗口,这样,大概率会让对方的TCP发送缓冲区中的滑动窗口变大,对方给本主机发送报文量变大,所以,ET模式的工作效率高。
LT模式下,也可以将文件描述符设置为非阻塞状态,也可以达到像ET模式一样高的效率,只要LT模式保证上层读取数据的意愿非常积极且每次都能全部读取完毕,LT也可以达到和ET一样的工作效率,ET是强制要求上层尽快全部读取完内核TCP接收缓冲区中的数据,LT可以将文件描述符设置为非阻塞状态,但也可以不设置非阻塞状态,在编码上有其他选择性。
所以,select、poll这样的多路转接系统调用使用的是类似与epoll的LT工作模式。当然,select、poll没有工作模式。
TCP水位线
PSH标志位与多路转接
http报头的PSH标志位的作用:要求对方主机尽快读取TCP接收缓冲区中的数据,即让对方主机将数据尽快交付给上层,本质作用是:尽快通知上层事件就绪。就是让上层进程尽快读取,操作系统已经尽完了自己的职责,也就是说,只要对方发了带有PSH标志位的http报文,哪怕本主机的TCP接收缓冲区中的数据小于读取阈值时,操作系统也会将此红黑树节点连入就绪队列中,通知上层事件就绪,让上层进城去获取数据。这可以忽略高低水位线的影响。
Reactor
对文件描述符的写入问题
Reactor概述
Reactor,称为:事件派发器。主要核心工作就是做事件派发。
Reactor底层会对所有的链接/事件统一做管理,Reactor就是一个容器。
Reactor工作模式:一旦底层操作系统检测到了事件就绪了,操作系统就会将此事件放入到就绪队列中,Reactor就会获取此就绪事件,再将这些就绪事件派发给各个链接/事件。由这些链接/事件再去处理,通过这些链接注册的方法。通过回调策略处理IO和业务之间的关系。Reactor称为:反应堆模式。即底层只要有就绪事件,就会激活链接对象去处理。
Reactor---事件派发器、Listener---链接管理器、HandlerEvents---业务处理器。
Reactor是服务器的一种编写方式,事件的管理工作让Reactor来做,每个事件对应的链接对象中有sockfd和输入输出缓冲区,在处理时,处理各自链接在Reactor中注册的方法,链接之间互不影响。
Reactor+线程池工作模式
Reactor可以处理的核心工作:
1、事件派发
2、事件派发+IO(send、recv)---最常见的Reactor核心工作。半同步-半异步工作模式。
3、事件派发+IO+报文解析
4、事件派发+IO+报文解析+业务处理---最推崇的做法。
半同步-半异步工作模式是Linux中最常见的。
半同步:由Reactor来进行等待和拷贝。---IO是同步的。
半异步:Reactor将报文解析、业务处理交给上层其他模块进行处理,各个线程处理时互不干扰。---Reactor层和上层是异步的。
Reactor层最推崇的就是让Reactor层做事件派发+IO+报文解析+业务处理工作。因为这样可以在一个执行流内部,会托管任何文件描述符的整个生命周期,单进程不会有任何并发问题,不会出现时序性问题。---Redis工作原理就是如此。
Reactor引入多进程/多线程
one thread one loop
Reactor-多进程
推荐做法:每一个线程/进程都配一个Reactor(操作系统会为每一个线程/进程配一个errno)。
Reactor-多线程
多线程的设计模式和多进程类似。