C++实现集群聊天服务器(2)

各个文件目录介绍

bin:生成的可执行文件

lib:生成的中间库文件

include:头文件

src:源文件

build:编译过程中产生的临时中间文件

test:测试文件

thirdparty:依赖的第三方库

CMakeLists.txt

autobuild.sh:Linux的Shell脚本 一键编译

集群聊天项目工程目录创建

根目录下创建一个名为CmakeLists.txt的文件 

CMakeLists.txt

cmake_minimum_required(VERSION 3.0)
project(chat)

# 配置编译选项
set(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} -g)

# 配置最终的可执行文件输出的路径
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)

#头文件的搜索路径
include_directories(${PROJECT_SOURCE_DIR}/include)
include_directories(${PROJECT_SOURCE_DIR}/include/server)
include_directories(${PROJECT_SOURCE_DIR}/thirdparty)

#加载子目录
add_subdirectory(src)

在src下创建一个CMakelists.txt  在src/server下也创建一个CMakelists.txt (未配图)

src下的CMakelists.txt 代码如下


add_subdirectory(server)

 src/server下Makelists.txt

#定义一个SRC_LIST变量包含了该目录下所有的代码源文件
aux_source_directory(. SRC_LIST)

#指定生成可执行文件
add_executable(ChatServer ${SRC_LIST} ${DB_LIST})

#指定可执行文件连接时需要依赖的库文件
target_link_libraries(ChatServer muduo_net muduo_base pthread)

网络模块代码 

include/server/chatserver.hpp

#ifndef CHATSERVER_H
#define CHATSERVER_H

#include <muduo/net/TcpServer.h>
#include <muduo/net/EventLoop.h>
using namespace muduo;
using namespace muduo::net;

// 聊天服务器的主类
class ChatServer
{
public:
    // 初始化聊天服务器对象
    ChatServer(EventLoop *loop,
               const InetAddress &listenAddr,
               const string &nameArg);

    // 启动服务
    void start();

private:
    // 上报链接相关信息的回调函数
    void onConnection(const TcpConnectionPtr &);

    // 上报读写事件相关信息的回调函数
    void onMessage(const TcpConnectionPtr &,
                   Buffer *,
                   Timestamp);

    TcpServer _server; // 组合的muduo库,实现服务器功能的类对象
    EventLoop *_loop;  // 指向事件循环对象的指针
};

#endif

 首先我们把Tcpserver这个类产生的对象作为它的组合对象,然后再创建EventLoop 指向事件循环的指针,保存事件循环。我们拿到事件循环的指针,可以在合适的时候调用quit来退出事件循环。

ChatServer需要准备构造函数 它准备构造是为了成员变量(TcpServer对象)因为它没有默认构造。

源文件我们就在src/server中创建chatserver.cpp实现,并再server下建立一个启动函数main.cpp

src/server/chatserver.cpp

#include "chatserver.hpp"
#include "json.hpp"
#include "chatservice.hpp"

#include <iostream>
#include <functional>
#include <string>
using namespace std;
using namespace placeholders;
using json = nlohmann::json;


// 初始化聊天服务器对象
ChatServer::ChatServer(EventLoop *loop,
                       const InetAddress &listenAddr,
                       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));

    // 设置线程数量
    _server.setThreadNum(4);
}

// 启动服务
void ChatServer::start()
{
    _server.start();
}

// 上报链接相关信息的回调函数
void ChatServer::onConnection(const TcpConnectionPtr &conn)
{
       //客户端断开链接
       if (!conn->connected())
    {
        conn->shutdown();//释放socket FD资源
    }
}
// 上报读写事件相关信息的回调函数
void ChatServer::onMessage(const TcpConnectionPtr &conn,
                           Buffer *buffer,
                           Timestamp time)
{
   string buf = buffer->retrieveAllAsString();
    // 数据的反序列化
    json js = json::parse(buf);
    // 达到的目的:完全解耦网络模块的代码和业务模块的代码
    // 通过js["msgid"] 获取=》业务handler=》conn  js  time
    auto msgHandler = ChatService::instance()->getHandler(js["msgid"].get<int>());
    // 回调消息绑定好的事件处理器,来执行相应的业务处理
    msgHandler(conn, js, time);
}

conn->shutdown();//释放socket FD资源

这里用户下线本身muduo库就有日志打印输出,自己不用打印输出。

 src/server/main.cpp 

#include "chatserver.hpp"
#include <iostream>
using namespace std;

int main()
{
    EventLoop loop;
    InetAddress addr("ip", 端口);
    ChatServer server(&loop, addr, "ChatServer");

    server.start();
    loop.loop();
    return 0;
}

到这里我们将简单的网络模块代码写完了,“为什么这么简单?” 因为我们使用了muduo库得到了一个相当强大的基于事件驱动的 I/O复用epoll+线程池的网络代码完全基于reactor模型。(一个主rector I/O线程,3个subrector 是工作线程,共设置4个线程 ,主rector主要负责新用户的链接,子rector负责已连接用户的读写事件处理。)

思考:

“怎么把网络模块收到的消息派发到业务模块,让网络模块代码和业务模块代码完全解耦”

思路:用户下线我们这里就收到一个消息了,从muduo网络库中数据缓冲区retrieveAllAsString可以把从网络上读取的数据,从缓冲区buffer里拿出来放到字符串buf中。

 在通过json反序列化,这个json里肯定包括了一个magid,因为我们客户端和服务端通信收发消息都有一个业务的标识即magid。

