目录
应用层进行read或者write时,本质上是把数据从内核中读出来(缓冲区有数据才能读出来,否则阻塞)或者写到内核中(缓冲区数据没满才能写进去,否则会阻塞住)。
任何IO过程中,都包含两个步骤,第一是等待(等待条件成立),第二是拷贝。而且在实际的应用场景中,等待消耗的时间往往都远远高于拷贝的时间,让IO更高效,最核心的办法就是让等待的时间尽量少。
什么是高效的IO呢?显然,只要等待的时间越短,IO的效率越高,也就是说单位时间内,IO过程中等待所占的比重越小,IO的效率越高。基本上所有提高IO效率的策略,本质上都是降低等待的比重。
1、五种IO模型
1、阻塞IO
阻塞IO:在内核将数据准备好之前,系统调用会一直等待。所有的套接字默认都是阻塞IO。
如下图所示:

线程调用一个recvfrom,如果资源没有准备好,那么整个线程就会阻塞等待,直到资源就绪才会返回。
阻塞IO的优缺点是:简单,实时性高;但是需要阻塞等待,性能差。
注:之前见到的大部分IO都是阻塞IO。
2、非阻塞IO
非阻塞IO:如果内核还未将数据准备好,系统调用仍然会直接返回。
如下图所示:

非阻塞IO往往需要循环的方式反复尝试读写文件描述符,这个过程称为轮询。
非阻塞IO的优缺点是:在等待期间并不会被阻塞住,可以干其他事情;但是轮询对CPU来说是较大的浪费。
注:阻塞和非阻塞在IO效率上没有本质上的区别,只不过非阻塞在轮询时还可以干其他事情。非阻塞IO一般只有特定场景下才使用。
3、信号驱动IO
信号驱动IO:内核将数据准备好的时候,使用信号通知应用程序进行IO操作。
如下图所示:

注:这种IO方式很少见。
4、多路转接IO
IO多路转接 (也叫IO多路复用):虽然从下面的图片上看起来和阻塞IO类似,实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态。
如下图所示:

注:这种IO的效率最高,后面会详细说这种IO模型。
5、异步IO
异步IO:由内核在数据拷贝完成时,通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)。简单来说就是异步IO是事情办完了,然后进行通知;信号驱动IO是通知可以去办事了。
如下图所示:

注:在这五种IO模型中,前面的四种IO都属于同步IO,第五种IO是异步IO。
2、同步和异步IO
简单来说同步IO和异步IO的区别就是,同步IO是本身参与了IO过程,异步IO就是本身没有参与IO,只是IO的发起者,最后直接拿结果。
注:在多线程中,也提到了同步,这里的同步IO和线程同步之间的同步是完全不相干的概念。线程同步也是线程之间的制约关系,是为完成某种任务而建立的两个或多个线程,需要协调他们的工作次序而等待、传递信息所产生的制约关系,尤其是在访问临界资源的时候。
3、非阻塞IO
可以使用fcntl函数设置非阻塞,函数如下:
cpp
#include <fcntl.h>
int fcntl(int fd, int op, ... /* arg */ );
其中第一个参数fd是要设置的文件描述符;第二个参数op是指定要操作的类型;第三个是可变参数,取决于op。成功返回值由op决定;失败返回 -1,并设置 errno。
fcntl函数有5种功能:
1、复制一个现有的描述符(op=F_DUPFD)。
2、获得或设置文件描述符标记(op=F_GETFD或F_SETFD)。
3、获得或设置文件状态标记(op=F_GETFL或F_SETFL)。
4、获得或设置异步I/O所有权(op=F_GETOWN或F_SETOWN)。
5、获得或设置记录锁(op=F_GETLK、F_SETLK或F_SETLKW)。
使用用第三种功能,获取或设置文件状态标记,就可以将一个文件描述符设置为非阻塞。如下:
cpp
#include <iostream>
#include <cstdio>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <fcntl.h>
using namespace std;
void SetNonBlock(int fd)
{
int f1 = fcntl(fd, F_GETFL); // 使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图)
if(f1 < 0)
{
perror("fcntl");
return;
}
fcntl(fd, F_SETFL, f1 | O_NONBLOCK); // 然后再使用F_SETFL,将文件描述符属性设置回去,同时,加上一个O_NONBLOCK参数。
cout << "set " << fd << " nonblock done" << endl;
}
int main()
{
char buffer[1024];
SetNonBlock(0);
sleep(1);
while(true)
{
printf("Please Enter# ");
fflush(stdout);
ssize_t n = read(0, buffer, sizeof(buffer) - 1);
if(n > 0)
{
buffer[n - 1] = 0; // 减1是为了去掉回车
cout << "echo: " << buffer << endl;
}
else if(n == 0) // ctrl + d
{
cout << "read done" << endl;
break;
}
else
{
/*
* 设置为非阻塞,如果底层fd数据没有就绪,接口会返回出错的返回值,那么如何区分是真出错了还是资源没有就绪呢?
* 通过errno来进行区分是资源未就绪还是出错了。
*/
if(errno == EWOULDBLOCK) // EWOULDBLOCK的值就是11
{
cout << "0 fd data not ready, try again" << endl;
// do something
sleep(1);
}
else
{
cerr << "read error " << n << " errno: " << errno << " "<< strerror(errno) << endl;
}
}
}
return 0;
}
另外,在Linux中,标准输入结束是使用ctrl+d,不同平台可能是不一样的。
注:除了使用fcntl函数来设置非阻塞,一些读写接口的flag参数也是可以设置非阻塞的;还有一些创建文件描述符接口也是可以设置非阻塞的,只不过fcntl这种设置非阻塞的方式比较通用。
5、多路转接
5.1、select
5.1.1、接口介绍
系统提供select函数来实现多路转接模型。IO等于等待加拷贝,select就是用来负责等待的,可以等待多个文件描述符。select系统调用是用来让程序监视多个文件描述符的状态变化的,程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变。函数原型如下:
cpp
#include <sys/select.h>
int select(int nfds, fd_set *_Nullable restrict readfds,
fd_set *_Nullable restrict writefds,
fd_set *_Nullable restrict exceptfds,
struct timeval *_Nullable restrict timeout);
其中,参数nfds是需要监视的最大的文件描述符的值+1。
readfds,writefds,exceptfds分别对应于需要检测的可读文件描述符的集合、可写文件描述符的集合以及异常文件描述符的集合;这三个参数都是输入输出型参数,输入进来的是要监视的文件描述符,输出的是资源就绪的文件描述符。
fd_set是内核提供的一种数据类型,是一种位图结构,使用位图中对应的位来表示要监视的文件描述符,还提供了一组操作fd_set的接口,来操作位图,如下:
cpp
void FD_CLR(int fd, fd_set *set); // 将指定fd移出集合
int FD_ISSET(int fd, fd_set *set); // 检查fd是否就绪
void FD_SET(int fd, fd_set *set); // 将指定fd加入集合
void FD_ZERO(fd_set *set); // 清空集合
参数timeout用来设置select的等待时间。若为参数timeout为NULL:则表示select没有timeout,select将一直阻塞等待,直到某个或多个文件描述符上资源就绪。
若为特定的时间值:如果特定的时间值为0,仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生,也就是非阻塞;如果不为0,则如果在指定的时间段里没有事件发生,select将超时返回。struct timeval结构如下:

其中tv_sec表示秒,tv_usec表示微秒。
函数返回值:
若返回值为正数,返回值表示文件描述符状态已改变的个数。
若返回值为0,则表示文件描述符状态未改变,超时返回。
若返回值为-1,则表示发生错误,errno会被设置。
select的缺点:
1、等待的文件描述符是有上限的(因为fd_set这个位图的比特位是有上限的)。当然,一个进程能打开的文件描述符也是有上限的,可以通过ulimit -a命令查看,如下:

