• 【项目】Reactor模式的服务器


    目录

    Reactor完整代码连接

    前置知识:

     1.普通的epoll读写有什么问题?

    2.Connection内的回调函数是什么

    3.服务器的初始化(Connection只是使用的一个结构体)

    4.等待就绪事件:有事件就绪,对使用Connection的不同对象(封装fd,回调方法)调用对应的回调方法;

    5._listenSock读取 :Accepter函数获取新链接怎么处理的

    6.普通套接字读取:Recver

    7.对写事件的关心是按需关系的:

    8.执行效果:

    9.Reactor的优势:


    Reactor完整代码连接

    前置知识:

    Reactor叫做反应堆模式;反应:对已经就绪的事件(读、写、异常)进行处理;

    我们使用epoll来实现,select、poll、epoll是多路转接的发展史,epoll完善了select、poll的缺点;

    • 需要程序员维护数组                                      select/poll都有这个缺点    
    • 有大量的遍历                                                 select/poll都有这个缺点 
    • 大量参数为输入输出型参数,需要重新设置  select有
    • 管理的fd有上限                                              select有

     1.普通的epoll读写有什么问题?

    • 使用的是一个静态数组读取;报文长,读到就是不完整报文,报文短,一次读取可能有多个报文,最后一个报文可能不是完整的;
    • 这样的错误报文没法分析和处理,就不能构造响应报文;

    综上所述:问题为没法保证读取到的是完整报文,导致没法分析和处理、构建响应报文;

    1. void Read(int fd)
    2. {
    3. char buff[1024];
    4. ssize_t s = read(fd, buff, 1023);
    5. if(s > 0){
    6. buff[s] = 0;
    7. LOG2(INFO, buff, fd);
    8. }

    解决办法:将文件描述符封装,并且有接受发送缓冲区,使用string就可以;

    1. 使用静态数组读取,然后添加到接受缓冲区保存;
    2. 读取完毕,对接受缓冲区分析是否有完整报文;
    3. 处理完整请求报文,构建响应报文添加到发送缓冲区;
    1. using func_t = std::function<void(Connection*)>;
    2. using cals_t = std::function<void(std::string &, Connection*)>;
    3. class Connection{
    4. public:
    5. Connection(int fd = -1 ):_fd(fd), _ts(nullptr)
    6. {}
    7. ~Connection()
    8. {
    9. if(_fd >= 0)
    10. close(_fd);
    11. }
    12. public:
    13. int _fd;
    14. //读写异常回调方法
    15. func_t _recver;
    16. func_t _sender;
    17. func_t _exception;
    18. //接受缓冲区
    19. std::string _outbuff;
    20. //发送缓冲区
    21. std::string _inbuff;
    22. //TcpServer的回指指针,对写事件的关心是按需打开
    23. TcpServer *_ts;
    24. //连接最近活跃活动的时间
    25. time_t _times;
    26. };

    2.Connection内的回调函数是什么

    一个包装器;返回值为void,参数为Connection*,的函数指针、仿函数、lamada表达,它都可以接受;

    using func_t = std::function<void(Connection*)>;

    优势:

    • _listenSock是读方法是接受新连接,普通是读取请求报文
    • 初始化时设置读写异常回调方法(回调:使用函数指针执行的函数),不需要判断是_listenSock还是普通套接字,统一使用con->_recver;

    3.服务器的初始化(Connection只是使用的一个结构体)

    1. 套接字创建,bind,监听;
    2. 构建epoll模型,epoll函数也封装了,_epfd封装在Epoll类内;
    3. 初始化_listenSock    Connection结构体;读取回调方法Accept是类函数,多一个this指针,需要使用std::bind来改变参数个数,进行传递给包装器;
    4. epoll_wait的事件就绪队列初始化;
    1. class TcpServer{
    2. const static int gport = 8080;
    3. const static int gnum = 128;
    4. TcpServer(int port = gport, int num = gnum):_port(gport), _evts_num(num)
    5. {
    6. //套接字,创建bind监听
    7. _listenSock = Sock::Socket();
    8. Sock::Bind(_listenSock,_port);
    9. Sock::Listen(_listenSock);
    10. //构建epoll模型
    11. _epoll.CreateEpoll();
    12. //listensock添加epoll模型和_connections管理起来
    13. AddConnection(_listenSock, std::bind(&TcpServer::Accepter, this, std::placeholders::_1), nullptr, nullptr);
    14. //epoll_wait就绪队列,获取已就绪的事件
    15. _evts = new epoll_event[_evts_num];
    16. }
    17. ~TcpServer()
    18. {
    19. if(_listenSock >= 0)
    20. close(_listenSock);
    21. if(_evts != nullptr)
    22. delete[] _evts;
    23. for(auto &pr : _connections)
    24. {
    25. _connections.erase(pr.first);
    26. delete pr.second;
    27. }
    28. }
    29. private:
    30. int _listenSock;
    31. //epoll
    32. Epoll _epoll;
    33. //就绪队列
    34. epoll_event* _evts;
    35. int _evts_num;
    36. //管理connection对象
    37. std::unordered_map<int, Connection*> _connections;
    38. int _port;
    39. //业务处理的回调指针
    40. cals_t _cb;
    41. };

    4.等待就绪事件:有事件就绪,对使用Connection的不同对象(封装fd,回调方法)调用对应的回调方法;

    1. void LoopOnce()
    2. {
    3. int n = _epoll.WaitEpoll(_evts, _evts_num);
    4. //有事件就绪
    5. if(n > 0)
    6. {
    7. //LOG2(INFO, "epoll wait success",fd);
    8. for(int i=0; i
    9. {
    10. int fd = _evts[i].data.fd;
    11. int events = _evts->events;
    12. //连接关闭或者错误,改为读写统一处理,读写失败调异常处理;
    13. if( events & EPOLLHUP){
    14. LOG2(INFO,"连接关闭",fd);
    15. events |= (EPOLLIN | EPOLLOUT);
    16. }
    17. if( events & EPOLLERR){
    18. LOG2(INFO,"错误",fd);
    19. events |= (EPOLLIN | EPOLLOUT);
    20. }
    21. if(_connections.count(fd) && events & EPOLLIN)
    22. {
    23. if(IsConnectionExits(fd) && _connections[fd]->_recver != nullptr){
    24. _connections[fd]->_recver(_connections[fd]);
    25. }
    26. }
    27. if(_connections.count(fd) && events & EPOLLOUT)
    28. {
    29. if(IsConnectionExits(fd) && _connections[fd]->_sender != nullptr)
    30. _connections[fd]->_sender(_connections[fd]);
    31. }
    32. }
    33. }
    34. else if(n == 0)
    35. {
    36. LOG(INFO, "timeout");
    37. }
    38. else
    39. {
    40. LOG(FATAL,"epoll wait fail");
    41. exit(4);
    42. }
    43. }
    44. void Dispatcher(cals_t cb)
    45. {
    46. _cb = cb;
    47. while(true)
    48. {
    49. //去除不活跃的连接
    50. DeleteInactivity();
    51. LoopOnce();
    52. }
    53. }

    5._listenSock读取 :Accepter函数获取新链接怎么处理的

    • 得到新连接,如果新的fd是合法的,设置对应的读写异常回调方法,读:读取请求报文,写:发送响应报文,异常:出现错误进行处理;
    • 所有的套接字都是使用ET模式(通知效率高,只支持非阻塞读写),EPOLLET因该被设置;
    1. void Accepter(Connection * con)
    2. {
    3. while(true)
    4. {
    5. con->_times = time(nullptr);
    6. struct sockaddr_in tmp;
    7. socklen_t tlen = sizeof(tmp);
    8. int new_sock = accept(con->_fd, (struct sockaddr *)&tmp, &tlen);
    9. if(new_sock < 0)
    10. {
    11. //所以事件处理完毕
    12. if(errno == EAGAIN || errno == EWOULDBLOCK)
    13. break;
    14. else if(errno == EINTR)//可能被信号中断,概率极小
    15. continue;
    16. else
    17. {
    18. std::cout << "accept fail , errno :" << errno << strerror(errno) << std::endl;
    19. break;
    20. }
    21. }
    22. else//添加到epoll模型和_connections中管理;
    23. {
    24. if(AddConnection(new_sock, std::bind(&TcpServer::Recver, this, std::placeholders::_1),
    25. std::bind(&TcpServer::Sender, this, std::placeholders::_1),
    26. std::bind(&TcpServer::Exception, this, std::placeholders::_1)
    27. ))
    28. LOG2(INFO, "add connection success",new_sock);
    29. else
    30. LOG2(RISK, "add connection fail", new_sock);
    31. }
    32. }
    33. }

    6.普通套接字读取:Recver

    1. 一直读取,直到错误或者读取完毕;每次读取的结果都放到接受缓冲区;
    2. 读取完毕,对接受缓冲区处理,拿出一个个完整的报文,对请求报文进行业务处理;
    1. void Recver(Connection *con)
    2. {
    3. con->_times = time(nullptr);
    4. const int buff_size = 1024;
    5. char buff[buff_size];
    6. while(true)
    7. {
    8. ssize_t s = recv(con->_fd, buff, buff_size - 1, 0);
    9. if(s > 0)
    10. {
    11. buff[s] = 0;
    12. con->_outbuff += buff;
    13. }
    14. else if(s == 0)
    15. {
    16. LOG2(INFO, "写端关闭", con->_fd);
    17. con->_exception(con);
    18. return;
    19. }
    20. else
    21. {
    22. //读取完毕
    23. if(errno == EAGAIN || errno == EWOULDBLOCK )
    24. {
    25. LOG2(INFO, "处理完毕", con->_fd);
    26. break;
    27. }
    28. else if(errno == EINTR)
    29. continue;
    30. else
    31. {
    32. LOG2(ERROR, "recv fail ,fd: ", con->_fd);
    33. con->_exception(con);
    34. return;
    35. }
    36. }
    37. }
    38. std::cout << "fd: " << con->_fd << "outbuff: " << con->_outbuff <
    39. //对outbuff内的完整报文,进行处理
    40. std::vector out;
    41. //分隔报文,函数在protocol.hpp
    42. SplitMessage(out, con->_outbuff);
    43. for(auto &s : out)
    44. _cb(s, con);//业务逻辑回调指针,在主函数
    45. }

    7.对写事件的关心是按需关系的:

    • 如果开启关心,还没有数据发送,写事件会一直就绪;所以按需关心;
    • 请求报文业务处理完毕,构建好响应报文,一定有响应,打开对写事件的关心;
    • 也是Connection为什么封装一个Tcperver指针的原因,这里开启写事件的关心;
    1. //业务处理
    2. void CalArguments(std::string &str, Connection *con)
    3. {
    4. //请求报文反序列化
    5. Request req;
    6. //std::cout<
    7. if(!req.Deserialize(str)){
    8. LOG2(ERROR, "deseroalize fail" ,con->_fd);
    9. return;
    10. }
    11. //对数据处理
    12. Response res;
    13. calculator(req, res);
    14. //响应报文序列化
    15. std::string s = res.Serialize();
    16. //添加到inbuff
    17. con->_inbuff += s;
    18. //一定有响应报文,打开写事件的关系
    19. con->_ts->EnableReadWrite(con->_fd, true, true);
    20. }

    8.执行效果:

    • 我写的协议是对任意两个数加减乘除;
    • 每个请求或者响应都是用 x  做为分割符的;

    9.Reactor的优势:

    和进程/线程做比较:

    • 它是一个单进程的就可以并发处理请求的服务器,比进程/线程减少了创建、销毁、调度的时间;
    • 它的等待一批fd,减少了单位等待时间;一个线程等待对应一个fd;
    • 有很高的复用性,替换业务逻辑就行了;
  • 相关阅读:
    全国5米高程DEM数据及衍生的全国地形起伏度数据
    【译】.NET 7 中的性能改进(五)
    思维导图:网络通信协议标准化
    287. 寻找重复数
    python urllib.request模块下载数据
    程序员视角下的AIGC技术:现状、挑战与未来展望
    成为会带团队的技术人 技术债务:如何带领团队从困境中突围而出?
    redis部署与管理
    二氧化钛纳米粒TIO2修饰多肽R8/CTT2/CCK8/GE11/cTAT/CPP/RVG29/SP94(无机纳米粒子偶联多肽)
    神经网络中的参数量和FLOPs的计算
  • 原文地址:https://blog.csdn.net/m0_72964546/article/details/132593844