一、网络服务器编程常用模型
【方案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上完成
一般来说, 线程的数量和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(多个)处理
三、muduo网络库服务器编程
muduo网络库给用户提供了两个主要的类:TcpServer 用于编写服务器程序的;TcpClient 用于编写客户端程序的
epoll + 线程池 好处:能够把网络I/O的代码和业务代码区分开。业务代码就是:用户的连接和断开,用户的可读写事件
我们只需要关注业务代码,如何监听事件的发生由muduo库去做
基于muduo网络库开发服务器程序,步骤如下:
- 组合TcpServer对象
- 创建EventLoop事件循环对象的指针,可以看做epoll,可以向loop上注册感兴趣的事件,如果有事件发生了,loop会上报
- 明确TcpServer构造函数需要什么参数,输出ChatServer的构造函数
- 在当前服务器类的构造函数当中,注册处理连接的回调函数和处理读写时间的回调函数;当我们知道一个函数什么时候发生我们就可以直接调用该函数,如果我们不知道什么时候发生,就需要使用回调机制,先用函数对象写好,等条件合适时通过函数对象调用
- 设置合适的服务端线程数量,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
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"
}