Muduo库基础使用——以简易词典为demo

Muduo库基础使用——以简易词典为demo

这个词典服务器是一个基于Muduo库的简单翻译服务,核心需求:客户端发一个英文单词,服务器返回对应的中文翻译

核心问题:

  1. 网络通信:如何让客户端和服务器建立连接、收发数据?
  2. 请求处理:服务器收到单词后,怎么查词典、返回结果?
  3. 高并发支撑:如果有多个客户端同时请求,怎么高效处理?

一、系统架构设计

1.1 整体架构

┌───────────────────────────────────────────────┐
│                  客户端                        │
│  ┌────────────────┐  ┌─────────────────────┐  │
│  │   发送英语单词   │  │    接收汉语翻译结果    │  │
│  └────────────────┘  └─────────────────────┘  │
└────────────────────┬──────────────────────────┘
                     │
                     ▼
┌───────────────────────────────────────────────┐
│                  服务器                        │
│  ┌─────────────────────────────────────────┐  │
│  │             TcpServer                   │  │
│  │  ┌───────────────────────────────────┐  │  │
│  │  │           EventLoop               │  │  │
│  │  └───────────────────┬───────────────┘  │  │
│  │                      │                  │  │
│  │  ┌───────────────────┴───────────────┐  │  │
│  │  │        DictServer                 │  │  │
│  │  │  ┌─────────────────────────────┐  │  │  │
│  │  │  │     词典哈希表 (static)       │  │  │  │
│  │  │  └─────────────────────┬───────┘  │  │  │
│  │  │                        │          │  │  │
│  │  │  ┌─────────────────────┴───────┐  │  │  │
│  │  │  │     消息处理回调 (onMessage)  │  │  │  │
│  │  │  └─────────────────────────────┘  │  │  │
│  │  └───────────────────────────────────┘  │  │
│  └─────────────────────────────────────────┘  │
└───────────────────────────────────────────────┘

1.2 核心组件

  • TcpServer:Muduo库的核心类,负责监听端口、接受连接和管理I/O事件
  • EventLoop:事件循环,驱动整个服务器运行
  • DictServer:自定义服务器类,封装业务逻辑
  • 词典哈希表:使用unordered_map存储英汉词汇映射关系

二、工作流程详解

1. 服务器启动流程

Main DictServer TcpServer EventLoop 创建DictServer(9090) 初始化TcpServer 设置连接回调(onConnection) 设置消息回调(onMessage) 调用start() 调用start() 注册Accept事件 调用loop() 进入事件循环 Main DictServer TcpServer EventLoop

2. 客户端请求处理流程

Client TcpServer EventLoop DictServer 建立TCP连接 触发onConnection(connected) 发送英语单词"hello" 检测到可读事件 调用onMessage 从Buffer读取单词 在哈希表中查找翻译 找到对应翻译"你好" 发送翻译结果 返回"你好" 关闭连接 触发onConnection(disconnected) Client TcpServer EventLoop DictServer

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()方法做了两件事:

  1. 创建新线程并在该线程中运行事件循环
  2. 返回该线程中EventLoop的指针

这样我们就得到了在独立线程中运行的EventLoop,不会阻塞主线程。这在客户端尤为重要,因为主线程需要处理用户输入。

3.3TcpServer:服务端网络管理

3.3.1初始化与配置
_server(& _baseloop, muduo::net::InetAddress("0.0.0.0", port), 
        "DictServer", muduo::net::TcpServer::kReusePort)

构造函数参数说明:

  • _baseloop:管理连接的EventLoop
  • InetAddress:绑定的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服务器,实际上会:

  1. 创建Acceptor对象监听新连接

  2. 在内部事件循环中注册accept事件

  3. 准备线程池(如有)

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. 以所需等待的事件数初始化(例如1表示等待一个事件)
  2. 调用wait()等待计数变为0
  3. 当事件发生时,调用countDown()减少计数
  4. 当计数变为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事件循环机制

在我们的词典项目中,事件循环的工作流程如下:

  1. 服务端:

    _baseloop.loop();  // 阻塞当前线程,进入事件循环
    

    此调用会使当前线程陷入循环,持续:

    • 等待IO事件(使用epoll/poll)
    • 处理到期定时器
    • 执行挂起的任务
    • 分发IO事件到对应回调
  2. 客户端:

    _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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值