我们不想在这里写什么if else或者switch case的,当你是登录业务的话,我们就调用相应的登录服务方法,注册业务的话就调用注册方法,这样就将网络模块代码和业务模块代码给强耦合到一块了,因为这块你就会直接调用服务层的方法,这样并没有完成之前所说的解耦。

这里我们希望做这样的一个事情,通过js里读出来的js["msgid"],每个消息都有id,我们事先给它们绑定成一个回调操作,一个id对应一个操作,一个id对应一个操作,当我这个网络模块不够你是哪个具体做什么业务,我只解析到你的msgid就可以通过msgid来获取一个业务处理区handler,处理器就事先绑定方法,这个方法在我网络模块是根本看不到的,是在业务模块绑定好的。handler回调时候可以把我们接收到的数据conn 或者js对象 time都会传过去。

我们的目的达到完全解耦网络模块代码和业务模块代码,不要在这里“指名道姓”的调用相关的方法

 include/server/chatservice.hpp

#ifndef CHATSERVICE_H
#define CHATSERVICE_H

#include <muduo/net/TcpConnection.h>
#include <unordered_map>
#include <functional>

using namespace std;
using namespace muduo;
using namespace muduo::net;


#include "json.hpp"
using json = nlohmann::json;

// 表示处理消息的事件回调方法类型
using MsgHandler = std::function<void(const TcpConnectionPtr &conn, json &js, Timestamp)>;
// 聊天服务器业务类
class ChatService
{
public:
    // 获取单例对象的接口函数
    static ChatService *instance();
    // 处理登录业务
    void login(const TcpConnectionPtr &conn, json &js, Timestamp time);
    // 处理注册业务
    void reg(const TcpConnectionPtr &conn, json &js, Timestamp time);
    // 获取消息对应的处理器
    MsgHandler getHandler(int msgid);

private:
    ChatService();

    // 存储消息id和其对应的业务处理方法
    unordered_map<int, MsgHandler> _msgHandlerMap;


};

#endif

这主要做业务,我们重要的是提供跟业务相关的代码,对象本身有一个实例还是多个实例呢?

实际上有一个实例就足够了,所以我们采用单例模式来设计一下这个聊天的服务类

首先我们在这里需要完成的一个事就是给思考里的msgid映射一个事件回调,这里我们用到map,说的很明显的了一个id对应一个操作,一个id对应一个操作。

我们用using定义一个MsgHandler 事件处理器我们用了c++11新语法using来给一个存在的类型定义新的类型名称。这就相当于消息Id绑定的事件,不需要返回值,参数的话就是上文提到的数据conn js对象 time都传过去。

using MsgHandler = std::function<void(const TcpConnectionPtr &conn, json &js, Timestamp)>;

单例设计需要先把构造函数私有化,再写一个唯一的实例,再暴漏一个static接口

 // 获取单例对象的接口函数
    static ChatService *instance();

 include/public.hpp

#ifndef PUBLIC_H
#define PUBLIC_H

/*
server和client的公共文件
*/
enum EnMsgType
{
    LOGIN_MSG = 1, // 登录消息
    REG_MSG ,// 注册消息

};


#endif

 将LOGIN_MSG与login方法绑定起来,当收到LOGIN_MSG的时候他就能过自动调用这个函数,当收到REG_MSG就能调用到reg函数。

src/server/chatservice.cpp

#include "chatservice.hpp"
#include "public.hpp"
#include <muduo/base/Logging.h>
using namespace std;
using namespace muduo;

// 获取单例对象的接口函数
ChatService *ChatService::instance()
{
    static ChatService service;
    return &service;
}

// 注册消息以及对应的Handler回调操作
ChatService::ChatService()
{
    _msgHandlerMap.insert({LOGIN_MSG, std::bind(&ChatService::login, this, _1, _2, _3)});
    _msgHandlerMap.insert({REG_MSG, std::bind(&ChatService::reg, this, _1, _2, _3)});
}
// 获取消息对应的处理器
MsgHandler ChatService::getHandler(int msgid)
{
    // 记录错误日志,msgid没有对应的事件处理回调
    auto it = _msgHandlerMap.find(msgid);
    if (it == _msgHandlerMap.end())
    {
        // 返回一个默认的处理器,空操作
        return [=](const TcpConnectionPtr &conn, json &js, Timestamp)
        {
            LOG_ERROR << "msgid:" << msgid << " can not find handler!";
        };
    }
    else
    {
        return _msgHandlerMap[msgid];
    }
}
// 处理登录业务  id  pwd   pwd
void ChatService::login(const TcpConnectionPtr &conn, json &js, Timestamp time)
{
}
// 处理注册业务  name  password
void ChatService::reg(const TcpConnectionPtr &conn, json &js, Timestamp time)
{
}

login这里只管做登录的操作,reg这里做注册的操作,至于什么时候做我们不知道,需要靠网络模块给我们来调。这就是我们之前说的,这件事情什么时候发生,以及发生了以后做了什么事情这俩个步骤没在一块发生。

只需要在服务层内部把相应的消息和对应回调做一个绑定,网络层代码就可以通过消息id拿到事件处理器执行相应的业务处理

thirdparty/json.hpp

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值