文章目录
全文约 8696 字,预计阅读时长: 25分钟
Reactor反应堆模式
Reactor
:负责检测关心的IO事件,当检测到IO事件时,分发给Handlers处理(对数据的读写处理+对数据的分析处理)。epoll ET
工作方式:只支持非阻塞IO文件。
简易的Reactor epoll ET TCP服务器
单进程基于ET非阻塞设计的一个Reactor模式+数据读写+数据分析。
socket封装
监听套接字的创建、绑定、监听、设置非阻塞I/O
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>
#include<fcntl.h>
using std::cerr;
using std::cin;
using std::cout;
using std::endl;
namespace ns_sock
{
enum
{
SOCK_ERR = 2,
BIND_ERR,
LISTEN_ERR
};
const int g_backlog = 5;
struct sock_package
{
static int SockCreate()
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
cerr << "socket failed" << endl;
exit(SOCK_ERR);
}
//设置可以立即重启绑定。
int opt = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
return sock;
}
static void Bind(const int &sock_fd, const uint16_t &port)
{
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_addr.s_addr = INADDR_ANY;
local.sin_port = htons(port);
local.sin_family = AF_INET;
if (bind(sock_fd, (sockaddr *)&local, sizeof(local)) < 0)
{
cerr << "Bind Error" << endl;
exit(BIND_ERR);
}
}
static void Listen(const int &sock_fd)
{
if (listen(sock_fd, g_backlog) < 0)
{
cerr << "Listen Error" << endl;
exit(LISTEN_ERR);
}
}
static void set_noblock(int listen_sk)
{
int ret_sock = fcntl(listen_sk,F_GETFL);
if(ret_sock<0)
{
cerr<<"fcntl error"<<endl;
exit(5);
}
fcntl(listen_sk,F_SETFL,ret_sock | O_NONBLOCK);
}
};
}
epoll。hpp
一:epoll文件描述符的创建,往epoll文件描述里、添加需要关心的I/O文件 、以及关心的I/O文件的事件;检测事件的发生。
二:当前I/O文件的错误回调功能具有:哈希表删除当前的fd值,epoll_del从epoll红黑树中删除该fd节点;最后关闭fd。
三:检测到所有的异常事件发生时,都交付给当前I/O文件的错误回调处理。
四:当检测有事件就绪时应检测当前I/O文件的FD是否还存在。因为可能发送事件触发时,上一秒该文件有异常,从epoll中删除了。
五:具有一个可以让:当前I/O文件读取完毕后,构建发送报文时,可以修改当前该文件的关心事件的功能。因为读事件时一直需要关心的,写事件需要服务器返回数据时开启,发送完毕取消关心。sock文件描述符的反应堆指针,是为了后续可以使用其内部相关功能。
#pragma once
#include <iostream>
#include <sys/epoll.h>
#include <unordered_map>
#include <string>
#include <utility>
#include <unistd.h>
using namespace std;
namespace ns_reactor
{
class reactor_sv;
class sock_pack;
typedef void (*sock_callback)(sock_pack &);//函数指针类型
class sock_pack//sock_fd配套设施。
{
public:
int sock_fd_;
reactor_sv *r;
string inbuffer;
string outbuffer;
sock_callback read_hand;//函数指针类型 声明 一个函数指针
sock_callback write_hand;
sock_callback error_hand;
public:
sock_pack() : sock_fd_(-1), r(nullptr)
{
read_hand = nullptr;
write_hand = nullptr;
error_hand = nullptr;
}
~sock_pack()
{
// if (sock_fd_ >= 0)
// {
// close(sock_fd_);
// }
cout << "dont Deconstruction me" << endl;
}
void sock_events_handler(sock_callback rc, sock_callback wc, sock_callback ec)
{
read_hand = rc;//函数指针格式一样即可。
write_hand = wc;
error_hand = ec;
}
};
----------
class reactor_sv//反应堆核心
{
private:
int epfd_;
unordered_map<int, sock_pack> react_mp;
public:
reactor_sv() : epfd_(-1)
{
}
~reactor_sv() {}
void epfd_create()//创建
{
epfd_ = epoll_create(128);
if (epfd_ < 0)
{
cerr << "epoll create error..." << endl;
return;
}
cout << "初始化完成..." << endl;
}
void epll_add(const sock_pack &sk, const uint32_t ext_events)//添加sock——fd至epoll中
{
epoll_event ske;
ske.events = ext_events;
ske.data.fd = sk.sock_fd_;
if (epoll_ctl(epfd_, EPOLL_CTL_ADD, sk.sock_fd_, &ske) < 0)
{
cerr << "epol_ctl failed.." << endl;
return;
}
else
{
react_mp.insert({sk.sock_fd_, sk});
}
cout << "ep_add success...!" << endl;
}
void Epoll_Del(int sock)//出错清除关心的IO文件。
{
auto iter = react_mp.find(sock);
if (iter == react_mp.end())
{
return;
}
epoll_ctl(epfd_, EPOLL_CTL_DEL, sock, nullptr);
react_mp.erase(iter);
close(sock);
cout << "Epoll_Del called..资源清理成功" << endl;
}
//当读取完毕修改写事件的关心。发送完毕以后,对写事件的取消关注。
void EventMod(int sock, bool isread, bool iswrite)
{
epoll_event ev;
ev.events = (EPOLLET | (isread ? EPOLLIN : 0) | (iswrite ? EPOLLOUT : 0));
ev.data.fd = sock;
if (epoll_ctl(epfd_, EPOLL_CTL_MOD, sock, &ev) == 0)
{
cout << "EventMod success!..." << endl;
}
}
bool Is_Exits(int sock)//判断fd是否还在。
{
auto iter = react_mp.find(sock);
return iter == react_mp.end() ? false : true;
}
#define NUM 10
//事件就绪时的事件派发
void dispather(int &timeout)
{
epoll_event epev[NUM];
int n = epoll_wait(epfd_, epev, NUM, timeout);//检测
for (size_t i = 0; i < n; i++)
{
// cout << "epoll_wait ready...!" << endl;
uint32_t events1 = epev[i].events;
int sock1 = epev[i].data.fd;
//所有的异常交给sock的错误回调
if (events1 & EPOLLERR)
{
events1 |= (EPOLLIN | EPOLLOUT);
}
if (events1 & EPOLLHUP)//客户挂断
{
events1 |= (EPOLLIN | EPOLLOUT);
}
if ((Is_Exits(sock1)) && (events1 & EPOLLIN))
if (react_mp[sock1].read_hand)
react_mp[sock1].read_hand(react_mp[sock1]);
if ((Is_Exits(sock1)) && events1 & EPOLLOUT)
if (react_mp[sock1].write_hand)
react_mp[sock1].write_hand(react_mp[sock1]);
}
}
};
}
epoll。cc
申请一个反应堆核心。创建绑定套接字。该文件相关设施的配置。堆核心的创建,添加、循环检测。
最开始添加的是监听套接字相关。
#include "epoll.hpp"
#include "sock.hpp"
#include "Accept.hpp"
using namespace ns_sock;
using namespace ns_reactor;
int main()
{
//动态开辟一个反应堆核心
reactor_sv *re_sv = new reactor_sv;
//创建监听套接字
int listen_sock_ = sock_package::SockCreate();
sock_package::Bind(listen_sock_, 8080);
sock_package::Listen(listen_sock_);
sock_package::set_noblock(listen_sock_);
//创建一个sock相关的对象
sock_pack sk;
sk.sock_fd_ = listen_sock_;
sk.r = re_sv;
sk.sock_events_handler(accpt_listen, nullptr, nullptr);
//堆核心初始化,添加需要关心的套接字文件以及读写事件
re_sv->epfd_create();
re_sv->epll_add(sk, EPOLLIN | EPOLLOUT);
int time1 = 1000;
//cout << "开始循环服务 " << endl;
while (true)
{
//循环检测就绪队列是否有事件就绪
//sleep(2);
re_sv->dispather(time1);
}
// re_sv->dispather(time1);
return 0;
}
Accept。hpp连接器
有连接到来时,添加进epoll中,设置新连接相关属性。
#pragma once
#include "sock.hpp"
#include "epoll.hpp"
#include "Awe_call.hpp"
using namespace ns_sock;
using namespace ns_reactor;
void accpt_listen(sock_pack &sk)
{
while (true)
{
//cout << "获取客户套接字..." << sk.sock_fd_ << endl;
sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock1 = accept(sk.sock_fd_, (sockaddr *)&peer, &len);
if (sock1 > 0) //新连接
{
sock_pack new_sk;
sock_package::set_noblock(sock1);
new_sk.sock_fd_ = sock1;
new_sk.r = sk.r;
new_sk.sock_events_handler(read_call, write_call, error_call);
(sk.r)->epll_add(new_sk, EPOLLIN | EPOLLET);
cout<<"添加的套接字:..."<<sock1<<endl;
}
else //全局错误码被设置
{
if (errno == EINTR)
{
continue; //被信号中断,并不代表底层没有新连接。
}
else if (errno == EAGAIN || errno == EWOULDBLOCK)
{
//错误码这样的是因为 底层没有链接了。
break;
}
else
{
//真错了
cerr << "accept error..." << endl;
continue;
}
}
}
}
读事件就绪回调
按照协议对接收到的报文进行读取。模拟协议:100+100X120+120X;X是报文之间的分隔符;网络计算器。这里只考虑加法。
一:一个函数只负责专心读。从网络sock中读取到自己的输入缓冲区中。
二:一个函数对批量读取到的数据进行粘包问题处理,分成多个报文。
三:再对单个报文进行分析。
四:分析完毕,计算结果。
五:发送响应给客户端;实际是写入到自己的输出缓冲区。
六:自己文件关心事件修改:关心写入事件(send)。
出错则调用己方的错误回调。
专心负责读。
static int ReadHelp(sock_pack &sk)
{
while (true)
{
char buff[1024];
ssize_t s = recv(sk.sock_fd_, buff, sizeof(buff) - 1, 0);
if (s > 0)
{
buff[s] = 0;
sk.inbuffer += buff;
}
else if (s < 0)
{
if (errno == EINTR) //信号错误
{
continue;
}
else if (errno == EAGAIN || errno == EWOULDBLOCK)
{
return 0; //把数据读完了。
}
else
{
return -1;
}
}
else // == 0 客户端下线
{
return -1;
}
}
}
// 1+1 分析单个报文 x,y,op、输入输出参数
static bool AnalyseData(string &scut, int *x, int *y, char *op)
{
auto pos = scut.find("+");
if (pos == string::npos)
{
return false;
}
*x = atoi(scut.substr(0, pos).c_str()); //[)不包含pos
*y = atoi(scut.substr(pos + 1).c_str());
*op = '+';
return true;
}
//缓冲区数据分成多个报文
//输入输出参数:vector<string> *out
static void StringUncouple(string &in, vector<string> *out, const string &op)
{
while (true)
{
auto pos = in.find(op);
if (pos == string::npos)
{
break;
}
string sub = in.substr(0, pos);
out->push_back(sub);
in.erase(0, pos + op.size());
}
}
void read_call(sock_pack &sk)
{
// cout<<"ready to read.."<<endl;
//专心负责读。出错了你告诉我。我使用错误回调。-1 失败 0 完全读完
if (ReadHelp(sk) < 0)
{
if (sk.error_hand)
sk.error_hand(sk);
return;
}
//缓冲区数据分成多个报文 1+1X2+3X...x切割符
vector<string> msgcut;
StringUncouple(sk.inbuffer, &msgcut, "X");
//分析单个报文
for (auto &scut : msgcut)
{
//反序列化
// Task t(sk.sock_, scut); 也可以采用线程池的方法
// ThreadPool.push(t);
int x = 0;
int y = 0;
char op = 0;
if (!AnalyseData(scut, &x, &y, &op))
{
continue;
}
//计算结果。
int result = 0;
switch (op)
{
case '+':
result = x + y;
break;
default:
break;
}
//构建单个响应报文
string res = scut;
res += "=";
res += to_string(result);
res += "X"; //添加分隔符
//发送
sk.outbuffer += res;
//设置写事件关心
(sk.r)->EventMod(sk.sock_fd_, true, true);
}
}
写事件就绪回调
发送时区分:自己的输出缓冲区是否全部发送完毕,是则清空;取消对写入事件的关心。
因为TCP发送缓冲区导致的,己方发送缓冲区未发送完毕,则清空己方书输出缓冲区已经发送的内容。还需持续关心写事件。
出错则调用己方的错误回调。
static int SendHelp(int sock,string& wrt_string)
{
int total =0;//一共发了多少个字节
int size= wrt_string.size();
//防止协议缓冲区满了,而自己缓冲区还有数据,删除已经发送的。
const char* start = wrt_string.c_str();
while (true)
{
//实际上发送的字节数是取决于返回值
ssize_t s = send(sock,start+total,size-total,0);
if(s>0)
{
total+=s;
if(total == size)
{
wrt_string.clear();
return 1;
}
}
else //错误的几种情况
{
if(errno == EINTR)//信号错误
{
continue;
}
else if(errno == EAGAIN || errno == EWOULDBLOCK)
{
//TCP发送缓冲区满了。清除已经发送的了。
wrt_string.erase(0,total);
return 0;
}
else
{
return -1;
}
}
}
}
void write_call(sock_pack &sk)
{
//cout << "ready to write.." << endl;
//-1,失败;1,发送完毕;0,未完全发送。
int ret = SendHelp(sk.sock_fd_, sk.outbuffer);
if (ret == -1)
{
if (sk.error_hand)
sk.error_hand(sk);
}
else if(ret ==1)//发送完毕
{
(sk.r)->EventMod(sk.sock_fd_,true,false);
}
else if(ret == 0)//需持续关心写事件就绪,一旦就绪就可以从自己缓冲区发。
{
(sk.r)->EventMod(sk.sock_fd_,true,true);
}
}
错误事件回调
调用核心中的Epoll_Del函数。
void error_call(sock_pack &sk)
{
cout << " error_call..." << endl;
sk.r->Epoll_Del(sk.sock_fd_);
}
总结与拓展
substr()
:前必后开[)
,返回起始位置到end参数(或直到字符串的结尾)的string对象。vector:push_back()、auto for...
,容器的find
使用。- 有一个意义清晰的变量命名和函数名。函数名首字母大写,下划线,首字母大写…
Acceptor_Connector
。 - 根据接受、发送错误时;全局错误码的设置。采取对应的处理方式。一般情况是:信号错误继续;网络套接字接受区的数据读完了,返回;发送缓冲区满了,清楚己方输出缓冲区的数据。
- 线程池、任务队列、多进程、多线程的考虑;STL容器及其功能的熟悉。
拓展
- 格言:All problems in computer science can be solved by another level of indirection.。计算机科学中的所有问题都可以通过增加一个间接层来解决。—David Wheeler(剑桥大学计算机科学教授)
- epoll的惊群问题