关于C++集群聊天服务器,第一步:先用 muduo 网络库写一个非常简单的"回显聊天服务器",其他功能后面再一步一步添加实现。
一、实现前的了解
1.首先我们先要明确本次实现一个简单的"回显聊天服务器"要具备哪些功能:
- 客户端连接上来,服务器打印"上线了";
- 客户端发任何消息,服务器原样把消息再发回去(回显);
- 客户端断开连接,服务器打印"下线了"并关闭连接。
它展示了 muduo 网络库最核心的用法:把"网络部分"(连接、收发数据)和"业务部分"(这里就是回显)分开写,让代码更清晰。
2.接着我再讲述一下muduo 库的核心设计理念:
- 提供了 TcpServer(写服务器用)和 TcpClient(写客户端用)两个类,让你不用自己去操作底层的 socket。
- 底层用 epoll(高效的I/O多路复用) + 线程池。
- 最大的好处是:网络代码(监听连接、收发数据)muduo 帮你做好了,你只需要写"业务代码"(比如收到消息后怎么处理)。
联系:后面整个程序都在体现这个理念------只写了两个回调函数(onConnection 和 onMessage),其他网络细节 muduo 都帮你处理了。
二、实现步骤
1.头文件
咱们先来看看需要的头文件:
cpp
#include<muduo/net/TcpServer.h>
#include<muduo/net/EventLoop.h>
#include<iostream>
#include<string>
#include<functional>
using namespace std;
using namespace muduo;
using namespace muduo::net;
using namespace placeholders;
包含 muduo 库需要的头文件(TcpServer 和 EventLoop)。
包含标准库头文件(输入输出、字符串、函数绑定)。
using namespace 是为了后面写代码时可以少打点前缀,写起来更简洁。
目的:
functional 和 placeholders 是为了后面用 std::bind 把类成员函数绑定成回调(muduo 需要回调是普通函数形式)。
这些都是为后面定义 ChatServer 类和使用 muduo 的类做准备。
2.ChatServer 类的整体框架
cpp
class ChatServer
{
public:
ChatServer(...){...}
void start(){...}
private:
void onConnection(...){...}
void onMessage(...){...}
TcpServer _server;
EventLoop *_loop;
};
上面是服务器的一个整体框架,其定义了一个 ChatServer 类,封装了整个服务器;目的是把所有和服务器相关的东西(对象、回调、启动)都放在一个类里,结构更清晰。
其中:
- _server 是 muduo 提供的 TcpServer 对象,真正干活的"大脑"。
- _loop 是事件循环(epoll)的指针,负责监听所有网络事件。
3.构造函数
cpp
ChatServer(EventLoop *loop, //事件循环
const InetAddress &listenAddr, //IP+Port
const string &nameArg) //服务器的名字
: _server(loop,listenAddr,nameArg), _loop(loop)
{
//给服务器注册用户连接的创建和断开回调
_server.setConnectionCallback(std::bind(&ChatServer::onConnection,this,_1));
//给服务器注册用户读写事件回调
_server.setMessageCallback(std::bind(&ChatServer::onMessage,this,_1,_2,_3));
//设置服务器端的线程数量 1个I/O线程 3个worker线程
_server.setThreadNum(4);
}
作用:创建 ChatServer 对象时,会同时创建一个 TcpServer 对象,并做三件事:
- 注册"连接建立/断开"的回调 → onConnection
- 注册"收到消息"的回调 → onMessage
- 设置线程数量(1个I/O线程 + 3个工作线程)
目的:
- 把"什么时候调用哪个函数"提前告诉 muduo。
- std::bind 的作用是把类的成员函数(需要 this 指针)包装成 muduo 需要的普通函数形式。
- 设置线程数是为了让 muduo 用线程池处理业务,提高并发能力。
这段是整个程序最关键的"配置"部分,后面的 onConnection 和 onMessage 就是这里注册的回调函数,_server.start() 会在后面调用,才真正开始监听。
4.start() 函数
cpp
void start(){
_server.start();
}
作用:启动服务器的监听(开始 accept 新连接)。
目的:把"配置"和"真正启动"分开,更加灵活。
5.onConnection 回调函数(处理连接和断开)
cpp
void onConnection(const TcpConnectionPtr &conn)
{
if(conn->connected())
{
cout<<conn->peerAddress().toIpPort()<<" -> "
<<conn->localAddress().toIpPort()<<" state:online "<<endl;
}
else
{
cout<<conn->peerAddress().toIpPort()<<" -> "
<<conn->localAddress().toIpPort()<<" state:offline "<<endl;
conn->shutdown(); //close(fd)
//_loop->quit();
}
}
作用:
- 当有新客户端连接上来,或者已有客户端断开时,muduo 会自动调用这个函数。
- 连接成功 → 打印"online"
- 断开连接 → 打印"offline" 并关闭写端(相当于 close)
目的:
- 让你知道当前连接状态。
- 在客户端异常断开时,主动关闭连接,释放资源。
这个函数是构造函数里用 setConnectionCallback 注册的,TcpConnectionPtr 是 muduo 提供的智能指针,代表一个连接。
6.onMessage 回调函数(处理读写事件)
cpp
void onMessage(const TcpConnectionPtr &conn, //连接
Buffer *buffer, //缓冲区
Timestamp time) //接收到数据的时间信息
{
string buf = buffer->retrieveAllAsString();
cout<<"recv data:"<<buf<<" time:"<<time.toString()<<endl;
conn->send(buf);
}
作用:
- 当某个连接收到数据时,muduo 会调用这个函数。
- 把收到的数据全部取出来,打印,然后原样再发回去(回显)。
这里就是"业务逻辑"。现在是简单回显,实际聊天服务器会把消息转发给其他人。
这是构造函数里用 setMessageCallback 注册的;buffer->retrieveAllAsString() 会把 muduo 内部缓冲区的数据取出来并清空;conn->send(buf) 把数据发回去。
7.main 函数(程序入口)
cpp
int main()
{
EventLoop loop; //epoll
InetAddress addr("127.0.0.1",6000);
ChatServer server(&loop,addr,"ChatServer");
server.start(); //listenfd epoll_ctl=>epoll
loop.loop(); //epoll_wait以阻塞方式等待新用户连接,已连接用户的读写事件等
return 0;
}
作用:程序启动的真正入口,顺序做了五件事:
- 创建一个事件循环对象 loop(这就是 epoll)。
- 定义监听地址:本地回环 127.0.0.1 的 6000 端口。
- 创建 ChatServer 对象(会完成所有注册和配置)。
- 先调用 server.start() 开始监听新连接。
- 最后调用 loop.loop() 进入阻塞等待:等着新连接、读事件、写事件发生。
这是使用 muduo 的标准启动流程。
loop 是整个程序的"心脏",所有事件都靠它驱动;
server.start() 把监听 socket 加入 epoll;
loop.loop() 不会返回,除非你手动调用 quit()。
三、整体流程总结
1.程序启动 → 创建 EventLoop(epoll)
2.创建 ChatServer → 内部创建 TcpServer,并注册两个回调(连接、消息)
3.调用 server.start() → 开始监听 6000 端口
4.调用 loop.loop() → 进入事件循环,等待:
- 新客户端连接 → 触发 onConnection(打印 online)
- 已有客户端发消息 → 触发 onMessage(打印并回显)
- 客户端断开 → 触发 onConnection(打印 offline 并关闭)
整个过程几乎没有手动操作 socket、epoll、accept、read、write,这些底层细节全被 muduo 封装了,你只关心"连接来了怎么办""收到消息怎么办"。
这就是 muduo 最大的优点:网络I/O代码和业务代码彻底分离,让你写服务器像写普通函数一样简单。
四、代码(包含注释)
cpp
/*
muduo网络库给用户提供了两个主要的类
TcpServer:用于编写服务器程序的
TcpClient:用于编写客户端程序的
epoll + 线程池
好处:能够把网络I/O的代码和业务代码区分开
用户的连接和断开 用户的可读写事件
*/
#include<muduo/net/TcpServer.h>
#include<muduo/net/EventLoop.h>
#include<iostream>
#include<string>
#include<functional> //绑定器
using namespace std;
using namespace muduo;
using namespace muduo::net;
using namespace placeholders;
/*基于muduo网络库开发服务器程序
1.组合TcoServer对象
2.创建EventLoop事件循环对象的指针
3.明确TcpServer构造函数需要什么参数,输出ChatServer的构造函数
4.在当前服务器类的构造函数当中,注册处理连接的回调函数和处理读写事件的回调函数
5.设置合适的服务端线程数量,muduo会自己分配I/O线程和worker线程
*/
class ChatServer
{
public:
ChatServer(EventLoop *loop, //事件循环
const InetAddress &listenAddr, //IP+Port
const string &nameArg) //服务器的名字
: _server(loop,listenAddr,nameArg), _loop(loop)
{
//给服务器注册用户连接的创建和断开回调
_server.setConnectionCallback(std::bind(&ChatServer::onConnection,this,_1));
//给服务器注册用户读写事件回调
_server.setMessageCallback(std::bind(&ChatServer::onMessage,this,_1,_2,_3));
//设置服务器端的线程数量 1个I/O线程 3个worker线程
_server.setThreadNum(4);
}
//开启事件循环
void start(){
_server.start();
}
private:
//专门处理用户的连接创建和断开 epoll listenfd accept
void onConnection(const TcpConnectionPtr &conn)
{
if(conn->connected())
{
cout<<conn->peerAddress().toIpPort()<<" -> "<<conn->localAddress().toIpPort()<<" state:online "<<endl;
}
else
{
cout<<conn->peerAddress().toIpPort()<<" -> "<<conn->localAddress().toIpPort()<<" state:offline "<<endl;
conn->shutdown(); //close(fd)
//_loop->quit();
}
}
//专门处理用户的读写事件
void onMessage(const TcpConnectionPtr &conn, //连接
Buffer *buffer, //缓冲区
Timestamp time) //接收到数据的时间信息
{
string buf = buffer->retrieveAllAsString();
cout<<"recv data:"<<buf<<"time:"<<time.toString()<<endl;
conn->send(buf);
}
TcpServer _server; //#1
EventLoop *_loop; //#2 epoll
};
int main()
{
EventLoop loop; //epoll
InetAddress addr("127.0.0.1",6000);
ChatServer server(&loop,addr,"ChatServer");
server.start(); //listenfd epoll_ctl=>epoll
loop.loop(); //epoll_wait以阻塞方式等待新用户连接,已连接用户的读写事件等
return 0;
}
运行步骤及结果:
