文章目录
Muduo库基础使用——以简易词典为demo
这个词典服务器是一个基于Muduo库的简单翻译服务,核心需求:客户端发一个英文单词,服务器返回对应的中文翻译。
核心问题:
- 网络通信:如何让客户端和服务器建立连接、收发数据?
- 请求处理:服务器收到单词后,怎么查词典、返回结果?
- 高并发支撑:如果有多个客户端同时请求,怎么高效处理?
一、系统架构设计
1.1 整体架构
┌───────────────────────────────────────────────┐
│ 客户端 │
│ ┌────────────────┐ ┌─────────────────────┐ │
│ │ 发送英语单词 │ │ 接收汉语翻译结果 │ │
│ └────────────────┘ └─────────────────────┘ │
└────────────────────┬──────────────────────────┘
│
▼
┌───────────────────────────────────────────────┐
│ 服务器 │
│ ┌─────────────────────────────────────────┐ │
│ │ TcpServer │ │
│ │ ┌───────────────────────────────────┐ │ │
│ │ │ EventLoop │ │ │
│ │ └───────────────────┬───────────────┘ │ │
│ │ │ │ │
│ │ ┌───────────────────┴───────────────┐ │ │
│ │ │ DictServer │ │ │
│ │ │ ┌─────────────────────────────┐ │ │ │
│ │ │ │ 词典哈希表 (static) │ │ │ │
│ │ │ └─────────────────────┬───────┘ │ │ │
│ │ │ │ │ │ │
│ │ │ ┌─────────────────────┴───────┐ │ │ │
│ │ │ │ 消息处理回调 (onMessage) │ │ │ │
│ │ │ └─────────────────────────────┘ │ │ │
│ │ └───────────────────────────────────┘ │ │
│ └─────────────────────────────────────────┘ │
└───────────────────────────────────────────────┘
1.2 核心组件
- TcpServer:Muduo库的核心类,负责监听端口、接受连接和管理I/O事件
- EventLoop:事件循环,驱动整个服务器运行
- DictServer:自定义服务器类,封装业务逻辑
- 词典哈希表:使用
unordered_map
存储英汉词汇映射关系
二、工作流程详解
1. 服务器启动流程
2. 客户端请求处理流程
3.工作流程全景
完整梳下整个应用的工作流程:
1.服务端启动:
-
创建EventLoop
-
创建TcpServer并绑定端口
-
设置回调
-
开始监听并进入事件循环
2.客户端启动:
-
创建EventLoopThread
-
在独立线程中启动EventLoop
-
创建TcpClient并设置回调
-
连接服务端,等待连接建立
-
连接建立后,构造函数返回
3.单词查询流程:
-
客户端从标准输入读取单词
-
通过保存的连接对象发送给服务端
-
服务端接收单词,在词典中查找
-
服务端将翻译结果返回给客户端
-
客户端接收并显示结果
三、涉及的Muduo库核心方法讲解:
3.1EventLoop:事件循环核心
EventLoop是Muduo库的核心类,负责IO事件的监听和分发。
关键方法讲解
// 服务端启动事件循环
_baseloop.loop();
loop()
方法启动事件循环,它会调用底层的poll/epoll来监听文件描述符的事件。一旦调用此方法,当前线程会陷入"事件循环"状态,持续处理各类事件,直到**显式调用quit()**方法。
在我们的服务端代码中:
void start() {
_server.start(); // 先启动服务
_baseloop.loop(); // 再进入事件循环
}
这里_baseloop.loop()
实际上会一直运行,处理所有连接、断开和消息收发事件,直到程序退出。
3.2EventLoopThread:单独的IO线程
在客户端代码中,我们使用了EventLoopThread:
_baseloop(_loop_thread.startLoop())
startLoop()
方法做了两件事:
- 创建新线程并在该线程中运行事件循环
- 返回该线程中EventLoop的指针
这样我们就得到了在独立线程中运行的EventLoop,不会阻塞主线程。这在客户端尤为重要,因为主线程需要处理用户输入。
3.3TcpServer:服务端网络管理
3.3.1初始化与配置
_server(& _baseloop, muduo::net::InetAddress("0.0.0.0", port),
"DictServer", muduo::net::TcpServer::kReusePort)
构造函数参数说明:
_baseloop
:管理连接的EventLoopInetAddress
:绑定的IP和端口"DictServer"
:服务名称,用于日志kReusePort
:端口复用选项,允许多个进程绑定同一端口
3.3.2设置回调函数
_server.setConnectionCallback(bind(&DictServer::onConnection, this, placeholders::_1));
_server.setMessageCallback(bind(&DictServer::onMessage, this, placeholders::_1, placeholders::_2, placeholders::_3));
Muduo使用std::function和std::bind实现回调机制:
setConnectionCallback
:设置连接建立/断开回调setMessageCallback
:设置消息到达回调
std::bind
创建一个函数适配器,将类的成员函数转换为回调所需的函数签名,同时绑定this
指针。placeholders::_1
等是参数占位符,表示将来调用时传入的实参位置。
3.3.3启动服务
_server.start();
start()
启动TCP服务器,实际上会:
-
创建Acceptor对象监听新连接
-
在内部事件循环中注册accept事件
-
准备线程池(如有)
3.4TcpClient:客户端网络管理
3.4.1初始化与配置
_client(_baseloop, muduo::net::InetAddress(server_ip, server_port), "DictClient")
构造函数参数:
_baseloop
:EventLoop指针InetAddress
:服务器地址"DictClient"
:客户端名称
与服务端类似,也需要设置回调函数:
_client.setConnectionCallback(bind(&DictClient::onConnection, this, placeholders::_1));
_client.setMessageCallback(bind(&DictClient::onMessage, this, placeholders::_1, placeholders::_2, placeholders::_3));
3.4.2连接服务器
_client.connect();
connect()
方法异步发起连接请求,不会阻塞等待连接完成。连接结果通过ConnectionCallback回调通知。这就是为什么需要使用CountDownLatch等待连接完成。
3.5TcpConnection:连接管理
TcpConnection是对单个TCP连接的封装,由TcpServer/TcpClient内部创建,通过回调传递给用户。
3.5.1连接状态检查
if(conn->connected()) {
// 连接已建立
}
connected()
方法返回连接是否处于已连接状态,常用于发送前检查。
3.5.2发送数据
conn->send(result); // 发送响应数据
send()
方法将数据发送给对端,支持多种参数形式:
send(const string& message)
:发送字符串send(const void* data, size_t len)
:发送二进制数据send(Buffer* message)
:发送缓冲区
注意,send()
是线程安全的,可以从任意线程调用。如果当前不在IO线程,会将任务转发到IO线程执行。
3.5.3连接管理
客户端保存了连接对象的智能指针:
_conn = conn; // 保存连接对象
这样设计可以在任何需要时发送数据。当连接断开时,需要重置这个指针:
_conn.reset(); // 断开连接时重置
reset()
方法释放智能指针所持有的对象,防止使用已断开的连接。
3.6 Buffer:高效数据缓冲区
Muduo的Buffer类提供了高性能的可变长度缓冲区,用于管理网络读写数据。
3.6.1 读取数据
string msg = buf->retrieveAllAsString();
retrieveAllAsString()
方法将Buffer内所有数据转换为字符串并清空缓冲区。
其他常用读取方法:
retrieveAsString(size_t len)
:读取指定长度数据peek()
:查看但不移除数据readInt32()/readInt64()
:读取整数(考虑字节序)
3.6.2 写入数据
虽然在我们的示例中没直接使用Buffer写入,但常用方法包括:
append(const std::string& str)
:添加字符串到缓冲区append(const char* data, size_t len)
:添加二进制数据appendInt32/appendInt64
:添加整数(处理字节序)
3.7 CountDownLatch:同步工具
CountDownLatch是一个简单而强大的同步工具,用于等待某些事件发生。
muduo::CountDownLatch _latch(1); // 初始化计数为1
// 等待计数变为0
_latch.wait();
// 在回调中减少计数
_latch.countDown(); // 计数减1
CountDownLatch的使用流程:
- 以所需等待的事件数初始化(例如1表示等待一个事件)
- 调用
wait()
等待计数变为0 - 当事件发生时,调用
countDown()
减少计数 - 当计数变为0时,所有wait()调用者被唤醒
在客户端中,该机制确保构造函数等待连接建立后才返回,使用户可以安全地调用send()
方法。
3.8 InetAddress:网络地址封装
muduo::net::InetAddress("0.0.0.0", port) // 服务端地址
muduo::net::InetAddress(server_ip, server_port) // 客户端连接地址
InetAddress封装了IPv4或IPv6地址和端口,提供格式转换和字符串表示功能。常用方法:
toIp()
:返回IP地址字符串toIpPort()
:返回"IP:端口"格式的字符串port()
:返回端口号
3.9 回调函数签名详解
Muduo库定义了几种标准回调函数类型:
3.9.1 ConnectionCallback
void onConnection(const muduo::net::TcpConnectionPtr &conn)
当连接状态变化(建立/断开)时调用,通过conn->connected()
判断当前状态。
3.9.2MessageCallback
void onMessage(const muduo::net::TcpConnectionPtr &conn,
muduo::net::Buffer *buf,
muduo::Timestamp receiveTime)
当收到新数据时调用,参数包括:
conn
:产生消息的连接buf
:接收缓冲区,包含新接收的数据receiveTime
:接收时间戳(在本例中未使用)
3.9.3WriteCompleteCallback
虽然本例未使用,但也是重要回调:
void onWriteComplete(const muduo::net::TcpConnectionPtr &conn)
当写缓冲区完全发送完毕时调用,常用于流量控制。
3.10 理解Muduo事件循环机制
在我们的词典项目中,事件循环的工作流程如下:
-
服务端:
_baseloop.loop(); // 阻塞当前线程,进入事件循环
此调用会使当前线程陷入循环,持续:
- 等待IO事件(使用epoll/poll)
- 处理到期定时器
- 执行挂起的任务
- 分发IO事件到对应回调
-
客户端:
_baseloop(_loop_thread.startLoop())
通过EventLoopThread创建独立线程运行事件循环,避免阻塞主线程。
理解EventLoop的工作方式是掌握Muduo库的关键。事件循环是整个库的核心,它将异步操作转化为事件回调,实现高效的非阻塞IO。
四、代码实现分析
4.1服务端实现
4.1.1服务器初始化和启动
class DictServer {
public:
DictServer(int port):
_server(& _baseloop,muduo::net::InetAddress("0.0.0.0", port), "DictServer",muduo::net::TcpServer::kReusePort)
//函数参数分别表示:EventLoop的实例,绑定的地址和端口,名字,端口复用选项
{
//设置回调函数
_server.setConnectionCallback(bind(&DictServer::onConnection, this, placeholders::_1));
//由于onConnection是类成员函数,所以需要绑定this指针,bind进行函数适配
//bind通过onConnection函数进行了参数绑定之后,生成了一个适配与当前函数的可调用对象
//设置消息回调处理函数
_server.setMessageCallback(bind(&DictServer::onMessage, this, placeholders::_1, placeholders::_2, placeholders::_3));
}
void start() {
//启动获取监听
_server.start();
//eventloop开启事件循环监控
_baseloop.loop();
//注意这里的顺序是不能够换的,因为如果先开始loop,那么连接就不会建立,因为还没有开始监听
}
//...
};
这段代码展示了服务器初始化的核心逻辑:
-
创建TcpServer实例,绑定到指定端口
-
设置连接和消息回调函数
-
start()方法启动服务并进入事件循环
**server.start()与baseloop.loop()**的顺序非常重要。先调用start()开始监听,再调用loop()进入事件循环处理连接请求和数据收发。如果顺序颠倒,事件循环会先启动,但此时还没有开始监听,无法建立连接。
4.1.2词典查询逻辑
//onMessage处理客户端发送的消息
void onMessage(const muduo::net::TcpConnectionPtr &conn, muduo::net::Buffer *buf, muduo::Timestamp){
static unordered_map<string, string> dict_map={
{"hello", "你好"},
{"world", "世界"},
{"good", "好"},
{"server", "服务器"},
{"client", "客户端"}
};
string msg = buf->retrieveAllAsString();
string result;
auto it = dict_map.find(msg);//查找字典
if(it!=dict_map.end()){
//找到词
result = it->second; //it->second获取字典中对应的值
}
else{
result = "未知单词";
}
conn->send(result); //响应,发送数据
}
当客户端发送单词时,服务器:
-
从Buffer中读取完整消息
-
在词典中查找对应翻译
-
将结果发送回客户端
这里用了**retrieveAllAsString()**方法一次性提取并清空缓冲区内容,适合处理简单的单词查询。对于复杂协议,可能需要分批处理Buffer内容。
4.2客户端实现
4.2.1连接建立与同步
客户端重点在于处理异步连接的建立:
DictClient(const string & server_ip, int server_port):
_client(_baseloop, muduo::net::InetAddress(server_ip, server_port), "DictClient"),
_baseloop(_loop_thread.startLoop()),
_latch(1) // 计数器初始化为1
{
//设置回调函数
_client.setConnectionCallback(bind(&DictClient::onConnection, this, placeholders::_1));
_client.setMessageCallback(bind(&DictClient::onMessage, this, placeholders::_1, placeholders::_2, placeholders::_3));
//客户端首先要连接服务器
_client.connect();
_latch.wait(); // 等待连接建立
}
这里使用了CountDownLatch(计数锁存器)来实现同步等待。流程是:
-
初始化计数器为1
-
启动连接
-
调用wait()等待计数器 – – 变为0
-
当连接成功建立时,回调函数中调用countDown(),计数器变为0,唤醒等待中的构造函数
这种设计将异步事件转换为同步流程,确保客户端对象构造完成时连接已就绪。
4.2.2事件循环线程设计
_baseloop(_loop_thread.startLoop())//通过EventLoopThread创建独立线程运行事件循环,避免阻塞主线程。
上面代码展示了客户端的另一关键设计:
在单独的线程中运行事件循环。这样主线程可以继续执行用户输入处理,而I/O线程负责网络事件。这避免了在构造函数中直接使用_baseloop.loop()导致的死锁问题。
4.2.3 连接管理
void onConnection(const muduo::net::TcpConnectionPtr &conn) {
if(conn->connected()){
cout<<"连接建立\n";
_latch.countDown(); // 计数器减1,唤醒等待的构造函数
_conn = conn; // 保存连接对象
}
else {
cout<<"连接断开\n";
_conn.reset(); // 连接断开时,重置连接对象
}
}
这段代码处理连接状态变化:
-
连接建立时,保存连接对象并唤醒等待线程
-
连接断开时,重置连接对象以避免使用失效连接
保存TcpConnectionPtr是客户端发送消息的关键,这也是为什么需要在类成员中保持这个智能指针。
4.2.4 消息发送封装
bool send(const string &msg){
if(_conn && _conn->connected()){
_conn->send(msg);
return true;
}
else{
cout<<"连接断开,无法发送消息\n";
return false;
}
}
send()方法封装消息发送逻辑,在发送前检查连接状态,提高代码健壮性。
五、demo改进方向
因为只是demo!只是demo!只是demo!所以很多地方写的只考虑了最基础的,复杂场景如多线程、粘包等问题都没有考虑
5.1 词典数据结构优化
- 从静态硬编码改为从文件或数据库加载
- 添加词典更新接口,支持动态添加/删除词汇
5.2 多线程支持
- 当前实现是单线程的,可通过
setThreadNum()
扩展为多线程模型 - 适合处理大量并发连接的场景
5.3 协议升级
- 支持更复杂的协议,如JSON格式请求/响应
- 增加错误码和状态信息返回
5.4 资源管理
- 添加连接超时管理,自动关闭空闲连接
- 实现优雅关闭机制,确保服务器正常退出
5.5 性能优化
- 使用更高效的哈希函数或数据结构
- 添加缓存机制,提高频繁查询的响应速度
六、代码
6.1服务端 dict_server.cpp
#include <muduo/net/TcpServer.h>
#include <muduo/net/EventLoop.h>
#include <muduo/net/TcpConnection.h>
#include <muduo/net/Buffer.h>
#include <iostream>
#include <string>
#include <unordered_map>
using namespace std;
class DictServer {
public:
DictServer(int port):
_server(& _baseloop,muduo::net::InetAddress("0.0.0.0", port), "DictServer",muduo::net::TcpServer::kReusePort)
//函数参数分别表示:EventLoop的实例,绑定的地址和端口,名字,端口复用选项
{
//设置回调函数
_server.setConnectionCallback(bind(&DictServer::onConnection, this, placeholders::_1));//由于onConnection是类成员函数,所以需要绑定this指针,bind进行函数适配
//bind通过onConnection函数进行了参数绑定之后,生成了一个适配与当前函数的可调用对象
//设置消息回调处理函数
_server.setMessageCallback(bind(&DictServer::onMessage, this, placeholders::_1, placeholders::_2, placeholders::_3));
}
void start() {
//启动获取监听
_server.start();
//eventloop开启事件循环监控
_baseloop.loop();
//注意这里的顺序是不能够换的,因为如果先开始loop,那么连接就不会建立,因为还没有开始监听
}
private:
void onConnection(const muduo::net::TcpConnectionPtr &conn) {
if(conn->connected()){
std::cout<<"连接建立\n";
}
else
{
std::cout<<"连接断开\n";
}
}
//onMessage处理客户端发送的消息
void onMessage(const muduo::net::TcpConnectionPtr &conn, muduo::net::Buffer *buf, muduo::Timestamp){
static unordered_map<string, string> dict_map={
{"hello", "你好"},
{"world", "世界"},
{"good", "好"},
{"server", "服务器"},
{"client", "客户端"}
};
string msg = buf->retrieveAllAsString();
string result;
auto it = dict_map.find(msg);//查找字典
if(it!=dict_map.end()){
//找到词
result = it->second; //it->second获取字典中对应的值
}
else{
result = "未知单词";
}
conn->send(result); //响应,发送数据
}
muduo::net::EventLoop _baseloop;//注意baseloop要放在serv的上面,因为是使用baseloop来构造serv的
muduo::net::TcpServer _server;
};
int main(){
DictServer server(9090);
server.start();
return 0;
}
6.2 客户端 dict_client.cpp
#include <muduo/net/TcpClient.h>
#include <muduo/net/EventLoop.h>
#include <muduo/net/EventLoopThread.h>
#include <muduo/net/TcpConnection.h>
#include <muduo/base/CountDownLatch.h>
#include <muduo/net/Buffer.h>
#include <iostream>
#include <string>
#include <unordered_map>
using namespace std;
class DictClient{
public:
DictClient(const string & server_ip, int server_port):
_client(_baseloop, muduo::net::InetAddress(server_ip, server_port), "DictClient"),
//参数分别为:EventLoop的实例,绑定的地址和端口,名字
_baseloop(_loop_thread.startLoop()),
_latch(1)//计数器初始化为1,等待连接建立,计数大于0时,会阻塞,--等待计数为0时会唤醒
{
//设置回调函数
_client.setConnectionCallback(bind(&DictClient::onConnection, this, placeholders::_1));
//设置消息回调处理函数
_client.setMessageCallback(bind(&DictClient::onMessage, this, placeholders::_1, placeholders::_2, placeholders::_3));
//客户端首先要连接服务器
_client.connect();
_latch.wait();//等待连接建立,计数为0时会唤醒阻塞
//_baseloop.loop();
//开始时间循环监控,但是这个循环监控是一个死循环,如果陷入死循环,则无法执行其他代码send发送不了数据了,所以对于客户端来说不能这么用
//所以需要使用其他方式来发送数据,比如使用muduo::net::EventLoopThread
//EventLoopThread是一个线程,用来处理事件循环,可以用来发送数据
//EventLoopThread::startLoop() 启动线程
//EventLoopThread::queueInLoop() 将数据添加到事件循环中
//EventLoopThread::getLoop() 获取事件循环
//EventLoopThread::stop() 停止线程
//EventLoopThread::join() 等待线程结束
};
bool send(const string &msg){
if(_conn->connected()){
_conn->send(msg);
return true;
}
else{
cout<<"连接断开,无法发送消息\n";
return false;
}
}
private:
void onConnection(const muduo::net::TcpConnectionPtr &conn) {
if(conn->connected()){
cout<<"连接建立\n";
_latch.countDown();//计数器减1,计数为0时会唤醒阻塞
_conn = conn; //建立连接的时候
}
else
{
cout<<"连接断开\n";
_conn.reset();//断开连接的时候,连接对象置空
}
}
//onMessage处理客户端发送的消息
void onMessage(const muduo::net::TcpConnectionPtr &conn, muduo::net::Buffer *buf, muduo::Timestamp){
string result = buf->retrieveAllAsString();//将缓冲区中的数据转换为字符串
cout<<"服务器响应: " << result << endl;
}
muduo::net::TcpConnectionPtr _conn; //连接对象,用来发送和接收消息
muduo::CountDownLatch _latch; //计数器,用来等待连接建立
muduo::net::EventLoopThread _loop_thread; //事件循环线程,用来处理事件
muduo::net::EventLoop *_baseloop; //事件循环,用来处理事件
muduo::net::TcpClient _client; //客户端,用来连接服务器
};
int main(){
DictClient client("127.0.0.1", 9090);
while(1){
string msg;
cin>>msg;
if(client.send(msg)){
cout<<"发送成功\n";
}
else{
cout<<"发送失败\n";
}
}
return 0;
}
6.3 Makefile
注意由于每个人库的路径不一致,所以用的时候需要把路径做下适配
CFLAG= -std=c++11 -I ../../build/release-install-cpp11/include/
LFLAG= -L../../build/release-install-cpp11/lib/ -lmuduo_net -lmuduo_base -pthread
all: dict_server dict_client
dict_server: dict_server.cpp
g++ $(CFLAG) $^ -o $@ $(LFLAG)
dict_client: dict_client.cpp
g++ $(CFLAG) $^ -o $@ $(LFLAG)
clean:
rm dict_server
rm dict_client