目录
[3.1.muduo::net::TcpServer 类基础介绍](#3.1.muduo::net::TcpServer 类基础介绍)
[3.2.muduo::net::EventLoop 类基础介绍](#3.2.muduo::net::EventLoop 类基础介绍)
[3.3.muduo::net::TcpConnection 类基础介绍](#3.3.muduo::net::TcpConnection 类基础介绍)
[3.4.muduo::net::TcpClient 类基础介绍](#3.4.muduo::net::TcpClient 类基础介绍)
[3.5.muduo::net::Buffer 类基础介绍](#3.5.muduo::net::Buffer 类基础介绍)
一.Muduo库
Muduo 是一个由陈硕开发的现代 C++ 网络编程库,其核心设计面向高并发 TCP 网络应用。该库采用非阻塞 I/O 和事件驱动机制,能够有效支持大量并发连接。
Muduo 基于主从 Reactor 模型构建,其线程模型被称为"one loop per thread"。该模型的具体含义如下:
-
每个线程中只能运行一个事件循环(EventLoop),该循环负责监听并响应定时器事件和 I/O 事件;
-
每个文件描述符(如 TCP 连接)只能由一个线程进行读写操作,也就是说,每个连接必须固定归属于某一个 EventLoop 进行管理。
这种设计使得 Muduo 在多线程环境下能够高效地处理网络 I/O,同时避免复杂的线程同步问题。
每个 EventLoop 在其所属线程中独立运行,处理自身的连接和事件,从而提升整体的并发性能。
对于初学者而言,理解"one loop per thread"模型是掌握 Muduo 工作机制的关键之一。
二.前置C++知识补充
2.1.function类型的介绍
首先,我们想要了解Muduo库的运行过程,我们需要先了解一些基础知识
在C++中,std::function是一个通用的函数包装器,它可以存储、复制和调用任何可调用对象(如函数、lambda表达式、绑定表达式、函数对象等)。下面我将详细说明如何使用std::function。
包含头文件
cpp
#include <functional>
定义一个std::function
cpp
std::function<返回类型(参数类型列表)> 对象名;
例如,定义一个返回void,无参数的function:
cpp
std::function<void()> func;
定义一个返回int,有两个int参数的function:
cpp
std::function<int(int, int)> func;
示例
示例1:无参数无返回值的函数
cpp
#include <iostream>
#include <functional>
// 普通函数1:无参数无返回值
void sayHello() {
std::cout << "Hello!" << std::endl;
}
int main() {
// 创建function对象并赋值普通函数
std::function<void()> myFunc = sayHello;
// 调用function对象
myFunc(); // 输出: Hello!
return 0;
}

示例2:带参数和返回值的函数
cpp
#include <iostream>
#include <functional>
// 普通函数2:带两个int参数,返回int
int add(int a, int b) {
return a + b;
}
int main() {
// 创建function对象并赋值add函数
std::function<int(int, int)> mathFunc = add;
// 调用function对象
int result = mathFunc(10, 20);
std::cout << "Result: " << result << std::endl; // 输出: Result: 30
return 0;
}

示例3:基本的typedef用法
cpp
#include <iostream>
#include <functional>
// 定义函数类型别名
typedef std::function<void()> SimpleFunc; // 无参数无返回值
typedef std::function<int(int, int)> MathFunc; // 两个int参数,返回int
typedef std::function<std::string(const std::string&)> StringFunc; // 字符串处理
// 普通函数
void greet() {
std::cout << "Hello!" << std::endl;
}
int add(int a, int b) {
return a + b;
}
std::string toUpper(const std::string& str) {
std::string result = str;
for (char& c : result) {
c = toupper(c);
}
return result;
}
int main() {
// 使用别名创建function对象
SimpleFunc func1 = greet;
MathFunc func2 = add;
StringFunc func3 = toUpper;
func1(); // 输出: Hello!
std::cout << func2(10, 20) << std::endl; // 输出: 30
std::cout << func3("hello") << std::endl; // 输出: HELLO
return 0;
}

示例4:function类型作为函数的参数
cpp
#include <iostream>
#include <functional>
// 定义函数类型别名
typedef std::function<int(int, int)> MathFunc;
typedef std::function<std::string(const std::string&)> StringFunc;
// 普通函数
int add(int a, int b) {
return a + b;
}
std::string toUpper(const std::string& str) {
std::string result = str;
for (char& c : result) {
c = toupper(c);
}
return result;
}
// 函数1:接受 MathFunc 作为参数
void useMathFunc(MathFunc func) {
int result = func(10, 5);
std::cout << "Math结果: " << result << std::endl;
}
// 函数2:接受 StringFunc 作为参数
void useStringFunc(StringFunc func) {
std::string result = func("hello");
std::cout << "String结果: " << result << std::endl;
}
int main() {
// 传递 add 函数给 useMathFunc
useMathFunc(add);
// 传递 toUpper 函数给 useStringFunc
useStringFunc(toUpper);
return 0;
}

2.2.bind类型
std::bind 是一个函数模板,它可以将函数和参数绑定在一起,创建一个新的可调用对象。
简单说:固定函数的一部分参数,方便后续调用。
2.2.1.基本用法
示例:绑定普通函数
cpp
#include <iostream>
#include <functional>
// 普通函数:两个参数
void printSum(int a, int b) {
std::cout << a << " + " << b << " = " << (a + b) << std::endl;
}
int main() {
// 绑定 printSum 函数,固定第一个参数为 10
auto boundFunc = std::bind(printSum, 10, std::placeholders::_1);
// 调用时只需要提供第二个参数
boundFunc(20); // 输出: 10 + 20 = 30
boundFunc(5); // 输出: 10 + 5 = 15
return 0;
}

占位符
占位符 _1, _2, _3... 在 std::bind 中表示:
- _1:绑定函数调用时的传递进去的第一个实参
- _2:绑定函数调用时的传递进去的第二个实参
- _3:绑定函数调用时的传递进去的第三个实参
- 以此类推
示例:理解占位符的含义
cpp
#include <iostream>
#include <functional>
void print(int a, int b, int c) {
std::cout << "a=" << a << ", b=" << b << ", c=" << c << std::endl;
}
int main() {
// _1 对应调用时的第一个参数,_2 对应第二个,_3 对应第三个
auto func = std::bind(print,
std::placeholders::_1, // 调用时的第一个参数
std::placeholders::_2, // 调用时的第二个参数
std::placeholders::_3); // 调用时的第三个参数
// 调用 func,传入 10, 20, 30
// 10 → _1 → print的第一个参数
// 20 → _2 → print的第二个参数
// 30 → _3 → print的第三个参数
func(10, 20, 30); // 输出: a=10, b=20, c=30
return 0;
}

占位符可以改变参数顺序
示例:改变参数顺序
cpp
#include <iostream>
#include <functional>
void print(int a, int b, int c) {
std::cout << "a=" << a << ", b=" << b << ", c=" << c << std::endl;
}
int main() {
// 把参数顺序反过来
auto func = std::bind(print,
std::placeholders::_3, // 第三个参数给a
std::placeholders::_2, // 第二个参数给b
std::placeholders::_1); // 第一个参数给c
// 调用 func(1, 2, 3)
// _1=1 → c=1
// _2=2 → b=2
// _3=3 → a=3
func(1, 2, 3); // 输出: a=3, b=2, c=1
return 0;
}

占位符可以和固定值混合使用
示例:混合使用
cpp
#include <iostream>
#include <functional>
void print(int a, int b, int c) {
std::cout << "a=" << a << ", b=" << b << ", c=" << c << std::endl;
}
int main() {
// a固定为100,b和c由占位符决定
auto func = std::bind(print,
100, // 固定值:a=100
std::placeholders::_1, // b = 调用时的第一个参数
std::placeholders::_2); // c = 调用时的第二个参数
func(20, 30); // 输出: a=100, b=20, c=30
func(5, 6); // 输出: a=100, b=5, c=6
return 0;
}

占位符的个数决定调用时需要多少参数
示例:占位符个数决定参数个数
cpp
#include <iostream>
#include <functional>
void print(int a, int b, int c) {
std::cout << "a=" << a << ", b=" << b << ", c=" << c << std::endl;
}
int main() {
// 使用2个占位符
auto func2 = std::bind(print,
std::placeholders::_1, // 需要第一个参数
std::placeholders::_2, // 需要第二个参数
300); // c固定为300
func2(10, 20); // 只需要2个参数,输出: a=10, b=20, c=300
// 使用1个占位符
auto func1 = std::bind(print,
100, // a固定为100
200, // b固定为200
std::placeholders::_1); // 只需要一个参数给c
func1(30); // 只需要1个参数,输出: a=100, b=200, c=30
return 0;
}

示例:占位符的真正含义
cpp
#include <iostream>
#include <functional>
void print(int a, int b, int c) {
std::cout << "a=" << a << ", b=" << b << ", c=" << c << std::endl;
}
int main() {
// 使用2个占位符
auto func2 = std::bind(print,
std::placeholders::_1, // 调用时的第一个参数
300,
std::placeholders::_2, // 调用时的第二个参数
); // c固定为300
func2(10, 20); // 只需要2个参数,输出: a=10, b=300, c=20
// 使用1个占位符
auto func1 = std::bind(print,
100, // a固定为100
200, // b固定为200
std::placeholders::_1); // 只需要一个参数给c
func1(30); // 只需要1个参数,输出: a=100, b=200, c=30
return 0;
}

2.2.2.绑定成员函数
关键点:当使用 std::bind绑定的函数是成员函数时,需要this指针来指定调用哪个对象的成员函数。
必须指定对象 :绑定成员函数时,第一个参数是函数指针,第二个参数必须是对象(指针、引用或值)
绑定成员函数时,
- 第一个参数是成员函数指针
- 第二个参数必须是具体的对象(通过指针、引用或值)
- 后续的参数才是函数的参数等
这样才能确定调用哪个对象的成员函数。
例1:最基本的成员函数绑定
cpp
#include <iostream>
#include <functional>
class Printer {
public:
void printMessage(const std::string& msg) {
std::cout << "Message: " << msg << std::endl;
}
};
int main() {
Printer printer;
// 绑定成员函数,需要传递对象指针或引用
//第一个参数是成员函数指针
//第二个参数必须是具体的对象(通过指针、引用或值)
auto func = std::bind(&Printer::printMessage, &printer, std::placeholders::_1);
func("Hello, World!");
// 输出: Message: Hello, World!
return 0;
}

例2:不同对象调用相同成员函数
cpp
#include <iostream>
#include <functional>
class Counter {
private:
int count = 0;
public:
void increment() {
count++;
std::cout << "Count: " << count << std::endl;
}
};
int main() {
Counter counter1;
Counter counter2;
// 绑定到counter1的increment方法
auto inc1 = std::bind(&Counter::increment, &counter1);
inc1(); // 输出: Count: 1
inc1(); // 输出: Count: 2
// 绑定到counter2的increment方法
auto inc2 = std::bind(&Counter::increment, &counter2);
inc2(); // 输出: Count: 1
return 0;
}

例3:带参数和返回值的成员函数
cpp
#include <iostream>
#include <functional>
class Calculator {
public:
double calculate(double x, double y, double z) {
return (x + y) * z;
}
};
int main() {
Calculator calc;
// 绑定带三个参数的成员函数
auto func = std::bind(&Calculator::calculate, &calc,
std::placeholders::_1,
std::placeholders::_2,
std::placeholders::_3);
double result = func(1.5, 2.5, 3.0);
std::cout << "Result: " << result << std::endl; // 输出: 12.0
return 0;
}

例4:固定部分参数的绑定
cpp
#include <iostream>
#include <functional>
class Multiplier {
public:
int multiply(int a, int b, int c) {
return a * b * c;
}
};
int main() {
Multiplier m;
// 固定第一个参数为2,其余参数使用占位符
auto func1 = std::bind(&Multiplier::multiply, &m,
2, // 固定a=2
std::placeholders::_1, // b
std::placeholders::_2); // c
std::cout << func1(3, 4) << std::endl; // 输出: 2*3*4 = 24
// 固定第二和第三个参数
auto func2 = std::bind(&Multiplier::multiply, &m,
std::placeholders::_1, // a
5, // 固定b=5
6); // 固定c=6
std::cout << func2(2) << std::endl; // 输出: 2*5*6 = 60
return 0;
}

三.关键类讲解
3.1.muduo::net::TcpServer 类基础介绍
这个是用于创建一个服务器类的。
cpp
// TcpConnection 的智能指针类型定义
typedef std::shared_ptr<TcpConnection> TcpConnectionPtr;
// 连接事件回调函数类型定义:当连接建立或关闭时被调用
typedef std::function<void (const TcpConnectionPtr&)> ConnectionCallback;
// 消息到达回调函数类型定义:当收到数据时被调用
// 参数分别为:连接指针、数据缓冲区、时间戳
typedef std::function<void (const TcpConnectionPtr&,
Buffer*,
Timestamp)> MessageCallback;
// 网络地址类,封装IP地址和端口号
// 继承自muduo::copyable,表示该类支持拷贝操作
class InetAddress : public muduo::copyable
{
public:
// 构造函数:通过IP字符串和端口号创建地址对象
// ip: IP地址字符串
// port: 端口号
// ipv6: 是否为IPv6地址,默认为IPv4
InetAddress(StringArg ip, uint16_t port, bool ipv6 = false);
};
// TCP服务器类,封装服务器端功能
// 继承自noncopyable,表示该类禁止拷贝
class TcpServer : noncopyable
{
public:
// 端口复用选项枚举
enum Option
{
kNoReusePort, // 不启用端口复用
kReusePort, // 启用端口复用(支持多个进程监听同一端口)
};
// 构造函数
// loop: 主EventLoop,用于接受新连接
// listenAddr: 服务器监听的地址
// nameArg: 服务器名称标识
// option: 端口复用选项,默认为不启用
TcpServer(EventLoop* loop,
const InetAddress& listenAddr,
const string& nameArg,
Option option = kNoReusePort);
// 设置工作线程数量(IO线程池大小)
void setThreadNum(int numThreads);
// 启动服务器,开始监听和接受连接
void start();
/// 设置连接回调函数
void setConnectionCallback(const ConnectionCallback& cb)
{ connectionCallback_ = cb; }
/// 设置消息回调函数
void setMessageCallback(const MessageCallback& cb)
{ messageCallback_ = cb; }
......
private:
ConnectionCallback connectionCallback_; // 连接回调函数:当新连接建立或现有连接关闭时被调用
MessageCallback messageCallback_; // 消息回调函数:当收到客户端数据时被调用,用于业务逻辑处理
// ... 其他成员变量
};
首先,定义了两个重要的回调类型:
- TcpConnectionPtr:这是TcpConnection的智能指针类型,用于安全地管理TcpConnection对象的生命周期。
- **ConnectionCallback:这是一个std::function类型,它封装了当连接建立或关闭时被调用的回调函数。**这个回调函数接受一个参数:const TcpConnectionPtr&,即当前连接的智能指针。
- **MessageCallback:这也是一个std::function类型,它封装了当收到数据时被调用的回调函数。**这个回调函数接受三个参数:const TcpConnectionPtr&(连接指针)、Buffer*(数据缓冲区)和Timestamp(时间戳)。
在TcpServer类中,有两个私有成员变量:
- connectionCallback_:类型为ConnectionCallback,用于存储用户设置的连接回调函数。
- messageCallback_:类型为MessageCallback,用于存储用户设置的消息回调函数。
然后,TcpServer类提供了两个公共成员函数来设置这些回调:
- setConnectionCallback:将传入的ConnectionCallback赋值给connectionCallback_。
- setMessageCallback:将传入的MessageCallback赋值给messageCallback_。
如何使用?
**用户需要先创建一个TcpServer对象,然后调用setConnectionCallback和setMessageCallback来设置自己的回调函数。**当事件发生时(如连接建立、连接断开、收到数据),TcpServer内部会调用这些回调函数。
例如,用户代码可能如下:
cpp
// 用户定义的连接回调函数
void onConnection(const TcpConnectionPtr& conn) {
if (conn->connected()) {
// 连接建立
} else {
// 连接断开
}
}
// 用户定义的消息回调函数
void onMessage(const TcpConnectionPtr& conn, Buffer* buffer, Timestamp time) {
// 处理接收到的数据
}
int main() {
EventLoop loop;
InetAddress listenAddr(8888);
TcpServer server(&loop, listenAddr, "MyServer");
// 设置回调
server.setConnectionCallback(onConnection);
server.setMessageCallback(onMessage);
server.start();
loop.loop();
}
回调的触发时机
- 当有新连接建立或者现有连接关闭时,TcpServer(实际上是通过TcpConnection)会调用connectionCallback_。
- 当收到客户端数据时,TcpServer会调用私有成员变量messageCallback_。
注意啊,是系统自动调用,所以connectionCallback_和messageCallback_的参数都是系统填充好了给我们使用的,那是输入型参数。
3.2.muduo::net::EventLoop 类基础介绍
cpp
// 事件循环类,是Muduo网络库的核心组件
// 继承自noncopyable,禁止拷贝构造和赋值操作
class EventLoop : noncopyable
{
public:
/// 开始事件循环,永久运行直到调用quit()
/// 必须在创建该对象的同一线程中调用
void loop();
/// 停止事件循环
/// 注意:这不是100%线程安全的,如果通过原始指针调用,
/// 建议通过shared_ptr<EventLoop>调用以确保100%安全
void quit();
/// 在指定时间点运行定时器回调
TimerId runAt(Timestamp time, TimerCallback cb);
/// 在延迟指定秒数后运行回调函数
/// 可以从其他线程安全调用
TimerId runAfter(double delay, TimerCallback cb);
/// 每隔指定秒数重复运行回调函数
/// 可以从其他线程安全调用
TimerId runEvery(double interval, TimerCallback cb);
/// 取消指定的定时器
/// 可以从其他线程安全调用
void cancel(TimerId timerId);
private:
std::atomic<bool> quit_; // 原子标志位,用于控制循环退出
std::unique_ptr<Poller> poller_; // 多路复用器,负责监听文件描述符事件
mutable MutexLock mutex_; // 互斥锁,保护pendingFunctors_的线程安全
std::vector<Functor> pendingFunctors_ GUARDED_BY(mutex_); // 待执行的函数队列,由mutex_保护
};
我们在这里先讲讲它的事件循环相关的功能:
EventLoop(事件循环) 是 Muduo 网络库的核心引擎 ,它是驱动整个服务器运行的主循环。就像汽车的发动机一样,EventLoop 不断运转,驱动整个程序处理各种网络事件。
EventLoop 的核心任务是:
-
监听:不断检查是否有事件发生(网络数据到达、定时器到期等)
-
分发:当事件发生时,调用对应的处理函数
EventLoop(事件循环)是一个程序结构,用于等待和分发事件。
在Muduo中,EventLoop主要用于处理网络I/O事件和定时器事件。
每个EventLoop对象都和一个线程绑定,该线程会执行EventLoop的loop()函数,进入一个无限循环,不断地监听事件并处理。
-
loop() :启动事件循环。该函数会一直运行,直到调用quit()。在循环中,它会调用Poller来监听注册的文件描述符(socket)上的事件,当有事件发生时,调用相应的回调函数。
-
quit():停止事件循环。设置退出标志,使loop()在下一次循环时退出。
3.3.muduo::net::TcpConnection 类基础介绍
cpp
// TCP连接类,表示一个已建立的TCP连接
// 继承自noncopyable禁止拷贝,同时继承enable_shared_from_this以支持安全地获取自身的shared_ptr
class TcpConnection : noncopyable,
public std::enable_shared_from_this<TcpConnection>
{
public:
/// 构造函数:使用已连接的socket文件描述符创建TcpConnection对象
/// 注意:用户不应该直接创建此对象,通常由TcpServer在接受连接时创建
TcpConnection(EventLoop* loop,
const string& name,
int sockfd, // 已连接的socket文件描述符
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 Buffer移动版本(注释状态)
void send(Buffer* message); // Buffer指针版本,此版本会交换数据
void shutdown(); // 关闭连接(非线程安全,禁止同时调用)
// 用户上下文数据管理函数
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_; // 所属的事件循环,用于处理该连接的IO事件
ConnectionCallback connectionCallback_; // 连接状态变化回调函数
MessageCallback messageCallback_; // 消息到达回调函数
WriteCompleteCallback writeCompleteCallback_; // 写完成回调函数(未在公共接口中展示设置方法)
boost::any context_; // 用户上下文数据,可以存储任意类型的用户数据
};
TcpConnection 是 Muduo 网络库中代表单个TCP连接的类。每个客户端与服务器建立的TCP连接都会对应一个 TcpConnection 对象。
它的作用还是很多的
连接生命周期管理
- 管理从连接建立到断开的全过程
- 跟踪连接状态(正在连接、已连接、正在断开、已断开)
数据收发管理
- 提供多种数据发送接口
- 接收对端发送的数据
- 管理发送和接收缓冲区
事件回调处理
- 处理连接状态变化事件
- 处理数据到达事件
- 处理数据发送完成事件
但是我们需要注意啊,这个TcpConnection在实际运用中是和智能指针绑定成一个TcpConnectionPtr来进行使用的,这个TcpConnectionPtr作为回调函数的第一个参数!!系统自动调用回调函数,会自动填充这个回调函数里面的参数,我们就可以通过这个TcpConnectionPtr参数来获取到本次连接的信息!!!
cpp
// TcpConnection 的智能指针类型定义
typedef std::shared_ptr<TcpConnection> TcpConnectionPtr;
// 连接事件回调函数类型定义:当连接建立或关闭时被调用
typedef std::function<void (const TcpConnectionPtr&)> ConnectionCallback;
// 消息到达回调函数类型定义:当收到数据时被调用
// 参数分别为:连接指针、数据缓冲区、时间戳
typedef std::function<void (const TcpConnectionPtr&,
Buffer*,
Timestamp)> MessageCallback;
3.4.muduo::net::TcpClient 类基础介绍
cpp
// TCP客户端类,用于主动创建到服务器的连接
// 继承自noncopyable,禁止拷贝构造和赋值操作
class TcpClient : noncopyable
{
public:
// 构造函数:创建TCP客户端
// loop: 事件循环,用于处理该客户端的IO事件
// serverAddr: 要连接的服务器地址
// nameArg: 客户端名称标识
TcpClient(EventLoop* loop,
const InetAddress& serverAddr,
const string& nameArg);
~TcpClient(); // 析构函数显式声明,用于std::unique_ptr成员的正确销毁
void connect(); // 连接服务器
void disconnect(); // 关闭连接
void stop(); // 停止客户端
// 获取客户端对应的通信连接TcpConnection对象
// 注意:调用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_); // TCP连接对象,由mutex_保护
};
3.5.CountDownLatch类
cpp
/*
使用注意事项:
由于Muduo库的服务端和客户端都采用异步操作模式,
对于客户端来说,如果在连接尚未完全建立成功时尝试发送数据,这是不被允许的。
因此,我们可以使用内置的CountDownLatch类进行同步控制,确保连接建立后再进行数据操作。
*/
// 倒计时门闩类,用于线程间同步
// 继承自noncopyable,禁止拷贝构造和赋值操作
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_; // 互斥锁,保护count_的线程安全
Condition condition_ GUARDED_BY(mutex_); // 条件变量,用于线程等待和通知
int count_ GUARDED_BY(mutex_); // 计数器,由mutex_保护
};
CountDownLatch 是一个线程同步工具。它的核心是一个计数器,这个计数器在创建时被初始化为一个正整数。这个工具提供了两个主要操作:等待和计数减一。
CountDownLatch 内部维护一个计数器。当创建 CountDownLatch 对象时,需要指定这个计数器的初始值。比如初始值设为1,表示有1个任务需要完成。
线程可以调用wait()方法。如果此时计数器值大于0,调用线程会被挂起,进入阻塞状态。线程会一直停留在wait()方法内部,不能继续执行后面的代码。操作系统会把这个线程从运行状态切换到等待状态,CPU不会分配时间片给这个线程。
其他线程可以调用countDown()方法。每次调用这个方法,计数器值就减少1。这个操作通常表示某个任务已经完成。
当计数器值减少到0时,CountDownLatch 会唤醒所有正在等待的线程。被唤醒的线程会从wait()方法中返回,继续执行后面的代码。
CountDownLatch 的计数器只能减少,不能增加。一旦计数器值变为0,就无法重置。这意味着 CountDownLatch 只能使用一次。
CountDownLatch 内部使用了互斥锁和条件变量。互斥锁确保对计数器的操作是原子性的,即多个线程同时调用计数减一方法时不会出现数据竞争。条件变量用于实现线程的等待和唤醒。
3.5.muduo::net::Buffer 类基础介绍
cpp
// 数据缓冲区类,用于网络数据传输的缓冲区管理
// 继承自muduo::copyable,支持拷贝操作
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; // 查找行结束符(CRLF)
const char* findEOL(const char* start) const; // 从指定位置开始查找行结束符
// 数据消费函数(移动读指针)
void retrieve(size_t len); // 消费指定长度的数据
void retrieveInt64(); // 消费一个64位整数
void retrieveInt32(); // 消费一个32位整数
void retrieveInt16(); // 消费一个16位整数
void retrieveInt8(); // 消费一个8位整数
// 数据提取函数(转换为字符串)
string retrieveAllAsString(); // 提取所有可读数据作为字符串
string retrieveAsString(size_t len); // 提取指定长度的数据作为字符串
// 数据添加函数(追加到写指针后)
void append(const StringPiece& str); // 追加字符串片段
void append(const char* data, size_t len); // 追加字符数组
void append(const void* data, size_t len); // 追加任意二进制数据
// 写操作辅助函数
char* beginWrite(); // 获取可写空间的起始指针(可修改)
const char* beginWrite() const; // 获取可写空间的起始指针(只读)
void hasWritten(size_t len); // 标记已写入指定长度的数据(移动写指针)
// 整型数据追加函数(网络字节序)
void appendInt64(int64_t x); // 追加64位整数
void appendInt32(int32_t x); // 追加32位整数
void appendInt16(int16_t x); // 追加16位整数
void appendInt8(int8_t x); // 追加8位整数
// 整型数据读取函数(网络字节序)
int64_t readInt64(); // 读取并消费64位整数
int32_t readInt32(); // 读取并消费32位整数
int16_t readInt16(); // 读取并消费16位整数
int8_t readInt8(); // 读取并消费8位整数
// 整型数据查看函数(不移动读指针)
int64_t peekInt64() const; // 查看64位整数(不消费)
int32_t peekInt32() const; // 查看32位整数(不消费)
int16_t peekInt16() const; // 查看16位整数(不消费)
int8_t peekInt8() const; // 查看8位整数(不消费)
// 数据预置函数(添加到读指针前)
void prependInt64(int64_t x); // 在数据前预置64位整数
void prependInt32(int32_t x); // 在数据前预置32位整数
void prependInt16(int16_t x); // 在数据前预置16位整数
void prependInt8(int8_t x); // 在数据前预置8位整数
void prepend(const void* data, size_t len); // 在数据前预置任意二进制数据
private:
std::vector<char> buffer_; // 底层数据存储容器
size_t readerIndex_; // 读指针位置
size_t writerIndex_; // 写指针位置
static const char kCRLF[]; // 行结束符常量定义(CRLF: "\r\n")
};
四.使用Muduo库搭建英译汉服务
在这里我们就搭建一个英译汉TCP服务器
由于我们这个项目需要使用到这个Muduo库,而我们的Muduo库是存在于这个build目录里面的

我们进去build目录看看



所以我们需要把这个/root/m/build/release-install-cpp11目录拷贝到我们的工作目录来
cpp
cp -r /root/m/build/release-install-cpp11/ ./muduo/

4.1.搭建TCP服务器
现在我们就来编写我们的源文件的框架
cpp
#include"muduo/include/muduo/net/TcpServer.h"
#include"muduo/include/muduo/net/EventLoop.h"
#include"muduo/include/muduo/net/TcpConnection.h"
#include<iostream>
#include<unordered_map>
// 字典服务器类 - 实现英汉互译功能的TCP服务器
class DictServer {
public:
// 构造函数:初始化服务器并绑定端口
DictServer(int port): _server(&_baseloop,
muduo::net::InetAddress("0.0.0.0",port), // 监听地址和端口
"DictServer", // 服务器名称
muduo::net::TcpServer::Option::kReusePort) { // 端口复用选项
}
// 启动服务器
void start() {
}
private:
// 连接状态变化回调函数
void onConnection(const muduo::net::TcpConnectionPtr &conn) {
}
// 消息接收回调函数
void onMessage(const muduo::net::TcpConnectionPtr &conn,
muduo::net::Buffer *buf, // 数据缓冲区
muduo::Timestamp t) { // 时间戳
}
// 翻译函数:实现英汉互译
std::string translate(const std::string &msg) {
}
private:
muduo::net::EventLoop _baseloop; // 主事件循环对象
muduo::net::TcpServer _server; // TCP服务器对象
};
int main()
{
DictServer dict_server(8085); // 创建字典服务器,监听8085端口
dict_server.start(); // 启动服务器
return 0;
}
注意这两个回调函数的参数一定要这样子写,只有这样子才能被赋值给TCP的私有成员变量。大家可以仔细对比下面这个
cpp
// TcpConnection 的智能指针类型定义
typedef std::shared_ptr<TcpConnection> TcpConnectionPtr;
// 连接事件回调函数类型定义:当连接建立或关闭时被调用
typedef std::function<void (const TcpConnectionPtr&)> ConnectionCallback;
// 消息到达回调函数类型定义:当收到数据时被调用
// 参数分别为:连接指针、数据缓冲区、时间戳
typedef std::function<void (const TcpConnectionPtr&,
Buffer*,
Timestamp)> MessageCallback;
我们的框架就这样子搭建出来了。
那么很快,我们就能搭建出下面这个
cpp
#include"muduo/include/muduo/net/TcpServer.h"
#include"muduo/include/muduo/net/EventLoop.h"
#include"muduo/include/muduo/net/TcpConnection.h"
#include<iostream>
#include<unordered_map>
#include <functional>
// 字典服务器类 - 实现英汉互译功能的TCP服务器
class DictServer {
public:
// 构造函数:初始化服务器并绑定端口
DictServer(int port): _server(&_baseloop,
muduo::net::InetAddress("0.0.0.0",port), // 监听地址和端口
"DictServer", // 服务器名称
muduo::net::TcpServer::Option::kReusePort) { // 端口复用选项
// 设置连接建立/断开时的回调函数
_server.setConnectionCallback(std::bind(&DictServer::onConnection,
this, // 绑定当前对象指针,用于调用成员函数
std::placeholders::_1)); // 占位符,表示回调函数的第一个参数(TcpConnectionPtr)
// 设置收到消息时的回调函数
_server.setMessageCallback(std::bind(&DictServer::onMessage,
this, // 绑定当前对象指针,用于调用成员函数
std::placeholders::_1, // 占位符,表示回调函数的第一个参数(TcpConnectionPtr)
std::placeholders::_2, // 占位符,表示回调函数的第二个参数(Buffer*)
std::placeholders::_3)); // 占位符,表示回调函数的第三个参数(Timestamp)
}
// 启动服务器
void start() {
_server.start(); // 启动TcpServer,开始监听端口
_baseloop.loop(); // 进入事件循环,开始处理网络事件,这个是死循环
}
private:
// 连接状态变化回调函数------只会在连接成功,连接关闭时被调用
//注意这个函数是被自动调用的,这些参数Muduo库都会自动填充好给我们使用,
//也就是说这些参数都是输入型参数
void onConnection(const muduo::net::TcpConnectionPtr &conn) {
if(conn->connected()==true)
{
std::cout<<"新连接建立成功"<<std::endl;
}
else{
std::cout<<"连接关闭"<<std::endl;
}
}
// 收到消息时的回调函数
//注意这个函数是被自动调用的,这些参数Muduo库都会自动填充好给我们使用,
//也就是说这些参数都是输入型参数
void onMessage(const muduo::net::TcpConnectionPtr &conn,
muduo::net::Buffer *buf, // 数据缓冲区
muduo::Timestamp t) { // 时间戳
// 从缓冲区中提取所有数据并转换为字符串
std::string msg = buf->retrieveAllAsString();
// 去除末尾的换行符(如果有)
if (msg.back() == '\n') msg.pop_back();
// 调用翻译函数处理消息
std::string rsp = translate(msg);
// 将翻译结果发送回客户端
conn->send(rsp);
}
// 翻译函数:实现英汉互译
std::string translate(const std::string &msg) {
// 静态字典映射表
static std::unordered_map<std::string, std::string> dict_map = {
{"Hello", "你好"},
{"你好", "Hello"},
{"World", "世界"},
{"世界", "World"},
};
// 在字典中查找对应的翻译
auto it = dict_map.find(msg);
if (it == dict_map.end()) {
return "UnKnow"; // 未找到对应的翻译
}
return it->second; // 返回翻译结果
}
private:
muduo::net::EventLoop _baseloop; // 主事件循环对象
muduo::net::TcpServer _server; // TCP服务器对象
};
int main()
{
DictServer dict_server(8085); // 创建字典服务器,监听8085端口
dict_server.start(); // 启动服务器
return 0;
}
注意我们这里的绑定函数:
由于我们使用bind绑定的是成员函数,那么必须
- 第一个参数是成员函数指针
- 第二个参数必须是具体的对象(通过指针、引用或值)
- 后续的参数才是函数的参数等
接下来我们编写这个makefile
cpp
server:dict_server.cpp
g++ -std=c++11 $^ -o $@ -I muduo/include -L muduo/lib -lmuduo_net -lmuduo_base -lpthread


这个就是很简单的
4.2.搭建TCP客户端
话不多说,我们直接看例子
cpp
#include "muduo/include/muduo/net/EventLoop.h"
#include "muduo/include/muduo/net/TcpClient.h"
#include "muduo/include/muduo/net/EventLoopThread.h"
#include "muduo/include/muduo/base/CountDownLatch.h"
#include <iostream>
#include <functional>
#include <unordered_map>
// 字典客户端类 - 实现连接字典服务器并进行翻译请求的客户端
class DictClient {
public:
// 构造函数:初始化客户端连接参数
DictClient(const std::string &ip, int port, bool is_wait_connect = true):
_is_wait_connect(is_wait_connect), // 是否等待连接建立完成的标志
_connect_latch(1), // 连接同步门闩,初始计数为1(用于等待连接建立)
_baseloop(_loopthread.startLoop()), // 启动事件循环线程并获取EventLoop指针
_client(_baseloop, // 创建TCP客户端对象,绑定到事件循环
muduo::net::InetAddress(ip, port), // 服务器地址(IP和端口)
"DictClient") // 客户端名称
{
// 设置连接状态变化回调函数
//注意我这里绑定的是成员函数,第一个参数是成员函数指针
//第二个参数必须是具体的对象(通过指针、引用或值)
_client.setConnectionCallback(std::bind(&DictClient::onConnection,
this,
std::placeholders::_1));
// 设置消息接收回调函数
//注意我这里绑定的是成员函数,第一个参数是成员函数指针
//第二个参数必须是具体的对象(通过指针、引用或值)
_client.setMessageCallback(std::bind(&DictClient::onMessage,
this,
std::placeholders::_1,
std::placeholders::_2,
std::placeholders::_3));
}
// 连接到服务器
void connect() {
_client.connect(); // 发起异步连接请求
// 因为连接是异步操作,外部发送消息的时候有可能连接还没有真正建立成功
// 因此这里通过门闩同步等待连接完成后的唤醒
if (_is_wait_connect)
{
_connect_latch.wait();//同步门闩的初始值是1,故主线程会阻塞在这个wait()里面
}
}
// 断开连接
void shutdown() {
_client.disconnect(); // 断开与服务器的连接
}
// 发送翻译请求
void translate(std::string &msg) {
msg.push_back('\n'); // 在消息末尾添加换行符作为结束标记
//注意这个Muduo库都是异步的情况,我们得先保证这个连接的存在
if (_conn) //TcpConnectionPtr存在,也就是连接存在
{
//通过TcpConnectionPtr来发送信息
_conn->send(msg); // 如果连接有效,发送消息到服务器
}
}
private:
// 连接状态变化回调函数
//注意这个函数是被自动调用的,这些参数Muduo库都会自动填充好给我们使用,
//也就是说这些参数都是输入型参数
void onConnection(const muduo::net::TcpConnectionPtr &conn) {
if (conn->connected()==true) { // 连接建立成功
_conn = conn; // 保存连接对象指针-TcpConnectionPtr
if (_is_wait_connect)
{
_connect_latch.countDown(); // 门闩计数减1,唤醒等待线程
}
std::cout << "CONNECT SERVER SUCCESS!\n";
}
else // 连接断开
{
_conn.reset(); // 通过TcpConnectionPtr重置连接指针(置为空)
std::cout << "CONNECT SHUTDOWN!\n";
}
}
// 消息接收回调函数(处理服务器返回的翻译结果)
//注意这个函数是被自动调用的,这些参数Muduo库都会自动填充好给我们使用,
//也就是说这些参数都是输入型参数
void onMessage(const muduo::net::TcpConnectionPtr &conn,
muduo::net::Buffer *buf, // 数据缓冲区
muduo::Timestamp t){ // 时间戳
// 从缓冲区提取所有数据并输出翻译结果
std::string msg = buf->retrieveAllAsString();
std::cout << msg << std::endl;
}
private:
bool _is_wait_connect; // 是否等待连接建立的标志
muduo::CountDownLatch _connect_latch; // 连接同步门闩,用于等待异步连接完成
muduo::net::EventLoopThread _loopthread; // 事件循环线程(在独立线程中运行事件循环)
muduo::net::EventLoop *_baseloop; // 事件循环指针,指向_loopthread中的事件循环
muduo::net::TcpClient _client; // TCP客户端对象
muduo::net::TcpConnectionPtr _conn; // TCP连接对象指针,它指向了TcpConnection,而TcpConnection保存当前活动的连接
};
int main()
{
// 创建字典客户端,连接到本地8085端口
DictClient dict_client("127.0.0.1", 8085);
// 连接到服务器(会等待连接建立完成)
dict_client.connect();
// 主循环:接收用户输入并发送翻译请求
while(1) {
std::string msg;
std::cout << "请输入:";
std::cout.flush(); // 刷新输出缓冲区,确保提示信息立即显示
std::cin >> msg; // 读取用户输入的待翻译文本
dict_client.translate(msg); // 发送翻译请求到服务器
}
// 断开连接(实际上这里的代码不会被执行,因为上面是无限循环)
dict_client.shutdown();
return 0;
}
我知道大家有很多问题,不过不必慌张,我们一一道来
问题1:EventLoopThread是干啥用的
我们注意到在DictClient的构造函数中,我们创建了一个EventLoopThread对象_loopthread,然后通过_loopthread.startLoop()启动了一个事件循环线程,并获取了该线程中的EventLoop指针,赋给_baseloop。
然后,我们使用这个_baseloop来初始化TcpClient对象_client。
那么,为什么我们需要一个EventLoopThread来创建一个运行在另一个线程中的EventLoop,而不是直接使用当前线程的EventLoop呢?
方案A:不使用 EventLoopThread(错误的)
cpp
class DictClient {
public:
DictClient(...) {
// 直接在主线程创建 EventLoop
muduo::net::EventLoop loop; // 主线程的 EventLoop
// 创建 TcpClient,绑定到主线程的 EventLoop
muduo::net::TcpClient _client(&loop, ...);
// 问题:必须在主线程运行 loop.loop()
loop.loop(); // 这会阻塞主线程,后面代码无法执行!
}
};
int main() {
DictClient client(...);
// 到这里 loop.loop() 正在运行,主线程被阻塞
// 无法执行用户输入等操作!
return 0;
}
问题:EventLoop::loop() 是一个阻塞调用,会一直运行事件循环直到停止。
原因在于:TcpClient需要一个EventLoop来执行网络IO操作。如果我们直接在当前线程(比如主线程)中运行EventLoop,那么当前线程就会被事件循环阻塞,无法执行其他任务(比如接收用户输入)。
在这个字典客户端中,我们希望主线程能够不断接收用户输入,然后发送翻译请求,同时又要处理网络IO(接收服务器返回的消息)。因此,我们需要将网络IO放在另一个线程中,这样主线程就不会被阻塞。
方案B:使用 EventLoopThread(正确的)
cpp
class DictClient {
public:
DictClient(...) {
// 创建 EventLoopThread,它会启动一个新线程
// 新线程中会创建一个 EventLoop 并运行 loop()
muduo::net::EventLoopThread _loopthread;
// 获取新线程中的 EventLoop 指针
muduo::net::EventLoop* _baseloop = _loopthread.startLoop();
// _baseloop 指向的是 EventLoopThread 线程中的 EventLoop
// 创建 TcpClient,绑定到 EventLoopThread 线程的 EventLoop
muduo::net::TcpClient _client(_baseloop, ...);
// 所有网络事件都在 EventLoopThread 线程中处理
}
};
int main() {
DictClient client(...); // 在主线程构造
// 主线程继续执行,可以接收用户输入
// 网络事件在 EventLoopThread 线程处理
return 0;
}
具体来说:
- 我们创建了一个EventLoopThread对象,创建这个对象的同时,它会在内部启动一个线程,并在该线程中运行一个EventLoop。
- 创建 EventLoopThread,它会启动一个新线程,新线程中会创建一个 EventLoop 并运行 loop(),然后我们可以通过EventLoopThread的startLoop()来获取到这个EventLoop指针。
- 然后,我们将TcpClient绑定到这个EventLoop上,这样TcpClient的所有网络操作(连接、发送、接收)都会在那个线程中进行。
- 主线程(即运行main函数的线程)则负责接收用户输入,然后通过TcpClient发送请求。
这样设计的好处是:主线程不会被网络IO阻塞,可以及时响应用户输入;而网络IO操作在单独的线程中,由事件循环驱动,可以高效处理。
如果我们不使用EventLoopThread,而是直接在主线程中创建EventLoop并运行,那么主线程就会进入事件循环,无法再接收用户输入。
因此,这里使用EventLoopThread是为了将网络IO放到后台线程中,避免阻塞主线程。
注意:在Muduo库中,每个TcpClient(或TcpServer)都必须关联一个EventLoop,而且这个EventLoop必须运行在另外一个线程中。
如果我们在主线程中运行EventLoop,那么主线程就不能做其他事情了。
所以,在这个例子中,我们使用EventLoopThread来创建一个专门用于网络IO的线程,然后在这个线程中运行EventLoop。
问题二:门闩(CountDownLatch)同步的运用
在 Muduo 中,TcpClient::connect() 是异步操作:
cpp
_client.connect(); // 发起连接请求,立即返回
- 立即返回:connect() 调用后立即返回,不会等待连接真正建立
- 后台处理:实际的 TCP 三次握手在后台进行
- 回调通知:连接成功或失败通过 onConnection 回调通知
但是这就会造成一个问题:万一我连接还没建立好的时候,我就使用客户端来发送消息,那么消息就会发送失败吗?
cpp
int main() {
DictClient dict_client("127.0.0.1", 8085);
dict_client.connect(); // 异步连接,立即返回
// 问题:这里连接可能还没有建立成功!
while(1) {
std::string msg;
std::cin >> msg;
dict_client.translate(msg); // 立即发送消息
}
}
为此,我们引入门闩(CountDownLatch)同步.
CountDownLatch(计数门闩) 是一个同步工具类,它允许一个或多个线程等待其他线程完成操作。核心思想是:
-
初始化时设置一个计数值
-
等待线程调用
wait()方法会被阻塞 -
其他线程完成任务后调用
countDown()方法,计数值减1 -
当计数值变为0时,所有等待的线程被唤醒继续执行
cpp
muduo::CountDownLatch _connect_latch(1); // 计数为1
// 等待方:阻塞等待计数变为0
_connect_latch.wait(); // 阻塞,直到countDown()被调用
// 通知方:计数减1,为0时唤醒所有等待线程
_connect_latch.countDown(); // 唤醒wait()
具体来说
- CountDownLatch 是一个线程同步工具。它的核心是一个计数器,这个计数器在创建时被初始化为一个正整数。这个工具提供了两个主要操作:等待和计数减一。
- CountDownLatch 内部维护一个计数器。当创建 CountDownLatch 对象时,需要指定这个计数器的初始值。比如初始值设为1,表示有1个任务需要完成。
- 线程可以调用wait()方法。如果此时计数器值大于0,调用线程会被挂起,进入阻塞状态。线程会一直停留在wait()方法内部,不能继续执行后面的代码。操作系统会把这个线程从运行状态切换到等待状态,CPU不会分配时间片给这个线程。
- 其他线程可以调用countDown()方法。每次调用这个方法,计数器值就减少1。这个操作通常表示某个任务已经完成。
- 当计数器值减少到0时,CountDownLatch 会唤醒所有正在等待的线程。被唤醒的线程会从wait()方法中返回,继续执行后面的代码。
- CountDownLatch 的计数器只能减少,不能增加。一旦计数器值变为0,就无法重置。这意味着 CountDownLatch 只能使用一次。
- CountDownLatch 内部使用了互斥锁和条件变量。互斥锁确保对计数器的操作是原子性的,即多个线程同时调用计数减一方法时不会出现数据竞争。条件变量用于实现线程的等待和唤醒。
大家需要知道:我们这里使用它的原因是:
在DictClient中,我们使用CountDownLatch来确保在连接建立成功之前,主线程不会继续执行(即不会发送消息)
-
如果没有重连需求,那么一次连接,使用一次CountDownLatch,然后程序运行直到连接断开,这是足够的。
-
如果有重连需求,那么我们需要在每次重新连接时重置CountDownLatch。但是,CountDownLatch不能重置,所以我们需要在每次重连时创建一个新的CountDownLatch。
问题三:标志位的使用
_is_wait_connect 是一个标志,用于控制是否使用门闩(CountDownLatch)来同步等待连接建立。
具体来说:
- 当 _is_wait_connect 为 true 时,在 connect() 方法中会调用 _connect_latch.wait() 等待,直到连接建立成功(在 onConnection 回调中调用 _connect_latch.countDown())才会继续执行。
- 当 _is_wait_connect 为 false 时,则不会调用 _connect_latch.wait(),即不会等待连接建立,connect() 方法会立即返回,而连接建立的过程将在后台异步进行。
这样设计给了用户选择的灵活性:如果用户希望确保连接建立后再继续执行后续代码(比如发送消息),则可以将 _is_wait_connect 设为 true;如果用户希望发起连接后立即返回,不等待连接结果,则可以设为 false。
注意一下:
_is_wait_connect 不是决定是否启动门闩,而是决定是否使用门闩进行等待。
区别在于:
- 门闩(CountDownLatch)一定会被创建:无论 _is_wait_connect 的值如何,_connect_latch(1) 都会执行
- 等待行为由 _is_wait_connect 控制:只在为 true 时才调用 wait()
问题四:主线程调用了wait()不就阻塞了吗,谁来唤醒主线程?
具体过程如下:
-
在主线程中,我们创建了DictClient对象,并调用了connect()方法。
-
connect()方法中,调用了_client.connect()发起异步连接,然后调用_connect_latch.wait(),此时主线程阻塞。
-
在EventLoopThread线程中,当连接建立完成(成功或失败)时,会调用onConnection回调函数。
-
在onConnection中,如果连接成功,我们会调用_connect_latch.countDown(),将计数器减1(从1减到0),这样就会唤醒在wait()上等待的主线程。
所以:主线程调用wait,网络IO线程调用countDown,从而实现了主线程等待网络IO线程完成连接建立的过程。
**在 Muduo 网络库中,所有的网络回调函数确实都是由网络IO线程调用的。**这是 Muduo 的一个核心设计原则。
问题五:TcpConnectionPtr的作用
首先我们需要知道这个TcpConnectionPtr本质 是一个智能指针,指向 TcpConnection 对象。
cpp
// TcpConnectionPtr 通常是 shared_ptr 的别名
typedef std::shared_ptr<TcpConnection> TcpConnectionPtr;
而我们TcpConnection 对象的作用其实很重要
TcpConnection 对象代表一个 TCP 连接,它包含了:
-
连接状态(已连接、正在连接、已断开等)
-
本地和远程的 IP 地址、端口
-
发送和接收缓冲区
-
各种回调函数
-
连接的生命周期管理
TcpConnection 对象在 DictClient 中的用途
保存当前活动的连接
cpp
muduo::net::TcpConnectionPtr _conn; // 保存当前连接
这个指针在以下时机被修改:
连接建立时(在 onConnection 中):
cpp
void onConnection(const muduo::net::TcpConnectionPtr &conn) {
if (conn->connected()) {
_conn = conn; // 保存新建立的连接
// ...
}
}
连接断开时(在 onConnection 中):
cpp
void onConnection(const muduo::net::TcpConnectionPtr &conn) {
if (!conn->connected()) {
_conn.reset(); // 清空连接指针
// ...
}
}
使用时(在 translate 中):
cpp
void translate(std::string &msg) {
if (_conn) { // 检查连接是否存在
_conn->send(msg); // 通过连接发送数据
}
}
4.3.编译运行
cpp
all:server client
server:dict_server.cpp
g++ -std=c++11 $^ -o $@ -I muduo/include -L muduo/lib -lmuduo_net -lmuduo_base -lpthread
client:dict_client.cpp
g++ -std=c++11 $^ -o $@ -I muduo/include -L muduo/lib -lmuduo_net -lmuduo_base -lpthread
接下来我们就编译我们的程序

我们打开另外一个终端


一点问题都没有啊。
五.使用Muduo库进行Protobuf通信
我们上面只是简单的做了一个网络通信,但是我们并没有使用什么协议,连最基本的TCP粘包问题我都没有解决。
5.1.Muduo库Protobuf通信流程示例
首先啊,Muduo库里面其实带有很多的protobuf的示例,我们可以去借鉴一下
还记得我们安装Muduo库的那个位置吗

这个build目录是我们安装完后的,而这个muduo-master是我们解压后的源代码目录。
我们进去muduo-master源代码目录啊
然后我们就去下面这个目录

我们打开这个client.cc看看
cpp
//这里的头文件是protobuf生成的
#include "examples/protobuf/codec/dispatcher.h"
#include "examples/protobuf/codec/codec.h"
#include "examples/protobuf/codec/query.pb.h"
//这里的头文件是Muduo库自带的
#include "muduo/base/Logging.h"
#include "muduo/base/Mutex.h"
#include "muduo/net/EventLoop.h"
#include "muduo/net/TcpClient.h"
#include <stdio.h>
#include <unistd.h>
using namespace muduo;
using namespace muduo::net;
typedef std::shared_ptr<muduo::Empty> EmptyPtr;
typedef std::shared_ptr<muduo::Answer> AnswerPtr;
google::protobuf::Message* messageToSend;
//自定义了一个类
class QueryClient : noncopyable // 继承noncopyable,禁止拷贝构造和赋值
{
public:
/**
* 构造函数:初始化查询客户端
*
* @param loop 事件循环指针,用于处理IO事件
* @param serverAddr 服务器地址,客户端将连接到此地址
*/
QueryClient(EventLoop* loop,
const InetAddress& serverAddr)
: loop_(loop), // 初始化事件循环
// 初始化TcpClient,用于建立到服务器的连接
// 参数:事件循环、服务器地址、客户端名称
client_(loop, serverAddr, "QueryClient"),
// 初始化Protobuf消息分发器
// 绑定onUnknownMessage作为默认回调,处理未知消息类型
// _1, _2, _3是占位符,分别对应:TcpConnectionPtr, MessagePtr, Timestamp
dispatcher_(std::bind(&QueryClient::onUnknownMessage, this, _1, _2, _3)),
// 初始化Protobuf编解码器
// 绑定dispatcher_的onProtobufMessage作为消息回调
// 当codec_成功解析消息后,会调用dispatcher_进行消息分发
codec_(std::bind(&ProtobufDispatcher::onProtobufMessage, &dispatcher_, _1, _2, _3))
{
// 注册消息类型muduo::Answer的回调处理函数
// 当收到Answer类型的Protobuf消息时,调用onAnswer方法处理
dispatcher_.registerMessageCallback<muduo::Answer>(
std::bind(&QueryClient::onAnswer, this, _1, _2, _3));
// 注册消息类型muduo::Empty的回调处理函数
// 当收到Empty类型的Protobuf消息时,调用onEmpty方法处理
dispatcher_.registerMessageCallback<muduo::Empty>(
std::bind(&QueryClient::onEmpty, this, _1, _2, _3));
// 设置TcpClient的连接状态回调
// 当连接建立或断开时,调用onConnection方法
client_.setConnectionCallback(
std::bind(&QueryClient::onConnection, this, _1));
// 设置TcpClient的消息回调
// 当收到原始网络数据时,由codec_的onMessage方法进行解析
// codec_会将解析后的Protobuf消息传递给dispatcher_
client_.setMessageCallback(
std::bind(&ProtobufCodec::onMessage, &codec_, _1, _2, _3));
}
void connect()
{
client_.connect();
}
private:
void onConnection(const TcpConnectionPtr& conn)
{
LOG_INFO << conn->localAddress().toIpPort() << " -> "
<< conn->peerAddress().toIpPort() << " is "
<< (conn->connected() ? "UP" : "DOWN");
if (conn->connected())
{
codec_.send(conn, *messageToSend);
}
else
{
loop_->quit();
}
}
void onUnknownMessage(const TcpConnectionPtr&,
const MessagePtr& message,
Timestamp)
{
LOG_INFO << "onUnknownMessage: " << message->GetTypeName();
}
void onAnswer(const muduo::net::TcpConnectionPtr&,
const AnswerPtr& message,
muduo::Timestamp)
{
LOG_INFO << "onAnswer:\n" << message->GetTypeName() << message->DebugString();
}
void onEmpty(const muduo::net::TcpConnectionPtr&,
const EmptyPtr& message,
muduo::Timestamp)
{
LOG_INFO << "onEmpty: " << message->GetTypeName();
}
EventLoop* loop_;
TcpClient client_;
ProtobufDispatcher dispatcher_;
ProtobufCodec codec_;
};
int main(int argc, char* argv[])
{
LOG_INFO << "pid = " << getpid();
if (argc > 2)
{
EventLoop loop;
uint16_t port = static_cast<uint16_t>(atoi(argv[2]));
InetAddress serverAddr(argv[1], port);
muduo::Query query;
query.set_id(1);
query.set_questioner("Chen Shuo");
query.add_question("Running?");
muduo::Empty empty;
messageToSend = &query;
if (argc > 3 && argv[3][0] == 'e')
{
messageToSend = ∅
}
QueryClient client(&loop, serverAddr);
client.connect();
loop.loop();
}
else
{
printf("Usage: %s host_ip port [q|e]\n", argv[0]);
}
}
在注册回调函数那里,我们可能会看到这么一个类:ProtobufCodec类,这个类其实很重要
我们仔细看一下
cpp
client_.setMessageCallback(
std::bind(&ProtobufCodec::onMessage, &codec_, _1, _2, _3));
我们发现这个QueryClient类居然把这个消息到来的回调函数设置成了ProtobufCodec类的onMessage函数。把消息处理的过程交给了ProtobufCodec类的onMessage函数
接收消息时:
-
当TcpConnection接收到数据时,会调用ProtobufCodec的onMessage函数。
-
onMessage函数会尝试从Buffer中解析出一条完整的消息。如果当前数据不够,就等待下次数据到来;如果解析成功,就调用用户设置的消息回调函数,并将解析出的消息对象传给它;如果解析出错,就调用错误回调函数。
那么这个ProtobufCodec类的onMessage函数具体是怎么进行处理的呢?
那么我们就得去codec.cc里面了
我们仔细看看这个函数
cpp
void ProtobufCodec::onMessage(const TcpConnectionPtr& conn,
Buffer* buf,
Timestamp receiveTime)
{
while (buf->readableBytes() >= kMinMessageLen + kHeaderLen)
{
const int32_t len = buf->peekInt32();
//陈硕大佬在处理protobuf的时候,自定义了一个协议来处理粘包问题,
//一个完整的数据包里面的前32位比特位存储着整个数据包的大小
//先读取了一个整型的数据(4个字节),为了获取一个包的前4个字节,这里存储着整个数据包的大小
//此时len就代表数据有多长了
if (len > kMaxMessageLen || len < kMinMessageLen)
{
errorCallback_(conn, buf, receiveTime, kInvalidLength);
break;
}
else if (buf->readableBytes() >= implicit_cast<size_t>(len + kHeaderLen))
{
ErrorCode errorCode = kNoError;
//typedef std::shared_ptr<google::protobuf::Message> MessagePtr;
//我们自己在protobuf自定义的那个message的父类就是google::protobuf::Message
//先在缓冲区里面读取指定长度的信息,然后再进行反序列化parse进行解析
MessagePtr message = parse(buf->peek()+kHeaderLen, len, &errorCode);
//可以理解为这个message里面就获取到了我们通过protobuf反序列化后的数据
if (errorCode == kNoError && message)//没有出错且消息存在
{
//messageCallback_;就是我们在构建ProtobufCodec时传入的那个回调函数
messageCallback_(conn, message, receiveTime);//进行消息处理
buf->retrieve(kHeaderLen+len);
}
else
{
errorCallback_(conn, buf, receiveTime, errorCode);
break;
}
}
else
{
break;
}
}
}
我们可以看到,对于没有错误存在,并且消息存在的情况下,ProtobufCodec类的onMessage函数调用了下面这么一个函数来对我们的消息进行处理

这个messageCallback_(conn, message, receiveTime);其实是在调用一个我们传递进去的回调函数。
messageCallback_到底是何方神圣呢?那么我就直接去codec.h里面看看源代码了
cpp
class ProtobufCodec : muduo::noncopyable // 继承noncopyable,禁止拷贝构造和赋值
{
// ... 其他成员 ...
// 成功解析Protobuf消息后的回调函数类型定义
// 参数说明:
// 1. const muduo::net::TcpConnectionPtr& - 发生消息的TCP连接
// 2. const MessagePtr& - 解析成功的Protobuf消息指针(基类指针,实际为具体消息类型)
// 3. muduo::Timestamp - 消息到达的时间戳
typedef std::function<void (const muduo::net::TcpConnectionPtr&,
const MessagePtr&,
muduo::Timestamp)> ProtobufMessageCallback;
// 解析错误时的回调函数类型定义
// 参数说明:
// 1. const muduo::net::TcpConnectionPtr& - 发生错误的TCP连接
// 2. muduo::net::Buffer* - 包含原始数据的缓冲区指针
// 3. muduo::Timestamp - 错误发生的时间戳
// 4. ErrorCode - 错误代码,表示具体的错误类型(如消息格式错误、长度错误等)
typedef std::function<void (const muduo::net::TcpConnectionPtr&,
muduo::net::Buffer*,
muduo::Timestamp,
ErrorCode)> ErrorCallback;
// 构造函数1:只传入消息回调,使用默认的错误回调
// 使用场景:用户只关心成功解析的消息,对错误处理使用默认逻辑(如断开连接)
explicit ProtobufCodec(const ProtobufMessageCallback& messageCb)
: messageCallback_(messageCb), // 初始化消息回调
errorCallback_(defaultErrorCallback) // 使用默认错误回调
{
}
// 构造函数2:传入消息回调和自定义错误回调
// 使用场景:用户需要自定义错误处理逻辑(如发送错误响应、记录日志等)
ProtobufCodec(const ProtobufMessageCallback& messageCb, const ErrorCallback& errorCb)
: messageCallback_(messageCb), // 初始化消息回调
errorCallback_(errorCb) // 使用用户提供的错误回调
{
}
// ... 其他成员函数 ...
private:
ProtobufMessageCallback messageCallback_; // 成功解析消息时的回调函数
ErrorCallback errorCallback_; // 解析错误时的回调函数
};
可以看到messageCallback_就是ProtobufCodec里面的一个私有成员变量,只不过呢,messageCallback_的类型是一个回调函数类型。
我们仔细观察一下ProtobufCodec类的构造函数:
可以看到,这个ProtobufCodec对象在被构造的时候就需要传递一个消息处理的回调函数进去,然后这个消息处理的回调函数就被赋值给messageCallback_了
那么在Muduo给的样例里面,也就是codec的样例里面,他是传递了什么回调函数给ProtobufCodec对象呢?
我们直接去client.cc里面看看

可以看到,这个QueryClient类里面在构造ProtobufCodec类型私有成员变量codec_的时候,又给它传递了一个新的回调函数------ProtobufDispatcher类的onProtobufMessage接口!!
我们来到dispatcher.h里面找
cpp
/**
* 处理接收到的Protobuf消息
* 根据消息类型分发到对应的回调函数处理
*
* @param conn 发生消息的TCP连接
* @param message 接收到的Protobuf消息(基类指针)
* @param receiveTime 消息接收时间戳
*/
void onProtobufMessage(const muduo::net::TcpConnectionPtr& conn,
const MessagePtr& message,
muduo::Timestamp receiveTime) const
{
// 1. 根据消息的描述符(Descriptor)查找对应的回调函数
// message->GetDescriptor() 返回消息类型的描述符,类似消息的"身份证"
// 每个Protobuf消息类型都有唯一的描述符
CallbackMap::const_iterator it = callbacks_.find(message->GetDescriptor());
// 2. 如果找到对应消息类型的回调函数
if (it != callbacks_.end())
{
// 执行注册的特定消息类型的回调函数
// it->second 是对应的回调对象(如ProtobufMessageCallbackT<T>)
it->second->onMessage(conn, message, receiveTime);
}
// 3. 如果没有找到对应消息类型的回调函数
else
{
// 执行默认回调函数,处理未注册的消息类型
// 通常用于记录日志、发送错误响应等
defaultCallback_(conn, message, receiveTime);
}
}
这里是根据消息的描述符(Descriptor)查找对应的回调函数,那么不同消息描述符对应的回调函数哪里来?
这里有两种情况啊
- 第一种:我们注册了对应消息类型的回调函数,那么就调用我们自己注册的
- 第二种:我们没有注册,就调用默认的消息回调函数
首先来看看注册了的情况啊
我们需要怎么进行注册呢?
我们仔细看看dispatcher.h的其他部分
cpp
typedef std::map<const google::protobuf::Descriptor*, std::shared_ptr<Callback> > CallbackMap;
template<typename T>
void registerMessageCallback(const typename CallbackT<T>::ProtobufMessageTCallback& callback)
{
// 1. 创建一个 CallbackT<T> 的智能指针,用传入的回调函数构造
std::shared_ptr<CallbackT<T> > pd(new CallbackT<T>(callback));
// 2. 将回调函数存储到 map 中,键是消息类型的描述符
callbacks_[T::descriptor()] = pd;
}
这个接口非常巧妙,基于proto中的请求类型将我们自己的业务处理函数与对 应的请求给关联起来了
相当于通过这个成员变量中的CallbackMap能够知道收到什么请求后应该用什么处理函数进行处理
简单理解就是注册针对哪种请求--应该用哪个我们自己的函数进行处理的映射关系
但是我们自己实现的函数中,参数类型都是不一样的比如翻译有翻译的请求类型,加法有加法请求类型 而map需要统一的类型,这样就不好整了,所以用CallbackT对我们传入的 接口进行了二次封装。
我们发现注册的函数他会保存在callbacks_里面,而这个CallbackMap callbacks_;其实也是ProtobufDispatcher类的一个私有成员
cpp
class ProtobufDispatcher
{
......
private:
typedef std::map<const google::protobuf::Descriptor*, std::shared_ptr<Callback> > CallbackMap;
CallbackMap callbacks_;
......
};
也就是说,我们注册的回调函数,都会被保存在这个ProtobufDispatcher对象里面
嗯?官方给的例子里面这个注册的回调函数是啥?我们在哪里进行了注册啊?
我们回去client.cc看看
cpp
class QueryClient : noncopyable // 继承noncopyable,禁止拷贝构造和赋值
{
public:
QueryClient(EventLoop* loop,
const InetAddress& serverAddr)
: loop_(loop), // 初始化事件循环
// 初始化TcpClient,用于建立到服务器的连接
// 参数:事件循环、服务器地址、客户端名称
client_(loop, serverAddr, "QueryClient"),
// 初始化Protobuf消息分发器
// 绑定onUnknownMessage作为默认回调,处理未知消息类型
// _1, _2, _3是占位符,分别对应:TcpConnectionPtr, MessagePtr, Timestamp
dispatcher_(std::bind(&QueryClient::onUnknownMessage, this, _1, _2, _3)),
// 初始化Protobuf编解码器
// 绑定dispatcher_的onProtobufMessage作为消息回调
// 当codec_成功解析消息后,会调用dispatcher_进行消息分发
codec_(std::bind(&ProtobufDispatcher::onProtobufMessage, &dispatcher_, _1, _2, _3))
{
// 注册消息类型muduo::Answer的回调处理函数
// 当收到Answer类型的Protobuf消息时,调用onAnswer方法处理
dispatcher_.registerMessageCallback<muduo::Answer>(
std::bind(&QueryClient::onAnswer, this, _1, _2, _3));
// 注册消息类型muduo::Empty的回调处理函数
// 当收到Empty类型的Protobuf消息时,调用onEmpty方法处理
dispatcher_.registerMessageCallback<muduo::Empty>(
std::bind(&QueryClient::onEmpty, this, _1, _2, _3));
// 设置TcpClient的连接状态回调
// 当连接建立或断开时,调用onConnection方法
client_.setConnectionCallback(
std::bind(&QueryClient::onConnection, this, _1));
// 设置TcpClient的消息回调
// 当收到原始网络数据时,由codec_的onMessage方法进行解析
// codec_会将解析后的Protobuf消息传递给dispatcher_
client_.setMessageCallback(
std::bind(&ProtobufCodec::onMessage, &codec_, _1, _2, _3));
}
// ... 其他成员函数和成员变量 ...
void onAnswer(const muduo::net::TcpConnectionPtr&,
const AnswerPtr& message,
muduo::Timestamp)
{
LOG_INFO << "onAnswer:\n" << message->GetTypeName() << message->DebugString();
}
void onEmpty(const muduo::net::TcpConnectionPtr&,
const EmptyPtr& message,
muduo::Timestamp)
{
LOG_INFO << "onEmpty: " << message->GetTypeName();
}
};
我们发现这个QueryClient的构造函数里面就有两条注册回调函数的
- 注册消息类型muduo::Answer的回调处理函数------自己写的onAnswer
- 注册消息类型muduo::Empty的回调处理函数------自己写的onEmpty
那么问题来了,这个消息类型muduo::Answer,muduo::Empty是哪里来的??
事实上呢,这个类型其实是.proto文件编译生成的C++文件里面的东西!!
在它给出的样例里面其实是query.proto
cpp
package muduo;
option java_package = "muduo.codec.tests";
option java_outer_classname = "QueryProtos";
message Query {
required int64 id = 1;
required string questioner = 2;
repeated string question = 3;
}
message Answer {
required int64 id = 1;
required string questioner = 2;
required string answerer = 3;
repeated string solution = 4;
}
message Empty {
optional int32 id = 1;
}
消息类型是由 Protobuf 的package 名称 和 message 名称 组合而成的。
在这个例子中:
- package 名称:muduo
- message 名称:Query、Answer、Empty
所以完整的消息类型是:
- muduo.Query
- muduo.Answer
- muduo.Empty
在 C++ 代码中,编译 Protobuf 后会生成对应的类,这些类位于 muduo 命名空间下:
- muduo::Query
- muduo::Answer
- muduo::Empty
在消息分发器注册回调时,使用的就是这些完整的消息类型,例如:
cpp
dispatcher.registerMessageCallback<muduo::Query>(...);
在运行时,系统通过 google::protobuf::Descriptor 来唯一标识每个消息类型,这个描述符包含了完整的类型信息,包括 package 和 message 名称。
此外,还有一个问题,我们仔细看看我们注册的这个不同类型的消息回调函数,下面这两个是什么玩意???

这两个类型其实是下面这个
cpp
typedef std::shared_ptr<muduo::Empty> EmptyPtr;
typedef std::shared_ptr<muduo::Answer> AnswerPtr;
为什么要这样子写呢?其实还是因为那个注册函数的原因
cpp
typedef std::map<const google::protobuf::Descriptor*, std::shared_ptr<Callback> > CallbackMap;
template<typename T>
void registerMessageCallback(const typename CallbackT<T>::ProtobufMessageTCallback& callback)
{
// 1. 创建一个 CallbackT<T> 的智能指针,用传入的回调函数构造
std::shared_ptr<CallbackT<T> > pd(new CallbackT<T>(callback));
// 2. 将回调函数存储到 map 中,键是消息类型的描述符
callbacks_[T::descriptor()] = pd;
}
这个CallbackT是何方神圣?其实他就是下面这个
cpp
// CallbackT 模板类:用于包装特定 Protobuf 消息类型的回调函数
template <typename T>
class CallbackT : public Callback
{
// 编译时类型检查:确保模板参数 T 是 google::protobuf::Message 的派生类
static_assert(std::is_base_of<google::protobuf::Message, T>::value,
"T must be derived from gpb::Message.");
public:
// 定义类型化的回调函数类型
// 这是一个 std::function,接受三个参数:
// 1. TcpConnectionPtr: TCP 连接的智能指针,包含连接信息
// 2. shared_ptr<T>: 具体消息类型的智能指针(如 Query、Answer 等)
// 3. Timestamp: 消息接收时间戳
// 注意:这里的消息参数已经是具体的类型 T,用户无需再手动转型
typedef std::function<void (const muduo::net::TcpConnectionPtr&,
const std::shared_ptr<T>& message,
muduo::Timestamp)> ProtobufMessageTCallback;
// 构造函数:接收用户提供的具体类型的回调函数并保存
// 参数 callback 是用户注册的类型化回调函数
CallbackT(const ProtobufMessageTCallback& callback)
: callback_(callback) // 将用户回调保存到成员变量中
{
}
// 重写基类 Callback 的虚函数 onMessage
// 这是消息分发的入口点,当收到任意 Protobuf 消息时被调用
// 参数:
// conn: TCP 连接
// message: 基类 Message 的智能指针(通用消息类型)
// receiveTime: 消息接收时间
void onMessage(const muduo::net::TcpConnectionPtr& conn,
const MessagePtr& message, // 通用消息指针
muduo::Timestamp receiveTime) const override
{
// 关键步骤:将通用的 Message 指针向下转型为具体的消息类型 T
// down_pointer_cast 是 muduo 库提供的安全向下转型函数
// 类似于 dynamic_pointer_cast,但可能有更严格的断言检查
std::shared_ptr<T> concrete = muduo::down_pointer_cast<T>(message);
// 断言确保转型成功(在注册时已保证类型匹配,这里应该总是成功)
assert(concrete != NULL);
// 调用用户注册的类型化回调函数
// 传入具体的消息类型指针,用户可以直接使用具体类型的接口
callback_(conn, concrete, receiveTime);
}
private:
// 存储用户注册的类型化回调函数
// 当收到对应类型的消息时,会通过 onMessage 调用这个回调
ProtobufMessageTCallback callback_;
};
它定义了一个ProtobufMessageTCallback,我们传递进去的函数就必须符合这个结构!!
接下来我们看看没有注册特定消息类型的回调处理函数的情况
ProtobufDispatcher类的onProtobufMessage接口会根据消息的类型来寻找注册的消息的回调函数,如果没有找到,就调用默认的消息处理回调函数
那么问题又来了,默认的消息处理回调函数在哪里?
我们去看看ProtobufDispatcher类的onProtobufMessage类的源代码,发现是下面这个:
defaultCallback_(conn, message, receiveTime);
这又是何方神圣?
事实上,这个就是ProtobufDispatcher类的一个私有成员,只不过这个成员也是一个回调函数
cpp
class ProtobufDispatcher
{
public:
typedef std::function<void (const muduo::net::TcpConnectionPtr&,
const MessagePtr& message,
muduo::Timestamp)> ProtobufMessageCallback;
explicit ProtobufDispatcher(const ProtobufMessageCallback& defaultCb)
: defaultCallback_(defaultCb) // 初始化默认回调函数
{
}
......
ProtobufMessageCallback defaultCallback_;
};
我们发现在构造这个ProtobufDispatcher类的时候,就需要传递一个默认的消息处理函数进去,然后会被赋值给这个defaultCallback_
可以看到,这个默认的消息处理函数的格式必须是符合下面这个样子的
cpp
typedef std::function<void (const muduo::net::TcpConnectionPtr&,
const MessagePtr& message,
muduo::Timestamp)> ProtobufMessageCallback;
可以看到,与那些特定消息类型的消息回调函数的区别也就是第二个参数有区别
- 默认的消息处理回调函数的参数类型是MessagePtr
- 而那些特定的消息类型的消息处理回调函数的参数类型其实是std::shared_ptr<T>
而这个MessagePtr本质是什么?其实他就是下面这个东西
cpp
typedef std::shared_ptr<google::protobuf::Message> MessagePtr;
而这个google::protobuf::Message是个什么玩意?
google::protobuf::Message 是所有生成的 Protobuf 消息类的基类。
当我们使用 Protobuf 编译器(protoc)编译 .proto 文件时,会为每个 message 生成一个类,这些类都继承自 google::protobuf::Message。
说到这里你是不是就恍然大悟了,为什么他能作为默认的消息处理回调函数,因为什么类型的消息它都能进行处理!!!
那么我们现在去client.cc看看它赋值了什么进去
cpp
class QueryClient : noncopyable
{
public:
QueryClient(EventLoop* loop,
const InetAddress& serverAddr)
: loop_(loop),
client_(loop, serverAddr, "QueryClient"),
dispatcher_(std::bind(&QueryClient::onUnknownMessage, this, _1, _2, _3)),//自定义函数
codec_(std::bind(&ProtobufDispatcher::onProtobufMessage, &dispatcher_, _1, _2, _3))
{
......
}
void onUnknownMessage(const TcpConnectionPtr&,
const MessagePtr& message,
Timestamp)
{
LOG_INFO << "onUnknownMessage: " << message->GetTypeName();
}
};
可以看到,也是传递了一个默认的消息处理函数进去。
可以看到,这个ProtobufDispatcher类就保存了
- 我们注册的自定义消息处理函数
- 我们注册的自定义默认消息处理函数
很完美!!
整个的运行过程就有点像

ProtobufDispatcher 类,这个类就比较重要了,这是一个 protobuf 请求的分发处理类, 我们用户在使用的时候,就是在这个类对象中注册哪个请求应该用哪个业务函数进行 处理。 它内部的onProtobufMessage接口就是给上边 ProtobufCodec::messageCallback_设 置的回调函数,相当于ProtobufCodec中onMessage接口会设置给服务器作为消息 回调函数,其内部对于接收到的数据进行基于protobuf协议的解析,得到请求后,通 过ProtobufDispatcher::onProtobufMessage 接口进行请求分发处理,也就是确定当前 请求应该用哪一个注册的业务函数进行处理。
ProtobufCodec 类是 muduo库中对于protobuf协议的处理类,其内部实现了 onMessage回调接口,对于接收到的数据进行基于protobuf协议的请求处理,然后将 解析出的信息,存放到对应请求的protobuf请求类对象中,然后最终调用设置进去的 消息处理回调函数进行对应请求的处理。
5.2.自己实现Muduo库protobuf通信
由于我们这个项目需要使用到这个Muduo库,而我们的Muduo库是存在于这个build目录里面的

我们进去build目录看看



所以我们需要把这个/root/m/build/release-install-cpp11目录拷贝到我们的工作目录来
cpp
cp -r /root/m/build/release-install-cpp11/ ./muduo/

现在我们就基于Muduo库中,对protobuf协议的处理代码,实现一个翻译+加法服务器与客户端
- 编写.proto文件,生成相关结构代码
- 编写服务端代码,搭建服务器
- 编写客户端代码,搭建客户端
5.2.1.编写.proto文件,生成相关结构代码
如果说,对这个protobuf协议还不明白的话,可以去看看:【Protobuf】proto3语法详解_proto文件语法-CSDN博客
话不多说,我们直接先通过这个.proto协议来定制我们的通信协议
request.proto
cpp
// 指定使用 Protocol Buffers 语言版本 3 的语法
syntax = "proto3";
// 定义包名,用于组织和管理 protobuf 定义,避免命名冲突
package bitmq;
// 定义 RPC 翻译服务的请求消息结构
message TranslateRequest {
// 需要翻译的文本消息,字段编号为1(在 Protocol Buffers 中用于二进制编码标识)
string msg = 1;
}
// 定义 RPC 翻译服务的响应消息结构
message TranslateResponse {
// 翻译后的文本消息,字段编号为1
string msg = 1;
}
// 定义 RPC 加法服务的请求消息结构
message AddRequest {
// 第一个加数,使用无符号32位整数类型,字段编号为1
int32 num1 = 1;
// 第二个加数,使用无符号32位整数类型,字段编号为2
uint32 num2 = 2;
}
// 定义 RPC 加法服务的响应消息结构
message AddResponse {
// 加法运算的结果,使用无符号32位整数类型,字段编号为1
int32 result = 1;
}
接下来我们就通过命令来生成对应的结构文件
cpp
protoc --cpp_out=. request.proto

没有报错的!!
request.pb.cc和request.pb.h就这样子生成了!!
消息类型相关
消息类型是由 Protobuf 的package 名称 和 message 名称 组合而成的。
在这个例子中:
- package 名称:bitmq
- message 名称:TranslateRequest、TranslateResponse、AddRequest,AddResponse
所以完整的消息类型是:
- muduo.Query
- muduo.Answer
- muduo.Empty
在 C++ 代码中,编译 Protobuf 后会生成对应的类,这些类位于 muduo 命名空间下:
- bitmq::TranslateRequest
- bitmq::TranslateResponse
- bitmq::AddRequest
- bitmq::AddResponse
那么对于它们的消息处理回调函数的格式其实是需要满足下面这样子的(因为消息注册函数的参数类型就是下面这个)
cpp
typedef std::function<void (const muduo::net::TcpConnectionPtr&,
const std::shared_ptr<T>& message,
muduo::Timestamp)> ProtobufMessageTCallback;
为了方便std::shared_ptr<T>呢,我们就定义了下面的这些别名
cpp
typedef std::shared_ptr<bitmq::TranslateRequest> TranslateRequestPtr;
typedef std::shared_ptr<bitmq::AddRequest> AddRequestPtr;
typedef std::shared_ptr<bitmq::TranslateResponse> TranslateResponcePtr;
typedef std::shared_ptr<bitmq::AddResponse> AddResponcePtr;
服务端的只需要
cpp
typedef std::shared_ptr<bitmq::TranslateRequest> TranslateRequestPtr;
typedef std::shared_ptr<bitmq::AddRequest> AddRequestPtr;
客户端的只需要
cpp
typedef std::shared_ptr<bitmq::TranslateResponse> TranslateResponcePtr;
typedef std::shared_ptr<bitmq::AddResponse> AddResponcePtr;
当然,还需要加上这个默认的消息处理回调函数的
cpp
typedef std::shared_ptr<google::protobuf::Message> MessagePtr;
5.2.2.编写服务端代码,搭建服务器
我们现在就需要借助这个Muduo库的样例来搭建出这个Protobuf的服务器。
首先我们需要将上一小节讲的那个codec的案例的源代码给复制过来
大家还记得下面这个muduo-master目录其实是我们在官网下载的源代码目录!!

我们直接进去这个源代码目录里面拷贝别人的代码来

注意我们只需要拷贝3个文件过来!!
codec.h,codec.cc,dispatcher.h
cpp
cp /root/m/muduo-master/examples/protobuf/codec/codec.h . && \
cp /root/m/muduo-master/examples/protobuf/codec/codec.cc . && \
cp /root/m/muduo-master/examples/protobuf/codec/dispatcher.h .
那么拷贝到哪里呢?

实际就拷贝到源代码目录里面去了
我们现在只需要包括这个头文件就能进行搭建这个服务器
如果说,我们不懂怎么设计的话,其实我们完全可以去看看例子codec例子里面看看别人的是怎么进行设计的
事实上呢,这个服务端的代码其实是仿照server.cc来写的

cpp
#include "muduo/include/muduo/protobuf/dispatcher.h"
#include "muduo/include/muduo/protobuf/codec.h"
#include "request.pb.h"
#include "muduo/include/muduo/base/Logging.h"
#include "muduo/include/muduo/base/Mutex.h"
#include "muduo/include/muduo/net/EventLoop.h"
#include "muduo/include/muduo/net/TcpServer.h"
#include<iostream>
#include<unordered_map>
#include<string>
using muduo::net::TcpConnectionPtr;
using muduo::Timestamp;
class Server{
public:
//这个是为了默认的消息处理函数的第二个参数设定的
typedef std::shared_ptr<google::protobuf::Message> MessagePtr;
//下面这些都是为了某个特定的消息处理函数的第二个参数设定的
typedef std::shared_ptr<bitmq::TranslateRequest> TranslateRequestPtr;
typedef std::shared_ptr<bitmq::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<bitmq::TranslateRequest>(
std::bind(&Server::onTranslateMessage, this,
std::placeholders::_1,
std::placeholders::_2,
std::placeholders::_3));
dispatcher_.registerMessageCallback<bitmq::AddRequest>(
std::bind(&Server::onAddMessage, this,
std::placeholders::_1,
std::placeholders::_2,
std::placeholders::_3));
server_.setConnectionCallback(std::bind(&Server::onConnection, this, std::placeholders::_1));
server_.setMessageCallback(std::bind(&ProtobufCodec::onMessage, &codec_,
std::placeholders::_1,
std::placeholders::_2,
std::placeholders::_3));
}
void start()
{
server_.start();
baseloop_.loop();
}
private:
//连接成功/断开时才会被调用
void onConnection(const TcpConnectionPtr& conn)
{
if(conn->connected()==true)//连接成功
{
LOG_INFO <<"新连接建立成功";
}
else//断开连接
{
LOG_INFO <<"连接断开";
}
}
//默认消息处理函数
void onUnknownMessage(const TcpConnectionPtr& conn,
const MessagePtr& message,
Timestamp)
{
LOG_INFO << "未知消息类型: " << message->GetTypeName();
}
//当消息类型是bitmq::TranslateRequest才会被回调调用的消息处理回调函数
void onTranslateMessage(const TcpConnectionPtr& conn,
const TranslateRequestPtr& message,
Timestamp)
{
//1.提取message里面的有效消息,也就是需要翻译的内容
std::string req_msg=message->msg();
//2.进行翻译,得到结果
std::string rsp_msg=translate(req_msg);
//3.组织protobuf的响应,因为请求是bitmq::TranslateRequest,所以我们需要使用bitmq::TranslateResponse进行响应
bitmq::TranslateResponse resp;
resp.set_msg(rsp_msg);
//4.bitmq::TranslateResponse本质是个二进制对象,还需进行序列化,所以不能使用下面这个
//conn->send(resp);
codec_.send(conn,resp);
}
//当消息类型是bitmq::AddRequest才会被回调调用的消息处理回调函数
void onAddMessage(const TcpConnectionPtr& conn,
const AddRequestPtr& message,
Timestamp)
{
int num1=message->num1();
int num2=message->num2();
int result=num1+num2;
//因为请求是bitmq::AddRequest,所以我们需要使用bitmq::AddResponse进行响应
bitmq::AddResponse resp;
resp.set_result(result);
codec_.send(conn,resp);
}
// 翻译函数:实现英汉互译
std::string translate(const std::string &msg) {
// 静态字典映射表
static std::unordered_map<std::string, std::string> dict_map = {
{"Hello", "你好"},
{"你好", "Hello"},
{"World", "世界"},
{"世界", "World"},
};
// 在字典中查找对应的翻译
auto it = dict_map.find(msg);
if (it == dict_map.end()) {
return "不认识"; // 未找到对应的翻译
}
return it->second; // 返回翻译结果
}
private:
muduo::net::EventLoop baseloop_;
muduo::net::TcpServer server_;//服务器对象
ProtobufDispatcher dispatcher_;//请求分发器对象------要向里面注册请求处理函数
ProtobufCodec codec_;//protobuf协议处理器------针对收到的信息进行protobuf协议处理
};
int main()
{
Server server(8085);
server.start();
}
5.2.3.编写客户端代码,搭建客户端
我们这个客户端还是按照别人的样例来进行编写的啊

仿照别人的代码来进行编写
cpp
#include "muduo/include/muduo/protobuf/dispatcher.h"
#include "muduo/include/muduo/protobuf/codec.h"
#include "request.pb.h"
#include "muduo/include/muduo/base/Logging.h"
#include "muduo/include/muduo/base/Mutex.h"
#include "muduo/include/muduo/net/EventLoop.h"
#include "muduo/include/muduo/net/TcpClient.h"
#include "muduo/include/muduo/net/EventLoopThread.h"
#include "muduo/include/muduo/base/CountDownLatch.h"
#include "muduo/include/muduo/net/TcpConnection.h"
#include <stdio.h>
#include <unistd.h>
using muduo::net::TcpConnectionPtr;
using muduo::Timestamp;
class Client
{
public:
typedef std::shared_ptr<google::protobuf::Message> MessagePtr;
typedef std::shared_ptr<bitmq::TranslateResponse> TranslateResponsePtr;
typedef std::shared_ptr<bitmq::AddResponse> AddResponsePtr;
Client(const std::string &ip, int port) : _connect_latch(1),
_loopthread(),
_baseloop(_loopthread.startLoop()),
_client(_baseloop, muduo::net::InetAddress(ip, 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<bitmq::TranslateResponse>(
std::bind(&Client::onTranslateMessage, this,
std::placeholders::_1,
std::placeholders::_2,
std::placeholders::_3));
dispatcher_.registerMessageCallback<bitmq::AddResponse>(
std::bind(&Client::onAddMessage, this,
std::placeholders::_1,
std::placeholders::_2,
std::placeholders::_3));
_client.setConnectionCallback(std::bind(&Client::onConnection, this, std::placeholders::_1));
_client.setMessageCallback(std::bind(&ProtobufCodec::onMessage, &codec_,
std::placeholders::_1,
std::placeholders::_2,
std::placeholders::_3));
}
// 连接到服务器
void connect() {
_client.connect(); // 发起异步连接请求
// 因为连接是异步操作,外部发送消息的时候有可能连接还没有真正建立成功
// 因此这里通过门闩同步等待连接完成后的唤醒
_connect_latch.wait();//同步门闩的初始值是1,故主线程会阻塞在这个wait()里面
}
// 断开连接
void shutdown() {
_client.disconnect(); // 断开与服务器的连接
}
// 发送翻译请求
void translate(std::string &msg) {
bitmq::TranslateRequest req;
req.set_msg(msg);
//注意这个Muduo库都是异步的情况,我们得先保证这个连接的存在
if (_conn && _conn->connected()) //TcpConnectionPtr存在,也就是连接存在
{
//通过TcpConnectionPtr来发送信息
codec_.send(_conn,req); // 如果连接有效,发送消息到服务器
}
}
//发送加法请求
void Add(int num1,int num2)
{
bitmq::AddRequest req;
req.set_num1(num1);
req.set_num2(num2);
//注意这个Muduo库都是异步的情况,我们得先保证这个连接的存在
if (_conn && _conn->connected()) //TcpConnectionPtr存在,也就是连接存在
{
//通过TcpConnectionPtr来发送信息
codec_.send(_conn,req); // 如果连接有效,发送消息到服务器
}
}
private:
//连接成功或者断开连接时的回调函数
void onConnection(const TcpConnectionPtr &conn) {
if (conn->connected()) { // 连接建立成功
_conn = conn; // 保存连接对象指针-TcpConnectionPtr
_connect_latch.countDown(); // 门闩计数减1,唤醒等待线程
std::cout << "连接服务器成功!\n";
}
else // 连接断开
{
_conn.reset(); // 通过TcpConnectionPtr重置连接指针(置为空)
std::cout << "连接断开!\n";
}
}
// 默认消息处理函数
void onUnknownMessage(const TcpConnectionPtr &conn,
const MessagePtr &message,
Timestamp)
{
std::cout << "未知消息类型: " << message->GetTypeName();
}
// 当消息类型是bitmq::TranslateResponse才会被回调调用的消息处理回调函数
void onTranslateMessage(const TcpConnectionPtr &conn,
const TranslateResponsePtr &message,
Timestamp)
{
std::cout <<"翻译结果: "<<message->msg()<<std::endl;
}
// 当消息类型是bitmq::AddResponse才会被回调调用的消息处理回调函数
void onAddMessage(const TcpConnectionPtr &conn,
const AddResponsePtr &message,
Timestamp)
{
std::cout <<"加法结果: "<<message->result()<<std::endl;
}
private:
muduo::CountDownLatch _connect_latch; // 连接同步门闩,用于等待异步连接完成
muduo::net::EventLoopThread _loopthread; // 事件循环线程(在独立线程中运行事件循环)
muduo::net::EventLoop *_baseloop; // 事件循环指针,指向_loopthread中的事件循环
muduo::net::TcpClient _client; // TCP客户端对象
muduo::net::TcpConnectionPtr _conn; // TCP连接对象指针,它指向了TcpConnection,而TcpConnection保存当前活动的连接
ProtobufDispatcher dispatcher_;
ProtobufCodec codec_;
};
int main()
{
Client client("127.0.0.1",8085);
client.connect();
std::string msg = "Hello";
client.translate(msg);
client.Add(11,22);
// 等待一段时间,确保收到回复
sleep(2);
client.shutdown();
}
现在我们就去
5.2.4.编译运行
我们直接编写makefile
all:server client
server:protobuf_server.cpp ./muduo/include/muduo/protobuf/codec.cc request.pb.cc
g++ -std=c++11 $^ -o $@ -I muduo/include -L muduo/lib -lmuduo_net -lmuduo_base -lpthread -lprotobuf -lz
client:protobuf_client.cpp ./muduo/include/muduo/protobuf/codec.cc request.pb.cc
g++ -std=c++11 $^ -o $@ -I muduo/include -L muduo/lib -lmuduo_net -lmuduo_base -lpthread -lprotobuf -lz

错误信息显示在编译 muduo 的 codec.cc 时,它试图包含一个头文件 "examples/protobuf/codec/codec.h",但是这个头文件在当前编译环境中不存在。
我们进去codec.h修改一下

我们接着编译

他说这个code.cc还需要一个头文件,这个头文件我们也去给他拷贝过来

注意拷贝到和codec.cc同一目录下面来啊

然后我们进去codec.cc里面修改一下


编译成功了
直接运行好吧

我们打开另外一个终端

完美啊
我们回去看看我们的server

也是一点问题都没有!!!!