其中open files所指的1024就是这台机器上一个进程能打开的文件描述符的上限。
2、每次调用select,都需要手动设置集合,从接口使用角度来说非常不便。
3、select的输入输出型参数比较多,从用户态到内核态需要拷贝,从内核态到用户态也需要进行拷贝,这个开销在等待的文件描述符很多时会比较高(用户态是完全不能碰内核空间的;内核态理论上可以读写用户空间,但是操作系统为了安全和稳定,不直接使用用户空间,而是会进行拷贝)。

4、用户层需要很多次的遍历,内核中也要遍历。
注:上面的四个缺点中,前两个是使用不便,后两个是性能问题。
5.1.2、接口使用
例如:
Log.hpp:
cpp
#pragma once
#include <iostream>
#include <stdarg.h> // 使用可变参数列表需要用到这个头文件
#include <ctime>
#include <cstdlib>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define SIZE 1024
#define Screen 1
#define Onefile 2
#define Classfile 3
#define LogFile "log.txt"
class Log
{
public:
Log()
{
printMethod = Screen; // 默认为屏幕打印
path = "./log/";
}
void Enable(int method) // 更换日志的打印方式
{
printMethod = method;
}
std::string levelToString(int level) // 返回日志等级的字符串
{
switch (level)
{
case Info:
return "Info";
case Debug:
return "Debug";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
default:
return "None";
}
}
void printLog(int level, const std::string &logtxt) // 日志打印
{
switch (printMethod)
{
case Screen:
std::cout << logtxt << std::endl;
break;
case Onefile:
printOneFile(LogFile, logtxt);
break;
case Classfile:
printClassFile(level, logtxt);
break;
default:
break;
}
}
void printOneFile(const std::string &logname, const std::string &logtxt) // 打印到一个文件中
{
std::string _logname = path + logname;
int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
if (fd < 0)
{
return;
}
write(fd, logtxt.c_str(), logtxt.size());
close(fd);
}
void printClassFile(int level, const std::string &logtxt) // 分类打印到对应的文件
{
std::string filename = LogFile;
filename += '.';
filename += levelToString(level);
printOneFile(filename, logtxt);
}
~Log()
{}
void operator()(int level, const char *format, ...)
{
char leftbuffer[1024]; // 一条日志左边的格式信息,包括日志等级和时间。
time_t t = time(nullptr); // 返回值是时间戳
struct tm *ctime = localtime(&t); // 该函数可以将时间戳转换成一个struct tm 结构。
// 下面的\是续行符,加不加都行。
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),\
ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,\
ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
va_list s;
va_start(s, format);
char rightbuffer[SIZE]; // 一条日志右边日志内容
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s);
char logtxt[SIZE * 2]; // 合成一条日志信息
snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);
// printf("%s", logtxt);
printLog(level, logtxt); // 打印
}
private:
int printMethod; // 日志打印的方式
std::string path; // 日志打印路径
};
Log lg;
cpp
#include "SelectServer.hpp"
#include <memory>
int main()
{
std::unique_ptr<SelectServer> svr(new SelectServer());
svr->Init();
svr->Start();
return 0;
}
SelectServer.hpp:
cpp
#pragma once
#include <sys/select.h>
#include <sys/time.h>
#include "Socket.hpp"
#include <iostream>
using namespace std;
static const uint16_t defaultport = 19000;
static const int fd_num_max = sizeof(fd_set) * 8; // 位图的等待文件描述符的上限
int defaultfd = -1; // 默认初始值
class SelectServer
{
public:
SelectServer(uint16_t port = defaultport) : _port(port)
{
for (int i = 0; i < fd_num_max; i++)
{
rfd_array[i] = defaultfd; // 初始化读文件描述符集合
// std::cout << "fd_array[" << i << "]" << " : " << rfd_array[i] << std::endl;
}
}
bool Init()
{
_listensockfd.Socket();
_listensockfd.Bind(_port);
_listensockfd.Listen();
return true;
}
void Accepter() // 连接处理
{
// 连接事件已就绪
std::string clientip;
uint16_t clientport;
int sock = _listensockfd.Accept(&clientip, &clientport); // 会不会阻塞在这里呢?不会,因为资源已经就绪
if (sock < 0) return;
lg(Info, "accept success: %s: %d, sock fd: %d", clientip.c_str(), clientport, sock);
// 添加套接字到数组中
int pos = 1;
for (; pos < fd_num_max; pos++)
{
if (rfd_array[pos] != defaultfd)
continue;
else
break;
}
if (pos == fd_num_max)
{
lg(Warning, "server is full, close: %d now", sock);
close(sock);
}
else
{
rfd_array[pos] = sock;
PrintFd();
}
}
void Recver(int fd, int pos) // 接收处理
{
char buffer[1024];
ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = 0;
cout << "get a message: " << buffer << endl;
}
else if (n == 0)
{
lg(Info, "client quit, me too, close fd is: %d", fd);
close(fd);
rfd_array[pos] = defaultfd;
}
else
{
lg(Warning, "read error, close fd is: %d", fd);
close(fd);
rfd_array[pos] = defaultfd;
}
}
void Dispatcher(fd_set &readfds)
{
for (int i = 0; i < fd_num_max; i++) // 寻找哪些文件描述符状态改变
{
int fd = rfd_array[i];
if (fd == defaultfd)
continue;
if (FD_ISSET(fd, &readfds))
{
if (fd == _listensockfd.Fd()) // 判断是否为监听文件描述符
{
Accepter(); // 连接管理器
}
else
{
Recver(fd, i);
}
}
}
}
void Start()
{
int listensockfd = _listensockfd.Fd();
rfd_array[0] = listensockfd;
for (;;)
{
fd_set readfds;
FD_ZERO(&readfds);
int maxfd = rfd_array[0]; // maxfd指集合中最大文件描述符
for (int i = 0; i < fd_num_max; i++)
{
if (rfd_array[i] == defaultfd)
continue;
FD_SET(rfd_array[i], &readfds);
if (maxfd < rfd_array[i])
{
maxfd = rfd_array[i];
lg(Info, "max fd update, maxfd: %d", maxfd);
}
}
// 能否直接accept呢?不能
struct timeval timeout = {200, 0}; // 超时时间
int n = select(maxfd + 1, &readfds, nullptr, nullptr, &timeout);
switch (n)
{
case 0:
cout << "timeout: " << timeout.tv_sec << "." << timeout.tv_usec << endl;
break;
case -1:
cerr << "select error" << endl;
break;
default:
cout << "resource already" << endl;
Dispatcher(readfds); // 处理
break;
}
}
}
void PrintFd()
{
cout << "fd_list: ";
for (int i = 0; i < fd_num_max; i++)
{
if (rfd_array[i] == defaultfd)
continue;
cout << rfd_array[i] << " ";
}
cout << endl;
}
~SelectServer()
{
_listensockfd.Close();
}
private:
Sock _listensockfd;
uint16_t _port;
int rfd_array[fd_num_max]; // 读文件描述符集合
// int wfd_array[fd_num_max]; // 写文件描述符集合
};
Socket.hpp:
cpp
#pragma once
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"
#include <cstring>
#include <string>
enum { SocketErr = 2, BindErr, ListenErr };
const int backlog = 10;
class Sock
{
public:
Sock() {}
~Sock() {}
public:
void Socket()
{
sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd_ < 0)
{
lg(Fatal, "socket error, %s: %d", strerror(errno), errno);
exit(SocketErr);
}
int opt = 1;
setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
}
void Bind(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;
if(bind(sockfd_, (struct sockaddr*)&local, sizeof(local)) < 0)
{
lg(Fatal, "bind error, %s: %d", strerror(errno), errno);
exit(BindErr);
}
}
void Listen()
{
if(listen(sockfd_, backlog) < 0)
{
lg(Fatal, "listen error, %s: %d", strerror(errno), errno);
exit(ListenErr);
}
}
int Accept(std::string* clientip, uint16_t* clientport)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int newfd = accept(sockfd_, (struct sockaddr*)&peer, &len);
if(newfd < 0)
{
lg(Warning, "accept error, %s: %d", strerror(errno), errno);
return -1;
}
char ipstr[64];
inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));
*clientip = ipstr;
*clientport = ntohs(peer.sin_port);
return newfd;
}
bool Connect(const std::string& ip, const uint16_t& port)
{
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr));
int n = connect(sockfd_, (const struct sockaddr*)&peer, sizeof(peer));
if(n == -1)
{
std::cerr << "connect to " << ip << ":" << port << " error" << std::endl;
return false;
}
return true;
}
void Close() // 关闭监听套接字
{
close(sockfd_);
}
int Fd()
{
return sockfd_;
}
private:
int sockfd_; // 监听套接字
};
5.2、poll
poll和select是一样的,都是只负责IO过程的等待。poll相较于select主要解决的是等待描述符上限的问题以及接口使用不太方便的问题。
5.2.1、接口介绍
poll函数原型如下:
cpp
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
其中第一个参数的struct pollfd结构如下:

