muduo网络库的多线程模型

550 阅读5分钟

一、网络服务器编程常用模型

【方案1】:accept + read/write,不是并发服务器

【方案2】:accept + fork - process-pre-connection,适合并发连接数不大,计算任务工作量大于fork的开销

【方案3】:accept + thread thread-pre-connection,比方案2的开销小了一点,但是并发造成线程堆积过多

【方案4】:muduo的网络设计:reactors in thread - one loop per thread。方案特点是one loop per thread,有一个main reactor负载accept连接,然后把连接分发到某个sub reactor(采用round-robin的方式来选择sub reactor),该连接的所有操作都在那个sub reactor所处的线程中完成。多个连接可能被分派到多个线程中,以充分利用CPU。

Reactor poll的大小是固定的,根据CPU的数目确定

// 设置EventLoop的线程个数,底层通过EventLoopThreadPool线程池管理线程类EventLoopThread
_server.setThreadNum(10);

一个Base IO thread负责accept新的连接,接收到新的连接以后,使用轮询的方式在reactor pool中找到合适的sub reactor将这个连接挂载上去,这个连接上的所有任务都在这个sub reactor上完成

image.png

一般来说, 线程的数量和CPU的核数对等,做到高并发。I/O复用的好处就是一个线程可以监听多个套接字,对于连接量大活跃量少的场景

工作线程会单独开一个线程做耗时的I/O操作,传送文件,音视频等。或者耗费CPU的计算任务,也可以提交到创建的ThreadPool线程池中专门处理耗时的计算任务,这样当前的工作线程就可以及时处理其他依然注册在epoll上的socket的读写事件

【方案5】 : reactors in process - one loop pre process,nginx服务器的网络模块设计,基于进程设计,采用多个Reactors充当I/O进程和工作进程,通过一把accept锁,完美解决多个Reactors的“惊群现象”

二、muduo中的reactor模型

The reactor design pattern is an event handling pattern for handling service requests delivered concurrently to a service handler by one or more inputs. The service handler then demultiplexes the incoming requests and dispatches them synchronously to the associated request handlers.

从上面的描述可以看到如下关键点:

  • 事件驱动(event handling)
  • 可以处理一个或多个输入源(one or more inputs)
  • 通过Service handler同步的将输入事件(Event)采用多路复用分发给相应的Request Handler(多个)处理

image.png

三、muduo网络库服务器编程

muduo网络库给用户提供了两个主要的类:TcpServer 用于编写服务器程序的;TcpClient 用于编写客户端程序的

epoll + 线程池 好处:能够把网络I/O的代码和业务代码区分开。业务代码就是:用户的连接和断开,用户的可读写事件

我们只需要关注业务代码,如何监听事件的发生由muduo库去做

基于muduo网络库开发服务器程序,步骤如下:

  1. 组合TcpServer对象
  2. 创建EventLoop事件循环对象的指针,可以看做epoll,可以向loop上注册感兴趣的事件,如果有事件发生了,loop会上报
  3. 明确TcpServer构造函数需要什么参数,输出ChatServer的构造函数
  4. 在当前服务器类的构造函数当中,注册处理连接的回调函数和处理读写时间的回调函数;当我们知道一个函数什么时候发生我们就可以直接调用该函数,如果我们不知道什么时候发生,就需要使用回调机制,先用函数对象写好,等条件合适时通过函数对象调用
  5. 设置合适的服务端线程数量,muduo库会自己分配I/O线程和worker线程
#include<muduo/net/TcpServer.h>
#include<muduo/net/EventLoop.h>
#include<iostream>
#include<functional>
#include<string>
using namespace std;
using namespace muduo;
using namespace muduo::net;
using namespace placeholders;

