目录
一、Moduo 库是什么
1.1 Moduo 库概念
Muduo由陈硕大佬开发,是⼀个基于非阻塞IO和事件驱动的C++高并发TCP⽹络编程库。它是⼀款基于主从Reactor模型的⽹络库,其使⽤的线程模型是 one loop per thread ,它指的是
- ⼀个线程只能有⼀个事件循环(EventLoop),⽤于响应计时器和IO事件
- ⼀个⽂件描述符只能由⼀个线程进⾏读写,换句话说就是⼀个TCP连接必须归属于某个EventLoop管理
1.2 Reactor 模式
原始的方案是每当有用户连接时,系统都为用户分配一个线程(或进程,下面统称线程)进行服务,但是如果用户已连接却一直无操作而且不退出,就会导致该线程被一直占用,倘若有很多这样的用户,就会造成系统资源极大的浪费:
为了提高效率,出现了多路转接模型,当一个执行流中有一个多路转接模型进行 socket 事件监控,触发lO事件后进行IO处理的这种通信处理模型叫做 Reactor 模型:
在 Reactor 模型中,底层会使用 select/poll/epoll 机制,系统会对监听套接字进行注册,然后注册关心的事件,当某个套接字事件就绪时,就会分配线程或进程对套接字进行事件处理。
当使用 Reactor 模型时,会有一个主循环,负责调用 select、poll 或 epoll 等函数来监视所有注册的套接字。一旦某个套接字的某个事件就绪(例如可读或可写),相应的处理器或处理函数就会被触发,进而处理这个事件,如接受新的连接、读取数据或发送数据等。
如果想深入了解,可以参考以下博客:
1.3 Moduo 库的原理
多路转接模型(如select或poll)在连接数量过多时,可能会导致性能下降,特别是当所有连接都需要频繁轮询时。
由于所有连接都在一个事件循环中处理,处理时间较长的连接可能会影响到其他连接的响应时间,从而导致一些连接得不到及时响应。
为了应对这种情况,可以采用更高效的模型,
例如使用主从(父子)Reactor模式。
主Reactor负责监听所有连接请求,并将这些请求分发给多个子Reactor。每个子Reactor处理一部分连接,从而将负载分散,提升整体响应效率。这种方式可以避免单个Reactor处理过多连接带来的性能瓶颈。
这种架构通常用于高并发场景下,以提高系统的伸缩性和响应速度。
二、Muduo 库常见接口
2.1 TcpServer类
TcpServer 类负责管理网络服务端的行为,包括启动服务器、设置网络连接的回调函数以及消息处理的回调函数。这样可以灵活地定义在不同网络事件发生时执行的具体操作,例如接受新连接、连接断开或数据接收等。
下面来认识一下 TcpServer 的主要接口,具体的使用会在以后进行讲解:
class TcpServer
{
TcpServer(EventLoop* loop, const InetAddress& listenAddr, const string& nameArg, Option option = kNoReusePort);
void start();// 启动服务器
setConnectionCallback();// 设置连接建立/关闭时的回调函数
setMessageCallback();// 设置消息处理回调函数
}
2.2 EventLoop类
EventLoop 类是事件循环的核心,负责启动和管理事件监控循环。通过这个循环,服务器可以非阻塞地监听和响应网络事件(如新的连接请求、数据到达等)。它还支持通过定时任务来调度将来某一时刻执行的任务,这对于需要定时检查或更新状态的应用非常有用。
class EventLoop
{
void loop();// 开始事件监控循环
void quit();// 停止循环
Timerld runAfter(delay, cb);// 定时任务
};
2.3 TcpConnection类
TcpConnection 类管理着每个网络连接的状态,提供发送数据、检查连接状态和关闭连接的功能。它的设计使得每个连接可以独立地处理其生命周期内的各种事件。
class TcpConnection
{
TcpClient(EventLoop* loop, const InetAddress& serverAddr, const string& nameArg);
void send(std::string &msg);// 发送数据
bool connected();// 当前连接是否连接正常
void shutdown();// 关闭连接
};
2.4 TcpClient类
TcpClient 是 Muduo 库中用于处理 TCP 客户端连接的类,它通过 EventLoop 来监控和处理IO事件。用户可以使用 connect() 和 disconnect() 控制连接,并通过 setConnectionCallback() 和 setMessageCallback() 设置回调函数,处理连接状态的变更和消息的接收。
class TcpClient()
{
void connect();// 连接服务器
void disconnect();// 关闭连接
TcpConnectionPtr connection() const;// 获取客⼾端对应的TcpConnection连接
//Moduo库的客户端也是通过EventLoop进行IO事件监控IO处理的
void setConnectionCallback(ConnectionCallback cb);// 连接服务器成功时的回调函数
void setMessageCallback(MessageCallback cb);// 收到服务器发送的消息时的回调函数
};
2.5 Buffer类
Buffer 类用于高效地管理数据缓冲区。它提供一系列操作来读取和处理存储在缓冲区中的数据,支持网络字节序的转换和数据的基本处理,如读取、检索和删除操作。这是处理TCP流数据的关键部分,因为网络数据可以随机到达,并且可能需要积累足够的数据才能进行处理。
class Buffer
{
size_t readableBytes() const;// 获取缓冲区大小
const char* peek() const;// 获取缓冲区中数据的起始地址
int32_t peekInt32() const;// 尝试从缓冲区获取4字节数据,进行网络字节序转换为整型,但不从缓冲区删除
void retrieveInt32();// 数据读取位置向后偏移4字节(本质就是删除起始位置的4字节数据)
int32_t readInt32();
string retrieveAllAsString();// 从缓冲区取出所有数据并删除,以string形式返回
string retrieveAsString(size_t len);// 从缓冲区读取len长度的数据并删除,以string形式返回
};
2.6 CountDownLatch 类
下面来看一下 muduo 库是如何实现的:
muduo::CountDownLatch 的核心思想是多个线程可以等待,直到计数器倒数到零,再同时继续执行。这与条件变量和互斥锁的配合有些相似:
- 互斥锁:保证对共享资源的互斥访问。
- 条件变量:允许线程在某个条件满足时被唤醒。
muduo::CountDownLatch 的工作方式可以理解为一种简化的同步机制:
- 主线程或某个线程可以阻塞,等待其他线程完成某些操作(类似于条件变量的等待)。
- 多个线程并行执行,并在执行结束后调用
countDown()
减少计数器,计数器减少到 0 时,等待的线程将被唤醒(类似于条件变量中的notifyAll()
或broadcast()
操作)。但与条件变量和互斥锁不同的是,muduo::CountDownLatch 更加简洁,它只关心计数器的变化,而不需要显式管理锁和条件的复杂逻辑。
在我们的客户端使用该类时,这种方式通过使用倒计时锁存器来确保在客户端与服务器建立连接之前,主线程会阻塞等待。这样可以避免主线程在连接尚未建立时就尝试发送数据,导致发送失败。只有在CountDownLatch
的计数器减为零时,主线程才会继续执行,这意味着连接已经建立。
下面是 CoutDownLatch 的部分接口:
CountDownLatch::CountDownLatch(int count)
: mutex_(),
condition_(mutex_),
count_(count)
{
}
void CountDownLatch::wait()
{
MutexLockGuard lock(mutex_);
while (count_ > 0)
{
condition_.wait();
}
}
void CountDownLatch::countDown()
{
MutexLockGuard lock(mutex_);
--count_;
if (count_ == 0)
{
condition_.notifyAll();
}
}
2.7 EventLoopThread 类
在客户端中一般要使用 EventLoopThread 类,其主要目的是为了 避免阻塞主线程 并实现高效的异步网络事件处理。可以确保客户端的事件循环能够在独立线程中运行,防止主线程因等待网络事件(如连接、数据传输)而阻塞。
具体来说,使用 EventLoopThread 具有以下几个好处和目的:
1. 防止主线程阻塞
网络 I/O 操作通常是异步的,如果在主线程中处理网络 I/O 事件(如等待连接、处理数据),主线程可能会被阻塞,无法处理其他逻辑。因此,
EventLoopThread
将EventLoop
放在独立的线程中运行,保证主线程能够继续处理其他任务,不受网络事件的影响。2. 提高异步处理效率
网络 I/O 操作(如 TCP 连接的建立、数据发送与接收)往往是异步的。如果不将这些操作放在一个独立的事件循环线程中处理,主线程可能需要不断轮询(polling)来检查连接是否建立,或等待数据到达,这样会浪费 CPU 资源。而 EventLoopThread 使用 Reactor 模型,事件触发时自动唤醒线程进行处理,极大提高了 CPU 的利用效率。
3. 处理连接的异步通知
当客户端调用 connect() 方法发起 TCP 连接时,这个连接操作是异步的。客户端不会立即知道连接是否成功,这就需要一个机制在连接成功时通知客户端程序。在 DictClient 的例子中,客户端通过 EventLoopThread 来监听服务端的响应:
- connect() 方法发起连接后,EventLoopThread 中的 EventLoop 开始监听事件。
- 当服务端可以接收连接时,客户端的 EventLoop 收到连接成功的事件通知。
- 这个事件被传递到 DictClient::onConnection ,然后唤醒等待的线程,使客户端知道连接已经建立。
class EventLoopThread : noncopyable
{
public:
typedef std::function<void(EventLoop*)> ThreadInitCallback;
EventLoopThread(const ThreadInitCallback& cb = ThreadInitCallback(),
const string& name = string());
~EventLoopThread();
EventLoop* startLoop();
private:
void threadFunc();
EventLoop* loop_ GUARDED_BY(mutex_);
bool exiting_;
Thread thread_;
MutexLock mutex_;
Condition cond_ GUARDED_BY(mutex_);
ThreadInitCallback callback_;
};
EventLoopThread::EventLoopThread(const ThreadInitCallback& cb,
const string& name)
: loop_(NULL),
exiting_(false),
thread_(std::bind(&EventLoopThread::threadFunc, this), name),
mutex_(),
cond_(mutex_),
callback_(cb)
{
}
EventLoopThread::~EventLoopThread()
{
exiting_ = true;
if (loop_ != NULL) // not 100% race-free, eg. threadFunc could be running callback_.
{
// still a tiny chance to call destructed object, if threadFunc exits just now.
// but when EventLoopThread destructs, usually programming is exiting anyway.
loop_->quit();
thread_.join();
}
}
EventLoop* EventLoopThread::startLoop()
{
assert(!thread_.started());
thread_.start();
EventLoop* loop = NULL;
{
MutexLockGuard lock(mutex_);
while (loop_ == NULL)
{
cond_.wait();
}
loop = loop_;
}
return loop;
}
void EventLoopThread::threadFunc()
{
EventLoop loop;
if (callback_)
{
callback_(&loop);
}
{
MutexLockGuard lock(mutex_);
loop_ = &loop;
cond_.notify();
}
loop.loop();
//assert(exiting_);
MutexLockGuard lock(mutex_);
loop_ = NULL;
}
仅仅是看接口还是比较抽象,不要担心,接口仅仅是混个眼熟,下面来写一个字典demo就可以比较清晰的了解Muduo库的具体使用啦!
三、 编写字典服务端
3.1 头文件包含
// 搭建服务器包含的四个头文件
#include <muduo/net/TcpServer.h>
#include <muduo/net/TcpConnection.h>
#include <muduo/net/EventLoop.h>
#include <muduo/net/Buffer.h>
#include <iostream>
#include <string>
#include <unordered_map>
3.2 基本框架
搭建 tcp 服务器,那么类成员肯定有一个 TcpServer ,定义该对象可以使用 muduo::net::TcpServer 类:
class DictServer
{
public:
DictServer(int port) : server_()
{
}
private:
muduo::net::TcpServer server_;
};
当对 server_ 进行初始化时,我们可以看到需要传入的参数:
传入的首个参数就是 muduo::net::EventLoop 类,所以就不可避免的需要在 DictServer 类中定义一个私有成员 muduo::net::EventLoop 类对象;其次,因为是 demo ,监听地址可以直接设置为 "0.0.0.0" ,表示服务器将监听所有可用的网络接口,端口号需要传入我们云服务器上开通的端口号;名字我们可以任意取;是否启动地址重用(地址重用(Address Reuse)在网络连接中指的是在特定条件下,允许多个网络连接或应用程序复用同一个IP地址或端口号。)在这里是需要的,所以设置为 muduo::net::TcpServer::kReusePort
class DictServer
{
public:
DictServer(int port) : server_(&baseloop_, muduo::net::InetAddress("0.0.0.0", port), "DictServer", muduo::net::TcpServer::kReusePort)
{
}
private:
muduo::net::EventLoop baseloop_;
muduo::net::TcpServer server_;
};
在构造函数中,还需要设置两个回调, 也就是在 TcpServer 常用接口中提到的:
现在我们不急着完成构造函数中的内容,先来看看两个回调函数的写法。
3.3 两个回调函数
这两个回调函数其实没有什么难度,只是单纯的C++语法。
3.3.1 传入参数介绍
首先来看看两个回调函数各自的传入参数,注意都有 const 修饰:
3.3.2 连接建立/断开时回调
在连接建立或关闭时,我想要看到是否连接成功或失败了,这时可以使用 muduo::net::TcpConnection 类中的 bool connected() 函数进行检查即可,注意这里使用的是muduo::net::TcpConnectionPtr ,且该对象是作为传参设置的:
void onConnection(const muduo::net::TcpConnectionPtr &conn)
{
if (conn->connected())
{
std::cout << "新连接建立成功" << std::endl;
}
else
{
std::cout << "连接断开成功" << std::endl;
}
}
注:为什么传入参数一定要使用 TcpConnectionPtr ?
在Muduo库中,
TcpConnection
对象的生命周期是由库内部进行管理的,因此不能直接使用一个TcpConnection
对象,而是必须使用TcpConnectionPtr
,这是一个boost::shared_ptr<TcpConnection>
类型的智能指针。这样做的原因是为了确保连接对象的正确管理和内存安全。具体来说,
TcpConnection
的生命周期和事件循环(EventLoop)紧密相关,使用智能指针能够保证在多个地方引用TcpConnection
时,不会因为提前释放导致未定义行为。在你提供的函数
onConnection
中,传入参数muduo::net::TcpConnectionPtr &conn
是一个智能指针的引用,能够避免复制智能指针对象,同时也保证了对连接对象的引用计数得以正确维护。因此,直接使用一个
TcpConnection
对象是不行的,必须使用TcpConnectionPtr
。
3.3.3 消息处理回调
现在来看第二个回调函数,
在Muduo库中,客户端发送的消息会被存储在 Buffer
类中。在代码中,通过buf->retrieveAllAsString()
函数将Buffer
中的数据转换成一个字符串,然后使用这个字符串在dict_map
中进行查找。这样可以确保获取到客户端发送的消息并在映射中查找相应的翻译结果。
void onMessage(const muduo::net::TcpConnectionPtr &conn, muduo::net::Buffer *buf, muduo::Timestamp)
{
std::unordered_map<std::string, std::string> dict_map = {
{"technology", "科技"}, {"change", "改变"}, {"life", "生活"},
{"happy", "开心的"}, {"excited", "激动的"}, {"love", "爱"}};
std::string msg = buf->retrieveAllAsString();
auto it = dict_map.find(msg);
if (it == dict_map.end()) // 未查找到
{
std::cout << "很抱歉,超出认知底线" << std::endl;
conn->send("Not found in dictionary\n"); // 向客户端返回错误信息
return;
}
conn->send(it->second);
}
3.4 补充构造函数
在前面我们已经知道了构造函数中设置回调时的参数,但是在类中会默认传入 this 指针,为了避免这种情况的影响,我们要进行函数的适配,可以使用 std::bind 对参数进行绑定,生成一个新的可调用对象,这个对象可以根据需要进行参数的传入。
DictServer(int port) : server_(&baseloop_, muduo::net::InetAddress("0.0.0.0", port), "DictServer", muduo::net::TcpServer::kReusePort)
{
// 设置连接事件(连接建立/管理)的回调
server_.setConnectionCallback(std::bind(&DictServer::onConnection, this, std::placeholders::_1));
//设置连接消息的回调
server_.setMessageCallback(std::bind(&DictServer::onMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
}
注:为什么默认传入 this 指针不可以后,进行了方案的修改,但最后还要显示传入 this 指针呢?
std::bind
可以将成员函数与对象实例绑定在一起。onConnection
和onMessage
都是DictServer
类的成员函数,它们在调用时需要一个具体的对象实例来访问类的成员变量和其他成员函数。因此,在std::bind
中传入this
指针,就是为了让绑定后的可调用对象能够正确地调用DictServer
实例的成员函数。总结一下,
this
指针确保了onConnection
和onMessage
函数在调用时,能够访问到DictServer
实例的内部状态和行为,这样它们才能正常工作。
3.5 补充
3.5.1 回调函数作用域
因为回调函数是在类内调用,所以3.3的两个回调函数作用域不要忘了是 private 哦!
3.5.2 启动函数
对类外提供一个启动函数,这样类外就可以只调用一个函数来完成服务器的建立啦:
void start()
{
server_.start();
baseloop_.loop();
}
3.5.3 main 函数
main 函数需要完成对 TcpServer 端口号的传入以及 TcpServer 的启动,这里假设使用 8085 端口
int main()
{
DictServer server(8085);
server.start();
return 0;
}
3.5.4 成员遍历声明顺序
在 DictServer
构造函数中,server_
的初始化顺序可能会导致问题,因为 server_
依赖于baseloop_
,但初始化顺序是按照成员变量声明的顺序,而不是初始化列表中的顺序。所以要确保成员变量声明顺序与初始化顺序一致,即先声明 baseloop_
,再声明 server_
。
四、编写字典客户端
4.3 基本框架
这里需要使用到 2.5CountDownLatch 与 2.6EventLoopThread 。
4.3.1 CountDownLatch 的使用
因为这⾥的连接是异步操作,因此外部发消息的时候有可能连接还没有真的建⽴成功,所以在客户端使用 CountDownLatch 主要是为了线程阻塞,它是一个计数器,用于同步等待连接的建立。我们可以在构造函数中将其初始值为 1,当连接成功时,通过调用 CountDownLatch::countDown() 来递减计数,当其计数变为 0 时,就会唤醒等待线程。
4.3.2 EventLoopThread 的使用
单独使用一个线程来控制 EventLoop 是为了将网络事件处理与主线程隔离开,以确保异步网络操作的高效执行。
EventLoop 是基于 Reactor 模型的事件驱动机制,负责监听网络事件(如连接建立、数据可读、数据可写等)并触发相应的回调函数。可以使用 EventLoopThread::startLoop() 启动一个 EventLoop ,让它持续运行并监听这些事件。
我们知道服务端在初始化时要设置两个回调函数,那么客户端在学习 TcpClient 类时,当服务端响应连接请求或有数据到达时,EventLoop 线程会检测到这些事件并触发回调函数,执行对应的逻辑。由于这些操作是在独立的事件循环线程中进行的,因此主线程可以专注于其他任务,提升整体效率。
4.3.3 类的私有成员
下面会列出类的私有成员,并在代码后进行解释:
private:
bool isconnect_;
muduo::CountDownLatch connect_latch_;
muduo::net::EventLoopThread loopthread_;
muduo::net::EventLoop *baseloop_;
muduo::net::TcpClient client_;
muduo::net::TcpConnectionPtr _conn;
bool isconnect_
表示是否需要同步等待连接成功。因为连接操作是异步的,外部可能在连接未完成时就开始发送消息,导致问题。如果设置为 true
,则会通过 CountDownLatch
机制阻塞调用,直到连接建立成功。
muduo::CountDownLatch connect_latch_
一个计数器,用于同步等待连接的建立。其初始值为 1,当连接成功时,通过调用 countDown()
来递减计数,唤醒等待线程。如果 _is_wait_connect
为 true
,调用 connect()
时会阻塞,直到连接完成。
muduo::net::EventLoopThread loopthread_
表示一个专门用于事件处理的线程。Muduo 使用 Reactor 模型处理网络事件,EventLoopThread
提供了一个独立的线程来运行事件循环 (EventLoop
),以便处理客户端的连接和消息。
muduo::net::EventLoop *baseloop_
该指针指向事件循环对象,是由 _loopthread
线程启动的。该循环用于监听和处理网络事件,如接受新连接、读取数据等。设置为指针的原因是为了与 muduo 库中的函数进行联动。
muduo::net::TcpClient client_
用于管理与服务器的 TCP 连接,它负责发起连接、维护连接状态,并提供接口发送和接收数据。该客户端对象依赖于 _baseloop
事件循环来驱动其异步操作。
muduo::net::TcpConnectionPtr conn_
这是一个智能指针,指向当前活动的 TCP 连接。当客户端成功与服务器建立连接时,conn_
会被赋值,以便进行数据发送和接收;当连接断开时,它会被重置为 nullptr
。
4.3.4 类的构造函数
class DictClient
{
public:
DictClient(const std::string &ip, int port, bool is_connect_ = true) : _is_wait_connect(is_wait_connect), _connect_latch(1), _baseloop(_loopthread.startLoop()), _client(_baseloop,muduo::net::InetAddress(ip, port), "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));
}
关于 is_connect_ 、 connect_latch_ 的初始化有关它们的特性,这里不在细说, _is_wait_connect 初始化为 true ,表示阻塞调用,与服务端连接成功后设置为 false ; _connect_latch 初始化为 1 ,因为它是用于线程阻塞,当 is_connect_ 连接不成功时,会使用它的成员函数 wait() 休眠线程;当连接成功时,使用 CountDownLatch::countDown() 将计数减为 0 ,就会唤醒线程。
baseloop_ 被初始化为 loopthread_.startLoop() 的返回值,这也就是为什么它需要是指针的原因,因为 startLoop() 返回一个指针对象,同时也调用了 loopthread_.startLoop() ,它用于新创建线程,该线程主要用于处理 I/O 事件监控和其他与事件循环相关的任务。当有新连接到来时,它会执行 EventLoop 的连接回调函数,也就是 setConnectionCallback() 。
最后就是对于 client_ 的初始化,分别是 EventLoop* ,也就是 baseloop_ ,服务端的 ip 与端口号,以及 client_ 的命名:
4.4 两个回调函数
4.4.1 连接建立/断开时回调
这里回调函数的写法参考服务端,当连接成功时,会使用 connect_latch_.countDown() ,就会唤醒阻塞的线程,关于线程在哪里阻塞,后面会有讲到。
void onConnection(const muduo::net::TcpConnectionPtr &conn)
{
if (conn->connected())
{
conn_ = conn;
if (isconnect_)
connect_latch_.countDown();
std::cout << "连接成功!\n";
}
else
{
std::cout << "连接失败!\n";
}
}
4.4.2 消息处理回调
这里回调函数的写法参考服务端,可以看出使用 Muduo 库时,不论是服务端收消息还是客户端收消息,都是要从 buffer 类中提取。
void onMessage(const muduo::net::TcpConnectionPtr &conn, muduo::net::Buffer *buf, muduo::Timestamp t)
{
std::string msg = buf->retrieveAllAsString();
std::cout << msg << std::endl;
}
4.5 客户端的连接
客户端最重要的肯定是类外提供的与服务端连接的接口啦,下面重点来看该接口:
void connect()
{
client_.connect();
// 因为这⾥的连接是异步操作,因此外部发消息的时候有可能连接还没有真的建⽴成功
// 因此这⾥同步等待连接完成后的唤醒
if (isconnect_)
connect_latch_.wait();
}
这里就是之前说的线程阻塞的地方啦!为了逻辑畅通,这里把 main 函数中的调用代码一起来讲
int main()
{
DictClient dict_client("127.0.0.1", 8085);
dict_client.connect();
while (1)
{
//用户输入消息进行翻译
}
return 0;
}
4.6 IO阻塞主要逻辑
在初始化时:
将 isconnect_ 设置为 true ,其目的是为了在 main 函数中调用 connect 接口时,如果连接不成功,可以使用 muduo::CountDownLatch::wait() 将整个主线程阻塞在 connect() 内部
在初始化 baseloop_ 是使用 loopthread_.startLoop() ,其目的是创建新线程,新线程会使用 EventLoop 方法不断轮询监控是否连接成功,如果连接成功会调用在 setConnectionCallback 绑定的方法 onConnection()
在 onConnection() 内部,再次检查连接是否建立成功(以下默认建立成功),因为初始化时将 connect_latch_ 设置为 1,所以在函数内使用 connect_latch_.countDown() 就可以重新唤醒线程执行 connect() 方法
至此,连接才算完整建立
4.7 客户端的业务处理
这里和服务端一样,比较简单,所以直接把客户端代码拿过来,大家可以进行参考对照:
#include "muduo/net/EventLoop.h"
#include "muduo/net/TcpClient.h"
#include "muduo/net/EventLoopThread.h"
#include "muduo/base/CountDownLatch.h"
#include <iostream>
#include <functional>
#include <unordered_map>
/*
在初始化时:
将 isconnect_ 设置为 true ,其目的是为了在 main 函数中调用 connect 接口时,如果连接不成功,可以使用 muduo::CountDownLatch::wait() 将整个主线程阻塞在 connect() 内部
在初始化 baseloop_ 是使用 loopthread_.startLoop() ,其目的是创建新线程,新线程会使用 EventLoop 方法不断轮询监控是否连接成功,如果连接成功会调用在 setConnectionCallback 绑定的方法 onConnection()
在 onConnection() 内部,再次检查连接是否建立成功(以下默认建立成功),因为初始化时将 connect_latch_ 设置为 1,所以在函数内使用 connect_latch_.countDown() 就可以重新唤醒线程执行 connect() 方法
至此,连接才算完整建立
*/
class DictClient
{
public:
DictClient(const string &sip, const string &sport) : isconnect_(true),
connect_latch_(1), baseloop_(loopthread_.startLoop()),
client_(baseloop_, const muduo::net::InetAddress &serverAddr("sip", "sport"), "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 (isconnect_)
connect_latch_.wait();
}
private:
void onConnection(const muduo::net::TcpConnectionPtr &conn)
{
if (conn->connected())
{
conn_ = conn;
if (isconnect_)
connect_latch_.countDown();
std::cout << "连接成功!\n";
}
else
{
std::cout << "连接失败!\n";
}
}
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 isconnect_;
muduo::CountDownLatch connect_latch_;
muduo::net::EventLoopThread loopthread_;
muduo::net::EventLoop *baseloop_;
muduo::net::TcpClient client_;
muduo::net::TcpConnectionPtr conn_;
};
int main()
{
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;
}
标签:Muduo,EventLoop,muduo,线程,关于,CPP,net,连接,connect From: https://blog.csdn.net/m0_75186846/article/details/141872034