其中fd指文件描述符;events表示监听的事件集合;revents表示返回的就绪的事件集合。events和revents的取值如下:

fds指向一个数组,每一个元素中,包含了三部分内容:文件描述符、监听的事件集合、返回的事件集合。
nfds表示fds数组的长度。
timeout表示poll函数的超时时间是多少,单位是毫秒;如果为0表示非阻塞,如果为-1表示阻塞。
函数的返回值:返回值小于0,表示出错;返回值等于0,表示poll函数超时;返回值大于0,表示有几个文件描述符就绪了。
poll的缺点:
1、从用户态到内核态需要拷贝,从内核态到用户态也需要进行拷贝,这个开销在等待的文件描述符很多时会比较高。
2、用户层需要很多次的遍历,内核中也要遍历。
5.2.2、接口使用
Log.hpp
cpp
#pragma once
#include <iostream>
#include <stdarg.h> // 使用可变参数列表需要用到这个头文件
#include <ctime>
#include <cstdlib>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define SIZE 1024
#define Screen 1
#define Onefile 2
#define Classfile 3
#define LogFile "log.txt"
class Log
{
public:
Log()
{
printMethod = Screen; // 默认为屏幕打印
path = "./log/";
}
void Enable(int method) // 更换日志的打印方式
{
printMethod = method;
}
std::string levelToString(int level) // 返回日志等级的字符串
{
switch (level)
{
case Info:
return "Info";
case Debug:
return "Debug";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
default:
return "None";
}
}
void printLog(int level, const std::string &logtxt) // 日志打印
{
switch (printMethod)
{
case Screen:
std::cout << logtxt << std::endl;
break;
case Onefile:
printOneFile(LogFile, logtxt);
break;
case Classfile:
printClassFile(level, logtxt);
break;
default:
break;
}
}
void printOneFile(const std::string &logname, const std::string &logtxt) // 打印到一个文件中
{
std::string _logname = path + logname;
int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
if (fd < 0)
{
return;
}
write(fd, logtxt.c_str(), logtxt.size());
close(fd);
}
void printClassFile(int level, const std::string &logtxt) // 分类打印到对应的文件
{
std::string filename = LogFile;
filename += '.';
filename += levelToString(level);
printOneFile(filename, logtxt);
}
~Log()
{}
void operator()(int level, const char *format, ...)
{
char leftbuffer[1024]; // 一条日志左边的格式信息,包括日志等级和时间。
time_t t = time(nullptr); // 返回值是时间戳
struct tm *ctime = localtime(&t); // 该函数可以将时间戳转换成一个struct tm 结构。
// 下面的\是续行符,加不加都行。
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),\
ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,\
ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
va_list s;
va_start(s, format);
char rightbuffer[SIZE]; // 一条日志右边日志内容
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s);
char logtxt[SIZE * 2]; // 合成一条日志信息
snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);
// printf("%s", logtxt);
printLog(level, logtxt); // 打印
}
private:
int printMethod; // 日志打印的方式
std::string path; // 日志打印路径
};
Log lg;
cpp
#include "PollServer.hpp"
#include <memory>
int main()
{
std::unique_ptr<PollServer> svr(new PollServer());
svr->Init();
svr->Start();
return 0;
}
PollServer.hpp
cpp
#pragma once
#include<poll.h>
#include <sys/time.h>
#include "Socket.hpp"
#include <iostream>
using namespace std;
static const uint16_t defaultport = 19000;
static const int fd_num_max = 64;
int defaultfd = -1; // 默认初始值
int non_event = 0;
class PollServer
{
public:
PollServer(uint16_t port = defaultport) : _port(port)
{
for (int i = 0; i < fd_num_max; i++)
{
_event_fds[i].fd = defaultfd;
_event_fds[i].events = non_event;
_event_fds[i].revents = non_event;
}
}
bool Init()
{
_listensockfd.Socket();
_listensockfd.Bind(_port);
_listensockfd.Listen();
return true;
}
void Accepter() // 连接处理
{
// 连接事件已就绪
std::string clientip;
uint16_t clientport;
int sock = _listensockfd.Accept(&clientip, &clientport); // 会不会阻塞在这里呢?不会,因为资源已经就绪
if (sock < 0) return;
lg(Info, "accept success: %s: %d, sock fd: %d", clientip.c_str(), clientport, sock);
// 添加套接字到数组中
int pos = 1;
for (; pos < fd_num_max; pos++)
{
if (_event_fds[pos].fd != defaultfd)
continue;
else
break;
}
if (pos == fd_num_max)
{
lg(Warning, "server is full, close: %d now", sock);
close(sock);
}
else
{
_event_fds[pos].fd = sock;
_event_fds[pos].events = POLLIN;
_event_fds[pos].revents = non_event;
PrintFd();
}
}
void Recver(int fd, int pos) // 接收处理
{
char buffer[1024];
ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = 0;
cout << "get a message: " << buffer << endl;
}
else if (n == 0)
{
lg(Info, "client quit, me too, close fd is: %d", fd);
close(fd);
_event_fds[pos].fd = defaultfd;
}
else
{
lg(Warning, "read error, close fd is: %d", fd);
close(fd);
_event_fds[pos].fd = defaultfd;
}
}
void Dispatcher()
{
for (int i = 0; i < fd_num_max; i++) // 寻找哪些文件描述符状态改变
{
int fd = _event_fds[i].fd;
if (fd == defaultfd)
continue;
if (_event_fds[i].revents & POLLIN)
{
if (fd == _listensockfd.Fd()) // 判断是否为监听文件描述符
{
Accepter(); // 连接管理器
}
else
{
Recver(fd, i);
}
}
}
}
void Start()
{
_event_fds[0].fd = _listensockfd.Fd();
_event_fds[0].events = POLLIN; // 对于listen套接字而言,新连接到来等于读事件就绪,对于listen套接字而言只关心它的读事件
int timeout = 3000; // 也就是3秒
for (;;)
{
int n = poll(_event_fds, fd_num_max, timeout); // 对于是-1的文件描述符,该接口默认是不处理的
switch (n)
{
case 0:
cout << "time out" << endl;
break;
case -1:
cerr << "poll error" << endl;
break;
default:
cout << "resource already" << endl;
Dispatcher();
break;
}
}
}
void PrintFd()
{
cout << "fd_list: ";
for (int i = 0; i < fd_num_max; i++)
{
if (_event_fds[i].fd == defaultfd)
continue;
cout << _event_fds[i].fd << " ";
}
cout << endl;
}
~PollServer()
{
_listensockfd.Close();
}
private:
Sock _listensockfd;
uint16_t _port;
struct pollfd _event_fds[fd_num_max];
};
Socket.hpp
cpp
#pragma once
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"
#include <cstring>
#include <string>
enum { SocketErr = 2, BindErr, ListenErr };
const int backlog = 10;
class Sock
{
public:
Sock() {}
~Sock() {}
public:
void Socket()
{
sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd_ < 0)
{
lg(Fatal, "socket error, %s: %d", strerror(errno), errno);
exit(SocketErr);
}
int opt = 1;
setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
}
void Bind(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;
if(bind(sockfd_, (struct sockaddr*)&local, sizeof(local)) < 0)
{
lg(Fatal, "bind error, %s: %d", strerror(errno), errno);
exit(BindErr);
}
}
void Listen()
{
if(listen(sockfd_, backlog) < 0)
{
lg(Fatal, "listen error, %s: %d", strerror(errno), errno);
exit(ListenErr);
}
}
int Accept(std::string* clientip, uint16_t* clientport)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int newfd = accept(sockfd_, (struct sockaddr*)&peer, &len);
if(newfd < 0)
{
lg(Warning, "accept error, %s: %d", strerror(errno), errno);
return -1;
}
char ipstr[64];
inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));
*clientip = ipstr;
*clientport = ntohs(peer.sin_port);
return newfd;
}
bool Connect(const std::string& ip, const uint16_t& port)
{
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr));
int n = connect(sockfd_, (const struct sockaddr*)&peer, sizeof(peer));
if(n == -1)
{
std::cerr << "connect to " << ip << ":" << port << " error" << std::endl;
return false;
}
return true;
}
void Close() // 关闭监听套接字
{
close(sockfd_);
}
int Fd()
{
return sockfd_;
}
private:
int sockfd_; // 监听套接字
};
5.3、epoll
为了从根本上解决select和poll的缺陷,于是就有了epoll,epoll成为了高并发网络编程中性能最优的解决方案。epoll有三个相关的系统调用。
5.3.1、接口介绍
使用epoll_create创建一个epoll文件描述符,函数原型如下:
cpp
#include <sys/epoll.h>
int epoll_create(int size);
其中size现在已经被废弃,只要填一个大于0的数即可。
函数成功返回一个epoll文件描述符,失败返回-1。
使用epoll_ctl管理监听事件,函数原型如下:
cpp
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *_Nullable event);
它不同于select是在监听事件时告诉内核要监听什么类型的事件,而是使用这个接口先注册要监听的事件类型。
第一个参数填的是epoll_create成功的返回值。
第二个参数表示动作,用三个宏来表示,如下:
1、EPOLL_CTL_ADD:注册新的fd到epfd中。
2、EPOLL_CTL_MOD:修改已经注册的fd的监听事件。
3、EPOLL_CTL_DEL:从epfd中删除一个fd。
第三个参数是需要监听的文件描述符fd。
第四个参数是要监听对应fd的什么事件。struct epoll_event结构如下:

