webserver服务器从零搭建到上线(四)|muduo网络库的基本原理和使用

avatar
作者
筋斗云
阅读量:0

文章目录

muduo源码编译安装

muduo依赖Boost库,所以我们应该先编译安装boost,方法如下:
https://blog.csdn.net/QIANGWEIYUAN/article/details/88792874
随后可以编译安装muduo,方法如下:
https://blog.csdn.net/QIANGWEIYUAN/article/details/89023980

muduo的底层其实就是采用nonblock io + one loop per thread 以及 epoll + 线程池的框架,采用sub-Reactor模式。
该方案的特点就是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的计算任务,可以提交到创建的ThreadPool线程池中专门处理耗时的计算任务

muduo框架讲解

在这里插入图片描述
无论是什么语言,想要做到高并发,
首先要有一个IO线程,它里面有一个epoll,我们叫做main Reactor,它的作用就是建立新用户的连接,新用户的连接上之后,会把这些连接通过一定的负载算法分发给不同的工作线程。

这些工作线程就是用来处理已连接用户的读写事件,一般这些线程的数量会和CPU的核数对等。尽量做到高并发。
用IO复用的好处就是一个线程可以监听多个套接字,尤其是对于连接量大而活跃量少(一般都是这样的场景),所以epoll有非常大的性能优势。
如果工作线程的每一个epoll所监听的连接用户要做比较耗时的IO操作(传送文件、音视频),我们可以在工作线程内单独开一个线程。如果我们不单独开线程的话,工作线程会阻塞在IO操作中,无法监听其他依然注册在epoll树上的fd的读写时间

muduo源代码有很多非常好的封装思想,里面大量得使用了智能指针、绑定器、函数对象、回调机制等等,可以做到模块化的解耦和软件的高内聚低耦合

muduo库编写服务器代码示例

muoduo库的使用需要链接 libmuduo_base.so libmuduo_net.so libpthread.so
动态库在linux系统默认的路径是
/usr/lib
/usr/local/lib
如果动态库放在这个路径下不需要在cmake中添加搜索路径,因为他们已经处于环境变量中。

//由于依赖关系的存在,net依赖于base,base依赖于pthread,连接顺序不能改变 -lmuduo_net -lmuduo_base -lpthread 

使用实例代码如下:

/* muduo网络库给用户提供了两个主要的类 TcpServer:用于编写服务器程序的 TcpClient:用于编写客户端程序的  epoll + 线程池 好处:能够把网络IO的代码和业务代码区分开,muduo库已经把网络端的代码封装好了。 业务代码暴露在两个,我们也只关心这两件事: 用户的连接和断开  用户的可读写事件 */  #include <muduo/net/TcpServer.h> #include <muduo/net/EventLoop.h> #include <iostream> #include <functional> #include <string.h> using namespace std; using namespace muduo; using namespace muduo::net; using namespace placeholders;  /*基于muduo网络库开发服务器程序 1.组合TcpServer对象 2.创建EventLoop事件循环对象的指针 3.明确TcpServer的构造函数需要什么参数,输出ChatServer的构造函数 4.在当前服务器类的构造函数中,注册处理连接的回调函数和处理读写事件的回调函数 5.设置合适的服务端线程数量,muduo库会自己划分I/O线程和worker线程 */  class ChatServer { public:     ChatServer(EventLoop* loop, //事件循环             const InetAddress& listenAddr, //IP+Port             const string& nameArg)  //服务器名字             :_server(loop, listenAddr, nameArg), _loop(loop) {                 //给服务器注册用户连接的创建和断开回调                 _server.setConnectionCallback(std::bind(&ChatServer::onConnection, this, _1));                  //给服务器注册用户读写事件回调                 _server.setMessageCallback(std::bind(&ChatServer::onMessage, this, _1, _2, _3));                  //设置服务器端的服务线程  设置为2,1个IO线程,1个worker线程                 _server.setThreadNum(2);                 }          // 开启事件循环     void start() {         _server.start();     } private:     //专门处理用户的链接创建和断开     void onConnection(const TcpConnecitonPtr &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(); //连接断开后,回收fd资源close(fd)             //_loop->quit(); //通知事件循环停止运行,这通常是在准备关闭服务器或者结束程序时执行的操作         }         }      //专门处理用户的读写事件     void onMessage(const TcpConnection &conn, //这是我们的连接,可以读数据也可以写数据                     Buffer *buf, // 用户的缓冲区                    Timestamp time) {    //接受数据的时间信息         string buf = buffer->retrieveAllAsString(); //可以把其中接收到的数据全部接收到自己的字符串当中         cout << "recv data: " << buf << " time: " << time.toString() << endl;         conn->send(buf);     }     TcpServer _server; //#1     EventLoop *_loop;  //#2 把它看作epoll  }  int main () {     EventLoop loop; //epoll     InetAddress addr("127.0.0.1", 10000);     ChatServer server(&loop, addr, "ChatServer");      server.start(); //listenfd epollctl=>epoll     loop.loop;      //epoll_wait以阻塞方式等待新用户连接,已连接用户的读写事件等      return 0; } 

