跟着施磊老师做C++项目,施磊老师_腾讯课堂 (qq.com)
一、网络模块ChatServer
- 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& conn);
// 上报读写事件相关信息的回调函数
void onMessage(const TcpConnectionPtr& conn,Buffer* buffer,Timestamp time);
TcpServer m_server; // 组合的muduo库,实现服务器功能的类对象
EventLoop *m_loop; // 指向事件循环的指针
};
#endif
- chatserver.cpp
#include "chatserver.hpp"
#include "chatservice.hpp"
#include "json.hpp"
#include <functional>
#include <string>
#include <iostream>
using namespace std;
using namespace placeholders;
using json = nlohmann::json;
ChatServer::ChatServer(EventLoop *loop, const InetAddress &listenAddr, const string &nameArg)
: m_server(loop, listenAddr, nameArg), m_loop(loop) {
// 注册链接回调
m_server.setConnectionCallback(std::bind(&ChatServer::onConnection, this, _1));
// 注册消息回调
m_server.setMessageCallback(std::bind(&ChatServer::onMessage, this, _1, _2, _3));
// 设置线程数量
m_server.setThreadNum(4);
}
// 启动服务
void ChatServer::start() {
m_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();
std::cout<<"buf: "<<buf.c_str()<<std::endl;
// 数据的反序列化
json js = json::parse(buf);
// 达到的目的:完全解耦网络模块的代码和业务模块的代码
// 通过js["msgid"] 获取 => 业务handler => conn js time
auto msghandler = ChatService::getInstance()->getHandler(js["msgid"].get<int>());
// 回调消息绑定好的事件处理器,来执行相应的业务处理
msghandler(conn,js,time);
}
json里边会包含一个msgid.由于客户端和服务器通信收发消息,需要判断这个消息是属于哪种业务的,就需要一个业务的标识,所以就用msgid来表示业务的标识.在onMessage函数中,并不想出现。
当有登录业务需求就调用相应的服务登录方法,当有注册业务需求就调用相应的服务注册方法,这样就用到if...else,或者switch case,但这种方式是直接调用服务层的方法,就把网络模块的代码和业务模块的代码给强耦合一起了,这不是好的方法.
方法二:每一个消息都有一个msgid(一个消息id映射一个事件处理),事先给它绑定一个回调操作,让一个id对应一个操作.不管具体做什么业务,并不会直接调用业务模块的相关的方法.
利用OOP回调思想,要想解耦模块之间的关系,一般有两种方法,一种就是使用基于面向接口的编程,在C++里边的"接口"可以理解为抽象基类.那也就是面向抽象基类的编程.另一种就是基于回调函数
这里使用基于回调函数来实现,m_msgHandlerMap存储消息id和其对应的业务处理方法.注册消息以及对应的Handler回调操作,就是把消息id对应的事件处理器给绑定了,LOGIN_MSG绑定的是login处理登录业务,REG_MSG绑定的是reg处理注册业务.
ChatService单例对象通过js["msgid"] 获取消息对应的处理器(业务handler)msghandler,由于回调消息绑定了事件处理器,可用它来执行相应的业务处理-->msghandler(conn,js,time);
二、业务模块ChatService
- public.hpp
#ifndef PUBLIC_H
#define PUBLIC_H
/*
server和client的公共文件
*/
enum EnMsgType {
LOGIN_MSG = 1, // 登录消息
REG_MSG // 注册消息
};
#endif // PUBLIC_H
- 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* getInstance();
// 处理登录业务
void login(const TcpConnectionPtr& conn,json& js,Timestamp time);
// 处理注册业务(register)
void reg(const TcpConnectionPtr& conn,json& js,Timestamp time);
// 获取消息对应的处理器
MsgHandler getHandler(int msgid);
ChatService(const ChatService&) = delete;
ChatService& operator=(const ChatService&) = delete;
private:
// 注册消息以及对应的Handler回调操作
ChatService();
// 存储消息id和其对应的业务处理方法
unordered_map<int,MsgHandler> m_msgHandlerMap;
};
#endif // CHATSERVICE_H
- chatservice.cpp
#include "chatservice.hpp"
#include "public.hpp"
#include <muduo/base/Logging.h>
using namespace muduo;
// 获取单例对象的接口函数 线程安全的单例对象
ChatService* ChatService::getInstance() {
static ChatService service;
return &service;
}
// 注册消息以及对应的Handler回调操作
ChatService::ChatService() {
m_msgHandlerMap.insert({LOGIN_MSG,std::bind(&ChatService::login, this, _1, _2, _3)});
m_msgHandlerMap.insert({REG_MSG,std::bind(&ChatService::reg, this, _1, _2, _3)});
}
// 处理登录业务
void ChatService::login(const TcpConnectionPtr &conn, json &js, Timestamp time) {
LOG_INFO << "do login service!!!";
}
// 处理注册业务
void ChatService::reg(const TcpConnectionPtr &conn, json &js, Timestamp time) {
LOG_INFO << "do reg service!!!";
}
// 获取消息对应的处理器
MsgHandler ChatService::getHandler(int msgid) {
// 记录错误日志,msgid没有对应的事件处理回调
auto it = m_msgHandlerMap.find(msgid);
if(it == m_msgHandlerMap.end()) {
// 返回一个默认的处理器,空操作
return [=](const TcpConnectionPtr &conn, json &js, Timestamp) {
LOG_ERROR << "msgid:" << msgid << " can not find handler!";
};
}
else {
return m_msgHandlerMap[msgid];
}
}
三、src/server目录下的main.cpp
#include "chatserver.hpp"
#include <iostream>
using namespace std;
int main() {
EventLoop loop;
InetAddress addr("127.0.0.1", 6000);
ChatServer server(&loop, addr, "ChatServer");
server.start();
loop.loop(); // 启动事件循环
return 0;
}
四、thirdparty目录下是json.hpp
下载:GitHub - nlohmann/json: JSON for Modern C++
在single_include/nlohmann里头有一个json.hpp,把它放到我们的项目中就可以了
五、CMake构建项目
(1)在src/server目录中的CMakeLists.txt
# 定义了一个SRC_LIST变量 包含了该目录下所有的源文件
aux_source_directory(. SRC_LIST)
# 指定生成可执行文件
add_executable(ChatServer ${SRC_LIST})
# 指定可执行文件链接时需要依赖的库文件
target_link_libraries(ChatServer muduo_net muduo_base pthread)
(2)在src目录下的CMakeLists.txt
add_subdirectory(server)
(3)与include和src,以及thirdparty同级目录的CMakeLists.txt
cmake_minimum_required(VERSION 3.28.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)
在此目录下打开终端,执行命令:
cmake -B build
cmake --build build
接着执行
./bin/ChatServer
打开另一个终端,执行
telnet 127.0.0.1 6000
输入以下内容进行测试:
{"msgid":1}
{"msgid":2}