第三方库介绍——Muduo

文章目录

  • [1. Muduo 库是什么](#1. Muduo 库是什么)
  • [2. Muduo 库常见接口介绍](#2. Muduo 库常见接口介绍)
    • [2.1 muduo::net::TcpServer 类基础介绍](#2.1 muduo::net::TcpServer 类基础介绍)
    • [2.2 muduo::net::EventLoop 类基础介绍](#2.2 muduo::net::EventLoop 类基础介绍)
    • [2.3 **muduo::net::TcpConnection 类基础介绍**](#2.3 muduo::net::TcpConnection 类基础介绍)
    • [2.4 **muduo::net::TcpClient 类基础介绍**](#2.4 muduo::net::TcpClient 类基础介绍)
    • [2.5 **muduo::net::Buffer 类基础介绍**](#2.5 muduo::net::Buffer 类基础介绍)
  • [3. **Muduo 库快速上手**](#3. Muduo 库快速上手)
    • [3.1 英译汉 TCP 服务器](#3.1 英译汉 TCP 服务器)
    • [3.2 英译汉客户端](#3.2 英译汉客户端)
  • [4. **基于 muduo 库函数实现 protobuf 协议的通信**](#4. 基于 muduo 库函数实现 protobuf 协议的通信)
    • [4.1 基于 protobuf 的接口类介绍](#4.1 基于 protobuf 的接口类介绍)
    • [4.2 定义具体的业务请求类型](#4.2 定义具体的业务请求类型)
    • [4.3 服务器](#4.3 服务器)
    • [4.4 客户端](#4.4 客户端)

1. Muduo 库是什么

Muduo 由陈硕大佬开发,是一个基于非阻塞 IO事件驱动 的 C++高并发 TCP 网络编程库 。 它是一款基于主从 Reactor 模型的网络库,其使用的线程模型是 **one loop per thread,**所谓 one loop per thread 指的是:

  • 一个线程只能有一个事件循环(EventLoop), 用于响应计时器和 IO 事件
  • 一个文件描述符只能由一个线程进行读写,换句话说就是一个 TCP 连接必须归属于某个 EventLoop 管理

2. Muduo 库常见接口介绍

2.1 muduo::net::TcpServer 类基础介绍

muduo库的TcpServer类是一个基于主从Reactor模型的高并发TCP服务器,其设计核心是事件驱动非阻塞I/O

cpp 复制代码
typedef std::shared_ptr<TcpConnection> TcpConnectionPtr;
typedef std::function<void (const TcpConnectionPtr&)> ConnectionCallback;
typedef std::function<void (const TcpConnectionPtr&,
                            Buffer*,
                            Timestamp)> MessageCallback;
class InetAddress : public muduo::copyable
{ 
public:
    InetAddress(StringArg ip, uint16_t port, bool ipv6 = false);
};
class TcpServer : noncopyable
{ 
public:
    enum Option
    { 
        kNoReusePort,
        kReusePort,
    };
    TcpServer(EventLoop* loop,
                const InetAddress& listenAddr,
                const string& nameArg,
                Option option = kNoReusePort);
    void setThreadNum(int numThreads);
    void start();
    // 当一个新连接建立成功的时候被调用
    void setConnectionCallback(const ConnectionCallback& cb)
    { connectionCallback_ = cb; }
    // 消息的业务处理回调函数---这是收到新连接消息的时候被调用的函数
    void setMessageCallback(const MessageCallback& cb)
    { messageCallback_ = cb; }
};

核心组件与回调

  • 智能指针与回调类型 :代码使用 TcpConnectionPtrstd::shared_ptr<TcpConnection>)来安全地管理 TCP 连接的生命周期。ConnectionCallbackMessageCallback是两个关键的函数对象类型,分别在新连接建立成功时和收到对方消息时被触发。用户通过设置这些回调来定义业务逻辑。
  • 网络地址封装InetAddress类封装了 IP 地址和端口号,用于标识网络端点。

TcpServer 的功能与配置

  • 服务器初始化TcpServer的构造函数需要传入一个 EventLoop(通常作为主 Reactor)、服务器的监听地址以及一个名称。Option枚举可用于设置套接字选项,如 kReusePort(端口复用)。
  • 线程模型配置setThreadNum(int numThreads)方法用于设置子 Reactor(SubLoop)线程池的大小,这些线程负责处理已建立连接的 I/O 事件,从而实现高效的并发。
  • 服务器启动 :调用 start()方法后,服务器会启动线程池并开始监听连接请求。

工作机制简述

TcpServer的工作机制围绕主从Reactor模型展开。

  • 主Reactor(Main Reactor):由用户提供的EventLoop* loop参数负责,通常只有一个线程。它的核心任务是监听新的客户端连接请求。其内部的关键组件是Acceptor,它负责创建监听套接字、绑定地址,并接受(accept)新连接
  • 从Reactor(Sub Reactor):通过setThreadNum(int numThreads)创建的线程池,每个线程都是一个独立的EventLoop(即one loop per thread模型)。当Acceptor接受一个新连接后,它会通过轮询等算法,将这个连接的socket文件描述符(connfd)分发给某个子Reactor线程。此后,该连接上的所有数据读写(I/O事件)都由这个指定的子Reactor线程负责,实现了连接的负载均衡。

回调函数是连接TcpServer框架与用户业务逻辑的桥梁。

  1. 新连接建立
    • 流程 :客户端发起连接 → 主Reactor中的Acceptor接受连接 → 创建TcpConnection对象代表此连接 → 将该连接分发给一个子Reactor线程。
    • 回调 :连接成功建立后,会调用用户通过setConnectionCallback设置的回调函数。你可以在这个函数中记录日志、进行初始化等操作。
  2. 消息接收与处理
    • 流程 :子Reactor线程监听到连接上有数据可读 → 触发TcpConnection的读事件 → 数据被读入内部的Buffer缓冲区。
    • 回调 :随后,用户通过setMessageCallback设置的回调函数被调用。这个函数的参数中会包含指向该Buffer的指针,你的业务逻辑(如解包、计算、回复)就在这里实现。Buffer类提供了丰富的接口(如retrieve, append, peekInt32等)来方便地处理数据。
  3. 连接关闭
    • 流程:检测到连接关闭(如对端关闭或错误)→ 子Reactor线程触发关闭事件 → 清理连接资源。
    • 回调 :连接关闭时也会触发ConnectionCallback,你可以在此进行资源清理工作。

2.2 muduo::net::EventLoop 类基础介绍

muduo::net::EventLoop是 muduo 网络库的核心,它实现了 Reactor 模式的事件循环,负责监听 I/O 事件、处理定时任务和执行异步回调。

cpp 复制代码
class EventLoop : noncopyable
{ 
public:
 // Loops forever.
 // Must be called in the same thread as creation of the object.
 void loop();
 // Quits loop.
 // This is not 100% thread safe, if you call through a raw pointer,
 // better to call through shared_ptr<EventLoop> for 100% safety.
 void quit();
 TimerId runAt(Timestamp time, TimerCallback cb);
 // Runs callback after @c delay seconds.
 // Safe to call from other threads.
 TimerId runAfter(double delay, TimerCallback cb);
 // Runs callback every @c interval seconds.
 // Safe to call from other threads.
 TimerId runEvery(double interval, TimerCallback cb);
 // Cancels the timer.
 // Safe to call from other threads.
 void cancel(TimerId timerId);
private:
 std::atomic<bool> quit_;
 std::unique_ptr<Poller> poller_;
 mutable MutexLock mutex_;
 std::vector<Functor> pendingFunctors_ GUARDED_BY(mutex_);
};

事件循环机制

EventLoop的核心是 loop()方法,它在一个循环中不断重复以下步骤:

  1. 等待事件 :通过 poller_->poll()调用,阻塞等待注册的文件描述符上发生 I/O 事件(如可读、可写)或定时器超时。
  2. 处理事件 :当 poller_返回后,EventLoop会遍历所有被激活的 Channel(事件分发器),并调用它们相应的回调函数,从而处理网络数据收发等 I/O 操作。
  3. 执行异步任务 :在每一轮事件处理结束后,它会执行 pendingFunctors_队列中由其他线程投递过来的回调函数。

这个过程确保了所有操作都在其绑定的I/O 线程内顺序执行,避免了复杂的锁竞争,是 muduo 高性能的关键。

定时器功能

EventLoop提供了灵活的定时器接口,用于调度未来某个时间点要执行的任务:

  • **runAt(Timestamp time, TimerCallback cb)**:在绝对时间 time执行回调 cb
  • **runAfter(double delay, TimerCallback cb)**:在相对当前时间延迟 delay秒后执行回调。
  • **runEvery(double interval, TimerCallback cb)**:每隔 interval秒重复执行一次回调。

这些定时器内部由一个 TimerQueue管理,poller_->poll()的超时时间通常会设置为下一个定时任务的到期时间,以保证定时精度。

跨线程任务管理

这是 EventLoop一个非常重要的特性。它允许其他线程安全地向其投递任务,并保证任务会在正确的 I/O 线程中执行。这是通过两个关键方法实现的:

  • void runInLoop(Functor cb) :如果调用此方法的线程就是 EventLoop所属的 I/O 线程,则回调 cb会被立即同步 执行;否则,cb会被加入任务队列。
  • void queueInLoop(Functor cb):无论调用者是谁,回调 cb都会被异步加入任务队列,等待执行。

为了实现跨线程唤醒,EventLoop内部维护了一个 wakeupFd_(通常是一个 eventfd)。当其他线程向任务队列 pendingFunctors_添加新任务后,会向这个 wakeupFd_写入数据,从而立即唤醒可能正阻塞在 poller_->poll()中的 I/O 线程,使其能够及时处理新任务。

使用要点

在使用 EventLoop时,有几点需要特别注意:

  • One Loop Per Thread :每个 EventLoop对象必须严格绑定到一个线程,并且只能在该线程中创建、运行和销毁。你可以通过 EventLoop::getEventLoopOfCurrentThread()来获取当前线程的 EventLoop对象。
  • 避免阻塞 :在 EventLoop的事件回调或任务回调中,严禁执行耗时的或可能阻塞的操作(如长时间的计算、同步的文件 I/O 等),否则会严重拖慢整个事件循环,影响所有连接的响应速度。
  • 生命周期管理 :由于回调是异步执行的,需要确保在回调被执行时,其所依赖的对象仍然是有效的。通常可以通过在回调中捕获 std::shared_ptr来延长对象的生命周期。

2.3 muduo::net::TcpConnection 类基础介绍

muduo::net::TcpConnection是 muduo 网络库中最复杂但也最核心的类,它代表一条已建立的 TCP 连接。无论是服务器接受的连接还是客户端发起的连接,其后续的所有通信都通过 TcpConnection 对象来管理

cpp 复制代码
class TcpConnection : noncopyable, public std::enable_shared_from_this<TcpConnection>
{ 
public:
 /// Constructs a TcpConnection with a connected sockfd
 ///
 /// User should not create this object.
 TcpConnection(EventLoop* loop,
                 const string& name,
                 int sockfd,
                 const InetAddress& localAddr,
                 const InetAddress& peerAddr);
 bool connected() const { return state_ == kConnected; }
 bool disconnected() const { return state_ == kDisconnected; }
 
 void send(string&& message); // C++11
 void send(const void* message, int len);
 void send(const StringPiece& message);
 // void send(Buffer&& message); // C++11
 void send(Buffer* message); // this one will swap data
 void shutdown(); // NOT thread safe, no simultaneous calling
 void setContext(const boost::any& context)
 { context_ = context; }
 const boost::any& getContext() const
 { return context_; }
 boost::any* getMutableContext()
 { return &context_; }
 void setConnectionCallback(const ConnectionCallback& cb)
 { connectionCallback_ = cb; }
 void setMessageCallback(const MessageCallback& cb)
 { messageCallback_ = cb; }
private:
 enum StateE { kDisconnected, kConnecting, kConnected, kDisconnecting };
 EventLoop* loop_;
 ConnectionCallback connectionCallback_;
 MessageCallback messageCallback_;
 WriteCompleteCallback writeCompleteCallback_;
 boost::any context_;
};

TcpConnection使用一个状态机(StateE)来精确跟踪连接的生命周期,主要包括 kConnecting(正在连接)、kConnected(已连接)、kDisconnecting(正在断开)和 kDisconnected(已断开)几种状态

  • 连接建立:当 TcpServer接受一个新连接或 TcpClient成功连接到服务器后,会创建一个 TcpConnection对象,初始状态为 kConnecting。随后调用其 connectEstablished方法,将状态置为 kConnected,并开始监听套接字上的可读事件,同时调用用户设置的 ConnectionCallback通知应用层
  • 连接断开:断开通常是"被动关闭",即当底层 Channel监听到套接字关闭事件(如对端关闭连接,导致read返回0)时,会触发 handleClose方法,最终将状态置为 kDisconnected,并清理资源。用户也可以主动调用 shutdown()来发起"主动关闭",该方法会将状态置为 kDisconnecting,并在确保所有待发送数据(outputBuffer_)都发出后,安全地关闭连接。

数据收发与缓冲区

TcpConnection高效处理数据收发的核心在于其双缓冲区设计(inputBuffer_和 outputBuffer_)

  • 数据接收 (handleRead) :当套接字有数据可读 时,Channel会触发 TcpConnection::handleRead回调 。该函数将数据从套接字读入 inputBuffer_应用层缓冲区,然后调用用户设置的 MessageCallback,将包含数据的 inputBuffer_传递给上层业务逻辑。这样设计避免了直接处理可能不完整的TCP字节流(粘包/半包问题),业务层可以从缓冲区中解析完整的应用层报文
  • 数据发送 (send与 sendInLoop) :用户通过 send系列接口发送数据。这些接口是线程安全 的,无论从哪个线程调用,最终都会通过 EventLoop::runInLoop将实际的发送任务转移到 TcpConnection所属的I/O线程中执行,即调用私有方法 sendInLoop
    • sendInLoop会尝试直接通过 ::write系统调用发送数据。如果数据未能一次性发送完,剩余部分会存入 outputBuffer_,并开始监听套接字的可写事件。
    • 当内核发送缓冲区有空间时,会触发可写事件,进而调用 TcpConnection::handleWrite,继续发送 outputBuffer_中积压的数据
  • 流量控制(高水位回调):为防止发送过快导致 outputBuffer_无限膨胀,可以设置高水位线(highWaterMark_)及相应的回调函数(HighWaterMarkCallback)。当待发送数据量超过阈值时,会触发该回调,提示上层应用暂停或减缓发送速度

上下文信息与回调机制

  • 上下文信息 (context_):setContext、getContext等方法允许用户为每个 TcpConnection绑定一个任意类型的上下文信息(使用 boost::any或 std::any)。这在需要维护连接会话状态时非常有用,例如,可以在一个连接上关联一个用户ID或一个数据库会话对象
  • 回调机制 :TcpConnection通过回调函数将网络事件向上传递。最重要的两个回调是:
    • MessageCallback:在收到新数据时被调用,是业务逻辑的入口点。
    • ConnectionCallback:在连接建立或关闭时被调用,用于处理连接生命周期事件

核心设计思想与使用要点

  1. 生命周期与智能指针:TcpConnection是 muduo 库中唯一默认使用 std::shared_ptr来管理生命周期的类(通过 TcpConnectionPtr)。这是因为其生命周期模糊,可能被多个地方持有(如 TcpServer的连接映射表、跨线程的任务队列)。其基类继承自 std::enable_shared_from_this,确保了在内部回调中能安全地获取指向自身的智能指针,避免在回调执行期间对象被意外销毁
  2. One Loop Per Thread:每个 TcpConnection对象完全归属于一个特定的 EventLoop(I/O线程)。其所有方法,特别是内部的事件处理函数,都必须在该I/O线程中调用,这消除了竞态条件,简化了并发编程模型
  3. 非阻塞IO与边缘触发(LT):TcpConnection基于 Channel和 Poller,采用电平触发(Level-Triggered, LT) 模式。对于可写事件,它采用了"需要时才监听"的优化策略:仅在 outputBuffer_中有数据待发送时才关注可写事件,一旦发送完毕就立即取消关注,避免了busy-loop问题

2.4 muduo::net::TcpClient 类基础介绍

muduo::net::TcpClient是 muduo 网络库中用于创建 TCP 客户端的核心类。它封装了连接到服务器、管理连接生命周期和处理网络通信的复杂细节。

cpp 复制代码
class TcpClient : noncopyable
{ 
public:
 // TcpClient(EventLoop* loop);
 // TcpClient(EventLoop* loop, const string& host, uint16_t port);
 TcpClient(EventLoop* loop,
             const InetAddress& serverAddr,
             const string& nameArg);
 ~TcpClient(); // force out-line dtor, for std::unique_ptr members.
 void connect();//连接服务器
 void disconnect();//关闭连接
 void stop();
 //获取客户端对应的通信连接 Connection 对象的接口,发起 connect 后,有
可能还没有连接建立成功
 TcpConnectionPtr connection() const
 { 
     MutexLockGuard lock(mutex_);
     return connection_;
 } 
 /// 连接服务器成功时的回调函数
 void setConnectionCallback(ConnectionCallback cb)
 { connectionCallback_ = std::move(cb); }
 /// 收到服务器发送的消息时的回调函数
 void setMessageCallback(MessageCallback cb)
 { messageCallback_ = std::move(cb); }
private:
 EventLoop* loop_;
 ConnectionCallback connectionCallback_;
 MessageCallback messageCallback_;
 WriteCompleteCallback writeCompleteCallback_;
 TcpConnectionPtr connection_ GUARDED_BY(mutex_);
};
/*
需要注意的是,因为 muduo 库不管是服务端还是客户端都是异步操作,
对于客户端来说如果我们在连接还没有完全建立成功的时候发送数据,这是不被允
许的。
因此我们可以使用内置的 CountDownLatch 类进行同步控制
*/
class CountDownLatch : noncopyable
{ 
public:
    explicit CountDownLatch(int count);
    void wait(){
        MutexLockGuard lock(mutex_);
        while (count_ > 0)
        { 
            condition_.wait();
        }
    } 
    void countDown(){
        MutexLockGuard lock(mutex_);
        --count_;
        if (count_ == 0)
        { 
            condition_.notifyAll();
        } 
    } 
    int getCount() const;
private:
    mutable MutexLock mutex_;
    Condition condition_ GUARDED_BY(mutex_);
    int count_ GUARDED_BY(mutex_);
};

TcpClient的核心设计目标是简化客户端的异步连接流程。在传统的阻塞编程中,connect调用会阻塞线程直到连接成功或失败。而 TcpClient利用非阻塞 I/O 和事件循环,使得连接请求是异步发起的,不会阻塞当前线程。

其内部通过 Connector类来完成实际的连接工作。当你调用 TcpClient::connect()时,流程如下:

  1. Connector创建一个非阻塞的 socket 并发起 connect系统调用,该调用通常会立即返回 EINPROGRESS错误,表示连接正在进行中
  2. Connector在 loop_中监听该 socket 的可写事件。当连接成功建立(或失败)时,操作系统会通知 EventLoop。
  3. 连接成功后,Connector会回调 TcpClient::newConnection函数。该函数会创建一个 TcpConnection对象来管理这个已连接的 socket,并设置好相应的回调函数。之后,所有该连接上的数据读写就交由 TcpConnection处理

连接生命周期与状态管理

TcpClient内部通过状态机和标志位来精细控制连接的生命周期:

  • 发起连接:调用 connect()后,Connector开始工作。此时 connection_还未被赋值。
  • 连接建立:连接成功后会触发 TcpClient::newConnection,此时 connection_被赋值,并且用户设置的 ConnectionCallback会被调用。
  • 连接断开:当连接因故断开(如服务器关闭或网络错误)时,TcpConnection会触发关闭回调,最终调用 TcpClient::removeConnection来清理 connection_资源。如果启用了 retry_,TcpClient还会自动尝试重连

异步特性与同步控制

TcpClient的一个关键特性是其操作的异步性。当你调用 connect()时,函数会立即返回,而连接建立过程在后台进行。因此,在连接完全建立成功之前,connection()返回的指针可能是空的,此时调用 connection()->send()发送数据是无效的。

为了解决这个问题,代码注释中提到了使用 CountDownLatch进行同步控制。它的工作原理是:

  • 在调用 connect()前,创建一个初始值为 1 的 CountDownLatch。
  • 在 TcpClient的 ConnectionCallback(连接建立成功的回调)中,调用 CountDownLatch::countDown()。
  • 在主线程中,在调用 connect()后立即调用 CountDownLatch::wait()。这样,主线程会阻塞在此处,直到连接成功建立、回调函数被触发、计数器减为 0 后才继续执行,从而确保后续的数据发送操作是安全的

核心使用模式

综合来看,正确使用 TcpClient的模式通常如下:

  1. 创建与配置:实例化 TcpClient,传入服务器地址,并设置好各种回调函数。
  2. 发起连接:调用 connect()。
  3. 等待连接就绪:使用 CountDownLatch或其他同步机制,等待连接成功建立。
  4. 进行通信:在连接确认建立后,通过 connection()->send()发送数据,并在 MessageCallback中处理接收到的数据。
  5. 清理:在需要时调用 disconnect()或 stop()来主动关闭连接。

2.5 muduo::net::Buffer 类基础介绍

muduo::net::Buffer是一个应用层缓冲区,它通过巧妙的内存布局和高效的操作接口,解决了非阻塞网络编程中的数据收发和内存管理难题。

cpp 复制代码
class Buffer : public muduo::copyable
{ 
public:
 static const size_t kCheapPrepend = 8;
 static const size_t kInitialSize = 1024;
 explicit Buffer(size_t initialSize = kInitialSize)
     : buffer_(kCheapPrepend + initialSize),
     readerIndex_(kCheapPrepend),
     writerIndex_(kCheapPrepend);
 void swap(Buffer& rhs)
 size_t readableBytes() const
 size_t writableBytes() const
 const char* peek() const
 const char* findEOL() const
 const char* findEOL(const char* start) const
 void retrieve(size_t len)
 void retrieveInt64()
 void retrieveInt32()
 void retrieveInt16()
 void retrieveInt8()
 string retrieveAllAsString()
 string retrieveAsString(size_t len)
 void append(const StringPiece& str)
 void append(const char* /*restrict*/ data, size_t len)
 void append(const void* /*restrict*/ data, size_t len)
 char* beginWrite()
 const char* beginWrite() const
 void hasWritten(size_t len)
 void appendInt64(int64_t x)
 void appendInt32(int32_t x)
 void appendInt16(int16_t x)
 void appendInt8(int8_t x)
 int64_t readInt64()
 int32_t readInt32()
 int16_t readInt16()
 int8_t readInt8()
 int64_t peekInt64() const
 int32_t peekInt32() const
 int16_t peekInt16() const
 int8_t peekInt8() const
 void prependInt64(int64_t x)
 void prependInt32(int32_t x)
 void prependInt16(int16_t x)
 void prependInt8(int8_t x)
 void prepend(const void* /*restrict*/ data, size_t len)
private:
 std::vector<char> buffer_;
 size_t readerIndex_;
 size_t writerIndex_;
 static const char kCRLF[];
};

内存布局与指针操作

Buffer内部使用一个 std::vector<char>作为底层存储,并通过 readerIndex_writerIndex_两个指针将内存划分为三个逻辑区域:

plain 复制代码
+-------------------+------------------+------------------+
| prependable bytes |  readable bytes  |  writable bytes  |
|                   |     (CONTENT)    |                  |
+-------------------+------------------+------------------+
0           <=      readerIndex  <=   writerIndex   <=   size()
  • 可读数据区(readable bytes) :位于 readerIndex_writerIndex_之间,存放着已接收但尚未被上层应用处理的数据。peek()函数返回指向这片区域起始位置的指针。
  • 可写空间(writable bytes) :位于 writerIndex_之后,是缓冲区中尚未使用的部分。beginWrite()函数返回指向这片区域起始位置的指针。
  • 预留空间(prependable bytes) :位于 readerIndex_之前,包括一个固定的 8字节头部( kCheapPrepend 以及之前已被 retrieve的已读数据空间。这片区域的一个妙用是可以在不移动大量数据的情况下,轻松地在数据包前添加协议头(如长度字段)。

这种设计的精髓在于,数据的"读取"并非物理上的删除,而是通过 移动 readerIndex_ 来逻辑上标记数据已被消费。当所有可读数据都被取走(retrieveAll)后,两个指针会被重置到 kCheapPrepend的位置,从而高效地回收整个缓冲区。

数据读写接口

Buffer提供了一系列直观的接口进行数据操作:

  • 数据读取(Retrieve)retrieve系列函数用于从可读区取出数据。例如,retrieveAsString(len)会返回一个包含指定长度数据的字符串,并同时移动读指针。peekInt32()等函数则允许你"窥视"数据(如读取网络字节序的整型)而不移动指针,常用于解析协议。
  • 数据追加(Append)append系列函数用于向可写区添加数据。在写入前,会调用 ensureWritableBytes(len)来确保有足够空间,如果不够则会触发内部的空间管理逻辑。
  • 头部预置(Prepend)prepend系列函数利用预留空间,允许在可读数据的前面插入数据(如添加消息长度头),而无需移动现有数据,非常高效。

智能空间管理

当调用 append而当前可写空间不足时,Buffer不会立即申请更多内存,而是先尝试 内部腾挪

  1. 检查总空闲空间 :计算 writableBytes() + prependableBytes()(不包括固定的8字节头部)。
  2. 内部腾挪 :如果总空闲空间足够,它会将当前可读数据([readerIndex_, writerIndex_])整体移动到缓冲区的起始位置(kCheapPrepend处),从而在尾部腾出大块的连续可写空间。这避免了不必要的内存分配。
  3. 外部扩容 :只有当总空闲空间也不够时,才会调用 buffer_.resize()来扩大底层 vector的容量。

这种策略极大地提高了内存利用率和性能。

高效的套接字读取: readFd

Buffer::readFd(int fd, int* savedErrno)Buffer性能的关键之一,它巧妙地解决了"一次性读尽数据"与"避免预分配过大缓冲区"的矛盾。

其工作流程如下:

  1. 准备两块缓冲区 :使用 struct iovec vec[2]
    • vec[0]指向 Buffer对象内部当前的可写空间。
    • vec[1]指向一个在栈上分配的、大小为64KB的临时缓冲区 extrabuf
  2. 一次系统调用 :通过 readv(fd, vec, 2)系统调用,内核会尝试将数据依次填充到这两块缓冲区中。
  3. 智能处理结果
    • 如果读取的数据量(n)小于 Buffer内部的可写空间,则数据全部在 Buffer内,只需移动 writerIndex_
    • 如果 n大于内部可写空间,说明栈上缓冲区也被使用了。此时先将 Buffer内部空间写满,然后将栈上缓冲区 extrabuf中剩余的数据通过 append追加到 Buffer中(此过程可能会触发扩容)。

这种方法既利用了一次性读取大量数据的优势,又通过栈上临时缓冲区避免了对每个连接都预分配超大内存,是空间和时间的绝佳平衡。

线程安全与在 Muduo 中的角色

需要明确的是,Buffer类本身不是线程安全的 。它的安全使用依赖于 Muduo 的 one-loop-per-thread 模型。在 TcpConnection中,每个连接都有两个 Buffer

  • input buffer:用于接收数据。数据的读取和应用的消费都在同一个 I/O 线程中完成。
  • output buffer :用于发送数据。应用程序通过线程安全的 TcpConnection::send()方法向其中写入数据,该方法会确保实际的操作最终在连接所属的 I/O 线程中执行。

因此,虽然 Buffer非线程安全,但在 Muduo 的框架下,每个 Buffer都只被一个特定线程访问,从而保证了安全性。


3. Muduo 库快速上手

我们使用 Muduo 网络库来实现一个简单英译汉服务器和客户端快速上手 Muduo 库。

3.1 英译汉 TCP 服务器

cpp 复制代码
#include "include/muduo/net/TcpServer.h"
#include "include/muduo/net/EventLoop.h"
#include "include/muduo/net/TcpConnection.h"
#include <iostream>
#include <unordered_map>
#include <functional>

class TranslateServer
{
public:
    TranslateServer(int port) : _server(&_baseloop, muduo::net::InetAddress("0.0.0.0", port), "TranslateServer", muduo::net::TcpServer::kReusePort)
    {
        // 将我们的类成员函数,设置为服务器的回调处理函数
        // std::bind 是一个函数适配器函数,对指定的函数进行参数绑定
        _server.setConnectionCallback(std::bind(&TranslateServer::onConnection, this, std::placeholders::_1));
        // 也可以使用 Lambda 表达式(现代方式)
        // _server.setConnectionCallback([this](const muduo::net::TcpConnectionPtr& conn) {
        //     this->onConnection(conn);
        // });
        _server.setMessageCallback(std::bind(&TranslateServer::onMessage, this, std::placeholders::_1,
                                             std::placeholders::_2, std::placeholders::_3));
    }

    // 启动服务器
    void start()
    {
        _server.start();  // 开始事件监听
        _baseloop.loop(); // 开始事件监控,这是一个死循环阻塞接口
    }

private:
    // onConnection,应该是在一个连接,建立成功,以及关闭的时候被调用
    void onConnection(const muduo::net::TcpConnectionPtr &conn)
    {
        // 新连接建立成功时的回调函数
        if (conn->connected() == true)
        {
            std::cout << "新连接建立成功!\n";
        }
        else
        {
            std::cout << "新连接关闭!\n";
        }
    }

    std::string translate(const std::string &str)
    {
        static std::unordered_map<std::string, std::string> dict_map = {
            {"hello", "你好"},
            {"Hello", "你好"},
            {"你好", "Hello"},
            {"good", "好的"},
            {"bad", "坏的"},
            {"pen", "钢笔"}};

        auto it = dict_map.find(str);
        if (it == dict_map.end())
        {
            return "None";
        }
        return it->second;
    }

    void onMessage(const muduo::net::TcpConnectionPtr &conn, muduo::net::Buffer *buf, muduo::Timestamp)
    {
        // 通信连接收到请求时的回调函数
        // 1. 从buf中把请求的数据取出来
        std::string str = buf->retrieveAllAsString();
        // 2. 调用translate接口进行翻译
        std::string resp = translate(str);
        // 3. 对客户端进行响应结果
        conn->send(resp);
    }

private:
    // _baseloop是epoll的事件监控,会进行描述符的事件监控,触发事件后进行io操作
    muduo::net::EventLoop _baseloop;
    // 这个server对象,主要用于设置回调函数,用于告诉服务器收到什么请求该如何处理
    muduo::net::TcpServer _server;
};

int main()
{
    TranslateServer server(8085);
    server.start();
    return 0;
}

这是一个基于 muduo 网络库实现的 TCP 翻译服务器。下面我用一个表格来汇总它的核心组件和功能,帮你快速把握整体结构:

组件/概念 在代码中的体现 功能简述
服务器类 TranslateServer 封装整个翻译服务器的功能。
事件循环 (EventLoop) muduo::net::EventLoop _baseloop 服务器的大脑,负责监听和处理所有I/O事件(如新连接、收到数据)。_baseloop.loop()是启动事件循环的阻塞调用。
TCP服务器 (TcpServer) muduo::net::TcpServer _server 核心网络服务对象,负责接受和管理TCP连接。在构造函数中初始化,绑定了IP和端口。
连接回调 (ConnectionCallback) onConnection成员函数 当有新的客户端连接建立或现有连接关闭时,此函数会被自动调用,用于处理连接生命周期的状态变化。
消息回调 (MessageCallback) onMessage成员函数 当服务器收到客户端发送的数据时,此函数会被自动调用,是业务逻辑处理的核心
翻译功能 translate成员函数 一个简单的字典查询功能,使用 std::unordered_map在内存中存储英文单词和中文的对应关系。

这个服务器的工作流程如下:

  1. 初始化阶段 ( TranslateServer构造函数)
    • 创建 TcpServer对象,指定监听所有网络接口(0.0.0.0)的 8085端口。
    • 使用 std::bind 将类的成员函数 onConnectiononMessage绑定为服务器的回调函数。这是因为成员函数隐含了 this指针,需要通过绑定将其适配成 muduo 库需要的函数签名(普通函数或静态函数)。这里也提到了使用 Lambda 表达式是现代 C++ 中更受推崇的替代方式。
  2. 启动阶段 ( start方法)
    • 调用 _server.start()启动服务器,开始监听端口。
    • 调用 _baseloop.loop()进入事件循环 。这是一个阻塞调用,程序会停留在此处,持续监听并处理网络事件,直到服务器被关闭。
  3. 运行阶段 (事件循环处理)
    • 新连接建立 :当有客户端连接到 8085端口时,触发 onConnection回调,打印连接成功信息。
    • 接收并处理消息 :当客户端发送一个字符串(如 "hello")后:
      • muduo 库将数据读入内部的 Buffer缓冲区。
      • 触发 onMessage回调函数。
      • onMessage调用 buf->retrieveAllAsString()从缓冲区中取出所有数据作为字符串。
      • 调用 translate(str)函数查询字典,得到翻译结果("你好")。
      • 通过 conn->send(resp)将结果发回给客户端。
    • 连接关闭 :当客户端断开连接时,再次触发 onConnection回调,打印连接关闭信息。

注意:muduo 库的调用是异步 的。TcpServerstart()方法会立即返回,真正的网络监听和事件处理是在调用 EventLooploop()方法后开始的。


3.2 英译汉客户端

cpp 复制代码
#include "include/muduo/net/TcpClient.h"
#include "include/muduo/net/EventLoopThread.h"
#include "include/muduo/net/TcpConnection.h"
#include "include/muduo/base/CountDownLatch.h"

#include <iostream>
#include <functional>

class TranslateClient
{
public:
    TranslateClient(const std::string &server_ip, int server_port)
        : _latch(1), _client(_loopthread.startLoop(), muduo::net::InetAddress(server_ip, server_port), "TranslateCilent")
    {
        _client.setConnectionCallback(std::bind(&TranslateClient::onConnection, this, std::placeholders::_1));
        _client.setMessageCallback(std::bind(&TranslateClient::onMessage, this, std::placeholders::_1,
                                             std::placeholders::_2, std::placeholders::_3));
    }

    // 连接服务器---需要阻塞等待连接建立成功之后再返回
    void connect()
    {
        _client.connect();
        _latch.wait();//阻塞等待,直到连接建立成功
    }

    bool send(const std::string &msg)
    {
        //连接状态正常,再发送,否则就返回false
        if (_conn->connected()) 
        {
            _conn->send(msg);
            return true;
        }
        return false;
    }
private:
        //连接建立成功时候的回调函数,连接建立成功后,唤醒上边的阻塞
        void onConnection(const muduo::net::TcpConnectionPtr&conn){
            if (conn->connected()) {
                _latch.countDown();//唤醒主线程中的阻塞
                _conn = conn;
            }else {
                //连接关闭时的操作
                _conn.reset();
            }
        }
        //收到消息时候的回调函数
        void onMessage(const muduo::net::TcpConnectionPtr& conn, muduo::net::Buffer* buf, muduo::Timestamp) {
            std::cout << "翻译结果:" << buf->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 buf;
        std::cin >> buf;
        if(!client.send(buf))
            break;
    }
    return 0;
}
  1. 初始化阶段 ( TranslateClient构造函数)
    • 创建 TcpClient对象,其关键之处在于第一个参数:_loopthread.startLoop()。这行代码启动了一个新的线程,该线程内部运行着一个 EventLoop事件循环。这意味着所有网络通信(连接、收发数据)都将在这个后台I/O线程中进行,不会阻塞主线程
    • 使用 std::bind设置回调函数,这与服务器端的做法一致。
  2. 连接阶段 ( connect方法) - 同步连接的关键
    • 调用 _client.connect()。注意,这是一个非阻塞调用,函数会立即返回,此时物理连接可能尚未建立。
    • 紧接着调用 _latch.wait()。这里使用 CountDownLatch(初始值为1)的目的是强制将异步操作同步化 。主线程会阻塞在 _latch.wait()这里,等待连接最终结果。
    • 连接建立成功 :当底层连接成功建立后,I/O线程会调用 onConnection回调函数。在该函数内:
      • 判断 conn->connected()true
      • 调用 _latch.countDown(),将计数器减到零。
      • 这个操作会立即唤醒 正阻塞在 _latch.wait()的主线程。
      • 同时,将成功的连接对象 conn保存到 _conn成员变量中,供后续发送数据使用。
    • 主线程被唤醒后,connect()方法返回,程序继续执行。这保证了在执行后续的 client.send()时,连接一定是可用的。
  3. 数据交互阶段 ( send onMessage)
    • 发送请求send方法首先检查 _conn->connected()状态,如果正常则调用 _conn->send(msg)发送数据。数据发送过程也是异步的,由I/O线程在后台完成。
    • 接收响应 :当客户端收到服务器发回的翻译结果时,I/O线程会触发 onMessage回调。该函数通过 buf->retrieveAllAsString()从缓冲区中取出数据并打印到控制台。

关键设计解析

  1. 为什么必须使用 CountDownLatch

这是 muduo 异步编程模型的典型要求。TcpClient::connect()非阻塞 的。如果在连接尚未建立成功时就调用 send方法,程序会因操作无效的连接而出错。CountDownLatch是一种简单可靠的同步原语,它确保了业务逻辑在正确的时机(连接已建立)执行。

  1. **EventLoopThread**的作用是什么?

它封装了 "one loop per thread" 模型。通过将 TcpClient绑定到一个专门的I/O线程,所有复杂的、可能阻塞的网络事件处理(如连接、重试、数据收发)都在这个后台线程中完成,而主线程(UI线程)可以保持响应,及时处理用户输入。这是一种资源隔离职责分离的良好设计。

  1. 连接的生命周期管理

onConnection回调中,不仅处理连接成功,也处理连接关闭(else分支)。当连接断开时,将 _conn.reset()是至关重要的,这避免了持有无效的连接指针,使得 send方法中的状态检查 _conn->connected()能够正确工作。

编译运行

我们知道,tcp网络通信是流式数据,但是我们这里一次性把所有数据全部读取上来看作一条完整的数据进行处理,并没有考虑粘包问题等情况(上面的demo测试没有问题是因为我们一次发送一条完整数据时就会处理一条,当我们大量连续发送时就会产生问题)。所以,我们下面再引入前面介绍的Protobuf,来解决流式数据产生的问题


4. 基于 muduo 库函数实现 protobuf 协议的通信

在实现具体服务前, 先介绍一下 muduo 库中内部实现的关于简单的基于 protobuf 的接口类

4.1 基于 protobuf 的接口类介绍

ProtobufCodec 类是 muduo 库中对于 protobuf 协议的处理类,其内部实现了 onMessage 回调接口,对于接收到的数据进行基于 protobuf 协议的请求处理,然后将解析出的信息,存放到对应请求的 protobuf 请求类对象中,然后最终调用设置进去的消息处理回调函数进行对应请求的处理。

cpp 复制代码
/*muduo-master/examples/protobuf/codec.h*/
typedef std::shared_ptr<google::protobuf::Message> MessagePtr;
//
// FIXME: merge with RpcCodec
//
class ProtobufCodec : muduo::noncopyable
{
public:
    enum ErrorCode
    {
        kNoError = 0,
        kInvalidLength,
        kCheckSumError,
        kInvalidNameLen,
        kUnknownMessageType,
        kParseError,
    };
    typedef std::function<void(const muduo::net::TcpConnectionPtr &,
                               const MessagePtr &,
                               muduo::Timestamp)>
        ProtobufMessageCallback;
    // 这里的 messageCb 是针对 protobuf 请求进行处理的函数,它声明在dispatcher.h 中的 ProtobufDispatcher 类 
    explicit ProtobufCodec(const ProtobufMessageCallback &messageCb)
        : messageCallback_(messageCb), // 这就是设置的请求处理回调函数
    errorCallback_(defaultErrorCallback)
    {
    }
    // 它的功能就是接收消息,进行解析,得到了 proto 中定义的请求后调用设置的messageCallback_进行处理 
    void onMessage(const muduo::net::TcpConnectionPtr &conn,
                                            muduo::net::Buffer *buf,
                                            muduo::Timestamp receiveTime);
    // 通过 conn 对象发送响应的接口
    void send(const muduo::net::TcpConnectionPtr &conn,
              const google::protobuf::Message &message)
    {
        // FIXME: serialize to TcpConnection::outputBuffer()
        muduo::net::Buffer buf;
        fillEmptyBuffer(&buf, message);
        conn->send(&buf);
    }
    static const muduo::string &errorCodeToString(ErrorCode errorCode);
    static void fillEmptyBuffer(muduo::net::Buffer *buf, const google::protobuf::Message &message);
    static google::protobuf::Message *createMessage(const std::string &type_name);
    static MessagePtr parse(const char *buf, int len, ErrorCode *errorCode);

private:
    static void defaultErrorCallback(const muduo::net::TcpConnectionPtr &,
                                     muduo::net::Buffer *,
                                     muduo::Timestamp,
                                     ErrorCode);
    ProtobufMessageCallback messageCallback_;
    ErrorCallback errorCallback_;
    const static int kHeaderLen = sizeof(int32_t);
    const static int kMinMessageLen = 2 * kHeaderLen + 2;                   // nameLen +
    typeName + checkSum const static int kMaxMessageLen = 64 * 1024 * 1024; // same as
    codec_stream.h kDefaultTotalBytesLimit
};

ProtobufDispatcher 类,这个类就比较重要了,这是一个 protobuf 请求的分发处理类,我们用户在使用的时候,就是在这个类对象中注册哪个请求应该用哪个业务函数进行处理。

它内部的 onProtobufMessage 接口就是给上边 ProtobufCodec::messageCallback_设置的回调函数,相当于 ProtobufCodec 中 onMessage 接口会设置给服务器作为消息回调函数,其内部对于接收到的数据进行基于 protobuf 协议的解析,得到请求后,通过 ProtobufDispatcher::onProtobufMessage 接口进行请求分发处理,也就是确定当前请求应该用哪一个注册的业务函数进行处理。

cpp 复制代码
typedef std::shared_ptr<google::protobuf::Message> MessagePtr;
class Callback : muduo::noncopyable
{
public:
    virtual ~Callback() = default;
    virtual void onMessage(const muduo::net::TcpConnectionPtr &,
                           const MessagePtr &message,
                           muduo::Timestamp) const = 0;
};
// 这是一个对函数接口进行二次封装生成一个统一类型对象的类
template <typename T>
class CallbackT : public Callback
{
    static_assert(std::is_base_of<google::protobuf::Message,T>::value,
                  "T must be derived from gpb::Message.");

public:
    typedef std::function<void(const muduo::net::TcpConnectionPtr &,
                               const std::shared_ptr<T> &message,
                               muduo::Timestamp)>
        ProtobufMessageTCallback;
    CallbackT(const ProtobufMessageTCallback &callback)
        : callback_(callback)
    {
    }
    void onMessage(const muduo::net::TcpConnectionPtr &conn,
                   const MessagePtr &message,
                   muduo::Timestamp receiveTime) const override
    {
        std::shared_ptr<T> concrete =
            muduo::down_pointer_cast<T>(message);
        assert(concrete != NULL);
        callback_(conn, concrete, receiveTime);
    }

private:
    ProtobufMessageTCallback callback_;
};
// 这是一个 protobuf 请求分发器类,需要用户注册不同请求的不同处理函数,
// 注册完毕后,服务器收到指定请求就会使用对应接口进行处理
class ProtobufDispatcher
{
public:
    typedef std::function<void(const muduo::net::TcpConnectionPtr &,
                               const MessagePtr &message,
                               muduo::Timestamp)>
        ProtobufMessageCallback;
    // 构造对象时需要传入一个默认的业务处理函数,以便于找不到对应请求的处理函数时调用。
    explicit ProtobufDispatcher(const ProtobufMessageCallback &
                                    defaultCb)
        : defaultCallback_(defaultCb)
    {
    }
    // 这个是人家实现的针对 proto 中定义的类型请求进行处理的函数,内部会调用我们自己传入的业务处理函数
    void onProtobufMessage(const muduo::net::TcpConnectionPtr &conn,
                           const MessagePtr &message,
                           muduo::Timestamp receiveTime) const
    {
        CallbackMap::const_iterator it = callbacks_.find(message -
                                                         > GetDescriptor());
        if (it != callbacks_.end())
        {
            it->second->onMessage(conn, message, receiveTime);
        }
        else
        {
            defaultCallback_(conn, message, receiveTime);
        }
    }
    /*
    这个接口非常巧妙,基于 proto 中的请求类型将我们自己的业务处理函数与对
   应的请求给关联起来了
    相当于通过这个成员变量中的 CallbackMap 能够知道收到什么请求后应该用
   什么处理函数进行处理
    简单理解就是注册针对哪种请求--应该用哪个我们自己的函数进行处理的映射
   关系

    但是我们自己实现的函数中,参数类型都是不一样的比如翻译有翻译的请求类
   型,加法有加法请求类型
    而 map 需要统一的类型,这样就不好整了,所以用 CallbackT 对我们传入的
   接口进行了二次封装。
    */
    template <typename T>
    void registerMessageCallback(const typename CallbackT<T>::ProtobufMessageTCallback &callback)
    {
        std::shared_ptr<CallbackT<T>> pd(new CallbackT<T>(callback));
        callbacks_[T::descriptor()] = pd;
    }

private:
    typedef std::map<const google::protobuf::Descriptor *,
                     std::shared_ptr<Callback>>
        CallbackMap;
    CallbackMap callbacks_;
    ProtobufMessageCallback defaultCallback_;
};

而能实现请求与函数之间的映射,还有一个非常重要的元素:那就是应用层协议

protobuf 根据我们的 proto 文件生成的代码中,会生成对应类型的类,比如 TranslateRequest 对应了一个 TranslateRequest 类,而不仅仅如此,protobuf 比我们想象中做的事情更多,每个对应的类中,都包含有一个描述结构的指针:

cpp 复制代码
static const ::google::protobuf::Descriptor* descriptor();

这个描述结构非常重要,其内部可以获取到当前对应类类型名称,以及各项成员的名称,因此通过这些名称,加上协议中的 typename 字段,就可以实现完美的对应关系了。

整个系统的完整工作流程(从接收原始数据到调用用户业务函数)可以归纳为以下步骤:

  1. 接收原始数据TcpConnection从网络接收数据,存入其 inputBuffer_中。
  2. 触发消息回调TcpConnection调用其设置的消息回调函数,即 ProtobufCodec::onMessage
  3. 协议解析 :在 ProtobufCodec::onMessage中:
    • 按照预定义的应用层协议 (长度 + 类型名长度 + 类型名 + 数据 + 校验和)从 Buffer中解析出一个完整的消息包。
    • 通过 parse函数反序列化出具体的 Protobuf请求对象(MessagePtr)。
  4. 消息分发ProtobufCodec将解析出的 MessagePtr传递给其构造函数中设置的 messageCallback_,即 ProtobufDispatcher::onProtobufMessage
  5. 查找处理函数 :在 ProtobufDispatcher::onProtobufMessage中:
    • 调用 message->GetDescriptor()获取此消息的描述符Descriptor*)。
    • 以此描述符为 Key ,在内部的 callbacks_映射表中查找对应的 Callback对象。
  6. 调用用户函数
    • 如果找到,则调用该 Callback对象的 onMessage方法。该方法内部会执行类型安全的向下转换down_pointer_cast<T>),将通用的 MessagePtr转换为用户注册时指定的具体类型(如 std::shared_ptr<TranslateRequest>),然后调用用户注册的具体业务函数(如 translate)。
    • 如果未找到,则调用默认回调函数 defaultCallback_进行处理。

核心设计思想解析

  1. 基于描述符(Descriptor)的类型映射

这是整个框架最巧妙的设计。Protobuf为每个消息类型生成的类都包含一个静态的 descriptor()方法,返回一个 const Descriptor。这个指针是全局唯一的,可以作为完美的类型标识符ProtobufDispatcher利用这一点,建立了 DescriptorCallback对象的映射,实现了类型安全的消息路由

  1. 类型擦除(Type Erasure)与统一接口

用户注册的业务函数签名各不相同,例如:

cpp 复制代码
void translate(const TcpConnectionPtr&, const std::shared_ptr<TranslateRequest>&, Timestamp);
void add(const TcpConnectionPtr&, const std::shared_ptr<AddRequest>&, Timestamp);

ProtobufDispatcher需要将它们统一存储 在一个 std::map中。这是通过 Callback基类和 CallbackT<T>模板类实现的:

  • Callback基类 :定义了统一的 onMessage接口,参数是通用的 MessagePtr
  • CallbackT<T>模板类 :继承自 Callback,内部保存用户具体的处理函数(ProtobufMessageTCallback)。在实现 onMessage时,将 MessagePtr安全地转换为 std::shared_ptr<T>,然后调用用户函数。

这种设计是典型的类型擦除模式,它隐藏了具体类型,提供了统一的接口。

  1. 应用层协议设计

ProtobufCodec定义了明确的消息格式:

plain 复制代码
[int32_t total_len][int16_t name_len][typename][protobuf_data][int32_t checksum]

这个协议解决了网络编程中的关键问题:

  • 消息边界 :通过固定的头部(长度字段)解决了TCP的粘包/半包问题。
  • 类型识别 :通过 typename字段,接收方可以知道应该将数据反序列化成哪种 Protobuf消息。
  • 数据完整性:通过校验和确保数据在传输过程中没有损坏。

4.2 定义具体的业务请求类型

protobuf 复制代码
syntax = "proto3";

package mq;

// 接下来定义 rpc 翻译请求信息结构
message TranslateRequest {
    string msg = 1;
}

// 接下来定义 rpc 翻译响应信息结构
message TranslateResponse {
    string msg = 1;
}

//定义 rpc 加法请求信息结构
message AddRequest {
    int32 num1 = 1;
    int32 num2 = 2;
}

//定义 rpc 加法响应信息结构
message AddResponse {
    uint32 result = 1;
}

编译 .proto 文件:

bash 复制代码
protoc --cpp_out=. request.proto

4.3 服务器

接下来就可以通过 muduo 库中陈硕大佬提供的接口来编写我们的客户端/服务器端通信了,其最为简便之处就在于我们可以把更多的精力放到业务处理函数的实现上,而不是服务器的搭建或者协议的解析处理上了。

cpp 复制代码
#include "muduo/proto/codec.h"
#include "muduo/proto/dispatcher.h"
#include "muduo/base/Logging.h"
#include "muduo/base/Mutex.h"
#include "muduo/net/EventLoop.h"
#include "muduo/net/TcpServer.h"

#include "request.pb.h"
#include <iostream>
#include <unordered_map>

class Server
{
public:
    typedef std::shared_ptr<google::protobuf::Message> MessagePtr;
    typedef std::shared_ptr<mq::TranslateRequest> TranslateRequestPtr;
    typedef std::shared_ptr<mq::AddRequest> AddRequestPtr;

    Server(int port) :_server(&_baseloop, muduo::net::InetAddress("0.0.0.0", port), "Server", muduo::net::TcpServer::kReusePort)
        ,_dispatcher(std::bind(&Server::onUnknownMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3))
        ,_codec(std::bind(&ProtobufDispatcher::onProtobufMessage, &_dispatcher, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3))
    {
        // 注册业务请求处理函数
        _dispatcher.registerMessageCallback<mq::TranslateRequest>(std::bind(&Server::onTranslate, this,
                                                                            std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));

        _dispatcher.registerMessageCallback<mq::AddRequest>(std::bind(&Server::onAdd, this,
                                                                      std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));

        _server.setMessageCallback(std::bind(&ProtobufCodec::onMessage, &_codec,
                                             std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
        _server.setConnectionCallback(std::bind(&Server::onConnection, this, std::placeholders::_1));
    }

    void start()
    {
        _server.start();
        _baseloop.loop();
    }

private:
    std::string translate(const std::string &str)
    {
        static std::unordered_map<std::string, std::string> dict_map = {
            {"hello", "你好"},
            {"Hello", "你好"},
            {"你好", "Hello"},
            {"good", "好的"},
            {"bad", "坏的"},
            {"pen", "钢笔"}};

        auto it = dict_map.find(str);
        if (it == dict_map.end())
        {
            return "None";
        }
        return it->second;
    }

    void onTranslate(const muduo::net::TcpConnectionPtr &conn, const TranslateRequestPtr &message, muduo::Timestamp)
    {
        // 1. 提取message中的有效消息,也就是需要翻译的内容
        std::string req_msg = message->msg();
        // 2. 进行翻译,得到结果
        std::string rsp_msg = translate(req_msg);
        // 3. 组织protobuf的响应
        mq::TranslateResponse resp;
        resp.set_msg(rsp_msg);
        // 4. 发送响应
        _codec.send(conn, resp);
    }
    void onAdd(const muduo::net::TcpConnectionPtr &conn, const AddRequestPtr &message, muduo::Timestamp)
    {
        int num1 = message->num1();
        int num2 = message->num2();
        int result = num1 + num2;
        mq::AddResponse resp;
        resp.set_result(result);
        _codec.send(conn, resp);
    }

    void onUnknownMessage(const muduo::net::TcpConnectionPtr& conn, const MessagePtr& message, muduo::Timestamp) 
    {
        LOG_INFO << "onUnknownMessage: " << message->GetTypeName();
        conn->shutdown();
    }
    void onConnection(const muduo::net::TcpConnectionPtr &conn) 
    {
        if (conn->connected())
        {
            LOG_INFO << "新连接建立成功!";
        }
        else
        {
            LOG_INFO << "连接即将关闭!";
        }
    }
private:
    muduo::net::EventLoop _baseloop;
    muduo::net::TcpServer _server;  // 服务器对象
    ProtobufDispatcher _dispatcher; // 请求分发器对象--要向其中注册请求处理函数
    ProtobufCodec _codec;           // protobuf协议处理器--针对收到的请求数据进行protobuf协议处理
};

int main()
{
    Server server(8085);
    server.start();
    return 0;
}

详细工作流程解析

  1. 初始化阶段 ( Server构造函数)
    • 组件初始化 :创建 TcpServerProtobufDispatcherProtobufCodec实例。关键在于 _codec的构造函数:它将 ProtobufDispatcher::onProtobufMessage方法设置为自己的消息回调。这意味着 _codec一旦完成消息反序列化,就会把消息对象交给 _dispatcher进行分发。
    • 业务注册 :通过 _dispatcher.registerMessageCallback方法,将不同的 Protobuf 消息类型(如 mq::TranslateRequest)与对应的业务处理函数(如 Server::onTranslate)绑定起来。这里利用了 Protobuf 的 Descriptor 作为唯一类型标识符来建立映射关系。
    • 回调设置 :将 ProtobufCodec::onMessage设置为 TcpServer的消息回调。这样,每当有网络数据到达,TcpServer就会将原始数据缓冲区(Buffer)交给 _codec进行处理。
  2. 启动阶段 ( start方法)
    • 调用 _server.start()启动服务器,开始监听端口。
    • 调用 _baseloop.loop()进入事件循环 。这是一个阻塞调用,程序会在此处持续等待并处理网络事件,直到服务器关闭。
  3. 运行阶段 (请求处理)
    • 连接管理 :当有新客户端连接或连接断开时,触发 onConnection回调,进行日志记录等操作。
    • 消息处理核心流程 (以翻译请求为例):
      • 客户端发送一个序列化后的 mq::TranslateRequest消息。
      • muduo 库接收数据并存入 Buffer,然后调用 ProtobufCodec::onMessage
      • ProtobufCodec按照预定义格式(如包含长度字段和校验和的协议)从 Buffer中解析出完整数据包,并反序列化成 TranslateRequest对象(MessagePtr)。
      • ProtobufCodec接着调用其消息回调,即 ProtobufDispatcher::onProtobufMessage,并传入消息对象。
      • ProtobufDispatcher调用 message->GetDescriptor()获取消息的描述符,并在内部映射表中查找对应的 Callback对象。找到后,调用其 onMessage方法。
      • 由于之前注册了 Server::onTranslateCallback对象内部会安全地将通用的 MessagePtr向下转型为 TranslateRequestPtr,然后调用 onTranslate函数。
      • onTranslate函数执行实际业务逻辑:提取请求内容、调用 translate函数进行翻译、构造 TranslateResponse响应对象。
      • 最后通过 _codec.send(conn, resp)将响应对象序列化并通过对应的 TcpConnection发送回客户端

核心设计思想解析

  1. 职责分离与多层回调 :这个架构是分层设计依赖倒置 的典范。TcpServer只负责网络I/O,ProtobufCodec只负责协议解析,ProtobufDispatcher只负责消息路由,而 Server类则专注于业务逻辑。它们通过回调函数(std::bindlambda表达式)进行松耦合的协作,每一层都不需要知道上一层的具体实现。
  2. 类型安全的动态分发ProtobufDispatcher的实现非常巧妙。它通过模板函数 registerMessageCallback和内部的 CallbackT<T>类,实现了类型擦除(Type Erasure) 。这使得你能够注册处理不同类型消息的函数(如 onTranslateonAdd),而 ProtobufDispatcher内部可以用一个统一的 std::map<const Descriptor*, std::shared_ptr<Callback>>来管理它们,并在运行时进行类型安全的动态调用。
  3. 健壮性处理 :代码包含了 onUnknownMessage回调来处理无法识别的消息类型,体现了良好的健壮性设计。一旦收到未知类型的消息,服务器会记录日志并主动断开连接,防止错误累积。

4.4 客户端

cpp 复制代码
#include "muduo/proto/dispatcher.h"
#include "muduo/proto/codec.h"
#include "muduo/base/Logging.h"
#include "muduo/base/Mutex.h"
#include "muduo/net/EventLoop.h"
#include "muduo/net/TcpClient.h"
#include "muduo/net/EventLoopThread.h"
#include "muduo/base/CountDownLatch.h"

#include "request.pb.h"
#include <iostream>

class Client
{
public:
    typedef std::shared_ptr<google::protobuf::Message> MessagePtr;
    typedef std::shared_ptr<mq::TranslateResponse> TranslateResponsePtr;
    typedef std::shared_ptr<mq::AddResponse> AddResponsePtr;
    Client(const std::string server_ip, int server_port)
        : _latch(1)
        , _client(_loopthread.startLoop(), muduo::net::InetAddress(server_ip, server_port), "Client")
        , _dispatcher(std::bind(&Client::onUnknownMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3))
        , _codec(std::bind(&ProtobufDispatcher::onProtobufMessage, &_dispatcher, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3))
    {
        _dispatcher.registerMessageCallback<mq::TranslateResponse>(std::bind(&Client::OnTranslate, this,
                                                                             std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
        _dispatcher.registerMessageCallback<mq::AddResponse>(std::bind(&Client::OnAdd, this,
                                                                             std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
        _client.setMessageCallback(std::bind(&ProtobufCodec::onMessage, &_codec, 
                                                                            std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
        _client.setConnectionCallback(std::bind(&Client::onConnection, this, std::placeholders::_1));
    }

    // 连接服务器---需要阻塞等待连接建立成功之后再返回
    void connect()
    {
        _client.connect();
        _latch.wait(); // 阻塞等待,直到连接建立成功
    }

    // 客户端构建翻译请求
    void Translate(const std::string& msg)
    {
        mq::TranslateRequest req;
        req.set_msg(msg);
        send(&req);
    }

    // 客户端构建加法请求
    void Add(int num1, int num2)
    {
        mq::AddRequest req;
        req.set_num1(num1);
        req.set_num2(num2);
        send(&req);
    }

private:
    bool send(const google::protobuf::Message *message) 
    {
        //连接状态正常,再发送,否则就返回false
        if (_conn->connected()) 
        {
            _codec.send(_conn, *message);
            return true;
        }
        return false;    
    }  
    void OnTranslate(const muduo::net::TcpConnectionPtr &conn, const TranslateResponsePtr& message, muduo::Timestamp)
    {
        std::cout << "翻译结果:" << message->msg() << std::endl;
    }
    void OnAdd(const muduo::net::TcpConnectionPtr &conn, const AddResponsePtr& message, muduo::Timestamp)
    {
        std::cout << "加法结果:" << message->result() << std::endl;
    }
    void onUnknownMessage(const muduo::net::TcpConnectionPtr &conn, const MessagePtr &message, muduo::Timestamp)
    {
        LOG_INFO << "onUnknownMessage: " << message->GetTypeName();
        conn->shutdown();
    }
    // 连接建立成功时候的回调函数,连接建立成功后,唤醒上边的阻塞
    void onConnection(const muduo::net::TcpConnectionPtr &conn)
    {
        if (conn->connected())
        {
            _latch.countDown(); // 唤醒主线程中的阻塞
            _conn = conn;
        }
        else
        {
            // 连接关闭时的操作
            _conn.reset();
        }
    }

private:
    muduo::CountDownLatch _latch;            // 实现同步的
    muduo::net::EventLoopThread _loopthread; // 异步循环处理线程
    muduo::net::TcpClient _client;           // 客户端
    muduo::net::TcpConnectionPtr _conn;      // 客户端对应的连接
    ProtobufDispatcher _dispatcher;          // 请求分发器对象--要向其中注册请求处理函数
    ProtobufCodec _codec;                    // protobuf协议处理器--针对收到的请求数据进行protobuf协议处理
};


int main() 
{
    Client client("127.0.0.1", 8085);
    client.connect();

    client.Translate("hello");
    client.Add(11, 22);

    sleep(1);
    return 0;
}

初始化流程

Client的构造函数中,各个核心组件被初始化并串联起来,形成一个高效的处理管道 :

  1. muduo::net::EventLoopThread _loopthread:这是关键所在。它内部启动了一个独立的I/O线程 ,其事件循环(EventLoop)用于处理所有网络I/O事件(连接、数据收发)。这确保了主线程不会被阻塞,可以及时响应用户输入或其他任务 。
  2. muduo::net::TcpClient _client:TCP客户端核心,负责异步地发起和管理与服务器的连接。
  3. ProtobufCodec _codec:Protobuf协议编解码器。它的核心职责是:
    • 发送时 :将 Protobuf 消息对象(如 TranslateRequest)按照自定义的应用层协议格式(通常包含长度、类型名、数据体、校验和等)进行序列化,然后通过连接发送 。
    • 接收时 :从网络数据流中反序列化 出完整的 Protobuf 消息对象(如 TranslateResponse),解决了TCP的粘包/半包问题
  4. ProtobufDispatcher _dispatcher:消息分发器。它维护着一个映射表,键是 Protobuf 消息的描述符(Descriptor),值是相应的回调函数。当 _codec解析出一个消息后,就交给 _dispatcher,后者会根据消息类型自动调用你注册的对应处理函数(如 OnTranslateOnAdd)。
  5. muduo::CountDownLatch _latch:同步工具,初始值为1。用于实现异步连接的同步等待,这是客户端代码中的一个重要技巧 。

连接建立与同步控制

connect()方法是连接阶段的核心,其设计巧妙解决了异步连接的同步问题 :

  1. 异步连接 :调用 _client.connect()。此方法会立即返回,实际的连接过程在 _loopthread的I/O线程中异步进行。
  2. 同步等待 :紧接着调用 _latch.wait()。这会阻塞主线程,直到计数器变为零。
  3. 连接成功回调 :当底层连接成功建立后,I/O线程会触发 onConnection回调函数。在该函数内,如果判断连接成功 (conn->connected()),则调用 _latch.countDown()将计数器减至零。
  4. 唤醒主线程 :计数器变为零后,阻塞在 _latch.wait()的主线程被唤醒connect()方法执行完毕。这保证了在 connect()返回后,连接一定是建立成功的,后续的 send操作是安全的。

数据请求与响应处理

  1. 发送请求(如 Translate("hello")
    • Translate方法中,构造了 mq::TranslateRequest对象并设置数据。
    • 调用 send方法,该方法内部通过 _codec.send(_conn, *message)将请求对象序列化并发送出去。所有的序列化和网络发送工作都由 ProtobufCodec在I/O线程中完成 。
  2. 接收并处理响应
    • 当服务器返回数据时,I/O线程会触发设置好的消息回调链:TcpClient-> ProtobufCodec::onMessage-> ProtobufDispatcher::onProtobufMessage
    • ProtobufCodec::onMessage负责将原始字节流反序列化成通用的 MessagePtr
    • ProtobufDispatcher::onProtobufMessage是关键一步。它调用 message->GetDescriptor()获取消息的唯一类型标识符 ,然后在内部映射表中查找对应的 Callback对象,最终调用你注册的 OnTranslateOnAdd函数,并将反序列化好的具体消息类型(如 TranslateResponsePtr)传给你 。
    • OnTranslate等函数中,你可以直接通过 message->msg()访问结果,并执行业务逻辑(如打印)。

编译运行

makefile 复制代码
all: client server
client:protobuf_client.cpp request.pb.cc ../include/muduo/proto/codec.cc
	g++ -o $@ $^ -std=c++11 -I../include -L../lib  -lmuduo_net -lmuduo_base -pthread /usr/local/lib/libprotobuf.so.32 -lz
server:protobuf_server.cpp request.pb.cc ../include/muduo/proto/codec.cc
	g++ -o $@ $^ -std=c++11 -I../include -L../lib  -lmuduo_net -lmuduo_base -pthread /usr/local/lib/libprotobuf.so.32 -lz
.PHONY:clean
clean:
	rm -f client server

注意:当系统中存在多个版本的 protobuf 时,简单的 -lprotobuf很可能链接到错误的版本,导致编译失败。其根本原因在于链接器在查找库文件时,遵循特定的路径顺序,它找到的第一个名为 libprotobuf.so的文件(通常是系统默认路径下的旧版本)就会被使用,从而引发版本冲突。

相关推荐
一路往蓝-Anbo2 小时前
【第48期】:嵌入式工程师的自我修养与进阶之路
开发语言·网络·stm32·单片机·嵌入式硬件
终端域名2 小时前
网络架构的变革将如何影响物联网设备的设计和开发?
网络·物联网·架构
郝学胜-神的一滴2 小时前
深入理解网络分层模型:数据封包与解包全解析
linux·开发语言·网络·程序人生·算法
门思科技2 小时前
ThinkLink 基于 RPC 的 LoRaWAN 告警通知机制
网络·网络协议·rpc
填满你的记忆2 小时前
【计算机网络·基础篇】TCP 的“三次握手”与“四次挥手”:后端面试的“生死线”
java·网络·网络协议·tcp/ip·计算机网络·面试
小李独爱秋2 小时前
计算机网络经典问题透视:什么是服务质量QoS?为什么说“互联网根本没有服务质量可言?”
网络·计算机网络·安全·qos·服务质量
sun0077002 小时前
android 系统中间件和 平台中间件 的区别,Framework等
网络
丁香结^2 小时前
VLAN详解
网络·智能路由器
llilian_163 小时前
gps对时扩展装置 抢险救灾中时间同步精确的重要性分析 电力系统同步时钟
网络·功能测试·单片机·嵌入式硬件