代码解析

以上代码就已经实现了一个高性能高并发的服务器程序,muduo库已经为我们把网络端封装好了,我们只需要进行应用层业务逻辑的代码编写。

在例子中,其实我们只需要关注两个函数的编写:

void onConnection(const TcpConnecitonPtr &conn); void onMessage(const TcpConnection &conn, //这是我们的连接,可以读数据也可以写数据                     Buffer *buf, // 用户的缓冲区                    Timestamp time) //接受数据的时间信息 

也就是用户连接的创建和断开回调函数、用户读写事件回调。

用户连接的创建和断开回调函数

//专门处理用户的链接创建和断开 void onConnection(const TcpConnecitonPtr &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(); //连接断开后,回收fd资源close(fd)         //_loop->quit(); //通知事件循环停止运行,这通常是在准备关闭服务器或者结束程序时执行的操作     }          } 

本案例中,我们在连接后打印远端的相关信息,断开后打印远端相关信息(这里就是典型的业务层逻辑)。

用户读写事件回调

这里写的是接受读缓冲区(读缓冲区的数据是客户端写入的)的数据,并且打印到终端。

//专门处理用户的读写事件 void onMessage(const TcpConnection &conn, //这是我们的连接,可以读数据也可以写数据                 Buffer *buf, // 用户的缓冲区                Timestamp time) {    //接受数据的时间信息     string buf = buffer->retrieveAllAsString(); //可以把其中接收到的数据全部接收到自己的字符串当中     cout << "recv data: " << buf << " time: " << time.toString() << endl;     conn->send(buf);     } 

如果是浏览器的话,在这里我们在读写事件回调中去解析浏览器的http消息,然后根据http的请求,组织http响应在发送回给浏览器(这就是所谓的业务代码)。

使用vscode编译程序

配置c_cpp_properties.json

按F1,这里可以把配置的对话框打印出来

{ 	"configurations": [ 		{ 			"name": "Linux", 			"includePath": [ 				"${workspaceFolder}/**$" 			], 			"defines": [], 			"compilerPath": "/usr/bin/gcc", 			"cStandard": "c11",  			"intelliSenseMode": "clang-x64" 		} 	], 	"version": 4 } 

这里的配置文件中,若果我们去执行一条编译命令,需要写

//gcc -I头文件搜索路径 -L库文件搜索路径 -lmuduo_net库名称 

一般我们可以在"includePath": []中加头文件、库文件的搜索路径(/usr/include /usr/local/include默认包含),库的名称在配置tasks.json
还可以添加C++标准"cppStandard": "c++17"

配置tasks.json

ctrl+shift+B(build)可以看到构建项目的配置文件,点击齿轮即可
在这里插入图片描述
在这里有一个tasks.json配置文件。

	"version": "2.0.0", 	"tasks": [ 		{ 			"type": "shell", 			"label": "g++ build active file", 			"command": "user/bin/g++", 			"arg": [ 				"-g", 				"${file}", 				"-o", 				"${fileDirname}/${fileBasenameNoExtension}", 			] 			"options": { 					"cwd": "/bsr/bin" 			}, 			"problemMatcher": [ 				"$gcc" 			], 			"group": "build"  		} 	] 

我们的-lmuduo_net库名称就是写在tasks.json文件中的"arg"

"arg": [ 	"-g", 	"${file}", 	"-o", 	"${fileDirname}/${fileBasenameNoExtension}", 	"-lmuduo_net", 	"-lmuduo_base", 	"-lpthread" ]	 

配置launch.json

只有调试的时候采用,我们最好使用gdb调试

编译

配置好c_cpp_properties.json和配置tasks.json后。
ctrl+shift+B(build)然后直接点击蓝色的地方而不是点击齿轮,就可以进行编译啦。
在这里插入图片描述

总结

这样,我们就很简单得写出了一个健壮的、基于事件驱动的、IO复用的高并发高性能服务器。

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!