C++集群聊天服务器(1)—— muduo网络库服务器编程

关于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;
}

运行步骤及结果:

相关推荐
carver w1 小时前
张氏相机标定,不求甚解使用篇
c++·python·数码相机
捷米研发三部1 小时前
CANopen 转 Modbus TCP:智能机床控制器与上位机监控系统的无缝对接方案
网络·网络协议
宇宙帅猴2 小时前
Ubuntu网络问题解决方案
linux·网络·ubuntu
Remember_9932 小时前
【数据结构】初识 Java 集合框架:概念、价值与底层原理
java·c语言·开发语言·数据结构·c++·算法·游戏
郝学胜-神的一滴2 小时前
QtOpenGL多线程渲染方案深度解析
c++·qt·unity·游戏引擎·godot·图形渲染·unreal engine
XiaoHu02072 小时前
Linux网络编程(第三弹)
linux·运维·网络
陌路202 小时前
RPC分布式通信(2)---四种典型式线程池(1)
java·开发语言·c++
Run_Teenage2 小时前
Linux:匿名管道(实现个进程池)和命名管道
linux·运维·服务器
微露清风2 小时前
系统性学习C++-第二十四讲-智能指针的使用及其原理
java·c++·学习