【C++】muduo基础使用

张开发
2026/4/18 1:10:59 15 分钟阅读

分享文章

【C++】muduo基础使用
目录一、muduo 介绍二、核心概念三、服务器端常用接口1. TcpServer 构造函数2. setConnectionCallback设置连接回调3. setMessageCallback设置消息回调4. start ()启动服务器5. EventLoop::loop ()启动事件循环四、客户端常用接口1. TcpClient 构造函数2. connect ()发起连接3. TcpConnection::send ()发送数据五、核心回调函数1. onConnection连接状态回调2. onMessage收到消息回调3. onUnknownMessage未知消息兜底回调六、Buffer 类常用接口1. retrieveAllAsString()2. append()3. retrieve()七、基于 muduo 的中英互译示例服务器端客户端编译运行八、muduo 常见坑九、总结一、muduo 介绍muduo 是陈硕大神基于 C11 编写的高性能多线程 Reactor 模式网络库专为 Linux 平台设计。核心特点one loop per thread一个线程一个事件循环、非阻塞 IO、线程安全、接口简洁适用场景TCP 服务器 / 客户端、RPC 框架、即时通讯、游戏服务器等高性能网络应用二、核心概念EventLoop事件循环muduo 的心脏。所有网络事件连接、消息、断开都在它里面处理一个线程只能有一个 EventLoopTcpServerTCP 服务器入口负责监听端口、接受新连接TcpClientTCP 客户端入口负责发起连接TcpConnection代表一个已建立的 TCP 连接是收发数据的核心Buffermuduo 自带的网络数据缓冲区解决 TCP 粘包问题回调函数muduo 是事件驱动的所有逻辑都通过回调函数实现这是 muduo 编程的核心三、服务器端常用接口完整实例代码放在第七点1. TcpServer 构造函数作用创建一个 TCP 服务器实例// 完整签名 TcpServer(EventLoop* loop, const InetAddress listenAddr, const string nameArg, Option option kNoReusePort);参数说明参数类型说明loopEventLoop*事件循环指针服务器的所有事件都在这个 loop 中处理listenAddrconst InetAddress监听的地址和端口nameArgconst string服务器名称用于日志区分多个实例optionOption端口选项kReusePort开启端口复用// 端口选项枚举 enum Option { kNoReusePort, // 不开启端口复用 kReusePort // 开启端口复用推荐 };示例// 翻译服务器构造函数 TranslateServer(int port) // 监听0.0.0.0:port服务器名Translate开启端口复用 : _server(_baseloop, muduo::net::InetAddress(0.0.0.0, port), Translate, muduo::net::TcpServer::kReusePort) {}注意事项必须传入非空的 EventLoop 指针监听地址写0.0.0.0表示监听所有网卡写127.0.0.1表示仅本地可访问强烈建议开启kReusePort避免服务器重启时出现 地址已被占用 的错误不要在多个线程中共享同一个 TcpServer 实例2. setConnectionCallback设置连接回调作用注册连接状态变化的回调函数当新连接建立或旧连接断开时自动调用// 完整签名 void setConnectionCallback(const ConnectionCallback cb); // ConnectionCallback 类型定义 using ConnectionCallback std::functionvoid(const TcpConnectionPtr);参数说明cb回调函数参数是当前 TCP 连接的智能指针TcpConnectionPtr示例// 绑定类成员函数作为回调 _server.setConnectionCallback( std::bind(TranslateServer::onConnection, this, std::placeholders::_1) ); // 回调函数实现 void onConnection(const muduo::net::TcpConnectionPtr conn) { if (conn-connected()) { std::cout 新连接建立成功 conn-peerAddress().toIpPort() std::endl; } else { std::cout 连接断开 conn-peerAddress().toIpPort() std::endl; } }注意事项绑定类成员函数时必须传入this指针_1是占位符对应回调的第一个参数通过conn-connected()可以判断是连接建立还是连接断开回调函数会在 EventLoop 线程中执行绝对不能执行耗时操作不要手动 deleteTcpConnectionPtr指向的对象它的生命周期由 muduo 自动管理3. setMessageCallback设置消息回调作用注册收到消息的回调函数当对端发送数据到达时自动调用// 完整签名 void setMessageCallback(const MessageCallback cb); // MessageCallback 类型定义 using MessageCallback std::functionvoid(const TcpConnectionPtr, Buffer*, Timestamp);参数说明参数类型说明connconst TcpConnectionPtr当前 TCP 连接bufferBuffer*收到的数据缓冲区receiveTimeTimestamp消息到达的时间戳示例// 绑定消息回调 _server.setMessageCallback( std::bind(TranslateServer::onMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3) ); // 回调函数实现 void onMessage(const muduo::net::TcpConnectionPtr conn, muduo::net::Buffer* buffer, muduo::Timestamp) { // 1. 从缓冲区取出所有数据并转为字符串 std::string data buffer-retrieveAllAsString(); // 2. 业务处理中英互译 std::string resp translate(data); // 3. 向对端发送响应 conn-send(resp); }注意事项必须调用buffer-retrieve*()系列方法消费数据否则缓冲区会累积数据导致回调被重复触发receiveTime可用于统计网络延迟不要在这个回调中执行耗时操作如数据库查询、复杂计算否则会阻塞整个事件循环不要手动修改 Buffer 的读写指针使用 muduo 提供的方法操作4. start ()启动服务器作用开始监听端口接受新连接// 完整签名 void start();示例void start() { _server.start(); // 启动服务器监听 _baseloop.loop(); // 启动事件循环阻塞调用 }注意事项start()是非阻塞调用真正的事件处理在loop()中必须在loop()之前调用start()不要多次调用start()5. EventLoop::loop ()启动事件循环作用进入无限循环等待并处理所有网络事件// 完整签名 void loop();注意事项这是一个阻塞调用会一直运行直到调用quit()必须在创建 EventLoop 的线程中调用loop()不要在回调函数中调用loop()会导致死循环四、客户端常用接口完整示例在第七点1. TcpClient 构造函数作用创建一个 TCP 客户端实例// 完整签名 TcpClient(EventLoop* loop, const InetAddress serverAddr, const string nameArg);参数说明参数类型说明loopEventLoop*客户端的事件循环serverAddrconst InetAddress服务器的地址和端口nameArgconst string客户端名称用于日志示例// 客户端构造函数 TranslateClient(const std::string sid, int port) : _latch(1), // 用EventLoopThread单独启动一个事件循环线程 _client(_loopthread.startLoop(), muduo::net::InetAddress(sid, port), TranslateClient) {}注意事项客户端通常用EventLoopThread单独启动一个事件循环线程避免阻塞主线程_loopthread.startLoop()会创建一个新线程并返回该线程的 EventLoop 指针2. connect ()发起连接作用异步向服务器发起 TCP 连接// 完整签名 void connect();示例void connect() { _client.connect(); // 异步发起连接立即返回 _latch.wait(); // 阻塞等待连接成功用CountDownLatch同步 } // 在onConnection回调中唤醒等待 void onConnection(const muduo::net::TcpConnectionPtr conn) { if (conn-connected()) { _conn conn; // 保存连接指针 _latch.countDown(); // 计数减1唤醒wait() } else { _conn.reset(); } }注意事项connect()是异步调用不会阻塞连接成功 / 失败会通过onConnection回调通知如果需要同步等待连接结果使用CountDownLatch如你的代码所示不要在主线程中循环轮询连接状态会浪费 CPU3. TcpConnection::send ()发送数据作用向对端发送数据// 常用签名 void send(const std::string message);参数说明message要发送的字符串数据示例bool send(const std::string msg) { if (_conn _conn-connected()) { _conn-send(msg); // 发送数据 return true; } return false; }注意事项send()是线程安全的可以在任意线程调用数据会被拷贝到 muduo 的输出缓冲区由 EventLoop 线程负责实际发送如果连接断开send()会静默丢弃数据所以发送前最好判断connected()五、核心回调函数muduo 的编程模型完全基于回调掌握这几个回调就掌握了 muduo 的 基础。1. onConnection连接状态回调触发时机新连接三次握手完成连接建立成功连接四次挥手完成连接断开核心用途记录连接日志初始化连接上下文同步连接状态如客户端的 CountDownLatch注意事项连接断开时TcpConnectionPtr会自动释放资源不要手动处理2. onMessage收到消息回调触发时机当对端发送的数据到达本端的内核缓冲区并被 muduo 读取到用户态 Buffer 中时核心用途解析网络数据处理业务逻辑发送响应数据注意事项必须消费 Buffer 中的数据否则会导致内存泄漏和重复回调耗时操作要放到线程池中处理不要阻塞事件循环3. onUnknownMessage未知消息兜底回调作用消息分发器的兜底处理当收到未注册对应处理逻辑的未知类型消息时调用// 完整签名 void onUnknownMessage(const TcpConnectionPtr conn, const MessagePtr message, Timestamp);示例void onUnknownMessage(const muduo::net::TcpConnectionPtr conn, const MessagePtr message, muduo::Timestamp) { // 记录未知消息类型方便排查问题 LOG_INFO 收到未知消息 message-GetTypeName(); // 关闭连接防御性编程 conn-shutdown(); }注意事项这是防御性编程的重要手段避免非法消息导致程序崩溃建议所有使用自定义消息协议的项目都实现这个回调优先使用shutdown()优雅关闭连接而不是forceClose()六、Buffer 类常用接口muduo 的 Buffer 常用来解决 TCP 粘包问题最常用的接口有1. retrieveAllAsString()作用取出缓冲区中所有可读数据并转为字符串同时清空缓冲区std::string retrieveAllAsString();示例std::string data buffer-retrieveAllAsString();2. append()作用向缓冲区追加数据void append(const std::string data);3. retrieve()作用移动读指针消费指定长度的数据void retrieve(size_t len);七、基于 muduo 的中英互译示例服务器端#include ../include/muduo/net/EventLoop.h #include ../include/muduo/net/TcpServer.h #include ../include/muduo/net/TcpConnection.h #include functional #include unordered_map #include iostream #include string class TranslateServer { public: TranslateServer(int port) :_server(_baseloop,muduo::net::InetAddress(0.0.0.0,port),Translate,muduo::net::TcpServer::kReusePort) { // 将onConnection成员函数设置为回调函数采用bind进行参数绑定以解决参数数量不匹配问题 _server.setConnectionCallback(std::bind(TranslateServer::onConnection,this,std::placeholders::_1)); // 将onMessage成员函数设置为回调函数 _server.setMessageCallback(std::bind(TranslateServer::onMessage,this,std::placeholders:: _1,std::placeholders::_2,std::placeholders::_3)); } // 启动服务器 void start() { _server.start(); // 启动事件监听 _baseloop.loop(); // 启动事件监控是死循环式的阻塞接口 } private: std::string translate(const std::string str) { static std::unordered_mapstd::string,std::string dict_map { {hello,你好}, {apple,苹果}, {你好,hello}, {苹果,apple} }; auto it dict_map.find(str); if(it dict_map.end()) { return 未找到词义; } return it-second; } // 新连接建立成功时的回调函数在连接建立成功和连接释放时自动调用 void onConnection(const muduo::net::TcpConnectionPtr conn) { if(conn-connected() true) { std::cout 连接建立成功 std::endl; } else { std::cout 连接建立失败 std::endl; } } // 通信连接收到请求时的回调函数 void onMessage(const muduo::net::TcpConnectionPtr conn,muduo::net::Buffer* buffer,muduo::Timestamp) { // 1.从buffer中读取数据 std::string data buffer-retrieveAllAsString(); // 2.处理数据 std::string resp translate(data); // 3.给客户端返回响应结果 conn-send(resp); } private: muduo::net::EventLoop _baseloop;// 是epoll的事件监控会进行描述符的事件监控在事件触发后进行IO操作 muduo::net::TcpServer _server; // 主要用于设置回调函数告诉服务器收到请求该如何处理 }; int main() { TranslateServer server(8085); server.start(); return 0; }客户端#include ../include/muduo/net/EventLoopThread.h #include ../include/muduo/net/TcpClient.h #include ../include/muduo/net/TcpConnection.h #include functional #include iostream #include string class TranslateClient { public: TranslateClient(const std::string sid,int port) :_latch(1),_client(_loopthread.startLoop(),muduo::net::InetAddress(sid,port),TranslateClient) { // 将onConnection成员函数设置为回调函数采用bind进行参数绑定以解决参数数量不匹配问题 _client.setConnectionCallback(std::bind(TranslateClient::onConnection,this,std::placeholders::_1)); // 将onMessage成员函数设置为回调函数 _client.setMessageCallback(std::bind(TranslateClient::onMessage,this,std::placeholders:: _1,std::placeholders::_2,std::placeholders::_3)); } // 负责连接服务器本身是非阻塞的需要等待连接成功才能返回通过CountDownLatch类实现 void connect() { _client.connect(); // 阻塞等待直到连接成功由连接成功时的回调函数来判断 _latch.wait(); } // 负责发送数据 bool send(const std::string msg) { if(_conn-connected()) { // 连接状态正常才发送 _conn-send(msg); return true; } return false; } private: // 新连接建立成功时的回调函数连接成功后唤醒上面的阻塞 void onConnection(const muduo::net::TcpConnectionPtr conn) // 参数需要const修饰 { if(conn-connected()) { _latch.countDown(); // 唤醒阻塞 _conn conn; } else { // 连接建立失败 _conn.reset(); } } // 通信连接收到消息时的回调函数 void onMessage(const muduo::net::TcpConnectionPtr conn,muduo::net::Buffer* buffer,muduo::Timestamp) { std::cout 翻译结果 buffer-retrieveAllAsString() std::endl; } private: muduo::CountDownLatch _latch; // 负责同步控制 muduo::net::EventLoopThread _loopthread; // 事件监控采用单独线程以免阻塞主线程 muduo::net::TcpClient _client; muduo::net::TcpConnectionPtr _conn; }; int main() { TranslateClient client(127.0.0.1,8085); client.connect(); while(1) { std::string buffer; std::cin buffer; client.send(buffer); } return 0; }编译运行# 编译服务器 g server.cpp -o server -lmuduo_net -lmuduo_base -lpthread # 编译客户端 g client.cpp -o client -lmuduo_net -lmuduo_base -lpthread # 运行服务器 ./server # 运行客户端 ./client关于示例中绑定bind的理解绑定 给「成员函数」找个家C 里有两种函数全局函数 / 静态函数无家可归谁都能直接调用成员函数有家属于某个对象必须知道「是哪个对象的函数」才能调用而 muduo 网络库的回调接口只接受「无家可归」的函数不认识「有家的成员函数」。std::bind的作用就是把「成员函数 它所属的对象this」打包成一个 muduo 能识别的回调函数。为什么成员函数不能直接当回调// 错误编译直接报错 _server.setConnectionCallback(Server::onConnection);为什么错Server::onConnection是成员函数它属于Server对象调用它必须知道是「哪个 Server 实例」的onConnection单独传函数地址没有 this 指针编译器根本不知道调用谁直接报错。绑定到底做了什么以这行为例// 正确绑定 this 成员函数 _server.setConnectionCallback( std::bind(Server::onConnection, this, std::placeholders::_1) );std::bind一次性做了 3 个操作1. 固定「调用对象」核心this 当前正在运行的Server对象告诉编译器调用onConnection时就用这个 Server 对象2. 预留「回调参数」std::placeholders::_1 占位符muduo 收到新连接时会自动把「连接对象」传给这个位置。3. 生成「通用回调」把上面两个打包生成一个 muduo 能直接调用的函数完美适配回调接口。为什么要绑定为了让「属于对象的成员函数」能被 muduo 当成普通回调函数使用不绑定成员函数缺this无法调用 → 编译报错绑定后函数有了归属对象参数位置留好 → muduo 可以正常触发回调这里绑定的 2 个核心作用作用 1让回调能访问业务对象的成员变量绑定this后onConnection里才能访问对应的成员变量没有绑定这些成员变量根本访问不到作用 2统一回调格式适配 muduo 接口muduo 的所有回调都有固定格式bind能把业务函数强行适配成 muduo 要的格式。八、muduo 常见坑绝对不要在回调函数中执行耗时操作会阻塞整个事件循环导致所有连接都卡住TcpConnection 的生命周期由 muduo 管理不要手动 delete使用智能指针即可EventLoop 有严格的线程归属所有 EventLoop 的方法都必须在创建它的线程中调用关闭连接优先使用conn-shutdown()半关闭只有在异常情况下才用conn-forceClose()处理 TCP 粘包不要假设一次 recv 就能收到完整的消息使用 Buffer 和自定义协议头来分包日志排查问题muduo 的日志系统非常完善遇到问题先看日志九、总结muduo 的接口设计非常简洁核心就是三个回调 两个入口服务器端TcpServeronConnectiononMessage客户端TcpClientonConnectiononMessagemuduo其它接口https://blog.csdn.net/2301_81298637/article/details/159752471?spm1001.2014.3001.5502muduo核心类https://blog.csdn.net/2301_81298637/article/details/159755916?spm1001.2014.3001.5502感谢阅读本文如有错漏之处烦请斧正。

更多文章