class ChatServer
{
public:
    //TcpServer没有默认的构造函数,所以这里需要指定相应的构造
    ChatServer(EventLoop* loop, //事件循环
            const InetAddress& listenAddr,  //ip+port
            const string& nameArg)  //服务器名字
            :_server(loop,listenAddr,nameArg)
            ,_loop(loop)
            {
                //给服务器注册用户连接的创建和断开回调,回调就是对端的相应事件发生了告诉网络库 ,然后网络库告诉我 ,我在回调函数开发业务
                //人家这个setConnectionCallback是没有返回值以及有一个形参变量,而我们现在写的是一个成员方法,写成成员方法是因为想访问对象的成员变量,
                //写成员方法的话就有一个this指针,和人家setConnectionCallback的类型就不同了,所以在这里我们用绑定器绑定
                _server.setConnectionCallback(std::bind(&ChatServer::onConnection,this,_1));
                //给服务器注册用户读写事件回调
                _server.setMessageCallback(std::bind(&ChatServer::onMessage,this,_1,_2,_3));
                //设置服务器端的线程数量    1个io线程,3个worker线程
                _server.setThreadNum(4);
            }
            //开始事件循环
            void start()
            {
                _server.start();
            }
private:
    //专门处理用户的连接创建和断开
    /*
    我们自己在编写epoll,从epoll拿过来事件以后,发现它如果是listenfd,会从listenfd上去accept,这就表示有新用户连接了,
    拿出来一个和该用户专门通信的socket,这一切底层muduo库都封装了,只暴露了一个回调的接口,只管写就行了,不用关心什么时候会调用,
    我们都把这个注册到muduo库上了,当有新用户连接的创建以及原来用户连接的断开,这个方法就会响应
    */
    void onConnection(const TcpConnectionPtr &conn)
    {
        if(conn->connected())
        {
            cout<<conn->peerAddress().toIpPort()<<"->"<<conn->localAddress().toIpPort()<<"state:online"<<endl;
        }
        else
        {
            cout<<conn->peerAddress().toIpPort()<<"->"<<conn->localAddress().toIpPort()<<"state:offline"<<endl;
            conn->shutdown();     //close(fd)
            //_loop->quit();//服务器就结束了
        }
    }
    //专门处理用户的读写事件
    void onMessage(const TcpConnectionPtr &conn,    // 连接
                            Buffer *buffer,    //缓冲区
                            Timestamp time) //接收到数据的时间信息
    {
        string buf = buffer->retrieveAllAsString();
        cout<<"recv data:"<<buf<<"time:"<<time.toString()<<endl;
        conn->send(buf);
    }
    TcpServer _server;  //#1
    EventLoop *_loop;   //#2
};
int main()
{
    EventLoop loop; //epoll
    InetAddress addr("127.0.0.1",6000);
    ChatServer server(&loop,addr,"ChatServer");
    
    server.start(); //启动服务,把listenfd通过epoll_ctl添加到epoll上
    
    loop.loop();    //epoll_wait以阻塞的方式等待新用户的链接,已连接用户的读写事件等
    return 0;
}

终端编译命令

g++ -o server muduo_server.cpp -lmuduo_net -lmuduo_base -lpthread

image.png

ctrl+shift+p,然后搜索Edit Configuration(JSON)就会进入c_cpp_properties.json
c_cpp_properties.json:可用于配置头文件路径,以及编译器识别的语言标准

{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                // 这里写头文件的路径,如果在/usr/include或/usr/local/include
                //下就不用添加了,这两个路径在Linux下自动搜索
                "${workspaceFolder}/**"
            ],
            "defines": [],
            "compilerPath": "/usr/bin/gcc",
            "cStandard": "gnu17",
            "cppStandard": ""c++17"",
            "intelliSenseMode": "linux-gcc-x64"
        }
    ],
    "version": 4
}
// gcc -I头文件搜索路径 -L库文件搜索路径 -l库名称

task.json:编译需要使用的配置文件,vscode编译命令ctrl+shift+b

{
    "tasks": [
        {
            "type": "cppbuild",
            "label": "C/C++: g++ 生成活动文件",
            "command": "/usr/bin/g++",
            "args": [
                "-fdiagnostics-color=always",
                "-g",
                "${file}",
                "-o",
                "${fileDirname}/${fileBasenameNoExtension}",
                "-lmuduo_net",
                "-lmuduo_base",
                "-lpthread"
            ],
            "options": {
                "cwd": "${fileDirname}"
            },
            "problemMatcher": [
                "$gcc"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "detail": "调试器生成的任务。"
        }
    ],
    "version": "2.0.0"
}