muduo网络库是陈硕大神开发的基于主从Reactor模式的,事件驱动的高性能网络库。
网络编程中有很多是事务性的工作,使用muduo网络库,用户只需要填上关键的业务逻辑代码,并将回调注册到框架中,就可以实现完整的网络服务。
muduo网络库的核心是one loop per thread + thread pool,有一个main Reactor负责accept连接,然后将连接挂在某个sub Reactor中(muduo通过round-robin算法选择一个sub Reactor)。这样该连接的所有操作都由该sub Reactor进行处理,多个连接可能被分派到多个线程中,以充分利用CPU。
muduo采用的是固定大小的Reactor poll,池的大小由用户进行设置,通常所有Reactor的个数应该等于CPU的数目。这样程序的总体处理能力不会随连接数增加而下降。由于一个连接完全由一个线程管理,那么请求的顺序性有保证,突发请求也不会占满所有CPU。把IO分配个多给线程,防止出现一个Reactor的处理能力饱和。
小规模计算可以在当前IO线程完成并发回结果,从而降低响应的延迟。如果需要大规模计算,可以再交给一个线程池让其进行处理,从而防止阻塞当前sub Reactor导致响应变慢。
如果TCP连接有优先级之分,可以将高优先级的连接放在一个单独的sub Reactor来处理,从而避免优先级反转。
参考博客:C++ muduo网络库知识分享01 - Linux平台下muduo网络库源码编译安装
由于网络库封装了网络IO代码,所以不同的服务器的区别主要在于业务逻辑代码的不同。echo服务器简单地将来自客户端的数据回发给客户端,应该是业务逻辑最简单的服务器了,通过了解如何使用muduo网络库实现echo服务器有助于我们了解使用muduo网络库的基本方法,如果需要对网络部分进行优化就需要深入源码了解muduo网络库的实现原理,本文不详细涉及这部分(以后可能会更新关于muduo源码剖析的博客)。
使用muduo网络库我们需要组合TcpServer
类,一般还需要保存EventLoop
指针。EventLoop
负责管理事件循环(epoll
),TcpServer
负责管理主Reactor(Acceptor
管理连接socket)和从Reactors(EventLoopThreadPool
管理客户端socket)以及对于每种事件的回调,这些回调会被合适的地方调用(对应事件发生的时候)。我们需要给TcpServer
传入base loop、监听端口(和IP地址,一般是0.0.0.0
)、服务器名称(打印日志),并且设置各种事件的回调,也就是在这里我们填入业务逻辑。
头文件EchoServer.h
// Copyright(C), Edward-Elric233
// Author: Edward-Elric233
// Version: 1.0
// Date: 2022/7/11
// Description:
#ifndef CHATSERVER_ECHOSERVER_H
#define CHATSERVER_ECHOSERVER_H
#include "muduo/net/TcpServer.h"
#include "muduo/net/EventLoop.h"
#include "muduo/net/InetAddress.h"
#include "muduo/net/TcpConnection.h"
#include <string>
namespace edward {
class EchoServer {
using TcpServer = muduo::net::TcpServer;
using EventLoop = muduo::net::EventLoop;
using InetAddress = muduo::net::InetAddress;
using TcpConnectionPtr = muduo::net::TcpConnectionPtr;
using Buffer = muduo::net::Buffer;
using Timestamp = muduo::Timestamp;
TcpServer tcpServer_;
EventLoop *loop_;
void onConnectionCallback(const TcpConnectionPtr& conn);
void onMessageCallback(const TcpConnectionPtr& conn, Buffer* buffer, Timestamp timestamp);
public:
EchoServer(EventLoop *loop, const InetAddress& address, const std::string &name);
void start();
};
}
#endif //CHATSERVER_ECHOSERVER_H
实现文件EchoServer.cpp
// Copyright(C), Edward-Elric233
// Author: Edward-Elric233
// Version: 1.0
// Date: 2022/7/11
// Description:
#include "EchoServer.h"
#include "utils.h"
#include <functional>
namespace edward {
using namespace std::placeholders;
EchoServer::EchoServer(EventLoop *loop, const InetAddress& address, const std::string &name)
: tcpServer_(loop, address, name)
, loop_(loop) {
//使用绑定器设置回调
tcpServer_.setConnectionCallback(std::bind(&EchoServer::onConnectionCallback, this, _1));
tcpServer_.setMessageCallback(std::bind(&EchoServer::onMessageCallback, this, _1, _2, _3));
//根据本机的核数设置线程/Reactor数量,如果不设置默认为1个
tcpServer_.setThreadNum(std::thread::hardware_concurrency());
}
//有新连接时的回调
void EchoServer::onConnectionCallback(const TcpConnectionPtr& conn) {
if (conn->connected()) {
} else {
}
}
//连接上有消息到来时的回调
void EchoServer::onMessageCallback(const TcpConnectionPtr& conn, Buffer* buffer, Timestamp timestamp) {
std::string msg = buffer->retrieveAllAsString();
print(conn->peerAddress().toIpPort(), ":[", msg, "]at", timestamp.toFormattedString());
conn->send(msg);
}
void EchoServer::start() {
tcpServer_.start(); //使用epoll_ctl将连接socket放在loop上进行监听并设置对应的回调
}
}
我们将回调都设置为成员函数,这样做的好处是我们往往要在回调中访问其他系统资源,成员函数可以访问数据成员避免传参。
测试文件test.cpp
#include "EchoServer.h"
void test_EchoServer() {
muduo::net::EventLoop loop;
edward::EchoServer echoServer(&loop, muduo::net::InetAddress(6789), "EchoServer");
echoServer.start();
loop.loop();
return;
}
测试结果
20220711 09:47:44.055888Z 26057 INFO TcpServer::newConnection [EchoServer] - new connection [EchoServer-0.0.0.0:6789#1] from 127.0.0.1:41678 - TcpServer.cc:80
127.0.0.1:41678 :[ Hello world
]at 20220711 09:47:49.515927
127.0.0.1:41678 :[ 123456
]at 20220711 09:47:56.117636
20220711 09:47:57.069940Z 26057 INFO TcpServer::removeConnectionInLoop [EchoServer] - connection EchoServer-0.0.0.0:6789#1 - TcpServer.cc:109
掌握了muduo网络基本的用法后就可以根据需要填充业务逻辑了,如果想要了解更多就需要深入源码去了解啦。