其中,events表示要监听的事件,本质是位图;data里面存放的是关联信息(最常用的就是fd)。events的取值如下:
1、EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭)。
2、EPOLLOUT:表示对应的文件描述符可以写。
3、EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)。
4、EPOLLERR:表示对应的文件描述符发生错误。
5、EPOLLHUP:表示对应的文件描述符被挂断。
6、EPOLLET:将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
7、EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。
函数成功返回0,失败返回-1。
使用epoll_wait等待就绪事件,函数原型如下:
cpp
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
其中epfd传入的就是epoll_create函数成功的返回值。
参数events是指epoll_event结构体数组,epoll将会把就绪的事件按数组的顺序赋值到events数组中 (events不可以是空指针),也就是返回已经就绪的描述符以及事件。就绪事件数超过数组的长度时,未拷贝的事件会留存在内核就绪链表中,下一次epoll_wait优先返回这些事件,不会丢失。
maxevents是指这个events有多大,这个值必须要大于0。
参数timeout是超时时间,单位为毫秒;如果是0会立即返回;如果是-1,则永久阻塞。
如果函数调用成功,返回已准备好的文件描述符数目;如返回0表示超时;返回小于0表示函数失败。
5.3.2、epoll工作原理
当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关,一个是红黑树,另一个是双向链表实现的队列。大致结构如下:
cpp
struct eventpoll{
....
/*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
struct rb_root rbr;
/*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
struct list_head rdlist;
....
};
每个epoll对象都拥有独立的eventpoll结构体。通过epoll_ctl添加进来的所有监听事件,都会挂载到这棵红黑树中进行管理;而已经就绪的事件,则会被链入到双向链表rdlist中。
在使用epoll_ctl注册文件描述符时,内核会为该 fd 对应的设备(如网卡)驱动程序注册一个回调函数,该回调的作用是当 fd 就绪时,将其对应的事件节点加入就绪链表。
硬件层面,网卡收到数据会触发硬件中断,内核在处理中断时,会通过网卡驱动将数据接收进来,并触发之前注册好的回调函数。内核中的这个回调函数名为ep_poll_callback,它会将就绪事件添加到 rdlist 双向链表中。
调用epoll_wait时,内核只需检查就绪链表是否为空,若为空,则根据 timeout 决定阻塞或返回 0;若非空,将就绪链表中的 fd 及事件拷贝到用户态的 events 数组中,直接返回就绪数量。
注:简单来说就是epoll实现的核心机制就是红黑树、就绪链表、回调函数。
epoll的优点:接口使用方便,虽然拆分成了三个函数,但是反而使用起来更方便高效。数据拷贝相对较轻。避免了多次遍历。文件描述符数目无上限。
5.3.3、接口使用
Epoll.hpp
cpp
#pragma once
#include <sys/epoll.h>
#include "nocopy.hpp"
#include "Log.hpp"
#include <cstring>
class Epoll : public nocopy // 防止拷贝
{
static const int size = 128;
public:
Epoll()
{
_epfd = epoll_create(size);
if(_epfd == -1)
{
lg(Error, "epoll_create error: %s", strerror(errno));
}
else
{
lg(Info, "epoll_create success: %d", _epfd);
}
}
int EpollWait(struct epoll_event revents[], int num) // 等待
{
int n = epoll_wait(_epfd, revents, num, _timeout);
return n;
}
int EpollUpdate(int oper, int sock, uint32_t event) // 增删改
{
int n = 0;
if(oper == EPOLL_CTL_DEL)
{
n = epoll_ctl(_epfd, oper, sock, nullptr);
if(n != 0)
{
lg(Error, "epoll_ctl delete error");
}
}
else
{
struct epoll_event ev;
ev.events = event;
ev.data.fd = sock; // 方便后续得知是哪一个fd就绪了
n = epoll_ctl(_epfd, oper, sock, &ev);
if(n != 0)
{
lg(Error, "epoll_ctl error");
}
}
return n;
}
~Epoll()
{
if(_epfd > 0) close(_epfd);
}
private:
int _epfd; // epoll文件描述符
int _timeout{-1}; // 超时时间
};
EpollServer.hpp
cpp
#pragma once
#include <iostream>
#include <memory>
#include <sys/epoll.h>
#include "Socket.hpp"
#include "Epoll.hpp"
#include "Log.hpp"
#include "nocopy.hpp"
uint32_t EVENT_IN = (EPOLLIN);
uint32_t EVENT_OUT = (EPOLLOUT);
class EpollServer : public nocopy
{
static const int num = 64;
public:
EpollServer(uint16_t port):_port(port), _listensock_ptr(new Sock()), _epoller_ptr(new Epoll())
{}
void Init()
{
_listensock_ptr->Socket();
_listensock_ptr->Bind(_port);
_listensock_ptr->Listen();
lg(Info, "create listen socket success: %d", _listensock_ptr->Fd());
}
void Accepter()
{
// 获取了一个新连接
std::string clientip;
uint16_t clientport;
int sock = _listensock_ptr->Accept(&clientip, &clientport);
if(sock > 0)
{
// 能直接进行读取吗?不能
_epoller_ptr->EpollUpdate(EPOLL_CTL_ADD, sock, EVENT_IN);
lg(Info, "get a new link, clientip: %s, clientport: %d", clientip.c_str(), clientport);
}
}
void Recver(int fd) // 接收处理
{
char buffer[1024];
ssize_t n = read(fd, buffer, sizeof(buffer) - 1); // bug?
if (n > 0)
{
buffer[n] = 0;
std::cout << "get a message: " << buffer << std::endl;
// write
std::string echo_ptr = "server echo$ ";
echo_ptr += buffer;
write(fd, echo_ptr.c_str(), echo_ptr.size());
}
else if (n == 0)
{
lg(Info, "client quit, me too, close fd is: %d", fd);
_epoller_ptr->EpollUpdate(EPOLL_CTL_DEL, fd, 0); // 删除时,事件这个参数是可以不添加的
close(fd); // 注:要先删除,再进行关闭文件描述符,上面的这一行代码和左边的代码不可以调换次序
}
else
{
lg(Warning, "read error, close fd is: %d", fd);
_epoller_ptr->EpollUpdate(EPOLL_CTL_DEL, fd, 0); // 删除时,事件这个参数是可以不添加的
close(fd); // 注:要先删除,再进行关闭文件描述符,上面的这一行代码和左边的代码不可以调换次序
}
}
void Dispatcher(struct epoll_event revs[], int num)
{
for(int i = 0; i < num; i++)
{
uint32_t events = revs[i].events;
int fd = revs[i].data.fd;
if(events & EVENT_IN)
{
if(fd == _listensock_ptr->Fd())
{
Accepter();
}
else
{
Recver(fd);
}
}
else if(events & EVENT_OUT)
{
}
else
{
}
}
}
void Start()
{
// 将listen套接字添加到epoll中
_epoller_ptr->EpollUpdate(EPOLL_CTL_ADD, _listensock_ptr->Fd(), EVENT_IN);
struct epoll_event revs[num];
for(;;)
{
int n = _epoller_ptr->EpollWait(revs, num);
if(n > 0)
{
// 有事件就绪
lg(Debug, "event happened, fd is: %d", revs[0].data.fd);
Dispatcher(revs, n);
}
else if(n == 0)
{
lg(Info, "time out");
}
else
{
lg(Error, "epoll wait error");
}
}
}
~EpollServer()
{
_listensock_ptr->Close();
}
private:
std::shared_ptr<Sock> _listensock_ptr;
std::shared_ptr<Epoll> _epoller_ptr;
uint16_t _port;
};
Log.hpp
cpp
#pragma once
#include <iostream>
#include <stdarg.h> // 使用可变参数列表需要用到这个头文件
#include <ctime>
#include <cstdlib>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define SIZE 1024
#define Screen 1
#define Onefile 2
#define Classfile 3
#define LogFile "log.txt"
class Log
{
public:
Log()
{
printMethod = Screen; // 默认为屏幕打印
path = "./log/";
}
void Enable(int method) // 更换日志的打印方式
{
printMethod = method;
}
std::string levelToString(int level) // 返回日志等级的字符串
{
switch (level)
{
case Info:
return "Info";
case Debug:
return "Debug";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
default:
return "None";
}
}
void printLog(int level, const std::string &logtxt) // 日志打印
{
switch (printMethod)
{
case Screen:
std::cout << logtxt << std::endl;
break;
case Onefile:
printOneFile(LogFile, logtxt);
break;
case Classfile:
printClassFile(level, logtxt);
break;
default:
break;
}
}
void printOneFile(const std::string &logname, const std::string &logtxt) // 打印到一个文件中
{
std::string _logname = path + logname;
int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
if (fd < 0)
{
return;
}
write(fd, logtxt.c_str(), logtxt.size());
close(fd);
}
void printClassFile(int level, const std::string &logtxt) // 分类打印到对应的文件
{
std::string filename = LogFile;
filename += '.';
filename += levelToString(level);
printOneFile(filename, logtxt);
}
~Log()
{}
void operator()(int level, const char *format, ...)
{
char leftbuffer[1024]; // 一条日志左边的格式信息,包括日志等级和时间。
time_t t = time(nullptr); // 返回值是时间戳
struct tm *ctime = localtime(&t); // 该函数可以将时间戳转换成一个struct tm 结构。
// 下面的\是续行符,加不加都行。
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),\
ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,\
ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
va_list s;
va_start(s, format);
char rightbuffer[SIZE]; // 一条日志右边日志内容
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s);
char logtxt[SIZE * 2]; // 合成一条日志信息
snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);
// printf("%s", logtxt);
printLog(level, logtxt); // 打印
}
private:
int printMethod; // 日志打印的方式
std::string path; // 日志打印路径
};
Log lg;
cpp
#include <iostream>
#include <memory>
#include "EpollServer.hpp"
int main()
{
std::unique_ptr<EpollServer> epoll_svr(new EpollServer(19000));
epoll_svr->Init();
epoll_svr->Start();
return 0;
}
nocopy.hpp
cpp
#pragma once
class nocopy
{
public:
nocopy(){}
nocopy(const nocopy&) = delete;
const nocopy& operator=(const nocopy&) = delete;
};
Socket.hpp
cpp
#pragma once
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"
#include <cstring>
#include <string>
enum { SocketErr = 2, BindErr, ListenErr };
const int backlog = 10;
class Sock
{
public:
Sock() {}
~Sock() {}
public:
void Socket()
{
sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd_ < 0)
{
lg(Fatal, "socket error, %s: %d", strerror(errno), errno);
exit(SocketErr);
}
int opt = 1;
setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
}
void Bind(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;
if(bind(sockfd_, (struct sockaddr*)&local, sizeof(local)) < 0)
{
lg(Fatal, "bind error, %s: %d", strerror(errno), errno);
exit(BindErr);
}
}
void Listen()
{
if(listen(sockfd_, backlog) < 0)
{
lg(Fatal, "listen error, %s: %d", strerror(errno), errno);
exit(ListenErr);
}
}
int Accept(std::string* clientip, uint16_t* clientport)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int newfd = accept(sockfd_, (struct sockaddr*)&peer, &len);
if(newfd < 0)
{
lg(Warning, "accept error, %s: %d", strerror(errno), errno);
return -1;
}
char ipstr[64];
inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));
*clientip = ipstr;
*clientport = ntohs(peer.sin_port);
return newfd;
}
bool Connect(const std::string& ip, const uint16_t& port)
{
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr));
int n = connect(sockfd_, (const struct sockaddr*)&peer, sizeof(peer));
if(n == -1)
{
std::cerr << "connect to " << ip << ":" << port << " error" << std::endl;
return false;
}
return true;
}
void Close() // 关闭监听套接字
{
close(sockfd_);
}
int Fd()
{
return sockfd_;
}
private:
int sockfd_; // 监听套接字
};
5.3.4、epoll工作方式
epoll有2种工作方式,分别为水平触发(LT) 和边缘触发(ET)。
水平触发Level Triggered工作模式:
LT 是 epoll 的默认工作模式。当 epoll 检测到某个文件描述符(如 TCP socket)就绪后,若用户未处理完该事件(或仅处理部分),该事件会持续被加入就绪链表,每次调用 epoll_wait 都会返回该就绪事件,直到事件被完全处理。简单来说:只要文件描述符的资源处于就绪状态 (如读缓冲区有数据),该事件就会一直被标记为就绪 ,epoll_wait 会持续返回。
如果是水平触发模式,那么当epoll检测到socket上事件就绪的时候,可以不立刻进行处理或者只处理一部分。LT模式支持阻塞读写和非阻塞读写。
边缘触发Edge Triggered工作模式:
若在 epoll_ctl 注册 socket 时指定 EPOLLET 标志,epoll 会进入 ET 工作模式。ET 模式仅在文件描述符的就绪状态发生变化时(如读缓冲区从无数据到有数据、数据量从少到多)触发一次就绪通知,将事件加入就绪链表;即便事件未处理完毕(如仅读取部分数据),后续也不会再将该事件加入就绪链表(除非有新的状态变化)。简单来说:仅当资源的就绪状态发生 "变化" 时 ,事件才会被加入就绪链表,且仅触发一次。
ET的性能比LT性能更高(并不绝对),Nginx默认采用ET模式使用epoll。ET模式只支持非阻塞的读写。select和poll只能工作在LT模式下的;epoll既可以支持LT,也可以支持ET。
注:LT是 epoll 的默认行为,使用 ET 能够减少 epoll 触发的次数,但是代价就是强逼上层一次响应就绪过程中就把所有的数据都处理完,相当于一个文件描述符就绪之后,不会反复被提示就绪,看起来就比 LT 更高效一些,但是在 LT 情况下如果也能做到每次就绪的文件描述符都立刻处理,不让这个就绪被重复提示的话,其实性能也是一样的。
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 返回文件描述符读就绪。
5.3.5、接口使用
TcpServer.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <memory>
#include <functional>
#include <unordered_map>
#include "Log.hpp"
#include "nocopy.hpp"
#include "Socket.hpp"
#include "Epoll.hpp"
#include "comm.hpp"
uint32_t EVENT_IN (EPOLLIN | EPOLLET);
uint32_t EVENT_OUT (EPOLLOUT | EPOLLET);
const static int g_buffer_size = 128; // 临时缓冲区大小
class Connection; // 声明
class TcpServer; // 声明
using func_t = std::function<void(std::weak_ptr<Connection>)>;
using except_func = std::function<void(std::weak_ptr<Connection>)>;
class Connection
{
public:
Connection(int sock):_sock(sock)
{}
void SetHandler(func_t recv_cb, func_t send_cb, except_func except_cb) // 设置回调方法
{
_recv_cb = recv_cb;
_send_cb = send_cb;
_except_cb = except_cb;
}
int SockFd() // 返回文件描述符
{
return _sock;
}
void AppendInBuffer(const std::string& info) // 追加信息
{
_inbuffer += info;
}
void AppendOutBuffer(const std::string& info) // 追加信息
{
_outbuffer += info;
}
std::string& Inbuffer() // 返回接收缓冲区
{
return _inbuffer;
}
std::string& Outbuffer() // 返回输出缓冲区
{
return _outbuffer;
}
void SetWeakPtr(std::weak_ptr<TcpServer> tcp_server_ptr)
{
_tcp_server_ptr = tcp_server_ptr;
}
~Connection()
{}
private:
int _sock; // 文件描述符
std::string _inbuffer; // 输入缓冲区(使用string是没法处理二进制数据的)
std::string _outbuffer; // 输出缓冲区
public:
func_t _recv_cb; // 读回调
func_t _send_cb; // 写回调
except_func _except_cb; // 异常回调
std::weak_ptr<TcpServer> _tcp_server_ptr; // 回指指针
std::string _ip; // 客户端IP地址
uint16_t _port; // 客户端端口号
};
// enable_shared_from_this:可以提供返回当前对象的this对应的shared_ptr
class TcpServer : public std::enable_shared_from_this<TcpServer>, public nocopy
{
static const int num = 64; // 就绪数组的大小
public:
TcpServer(uint16_t port, func_t OnMessage):_port(port), _OnMessage(OnMessage), _quit(true)
, _epoller_ptr(new Epoll()), _listensock_ptr(new Sock())
{}
void Init()
{
// 初始化socket
_listensock_ptr->Socket();
SetNonBlockOrDie(_listensock_ptr->Fd());
_listensock_ptr->Bind(_port);
_listensock_ptr->Listen();
lg(Info, "create listen socket success: %d", _listensock_ptr->Fd());
// 添加连接
AddConnection(_listensock_ptr->Fd(), EVENT_IN, std::bind(&TcpServer::Accepter, this, std::placeholders::_1), nullptr, nullptr);
}
void AddConnection(int sock, uint32_t event, func_t recv_cb, func_t send_cb, except_func except_cb, \
const std::string& ip = "0.0.0.0", uint16_t port = 0) // 连接添加函数
{
std::shared_ptr<Connection> new_connection(new Connection(sock));
new_connection->SetWeakPtr(shared_from_this()); // shared_from_this(): 返回当前对象的shared_ptr
new_connection->SetHandler(recv_cb, send_cb, except_cb);
new_connection->_ip = ip; // 客户端的IP地址
new_connection->_port = port; // 客户端的端口号
_connections.insert(std::make_pair(sock, new_connection));
_epoller_ptr->EpollUpdate(EPOLL_CTL_ADD, sock, event);
lg(Debug, "add a new connection success, sockfd is %d", sock);
}
void Accepter(std::weak_ptr<Connection> conn) // 获取新连接
{
auto connection = conn.lock();
while(true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = ::accept(connection->SockFd(), (struct sockaddr*)&peer, &len); // 这个::表示使用系统的原生接口
if(sock > 0)
{
uint16_t peerport = ntohs(peer.sin_port);
char ipbuf[128];
inet_ntop(AF_INET, &peer.sin_addr.s_addr, ipbuf, sizeof(ipbuf));
lg(Debug, "get a new client, ip: %s, port: %d, fd: %d", ipbuf, peerport, sock);
SetNonBlockOrDie(sock); // 设置非阻塞
AddConnection(sock, EVENT_IN, \
std::bind(&TcpServer::Recver, this, std::placeholders::_1), \
std::bind(&TcpServer::Sender, this, std::placeholders::_1), \
std::bind(&TcpServer::Excepter, this, std::placeholders::_1), ipbuf, peerport);
}
else
{
if(errno == EWOULDBLOCK) break; // 未就绪
else if(errno == EINTR) continue; // 信号原因
else break;
}
}
}
void Recver(std::weak_ptr<Connection> conn) // 接收
{
if(conn.expired()) return;
auto connection = conn.lock();
int sock = connection->SockFd();
while(true)
{
char buffer[g_buffer_size]; // 临时缓冲区
memset(buffer, 0, sizeof(buffer)); // 初始化
ssize_t n = recv(sock, buffer, sizeof(buffer) - 1, 0); // 非阻塞读取
if(n > 0)
{
connection->AppendInBuffer(buffer);
}
else if(n == 0)
{
lg(Info, "sockfd: %d, client ip: %s, client port: %d quit", sock, connection->_ip.c_str(), connection->_port);
connection->_except_cb(connection);
return;
}
else
{
if(errno == EWOULDBLOCK) break;
else if(errno == EINTR) continue;
else
{
lg(Warning, "sockfd: %d, client ip: %s, client port: %d recv error", sock, connection->_ip.c_str(), connection->_port);
connection->_except_cb(connection);
return;
}
}
}
_OnMessage(connection);
}
void Sender(std::weak_ptr<Connection> conn) // 发送
{
if(conn.expired()) return;
auto connection = conn.lock();
auto& outbuffer = connection->Outbuffer();
while(true)
{
ssize_t n = send(connection->SockFd(), outbuffer.c_str(), outbuffer.size(), 0);
if(n > 0)
{
outbuffer.erase(0, n);
if(outbuffer.empty()) break;
}
else if(n == 0)
{
return;
}
else
{
if(errno == EWOULDBLOCK) break;
else if(errno == EINTR) continue;
else
{
lg(Warning, "sockfd: %d, client ip: %s, client port: %d send error", connection->SockFd(), \
connection->_ip.c_str(), connection->_port);
connection->_except_cb(connection);
return;
}
}
}
if(!outbuffer.empty())
{
// 开启对写事件的关心
EnableEvent(connection->SockFd(), true, true);
}
else
{
// 关闭对写事件的关心
EnableEvent(connection->SockFd(), true, false);
}
}
void EnableEvent(int sock, bool readable, bool writeable) // 是否对读写事件关心
{
uint32_t events = 0;
events |= ((readable ? EPOLLIN : 0) | (writeable ? EPOLLOUT : 0) | EPOLLET);
_epoller_ptr->EpollUpdate(EPOLL_CTL_MOD, sock, events);
}
void Excepter(std::weak_ptr<Connection> connection) // 异常
{
if(connection.expired()) return;
auto conn = connection.lock();
int fd = conn->SockFd();
lg(Debug, "Excepter sockfd: %d, client ip: %s, client port: %d excepter handler", conn->SockFd(), \
conn->_ip.c_str(), conn->_port);
// 移除关心
_epoller_ptr->EpollUpdate(EPOLL_CTL_DEL, fd, 0);
// 关闭异常的文件描述符
lg(Debug, "close %d done", fd);
close(fd);
// 移除键值
lg(Debug, "remove %d from _connection", fd);
_connections.erase(fd);
}
bool IsConnectionSafe(int fd) // 判断连接是否安全
{
auto iter = _connections.find(fd);
if(iter == _connections.end()) return false;
else return true;
}
void Dispatcher(int timeout) // 分发
{
int n = _epoller_ptr->EpollWait(revs, num, timeout);
for(int i = 0; i < n; i++)
{
uint32_t events = revs[i].events;
int sock = revs[i].data.fd;
// 这样只需要处理读写问题即可
if((events & EPOLLIN) && IsConnectionSafe(sock)) // 读
{
if(_connections[sock]->_recv_cb) _connections[sock]->_recv_cb(_connections[sock]);
}
if((events & EPOLLOUT) && IsConnectionSafe(sock)) // 写
{
if(_connections[sock]->_send_cb) _connections[sock]->_send_cb(_connections[sock]);
}
}
}
void Loop() // 循环
{
_quit = false;
while(!_quit)
{
Dispatcher(-1);
}
_quit = true;
}
~TcpServer(){}
private:
std::shared_ptr<Epoll> _epoller_ptr; // epoll
std::shared_ptr<Sock> _listensock_ptr; // 套接字
std::unordered_map<int, std::shared_ptr<Connection>> _connections; // 文件描述符和连接
struct epoll_event revs[num]; // 就绪事件
uint16_t _port; // 端口号
bool _quit;
func_t _OnMessage; // 回调函数,让上层处理信息
};
Socket.hpp
cpp
#pragma once
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <fcntl.h>
#include "Log.hpp"
#include <cstring>
#include <string>
enum { SocketErr = 2, BindErr, ListenErr, NON_BLOCK_ERR };
const int backlog = 10;
class Sock
{
public:
Sock() {}
~Sock() {}
public:
void Socket()
{
sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd_ < 0)
{
lg(Fatal, "socket error, %s: %d", strerror(errno), errno);
exit(SocketErr);
}
int opt = 1;
setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
}
void Bind(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;
if(bind(sockfd_, (struct sockaddr*)&local, sizeof(local)) < 0)
{
lg(Fatal, "bind error, %s: %d", strerror(errno), errno);
exit(BindErr);
}
}
void Listen()
{
if(listen(sockfd_, backlog) < 0)
{
lg(Fatal, "listen error, %s: %d", strerror(errno), errno);
exit(ListenErr);
}
}
int Accept(std::string* clientip, uint16_t* clientport)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int newfd = accept(sockfd_, (struct sockaddr*)&peer, &len);
if(newfd < 0)
{
lg(Warning, "accept error, %s: %d", strerror(errno), errno);
return -1;
}
char ipstr[64];
inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));
*clientip = ipstr;
*clientport = ntohs(peer.sin_port);
return newfd;
}
bool Connect(const std::string& ip, const uint16_t& port)
{
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr));
int n = connect(sockfd_, (const struct sockaddr*)&peer, sizeof(peer));
if(n == -1)
{
std::cerr << "connect to " << ip << ":" << port << " error" << std::endl;
return false;
}
return true;
}
void Close() // 关闭监听套接字
{
close(sockfd_);
}
int Fd()
{
return sockfd_;
}
private:
int sockfd_; // 监听套接字
};
Protocol.hpp
cpp
#pragma once
/*
* 协议
*/
#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>
const std::string blank_space_sep = " "; // 空格分隔符
const std::string protocol_sep = "\n"; // 换行分割符
std::string Encode(std::string& content) // 封装上内容的大小
{
std::string package = std::to_string(content.size());
package += protocol_sep;
package += content;
package += protocol_sep;
return package;
}
// "len"\n"x op y"\n
bool Decode(std::string& package, std::string* content) // 解包获取内容
{
size_t pos = package.find(protocol_sep);
if(pos == std::string::npos) return false;
std::string len_str = package.substr(0, pos);
size_t len = std::stoi(len_str);
size_t total_len = len_str.size() + len + 2;
if(package.size() < total_len) return false;
*content = package.substr(pos + 1, len);
package.erase(0, total_len);
return true;
}
class Request // 请求
{
public:
Request()
{}
Request(int data1, int data2, char oper):x(data1), y(data2), op(oper)
{}
public:
bool Serialize(std::string* out) // 对请求进行序列化
{
#ifdef MySelf
// "x op y"
std::string s = std::to_string(x);
s += blank_space_sep;
s += op;
s += blank_space_sep;
s += std::to_string(y);
*out = s;
return true;
#else
Json::Value root;
root["x"] = x;
root["y"] = y;
root["op"] = op;
//Json::FastWriter w;
Json::StyledWriter w;
*out = w.write(root);
return true;
#endif
}
// "x op y"
bool Deserialize(const std::string& in) // 对请求进行反序列化
{
#ifdef MySelf
size_t left = in.find(blank_space_sep);
if(left == std::string::npos) return false;
std::string part_x = in.substr(0, left);
size_t right = in.rfind(blank_space_sep);
if(right == std::string::npos) return false;
std::string part_y = in.substr(right + 1);
if(left + 2 != right) return false;
op = in[left + 1];
x = std::stoi(part_x);
y = std::stoi(part_y);
return true;
#else
Json::Value root;
Json::Reader r;
r.parse(in, root);
x = root["x"].asInt();
y = root["y"].asInt();
op = root["op"].asInt();
return true;
#endif
}
void DebugPrint()
{
std::cout << "新请求构建: " << x << " " << op << " " << y << std::endl;
}
public:
int x;
int y;
char op; // + - * / %
};
class Response // 应答
{
public:
Response()
{}
Response(int res, int c):result(res), code(c)
{}
public:
bool Serialize(std::string* out) // 序列化
{
#ifdef MySelf
// "result code"
std::string s = std::to_string(result);
s += blank_space_sep;
s += std::to_string(code);
*out = s;
return true;
#else
Json::Value root;
root["result"] = result;
root["code"] = code;
//Json::FastWriter w;
Json::StyledWriter w;
*out = w.write(root);
return true;
#endif
}
// "result code"
bool Deserialize(const std::string& in) // 反序列化
{
#ifdef MySelf
size_t pos = in.find(blank_space_sep);
if(pos == std::string::npos) return false;
std::string part_left = in.substr(0, pos);
std::string part_right = in.substr(pos + 1);
result = std::stoi(part_left);
code = std::stoi(part_right);
return true;
#else
Json::Value root;
Json::Reader r;
r.parse(in, root);
result = root["result"].asInt();
code = root["code"].asInt();
return true;
#endif
}
void DebugPrint()
{
std::cout << "响应完成: " << result << " " << code << std::endl;
}
public:
int result;
int code; // 0表示可信,非零的具体数值表明错误原因
};
nocopy.hpp
cpp
#pragma once
class nocopy
{
public:
nocopy(){}
nocopy(const nocopy&) = delete;
const nocopy& operator=(const nocopy&) = delete;
};
cpp
#include <iostream>
#include <memory>
#include <functional>
#include "TcpServer.hpp" // 处理IO的
#include "Calculator.hpp" // 处理业务的
#include "Log.hpp"
Calculator calculator;
void DefaultOnMessage(std::weak_ptr<Connection> conn)
{
if(conn.expired()) return;
auto connection_ptr = conn.lock();
std::cout << "上层得到了数据" << connection_ptr->Inbuffer() << std::endl;
std::string response_str = calculator.Handler(connection_ptr->Inbuffer());
if(response_str.empty()) return;
lg(Debug, "%s", response_str.c_str());
connection_ptr->AppendOutBuffer(response_str);
auto tcpserver = connection_ptr->_tcp_server_ptr.lock();
tcpserver->Sender(connection_ptr);
}
int main()
{
std::shared_ptr<TcpServer> epoll_svr(new TcpServer(19000, DefaultOnMessage));
epoll_svr->Init();
epoll_svr->Loop();
return 0;
}
Log.hpp
cpp
#pragma once
#include <iostream>
#include <stdarg.h> // 使用可变参数列表需要用到这个头文件
#include <ctime>
#include <cstdlib>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define SIZE 1024
#define Screen 1
#define Onefile 2
#define Classfile 3
#define LogFile "log.txt"
class Log
{
public:
Log()
{
printMethod = Screen; // 默认为屏幕打印
path = "./log/";
}
void Enable(int method) // 更换日志的打印方式
{
printMethod = method;
}
std::string levelToString(int level) // 返回日志等级的字符串
{
switch (level)
{
case Info:
return "Info";
case Debug:
return "Debug";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
default:
return "None";
}
}
void printLog(int level, const std::string &logtxt) // 日志打印
{
switch (printMethod)
{
case Screen:
std::cout << logtxt << std::endl;
break;
case Onefile:
printOneFile(LogFile, logtxt);
break;
case Classfile:
printClassFile(level, logtxt);
break;
default:
break;
}
}
void printOneFile(const std::string &logname, const std::string &logtxt) // 打印到一个文件中
{
std::string _logname = path + logname;
int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
if (fd < 0)
{
return;
}
write(fd, logtxt.c_str(), logtxt.size());
close(fd);
}
void printClassFile(int level, const std::string &logtxt) // 分类打印到对应的文件
{
std::string filename = LogFile;
filename += '.';
filename += levelToString(level);
printOneFile(filename, logtxt);
}
~Log()
{}
void operator()(int level, const char *format, ...)
{
char leftbuffer[1024]; // 一条日志左边的格式信息,包括日志等级和时间。
time_t t = time(nullptr); // 返回值是时间戳
struct tm *ctime = localtime(&t); // 该函数可以将时间戳转换成一个struct tm 结构。
// 下面的\是续行符,加不加都行。
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),\
ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,\
ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
va_list s;
va_start(s, format);
char rightbuffer[SIZE]; // 一条日志右边日志内容
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s);
char logtxt[SIZE * 2]; // 合成一条日志信息
snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);
// printf("%s", logtxt);
printLog(level, logtxt); // 打印
}
private:
int printMethod; // 日志打印的方式
std::string path; // 日志打印路径
};
Log lg;
Epoll.hpp
cpp
#pragma once
#include <sys/epoll.h>
#include "nocopy.hpp" // 防拷贝
#include "Log.hpp"
#include <cstring>
class Epoll : public nocopy
{
static const int size = 128;
public:
Epoll()
{
_epfd = epoll_create(size);
if(_epfd == -1)
{
lg(Error, "epoll_create error: %s", strerror(errno));
}
else
{
lg(Info, "epoll_create success: %d", _epfd);
}
}
int EpollWait(struct epoll_event revents[], int num, int timeout)
{
int n = epoll_wait(_epfd,revents, num, timeout);
return n;
}
int EpollUpdate(int oper, int sock, uint32_t event)
{
int n = 0;
if(oper == EPOLL_CTL_DEL)
{
n = epoll_ctl(_epfd, oper, sock, nullptr);
if(n != 0)
{
lg(Error, "epoll_ctl delete error, sockfd: %d", sock);
}
}
else
{
struct epoll_event ev;
ev.events = event;
ev.data.fd = sock; // 方便后续得知是哪一个fd就绪了
n = epoll_ctl(_epfd, oper, sock, &ev);
if(n != 0)
{
lg(Error, "epoll_ctl error");
}
}
return n;
}
~Epoll()
{
if(_epfd > 0) close(_epfd);
}
private:
int _epfd;
int _timeout{-1};
};
comm.hpp
cpp
#pragma once
#include <unistd.h>
#include <fcntl.h>
void SetNonBlockOrDie(int sock) // 设置非阻塞
{
int f1 = fcntl(sock, F_GETFL);
if (f1 < 0)
exit(NON_BLOCK_ERR);
fcntl(sock, F_SETFL, f1 | O_NONBLOCK);
}
cpp
#include "Socket.hpp"
#include "Protocol.hpp"
#include <iostream>
#include <ctime>
#include <cassert>
#include <unistd.h>
static void Usage(const std::string& proc)
{
std::cout << "\nUsage: " << proc << " serverip serverport\n" << std::endl;
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(0);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
Sock sockfd;
sockfd.Socket();
bool r = sockfd.Connect(serverip, serverport);
if(!r) return 1;
srand(time(nullptr) ^ getpid());
int cnt = 1;
const std::string opers = "+-*/%=^&";
std::string inbuffer_stream;
while(cnt <= 10)
{
std::cout << "=========================\ntest number: " << cnt << std::endl;
int x = rand() % 100 + 1;
usleep(1234);
int y = rand() % 100;
usleep(4321);
char oper = opers[rand() % opers.size()];
Request req(x, y, oper);
req.DebugPrint();
std::string package;
req.Serialize(&package);
package = Encode(package);
write(sockfd.Fd(), package.c_str(), package.size());
char buffer[128];
ssize_t n = read(sockfd.Fd(), buffer, sizeof(buffer));
if(n > 0)
{
buffer[n] = 0;
inbuffer_stream += buffer;
std::string content;
bool r = Decode(inbuffer_stream, &content);
assert(r);
Response resp;
r = resp.Deserialize(content);
assert(r);
resp.DebugPrint();
}
sleep(1);
cnt++;
}
sockfd.Close();
return 0;
}
Calculator.hpp
cpp
#pragma once
#include "Protocol.hpp"
#include <iostream>
enum { Div_Zero = 1, Mod_Zero, Other_Oper };
class Calculator // 计算器
{
public:
Calculator() // 计算器
{}
Response CalculatorHelper(const Request& req)
{
Response resp(0, 0);
switch (req.op)
{
case '+':
resp.result = req.x + req.y;
break;
case '-':
resp.result = req.x - req.y;
break;
case '*':
resp.result = req.x * req.y;
break;
case '/':
{
if(req.y == 0) resp.code = Div_Zero;
else resp.result = req.x / req.y;
}
break;
case '%':
{
if(req.y == 0) resp.code = Mod_Zero;
else resp.result = req.x % req.y;
}
break;
default:
resp.code = Other_Oper;
break;
}
return resp;
}
std::string Handler(std::string& package)
{
std::string content;
bool r = Decode(package, &content);
if(!r) return "";
Request req;
r = req.Deserialize(content);
if(!r) return "";
content = "";
Response resp = CalculatorHelper(req);
resp.Serialize(&content);
content = Encode(content);
return content;
}
~Calculator()
{}
};
5.3.6、使用场景
epoll的高性能,是有一定的特定场景的,如果场景选择的不适宜,epoll的性能可能适得其反。
对于多连接,且多连接中只有一部分连接比较活跃时,比较适合使用epoll,例如:典型的一个需要处理上万个客户端的服务器(各种互联网APP的入口服务器),这样的服务器就很适合epoll。
如果只是系统内部,服务器和服务器之间进行通信,只有少数的几个连接,这种情况下用epoll就并不合适。
注:具体用哪种IO模型需要根据需求和场景特点来决定。