目录
关于io:
1.应用层read 或write 的时候,本质是把数据从操作系统拿到用户层或者用户层写给操作系统---本质上就是拷贝函数。
2.IO = 等待 + 拷贝
(为什么需要等待? 因为要进行拷贝,必须先判断条件成立。我们将这个条件称为读写事件)
那么什么叫做高效的IO呢? 单位时间内,IO过程中,等待的比重越小,IO的效率就越高。
几乎所有提高IO效率的策略,本质都是使得等待的比重减少。
五种IO模型
1.阻塞IO
阻塞IO: 在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式
不设置的情况下 默认都是阻塞的。
阻塞io代码
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <cstdio>
using namespace std;
int main()
{
char buffer[1024];
while(true)
{
printf("Please Enter# ");
fflush(stdout);
ssize_t n = read(0, buffer, sizeof(buffer)-1);
if(n > 0)
{
buffer[n] = 0;
cout << "echo : " << buffer << endl;
}
else if(n == 0)
{
cout << "read done" << endl;
}
else
{
cerr << "read error" << endl;
}
}
return 0;
}
2. 非阻塞IO
如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码.
非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一般只有特定场景下才使用.
非阻塞io 代码
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <cstdio>
#include <cerrno>
#include <cstring>
using namespace std;
void SetNonBlock(int fd)//设置文件描述符的方式有很多,使用fcntl比较
{
int fl = fcntl(fd, F_GETFL);
if(fl < 0)
{
perror("fcntl");
return;
}
fcntl(fd,F_SETFL, fl | O_NONBLOCK);
}
int main()
{
char buffer[1024];
SetNonBlock(0);
while(true)
{
sleep(1);
printf("Please Enter# ");
fflush(stdout);
ssize_t n = read(0, buffer, sizeof(buffer)-1);
if(n > 0)
{
buffer[n] = 0;
cout << "echo : " << buffer << endl;
}
else if(n == 0)
{
cout << "read done" << endl;
}
else
{
if(errno == EWOULDBLOCK)
{
cout << "0 fd data not ready, try again!" << endl;
}
else
{
cerr << "read error, n = " << n << "errno code: "
<< errno << ", error str: " << strerror(errno) << endl;
}
}
}
return 0;
}
3.信号驱动IO
内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作
用的比较少
4.IO多路转接:
虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态.
5.异步IO
由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据).
6.小结
任何IO过程中, 都包含两个步骤. 第一是等待, 第二是拷贝. 而且在实际的应用场景中, 等待消耗的时间往往都远远高于拷贝的时间. 让IO更高效, 最核心的办法就是让等待的时间尽量少.
高级IO重要概念
同步通信 vs 异步通信
(synchronous communication/ asynchronous communication)
同步和异步关注的是消息通信机制.
所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回. 但是一旦调用返回,就得到返回值了; 换句话说,就是由调用者主动等待这个调用的结果;
异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果; 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用.
(其根本就在于调用者有没有“参与”IO,即有无参与等或拷贝)
(我们回忆在讲多进程多线程的时候, 也提到同步和互斥. 这里的同步通信和进程之间的同步是完全不相干的概念.)
阻塞 vs 非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.
1.阻塞调用是指调用结果返回之前,当前线程会被挂起. 调用线程只有在得到结果之后才会返回.
2.非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程
其他高级IO
非阻塞IO,纪录锁,系统V流机制,I/O多路转接(也叫I/O多路复用),readv和writev函数以及存储映射IO(mmap),这些统称为高级IO.
我们此处重点讨论的是I/O多路转接
非阻塞IO
fcntl
一个文件描述符, 默认都是阻塞IO.
函数原型如下.
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
传入的cmd的值不同, 后面追加的参数也不相同.
fcntl函数有5种功能:
1.复制一个现有的描述符(cmd=F_DUPFD).
2.获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
3.获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
4.获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
5.获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).
我们此处只是用第三种功能, 获取/设置文件状态标记, 就可以将一个文件描述符设置为非阻塞.
实现函数SetNoBlock
基于fcntl, 我们实现一个SetNoBlock函数, 将文件描述符设置为非阻塞.
void SetNoBlock(int fd)
{
int fl = fcntl(fd, F_GETFL);
if (fl < 0)
{
perror("fcntl");
return;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图).
然后再使用F_SETFL将文件描述符设置回去. 设置回去的同时, 加上一个O_NONBLOCK参数.
如果我们设置成非阻塞,并且底层fd数据没有就绪,recv/read/write/send,返回值会以出错的形式返回。
不过此时有两种情况a.真的出错b.底层没有就绪
那么我们怎么区分呢? 通过errno区分! 如果错误码为11,那么说明我们当前根本没有出错。只不过是底层数据没就绪。这个11实际上就是我们之前提到的 EWOULDBLOCK。
I/O多路转接之select
io = 等 + 拷贝
select只负责等 ,一次可以等待多个fd
简单介绍select函数
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
第一个参数nfds是需要检查的文件描述符的最大值加 1,比如我们要检查的最大的文件描述符为5,那么nfds就是6.
第二三四个参数都是fd_set 类型的,fd_set是内核提供的一个类型,是一个位图。我们目前关心的fd上面的事件主要有1.读2.写3.异常。对应的就是第二三四个参数了,但是主要还是关心读和写。我们关心哪个事件就把变量填进去,如果都关心就都填进去。如果先关心读后关心写,可以先填读变量后填写变量。这三个参数是输入输出型参数。我们输入就是代表:用户告诉内核,我给你的一个或多个文件描述符,你要帮我关心文件描述符上面的读事件,如果该事件就绪了,你要告诉我。返回的时候就代表:内核告诉用户,用户你让我关心的多个fd中有哪些已经就绪了。
为了举例方便,我们把位图简略成八位的位图,我们如果想让系统为我们关心0 1 2 3这四个文件描述符,那么我们就可以传入 0000 1111这样的位图。如果2号文件描述符就绪了,那么返回的时候就会返回0000 0100这样子的位图,来告诉我们2号描述符已经就绪。这就注定了select的时候一定会存在大量位图操作。下面这些就是操作位图的接口。
最后一个参数是一个内核数据结构,里面有两个字段,一个字段以秒为单位,一个字段以微秒为单位。这个参数的意思是每隔多长时间timeout一次。即一次检查的时候如果所有文件描述符都没有准备好,那么等待一段时间后就返回,而如果这个参数为null,那么即为阻塞等待。同时这个参数也是一个输入输出型参数,要是等待的时候,例如我们传入的参数是{5,0},我们等待了2秒此时有文件描述符准备好了,那么最后这个参数就会变成{3,0}。
返回值有三种情况,设返回这存在 n里面,第一种n大于0,即有n个文件描述符就绪了。
第二种n=0,为超时,没有错误,但是也没有fd就绪
第三中n < 0,即为等待出错了。
如果事件就绪,而上层不处理,select会一直通知你
select效果演示
select 代码
Log.hpp
#pragma once
#include <iostream>
#include<time.h>
#include<stdarg.h>
#include <fcntl.h>
#include<unistd.h>
#define SIZE 1024
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define Screen 1
#define Onefile 2
#define Classfile 3
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 logmessage(int level,const char *format, ...)
// {
// time_t t = time(nullptr);//时间戳
// struct tm *ctime = localtime(&t);//用时间戳得到一个结构体,可以从里面取年月日时分秒
// char leftbuffer[SIZE];
// 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);//把字符串存到leftbuffer里面
// va_list s;
// va_start(s, format);
// char rightbuffer[SIZE];
// vsnprintf(rightbuffer, sizeof(rightbuffer),format, s);//用这个库函数我们就不用自己作字符串解析了
// //格式 默认部分(左)+自定义部分(右)
// char logtxt[SIZE*2];
// snprintf(logtxt, sizeof(logtxt),"%s %s\n", leftbuffer, rightbuffer);
// printLog(level, logtxt);//暂时打印
// }
void printLog(int level, std::string logtxt)
{
switch (printMethod)
{
case Screen:
std::cout << logtxt << std:: endl;
break;
case Onefile:
printOneFile("LogFile" ,logtxt);
break;
case Classfile:
printClassFile(level, logtxt);
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);//LogFile
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);//LogFile.Debug/Warning/Fatal
printOneFile(filename, logtxt);
}
~Log()//这里析构只是为了让类看起来完整
{
}
void operator()(int level,const char *format, ...)
{
time_t t = time(nullptr);//时间戳
struct tm *ctime = localtime(&t);//用时间戳得到一个结构体,可以从里面取年月日时分秒
char leftbuffer[SIZE];
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);//把字符串存到leftbuffer里面
va_list s;
va_start(s, format);
char rightbuffer[SIZE];
vsnprintf(rightbuffer, sizeof(rightbuffer),format, s);//用这个库函数我们就不用自己作字符串解析了
//格式 默认部分(左)+自定义部分(右)
char logtxt[SIZE*2];
snprintf(logtxt, sizeof(logtxt),"%s %s", leftbuffer, rightbuffer);
printLog(level, logtxt);
}
private:
int printMethod;
std :: string path;
};
Log lg;
Main.cc
#include "SelectServer.hpp"
#include <memory>
int main()
{
std::unique_ptr<SelectServer> svr(new SelectServer());
svr->Init();
svr->Start();
return 0;
}
Makefile
select_server:Main.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f select_server
SelectServer.hpp
#pragma once
#include <iostream>
#include <sys/select.h>
#include "Socket.hpp"
using namespace std;
static const uint16_t defaultport = 12345;
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++)
{
fd_array[i] = defaultfd;
}
}
bool Init()
{
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
return true;
}
void Accepter()
{
// 我们的链接事件就绪
std::string clientip;
uint16_t clientport = 0;
int sock = _listensock.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 (fd_array[pos] != defaultfd)
continue;
else
break;
}
if (pos == fd_num_max) // 到达上限 无法处理
{
lg(Warning, "server is full, close %d now", sock);
close(sock);
}
else
{
fd_array[pos] = sock;
PrintFd();
}
}
void Recver(int fd, int pos)
{
char buffer[1024];
ssize_t n = read(fd, buffer, sizeof(buffer) - 1); // 这里其实有bug 因为我们并不能保证一次读到的就是一个完整的报文
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);
fd_array[pos] = defaultfd; // 这里本质是从select中移除
}
else
{
lg(Warning, "recv error: fd is : %d", fd);
close(fd);
fd_array[pos] = defaultfd;
}
}
void Dispatcher(fd_set &rfds) // 因为我们今天只关心读事件 因此这里我们处理的时候也只处理了读事件相关,只传了一个参数
{
for (int i = 0; i < fd_num_max; i++)
{
int fd = fd_array[i];
if (fd == defaultfd)
continue;
if (FD_ISSET(fd, &rfds)) // 读事件已经就绪了
{
if (fd == _listensock.Fd()) // 处理listen套接字,建立新链接
{
Accepter();
}
else // 不是监听套接字 那么就是普通的读事件就绪
{
Recver(fd, i);
}
}
}
}
void Start()
{
int listensock = _listensock.Fd();
fd_array[0] = listensock; // 初始化一下
while (true)
{
fd_set rfds;
FD_ZERO(&rfds);
int maxfd = fd_array[0];
for (int i = 0; i < fd_num_max; i++)
{
if (fd_array[i] == defaultfd)
continue;
FD_SET(fd_array[i], &rfds); // 把合法的文件描述符都添加进去
if (maxfd < fd_array[i])
{
maxfd = fd_array[i];
lg(Info, "max fd update, max fd is : %d", maxfd);
}
}
// 能不能直接accept? 不能! 因为accept本质上就是检测并获取listensock上面的事件--即新链接到来。它等价于读事件就绪。
struct timeval timeout = {5, 0}; // 因为是输入输出参数,可能需要进行周期的重复设置
int n = select(maxfd + 1, &rfds, nullptr, nullptr, &timeout);
// 如果事件就绪,而上层不处理,select会一直通知你
// select 告诉你就绪了,接下来的一次读取,我们读取fd的时候,不会被阻塞
switch (n)
{
case 0:
cout << "time out, timeout: " << timeout.tv_sec << "." << timeout.tv_usec << endl;
break;
case -1:
cerr << "select error" << endl;
break;
default:
// 有事件就绪了
cout << "get a new link !!" << endl;
Dispatcher(rfds);
break;
}
}
}
void PrintFd()
{
cout << "online fd list: ";
for (int i = 0; i < fd_num_max; i++)
{
if (fd_array[i] == defaultfd)
continue;
cout << fd_array[i] << ", ";
}
cout << endl;
}
~SelectServer()
{
_listensock.Close();
}
private:
Sock _listensock;
uint16_t _port;
int fd_array[fd_num_max]; // 为了让文件描述符可以在各个函数中传递,我们采用辅助数组的方式
// int wfd_array[fd_num_max]; 如果以后需要关心其它事件,则可以添加数组
};
Socket.hpp
#pragma
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include "Log.hpp"
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, "socker error, %s: %d", strerror(errno), errno);
exit(SocketErr);
}
int opt = 1;
setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR,&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, "socker error, %s: %d", strerror(errno), errno);
exit(SocketErr);
}
}
void Listen()
{
if (listen(sockfd_, backlog) < 0)
{
lg(Fatal, "bind 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, "listen 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;
}
int 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_, (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_;
};
Select的缺点
1.等待的文件描述符是有上限的
2.输入输出型参数比较多,数据拷贝的频率比较高
3.输入输出型参数比较多,每次都要对关心的文件描述符进行事件重置
4.用户层,使用第三方数组管理用户的fd,用户层需要很多次遍历。同时内核中检测文件描述符事件就绪,也需要遍历
I/O多路转接之poll (了解)
Poll主要对select等待的文件描述符有上限,以及输入输出型参数比较多的问题作了优化。
Poll需要引入第三方数组来辅助管理文件描述符,对于操作系统来说,这个值肯定是有一个上限的,但是这并非Poll的问题,而是操作系统的性能决定,所以这个上限并不是像select的上限,是被它本身限制。
poll也优化了参数,使得 我们需要关心的事件 和 操作系统通知我们的事件就绪情况 得以分离。
不过poll最大的问题是,poll依旧需要在用户层检测已经就绪的revents,并且在内核也是需要进行遍历来确认哪些文件描述符是已经就绪了。
poll使用代码
Log.hpp
#pragma once
#include <iostream>
#include<time.h>
#include<stdarg.h>
#include <fcntl.h>
#include<unistd.h>
#define SIZE 1024
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define Screen 1
#define Onefile 2
#define Classfile 3
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 logmessage(int level,const char *format, ...)
// {
// time_t t = time(nullptr);//时间戳
// struct tm *ctime = localtime(&t);//用时间戳得到一个结构体,可以从里面取年月日时分秒
// char leftbuffer[SIZE];
// 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);//把字符串存到leftbuffer里面
// va_list s;
// va_start(s, format);
// char rightbuffer[SIZE];
// vsnprintf(rightbuffer, sizeof(rightbuffer),format, s);//用这个库函数我们就不用自己作字符串解析了
// //格式 默认部分(左)+自定义部分(右)
// char logtxt[SIZE*2];
// snprintf(logtxt, sizeof(logtxt),"%s %s\n", leftbuffer, rightbuffer);
// printLog(level, logtxt);//暂时打印
// }
void printLog(int level, std::string logtxt)
{
switch (printMethod)
{
case Screen:
std::cout << logtxt << std:: endl;
break;
case Onefile:
printOneFile("LogFile" ,logtxt);
break;
case Classfile:
printClassFile(level, logtxt);
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);//LogFile
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);//LogFile.Debug/Warning/Fatal
printOneFile(filename, logtxt);
}
~Log()//这里析构只是为了让类看起来完整
{
}
void operator()(int level,const char *format, ...)
{
time_t t = time(nullptr);//时间戳
struct tm *ctime = localtime(&t);//用时间戳得到一个结构体,可以从里面取年月日时分秒
char leftbuffer[SIZE];
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);//把字符串存到leftbuffer里面
va_list s;
va_start(s, format);
char rightbuffer[SIZE];
vsnprintf(rightbuffer, sizeof(rightbuffer),format, s);//用这个库函数我们就不用自己作字符串解析了
//格式 默认部分(左)+自定义部分(右)
char logtxt[SIZE*2];
snprintf(logtxt, sizeof(logtxt),"%s %s", leftbuffer, rightbuffer);
printLog(level, logtxt);
}
private:
int printMethod;
std :: string path;
};
Log lg;
Main.cc
#include "PollServer.hpp"
#include <memory>
int main()
{
//std::unique_ptr<SelectServer> svr(new SelectServer());
std::unique_ptr<PollServer> svr(new PollServer());
svr->Init();
svr->Start();
return 0;
}
Makefile
poll_server:Main.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f poll_server
PollServer.hpp
#pragma once
#include <iostream>
#include <poll.h>
#include "Socket.hpp"
using namespace std;
static const uint16_t defaultport = 12345;
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()
{
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
return true;
}
void Accepter()
{
// 我们的链接事件就绪
std::string clientip;
uint16_t clientport = 0;
int sock = _listensock.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); // 这里其实有bug 因为我们并不能保证一次读到的就是一个完整的报文
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; // 这里本质是从select中移除
}
else
{
lg(Warning, "recv error: 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 == _listensock.Fd()) // 处理listen套接字,建立新链接
{
Accepter();
}
else // 不是监听套接字 那么就是普通的读事件就绪
{
Recver(fd, i);
}
}
}
}
void Start()
{
_event_fds[0].fd = _listensock.Fd();
_event_fds[0].events = POLLIN;
int timeout = 3000; // 3s
while (true)
{
int n = poll(_event_fds, fd_num_max, timeout);
switch (n)
{
case 0:
cout << "time out..." << endl;
break;
case -1:
cerr << "poll error" << endl;
break;
default:
// 有事件就绪了
cout << "get a new link !!" << endl;
Dispatcher();
break;
}
}
}
void PrintFd()
{
cout << "online 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()
{
_listensock.Close();
}
private:
Sock _listensock;
uint16_t _port;
struct pollfd _event_fds[fd_num_max];
// int fd_array[fd_num_max]; // 为了让文件描述符可以在各个函数中传递,我们采用辅助数组的方式
// int wfd_array[fd_num_max]; 如果以后需要关心其它事件,则可以添加数组
};
Socket.hpp
#pragma
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include "Log.hpp"
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, "socker error, %s: %d", strerror(errno), errno);
exit(SocketErr);
}
int opt = 1;
setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR,&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, "socker error, %s: %d", strerror(errno), errno);
exit(SocketErr);
}
}
void Listen()
{
if (listen(sockfd_, backlog) < 0)
{
lg(Fatal, "bind 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, "listen 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;
}
int 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_, (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_;
};
poll结果示例
I/O多路转接之epoll
1.快速认识epoll的接口
epoll有三个接口
1.epoll_create
int epoll_create(int size);
创建一个epoll模型
1.自从linux2.6.8之后,size参数是被忽略的.
2.用完之后, 必须调用close()关闭.
2.epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
它是 epoll的事件注册函数.
它不同于select()是在监听事件时告诉内核要监听什么类型的事件, 而是在这里先注册要监听的事件类型.
第一个参数是epoll_create()的返回值(epoll的句柄).
第二个参数表示动作,用三个宏来表示.
第三个参数是需要监听的fd.
第四个参数是告诉内核需要监听什么事.
第二个参数的取值:
EPOLL_CTL_ADD :注册新的fd到epfd中;
EPOLL_CTL_MOD :修改已经注册的fd的监听事件;
EPOLL_CTL_DEL :从epfd中删除一个fd;
struct epoll_event结构如下:
epoll_event 中的events可以是以下几个宏的集合(以位图的形式传递标记位):
EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
EPOLLOUT : 表示对应的文件描述符可以写;
EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
EPOLLERR : 表示对应的文件描述符发生错误;
EPOLLHUP : 表示对应的文件描述符被挂断;
EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里.
3.epoll_ctl
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
收集在epoll监控的事件中已经发送的事件.
参数events是分配好的epoll_event结构体数组.
epoll将会把发生的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存).
maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size.
参数timeout是超时时间 (毫秒,0会立即返回,-1是永久阻塞).
如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时, 返回小于0表示函数失败.
2.epoll工作原理
我们的操作系统里面有红黑树,里面的节点内容包含内核要关心的文件描述符,要关心的事件,以及相应的链接字段等。
操作系统里面也有一个就绪队列,里面的节点内容包含已经就绪的文件描述符,已经就绪的事件。
我们的操作系统里还有回调函数。
网卡以硬件中断的方式把数据搬到网卡驱动里面,此时数据链路层会自动调用callback,这个函数把数据向上交付,交付给tcp的接收队列。同时查找操作系统里面的红黑树(文件描述符的特性天然可以作为红黑树的键值),确认这个接收队列和哪一个文件描述符是关联的,这个文件描述符有没有关心对应的读事件写事件等等。如果这次到来的数据真的属于某个文件描述符关心的事件(如读事件),那么它还会构建一个就绪节点,插入到就绪队列中。所以从此以后用户只需要从就绪队列里面拿就绪事件就可以了。
我们把以上三个机制称为epoll模型(它们其实是会被整合到一起的,举个例子就像把红黑树的根节点,就绪队列的头指针放在一起,那么这两个数据结构不就被整合起来了吗)。因此我们epoll_create创建epoll模型,实际上就是创建相应的红黑树,相应的就绪队列以及注册相应的回调函数。
因为一个进程可以创建多个epoll模型,因此epoll模型也需要通过先描述再组织的方式管理这些epoll模型。
我们把epoll模型也当成文件,那么它也就会有一个struct file结构体来管理。struct file中有指针来指向epoll模型,而只要我们把这个struct file的文件描述符添加到其它文件的文件描述符表里,那么相应的epoll模型也会很容易被找到了。
所以我们epoll create的返回值也是一个文件描述符。
epoll的优势
1. 我们发现,从我们的底层数据交到tcp队列里收到数据 到事件就绪,整个过程是操作系统一路回调进行的,所以操作系统再也不需要去轮询检测了。
2. 而我们的用户检测就绪的时间复杂度是O(1),(因为只需要判断队列空不空),获取就绪时间复杂度为O(N)。
3.对于它本身来说它的fd,event是没有上限的。这个上限是取决于红黑树和就绪队列的。这个红黑树其实就是相当于我们前面select和poll中我们用户自己维护的管理文件描述符的数组。
4.epoll wait的返回值n表示有几个文件描述符就绪,相对应的epoll event全都被放在输出型参数里,所以我们从此之后就不需要检测过滤非法的文件描述符了,直接从相应的输出型参数里面取就可以了。这也是epoll有别于poll和select的一个很重要的点。
如图,epoll create会创建epoll模型,返回相应的文件描述符,epoll ctl与红黑树相关联,可以增删改相应的节点,epoll wait与就绪队列相关联,它的输出型参数里面的epoll event存着已经就绪的文件描述符和事件类型等。
3.快速用epoll实现一个echo server
CMakeLists.txt
# 最低CMake版本要求
cmake_minimum_required(VERSION 3.10)
set(CMAKE_CXX_STANDARD 11) # 指定C++标准为C++11
# 项目名称
project(EpollServer)
# 添加可执行文件
add_executable(epoll_server Main.cc)
Epoller.hpp
#pragma once
#include "nocopy.hpp"
#include "Log.hpp"
#include <sys/epoll.h>
#include <cstring>
#include <cerrno>
class Epoller:public nocopy
{
static const int size = 128;
public:
Epoller()
{
_epfd = epoll_create(size);
if(_epfd == -1)
{
lg(Error, "epoll_create error: %s", strerror(errno));
}
else{
lg(Info, "epoll_create success: %d", _epfd);
}
}
int EpollerWait(struct epoll_event revents[], int num)
{
int n = epoll_wait(_epfd,revents, num, _timeout);
return n;
}
int EpollerUpdate(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
{
//EPOLL_CTL_MOD || EPOLL_CTL_ADD
struct epoll_event ev;
ev.events = event;
ev.data.fd = sock;//目前,这么写是为了方便我们后期得知是哪一个fd就绪了
int n = epoll_ctl(_epfd, oper, sock, &ev);
if(n != 0)
{
lg(Error,"epoll_ctl error!");
}
}
return n;
}
~Epoller()
{
if(_epfd >= 0)
{
close(_epfd);
}
}
private:
int _epfd;
int _timeout{3000};
};
EpollerServer.hpp
#pragma once
#include <iostream>
#include <memory>
#include <sys/epoll.h>
#include "Socket.hpp"
#include "Epoller.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),
_listsocket_ptr(new Sock()),
_epoller_ptr(new Epoller())
{
}
void Init()
{
_listsocket_ptr->Socket();
_listsocket_ptr->Bind(_port);
_listsocket_ptr->Listen();
lg(Info, "create listen socket success: %d\n", _listsocket_ptr->Fd());
}
void Accepter()
{
// 获取了一个新链接
std::string clientip;
uint16_t clientport;
int sock = _listsocket_ptr->Accept(&clientip, &clientport);
if (sock > 0)
{
// 我们不能直接读,还是需要先把事件放入epoll模型中进行关心
_epoller_ptr->EpollerUpdate(EPOLL_CTL_ADD, sock, EVENT_IN);
lg(Info, "get a new link, client info@ %s:%d", clientip.c_str(), clientport);
}
// 不成功先暂时不考虑,直接continue了
}
//这整个Recver只是为了使用接口而已,实际上还是有比较多的问题的
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_str = "server echo $ ";
echo_str += buffer;
write(fd, echo_str.c_str(), echo_str.size());
}
else if (n == 0)
{
lg(Info, "client quit, me too, close fd is : %d", fd);
//我们需要把epoll中对应文件描述符以及关心的事件给移除掉
_epoller_ptr->EpollerUpdate(EPOLL_CTL_DEL, fd,0);//移除的时候需要保证这个文件描述符是合法的,先移除再关闭
close(fd);
}
else
{
lg(Warning, "recv error: fd is : %d", fd);
_epoller_ptr->EpollerUpdate(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 == _listsocket_ptr->Fd())
{
Accepter();
}
else
{
// 其它fd上面的普通读取事件就绪
Recver(fd);
}
}
else if (events & EVENT_OUT)
{
}
else
{
}
}
}
void Start()
{
// 将listensock添加到epoll中 ->listensock和关心的事件,添加到内核epoll模型中
_epoller_ptr->EpollerUpdate(EPOLL_CTL_ADD, _listsocket_ptr->Fd(), EVENT_IN);
struct epoll_event revs[num];
while (true)
{
int n = _epoller_ptr->EpollerWait(revs, num);
if (n > 0)
{
// 有事件就绪
lg(Debug, "event happend, 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()
{
_listsocket_ptr->Close();
}
private:
std::shared_ptr<Sock> _listsocket_ptr;
std::shared_ptr<Epoller> _epoller_ptr;
uint16_t _port;
};
Log.hpp
#pragma once
#include <iostream>
#include<time.h>
#include<stdarg.h>
#include <fcntl.h>
#include<unistd.h>
#include <cstddef> // nullptr
#include <cstdio> // snprintf, vsnprintf
#include <ctime> // time
#define SIZE 1024
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define Screen 1
#define Onefile 2
#define Classfile 3
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 logmessage(int level,const char *format, ...)
// {
// time_t t = time(nullptr);//时间戳
// struct tm *ctime = localtime(&t);//用时间戳得到一个结构体,可以从里面取年月日时分秒
// char leftbuffer[SIZE];
// 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);//把字符串存到leftbuffer里面
// va_list s;
// va_start(s, format);
// char rightbuffer[SIZE];
// vsnprintf(rightbuffer, sizeof(rightbuffer),format, s);//用这个库函数我们就不用自己作字符串解析了
// //格式 默认部分(左)+自定义部分(右)
// char logtxt[SIZE*2];
// snprintf(logtxt, sizeof(logtxt),"%s %s\n", leftbuffer, rightbuffer);
// printLog(level, logtxt);//暂时打印
// }
void printLog(int level, std::string logtxt)
{
switch (printMethod)
{
case Screen:
std::cout << logtxt << std:: endl;
break;
case Onefile:
printOneFile("LogFile" ,logtxt);
break;
case Classfile:
printClassFile(level, logtxt);
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);//LogFile
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);//LogFile.Debug/Warning/Fatal
printOneFile(filename, logtxt);
}
~Log()//这里析构只是为了让类看起来完整
{
}
void operator()(int level,const char *format, ...)
{
time_t t = time(nullptr);//时间戳
struct tm *ctime = localtime(&t);//用时间戳得到一个结构体,可以从里面取年月日时分秒
char leftbuffer[SIZE];
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);//把字符串存到leftbuffer里面
va_list s;
va_start(s, format);
char rightbuffer[SIZE];
vsnprintf(rightbuffer, sizeof(rightbuffer),format, s);//用这个库函数我们就不用自己作字符串解析了
//格式 默认部分(左)+自定义部分(右)
char logtxt[SIZE*2];
snprintf(logtxt, sizeof(logtxt),"%s %s", leftbuffer, rightbuffer);
printLog(level, logtxt);
}
private:
int printMethod;
std :: string path;
};
Log lg;
Main.cc
#include <iostream>
#include <memory>
#include "Epoller.hpp"
#include "EpollServer.hpp"
int main()
{
std::unique_ptr<EpollServer> epoll_svr(new EpollServer(12345));
epoll_svr->Init();
epoll_svr->Start();
//Epoller ep;
return 0;
}
nocopy.hpp
#pragma once
class nocopy
{
public:
nocopy()
{}
nocopy(const nocopy &) = delete;
const nocopy&operator=(const nocopy &)= delete;
};
Socket.hpp
#pragma
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cerrno> // errno
#include <cstring> // strerror
#include <cstdlib> // exit
#include "Log.hpp"
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, "socker error, %s: %d", strerror(errno), errno);
exit(SocketErr);
}
int opt = 1;
setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR,&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, "socker error, %s: %d", strerror(errno), errno);
exit(SocketErr);
}
}
void Listen()
{
if (listen(sockfd_, backlog) < 0)
{
lg(Fatal, "bind 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, "listen 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;
}
int 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_, (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_;
};
4.epoll的工作模式
假如有这样一个例子:
我们已经把一个tcp socket添加到epoll描述符
这个时候socket的另一端被写入了2KB的数据
调用epoll_wait,并且它会返回. 说明它已经准备好读取操作
然后调用read, 只读取了1KB的数据
继续调用epoll_wait......
水平触发Level Triggered 工作模式
epoll默认状态下就是LT工作模式.
一旦有新的链接到来,或者有新的数据到来,上层如果不取走,底层就会一直通知你,让你取走,这种模式就叫做LT。
1.当epoll检测到socket上事件就绪的时候, 可以不立刻进行处理. 或者只处理一部分.
2.如上面的例子, 由于只读了1K数据, 缓冲区中还剩1K数据, 在第二次调用 epoll_wait 时, epoll_wait仍然会立刻返回并通知socket读事件就绪.
3.直到缓冲区上所有的数据都被处理完, epoll_wait 才不会立刻返回.
4.支持阻塞读写和非阻塞读写
边缘触发Edge Triggered工作模式
如果我们将socket添加到epoll描述符的时候使用了EPOLLET标志, epoll进入ET工作模式.
简单来说就是数据从无到有,从有到多,才会通知我们一次
1.当epoll检测到socket上事件就绪时, 必须立刻处理.
2.如上面的例子, 虽然只读了1K的数据, 缓冲区还剩1K的数据, 在第二次调用 epoll_wait 的时候,
epoll_wait 不会再返回了.
3.也就是说, ET模式下, 文件描述符上的事件就绪后, 只有一次处理机会.
4.ET的性能比LT性能更高( epoll_wait 返回的次数少了很多,通知的效率更高). Nginx默认采用ET模式使用epoll.
5.只支持非阻塞的读写
select和poll其实也是工作在LT模式下. epoll既可以支持LT, 也可以支持ET.
因为ET模式只有数据从无到有,从有到多,才会通知我们一次,因此这种模式会倒逼程序员,每次通知,都必须把本轮的数据全部取走->即需要循环读取,直到读取出错。
正因为这种特性,因此我们需要把文件描述符设置成非阻塞的,因为文件描述符默认是阻塞的。
以及,ET模式不仅通知效率高,ET的io效率也更高,因为ET模式要求一次把所有数据都读走,那么因此tcp就可以向对方通告一个更大的窗口,从而在概率上让对方一次给我发送更多的数据。
但是在特定条件下,LT模式通知效率也可能更高,LT也可以将所有的文件描述符设置成non_block,然后循环读取。通知第一次的时候,就全部取走,不就和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的使用场景
epoll的高性能, 是有一定的特定场景的. 如果场景选择的不适宜, epoll的性能可能适得其反.
对于多连接, 且多连接中只有一部分连接比较活跃时, 比较适合使用epoll.
例如, 典型的一个需要处理上万个客户端的服务器, 例如各种互联网APP的入口服务器, 这样的服务器就很适合epoll.
如果只是系统内部, 服务器和服务器之间进行通信, 只有少数的几个连接, 这种情况下用epoll就并不合适. 具体要根据需求和场景特点来决定使用哪种IO模型.
我们上面简单写了一个epoll的代码,但是这段代码中有一个比较大的问题,即我们不能保证缓冲区一次就把完整的请求读完,并且这个缓冲区还是一个临时缓冲区。
因此我们需要给每一个文件描述符都定义一套对应的输入输出缓冲区,以及对应的结构体来描述它。我们对socket作封装后,对socket进行管理即可。
如何理解写
Reactor代码 (存在一些问题)
Calculator.hpp
// #pragma <iostream>
// #include <iostream>
// #include"Protocol.hpp"
// enum
// {
// Div_ZERO = 1,
// MOD_ZERO,
// Other_Oper
// };
// class ServerCal
// {
// public:
// ServerCal()
// {}
// 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;
// case '*':
// resp.result = req.x * req.y;
// 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 Calculator(std::string &package)
// {
// std::string content;
// bool r = Decode(package, &content);//"len"\n"10 + 20"\n
// if(!r)return "";
// Request req;
// r = req.Deserialize(content);//"10 + 20" ->x=10 op=+ y=20
// if(!r)return "";
// //解析不出来 或者序列化失败我们就return
// //并没有对报文进行修改
// //让它继续读,所以在TcpServer.hpp中是加等 即inbuffer_stream += buffer;
// content = "";
// Response resp = CalculatorHelper(req);//result=30 code=0
// resp.Serialize(&content);//"30 0"
// content = Encode(content);//"len"\n"30 0"\n
// return content;
// }
// };
#pragma once
#include <iostream>
#include "Protocol.hpp"
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;
}
// "len"\n"10 + 20"\n
std::string Handler(std::string &package)
{
std::string content;
bool r = Decode(package, &content); // "len"\n"10 + 20"\n
if (!r)
return "";
// "10 + 20"
Request req;
r = req.Deserialize(content); // "10 + 20" ->x=10 op=+ y=20
if (!r)
return "";
content = ""; //
Response resp = CalculatorHelper(req); // result=30 code=0;
resp.Serialize(&content); // "30 0"
content = Encode(content); // "len"\n"30 0"
return content;
}
~Calculator()
{
}
};
ClientCal.cc
#include "Socket.hpp"
#include <string>
#include <iostream>
#include <time.h>
#include "Protocol.hpp"
#include <unistd.h>
#include<assert.h>
static void Usage(const std::string &proc)
{
std::cout << "\nUsage:" << proc << " serverip serverport\n" << std::endl;
}
//./client ip port
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 <<"===================第" << cnt << "次测试..." << std::endl;
int x = rand() % 100 + 1;
usleep(123);
int y = rand() % 100;
usleep(321);
char oper = opers[rand()%opers.size()];
Request req(x, y, oper);
req.DebugPrint();
std::string package;
req.Serialize(&package);
package = Encode(package);
//std:: cout << "这是最新的发出请求: \n" << package;
write(sockfd.Fd(), package.c_str(), package.size());
// std:: cout << "这是最新的发出请求: \n" << package;
// write(sockfd.Fd(), package.c_str(), package.size());
// std:: cout << "这是最新的发出请求: \n" << package;
// write(sockfd.Fd(), package.c_str(), package.size());
// std:: cout << "这是最新的发出请求: \n" << package;
// write(sockfd.Fd(), package.c_str(), package.size());
char buffer[1280];
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);//服务器那边已经做过类似处理 这里就直接用assert判断了 注意,带assert的话需要在编译的时候加-g
Response resp;
r = resp.Deserialize(content);
assert(r);
resp.DebugPrint();
}
sleep(1);
std::cout <<"=====================================" << std::endl;
cnt++;
}
sockfd.Close();
return 0;
}
CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
set(CMAKE_CXX_STANDARD 11) # 指定C++标准为C++11
project(Reactor)
add_executable(reactor_server Main.cc)
target_link_libraries(reactor_server jsoncpp)
add_executable(client ClientCal.cc)
target_link_libraries(client jsoncpp)
Comm.hpp
#pragma once
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
void SetNonBlockOrDie(int sock)
{
int f1 = fcntl(sock, F_GETFD);
if (f1 < 0)
exit(NON_BLOCK_ERR);
fcntl(sock, F_SETFL, f1 | O_NONBLOCK);
}
Epoller.hpp
#pragma once
#include "nocopy.hpp"
#include "Log.hpp"
#include <sys/epoll.h>
#include <cstring>
#include <cerrno>
class Epoller:public nocopy
{
static const int size = 128;
public:
Epoller()
{
_epfd = epoll_create(size);
if(_epfd == -1)
{
lg(Error, "epoll_create error: %s", strerror(errno));
}
else{
lg(Info, "epoll_create success: %d", _epfd);
}
}
int EpollerWait(struct epoll_event revents[], int num, int timeout)
{
int n = epoll_wait(_epfd,revents, num, timeout);
return n;
}
int EpollerUpdate(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
{
//EPOLL_CTL_MOD || EPOLL_CTL_ADD
struct epoll_event ev;
ev.events = event;
ev.data.fd = sock;//目前,这么写是为了方便我们后期得知是哪一个fd就绪了
int n = epoll_ctl(_epfd, oper, sock, &ev);
if(n != 0)
{
lg(Error,"epoll_ctl error!");
}
}
return n;
}
~Epoller()
{
if(_epfd >= 0)
{
close(_epfd);
}
}
private:
int _epfd;
int _timeout{3000};
};
Log.hpp
#pragma once
#include <iostream>
#include<time.h>
#include<stdarg.h>
#include <fcntl.h>
#include<unistd.h>
#include <cstddef> // nullptr
#include <cstdio> // snprintf, vsnprintf
#include <ctime> // time
#define SIZE 1024
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define Screen 1
#define Onefile 2
#define Classfile 3
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 logmessage(int level,const char *format, ...)
// {
// time_t t = time(nullptr);//时间戳
// struct tm *ctime = localtime(&t);//用时间戳得到一个结构体,可以从里面取年月日时分秒
// char leftbuffer[SIZE];
// 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);//把字符串存到leftbuffer里面
// va_list s;
// va_start(s, format);
// char rightbuffer[SIZE];
// vsnprintf(rightbuffer, sizeof(rightbuffer),format, s);//用这个库函数我们就不用自己作字符串解析了
// //格式 默认部分(左)+自定义部分(右)
// char logtxt[SIZE*2];
// snprintf(logtxt, sizeof(logtxt),"%s %s\n", leftbuffer, rightbuffer);
// printLog(level, logtxt);//暂时打印
// }
void printLog(int level, std::string logtxt)
{
switch (printMethod)
{
case Screen:
std::cout << logtxt << std:: endl;
break;
case Onefile:
printOneFile("LogFile" ,logtxt);
break;
case Classfile:
printClassFile(level, logtxt);
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);//LogFile
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);//LogFile.Debug/Warning/Fatal
printOneFile(filename, logtxt);
}
~Log()//这里析构只是为了让类看起来完整
{
}
void operator()(int level,const char *format, ...)
{
time_t t = time(nullptr);//时间戳
struct tm *ctime = localtime(&t);//用时间戳得到一个结构体,可以从里面取年月日时分秒
char leftbuffer[SIZE];
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);//把字符串存到leftbuffer里面
va_list s;
va_start(s, format);
char rightbuffer[SIZE];
vsnprintf(rightbuffer, sizeof(rightbuffer),format, s);//用这个库函数我们就不用自己作字符串解析了
//格式 默认部分(左)+自定义部分(右)
char logtxt[SIZE*2];
snprintf(logtxt, sizeof(logtxt),"%s %s", leftbuffer, rightbuffer);
printLog(level, logtxt);
}
private:
int printMethod;
std :: string path;
};
Log lg;
Main.cc
#include <iostream>
#include <memory>
#include"Log.hpp"
#include <functional>
#include "TcpServer.hpp"//这是处理IO的
#include "Calculator.hpp"//处理业务
Calculator calculator;
//for debug
void DefaultOnMessage(std::shared_ptr<Connection> connection_ptr)
{
//对报文进行处理 有bug
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());
//response_str 发送出去
connection_ptr->AppendOutBuffer(response_str);
//正确理解发送
//connection_ptr->_send_cb(connection_ptr);
connection_ptr->_tcp_server_ptr->Sender(connection_ptr);//是服务器来调用,这样写更合适一些
}
int main()
{
std::unique_ptr<TcpServer> epoll_svr(new TcpServer(12345,DefaultOnMessage));
epoll_svr->Init();
epoll_svr->Loop();
//Epoller ep;
return 0;
}
nocopy.hpp
#pragma once
class nocopy
{
public:
nocopy()
{}
nocopy(const nocopy &) = delete;
const nocopy&operator=(const nocopy &)= delete;
};
Protocol.hpp
#pragma
#include <iostream>
#include <jsoncpp/json/json.h>
//#define MySelf 1
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;
}
bool Decode(std::string &package, std::string *content)
{
std::size_t pos = package.find(protocol_sep);
if(pos == std::string::npos)return false;
std::string len_str = package.substr(0, pos);
std::size_t len = std::stoi(len_str);
//package = len_str + content_str + 2
std::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
//struct => string "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;
*out = w.write(root);
return true;
#endif
}
bool Deserialize(const std::string & in)//"x op y"
{
#ifdef MySelf
std::size_t left = in.find(blank_space_sep);
if(left == std::string::npos)return false;
std::string part_x = in.substr(0, left);
std::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:
//x op y
int x;
int y;
char op;
};
class Response
{
public:
Response(int res, int c):result(res), code(c)
{
}
Response()
{
}
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;
*out = w.write(root);
return true;
#endif
}
bool Deserialize(const std::string & in)
{
#ifdef MySelf
std::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: " << result << "code: " << code << std::endl;
}
public:
int result;
int code;//0 可信,否则!0 具体是几表明对应的错误原因
};
Socket.hpp
#pragma
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cerrno> // errno
#include <cstring> // strerror
#include <cstdlib> // exit
#include "Log.hpp"
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, "socker error, %s: %d", strerror(errno), errno);
exit(SocketErr);
}
int opt = 1;
setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR,&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, "socker error, %s: %d", strerror(errno), errno);
exit(SocketErr);
}
}
void Listen()
{
if (listen(sockfd_, backlog) < 0)
{
lg(Fatal, "bind 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, "listen 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;
}
int 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_, (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_;
};
TcpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <memory>
#include <cerrno>
#include <functional>
#include <unordered_map>
#include "Log.hpp"
#include "nocopy.hpp"
#include "Epoller.hpp"
#include "Socket.hpp"
#include "Comm.hpp"
class Connection;
class TcpServer;
uint32_t EVENT_IN = (EPOLLIN | EPOLLET); // 设置|EPOLLET 设置为ET模式 因此文件描述符也得设置成非阻塞
uint32_t EVENT_OUT = (EPOLLOUT | EPOLLET);
const static int g_buffer_size = 128;
using func_t = std::function<void(std::shared_ptr<Connection>)>;
using except_func = std::function<void(std::weak_ptr<Connection>)>;
class Connection
{
public:
Connection(int sock, std::shared_ptr<TcpServer> tcp_server_ptr) : _sock(sock), _tcp_server_ptr(tcp_server_ptr)
{
}
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()//for debug
{
return _inbuffer;
}
std::string &Outbuffer()//for debug
{
return _outbuffer;
}
~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::shared_ptr<TcpServer> _tcp_server_ptr;
//std::weak_ptr<TcpServer> _tcp_server_ptr;
std::string _ip;
uint16_t _port;
};
class 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 Epoller()),
_listensock_ptr(new Sock())
{
}
void Init()
{
_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, uint16_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)
{
// 1.将listensocke也建立一个connection对象,将listensock添加到Connection中,同时listensock和Connection放入_connections中
std::shared_ptr<Connection> new_connection = std::make_shared<Connection>(sock, (std::shared_ptr<TcpServer>)this);
// std::make_shared<Connection>(...):这是 C++11 引入的标准库函数,用于安全地创建 std::shared_ptr 对象。它会在单个内存分配中同时创建 Connection 对象和引用计数,比直接使用 new 更高效。
new_connection->SetHandler(recv_cb, send_cb, except_cb);
new_connection->_ip = ip;
new_connection->_port = port;
// 2.添加到unordered_map
_connections.insert(std::make_pair(sock, new_connection));
// 3.我们添加对应的事件,除了要加到内核中,关心哪个文件描述符上的哪个文件fd event
_epoller_ptr->EpollerUpdate(EPOLL_CTL_ADD, sock, event);
lg(Debug,"add a new connection success, sockfd is : %d,sock");
}
//链接管理器
void Accepter(std::shared_ptr<Connection> connection)
{
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, get info-> [%s:%d], sockfd : %d", ipbuf, peerport, sock);
SetNonBlockOrDie(sock);
//listensock只需要设置_recv_cb,而其它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;
}
}
}
//事件管理器
//我们应不应该关心数据的格式? 不应该! 服务器只需要关心IO数据即可。有没有读完,报文的格式细节,你不用管。
void Recver(std::shared_ptr<Connection> connection)
{
//std::cout << "haha, got you!!!, sockfd: " << connection->SockFd() << std::endl;
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);//因为我们前面已经设置过了,所以虽然这里参数是0,但是是非阻塞读取
if(n > 0)
{
connection->AppendInBuffer(buffer);
}
else if(n == 0)
{
lg(Info, "sockfd: %d, client info %s:%d quit..",sock, connection->_ip.c_str(),connection->_port);
connection->_except_cb(connection);
}
else
{
if(errno == EWOULDBLOCK) break;
else if(errno == EINTR) continue;
else
{
lg(Warning, "sockfd: %d, client info %s:%d recv error...",sock, connection->_ip.c_str(),connection->_port);
connection->_except_cb(connection);
return;
}
}
}
//数据有了 但是不一定全, 1.检测 2.如果有完整报文,就处理
_OnMessage(connection);//读到的sock的所有数据 都在connection中
}
void Sender(std::shared_ptr<Connection> connection)
{
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 info %s:%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 Excepter(std::weak_ptr<Connection> connection)
{
if(connection.expired()) return;
auto conn = connection.lock();
int fd = conn->SockFd();
lg(Warning, "Excepter hander sockfd: %d, client info %s:%d excepter handler",
conn->SockFd(), conn->_ip.c_str(),conn->_port);
//1.移除对特定fd的关心
//EnableEvent(connection->SockFd(),false,false);
_epoller_ptr->EpollerUpdate(EPOLL_CTL_DEL,fd, 0);
//2.关闭异常的文件描述符
lg(Debug, "close %d done...\n", fd);
close(fd);
//3.从unordered_map中移除
lg(Debug, "remove %d from _connection...\n", fd);
auto iter = _connections.find(fd);
if(iter == _connections.end())return ;
_connections.erase(iter);
//_connections[fd].reset();
//_connections.erase(fd);
}
void EnableEvent(int sock, bool readable, bool writeable)
{
uint32_t events = 0;
events |= ((readable ? EPOLLIN : 0) | (write ? EPOLLOUT : 0) | EPOLLET);
_epoller_ptr->EpollerUpdate(EPOLL_CTL_MOD, sock, events);
}
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->EpollerWait(revs, num, timeout);
for (int i = 0; i < n; i++)
{
uint32_t events = revs[i].events;
int sock = revs[i].data.fd;
// 统一把事件异常转化为读写问题
if (events & EPOLLERR)
events |= (EPOLLIN | EPOLLOUT);
if (events & EPOLLHUP)
events |= (EPOLLIN | EPOLLOUT);
//只需要处理EPOLLIN EPOLLOUT
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(3000);
Dispatcher(-1);
PrintConnection();
}
_quit = true;
}
void PrintConnection()
{
std::cout << "_connections fd list: ";
for(auto & connection: _connections)
{
std::cout << connection.second->SockFd() << ", ";
std::cout <<"inbuffer: "<< connection.second->Inbuffer().c_str();
}
std::cout << std::endl;
}
~TcpServer()
{
}
private:
std::shared_ptr<Epoller> _epoller_ptr;//内核
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;
};