各个文件目录介绍
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拿到事件处理器执行相应的业务处理