目标:服务端能跑起来,用户能注册和登录,单线程处理一切。
第1步:搭建项目骨架

项目目录:

第2步:定义消息类型枚举类型
文件:include/public.hpp
这是整个系统通信的"字典",所有消息都带一个 msgid 来标识类型:

为什么 LOGIN_MSG_ACK = 2 而不是显式赋值?
enum 默认递增,LOGIN_MSG=1 后自动 LOGIN_MSG_ACK=2。这样添加/删除消息类型时不会破坏已有编号。
第 3 步:User ORM 类
文件:include/server/model/user.hpp
把一个数据库行映射为 C++ 对象:

关键设计:默认参数 id=-1,当 query() 找不到用户时返回 User(),调用方检查 getId() != -1 即可判断是否查到。
为什么要这么设计?
- 避免空指针崩溃
如果查询不到用户,返回
nullptr或者空引用:
- 调用方一不小心没判断,直接
user->getId()- 程序直接段错误(Segmentation fault)崩溃
返回一个
id=-1的默认空对象:
- 调用方怎么访问都不会崩
- 安全、健壮、不会导致服务宕机
2. 接口统一,不用处理两种返回类型
不管查没查到,返回的都是 User 对象 调用方永远只需要写一种逻辑:
User user = query(); if (user.getId() != -1) { // 查到了 } else { // 没查到 }代码干净、统一、不易出错。
- C++ 不能返回空对象,只能用 "标记值" 代表不存在
C++ 函数不能返回 "空",必须返回一个合法对象。 所以用 id=-1 作为无效标记,代表:
这个对象是空的,没有查到数据
这是数据库查询层最标准的设计模式。
第 4 步:MySQL 操作类
文件:include/server/db/db.h + src/server/db/db.cpp
封装 MySQL C API 的基础操作:
cpp
class MySQL
{
public:
MySQL();
~MySQL();
bool connect(); // 连接数据库
bool update(string sql); // INSERT/UPDATE/DELETE
MYSQL_RES *query(string sql); // SELECT
MYSQL* getConnection();
string escape(const string &str); // 防SQL注入
private:
MYSQL *_conn;
};
MySQL::MySQL() : _conn(nullptr) {}
MySQL::~MySQL() {
if (_conn != nullptr) {
mysql_close(_conn);
_conn = nullptr;
}
}
bool MySQL::connect() {
_conn = mysql_init(nullptr);
MYSQL *p = mysql_real_connect(_conn, "127.0.0.1", "root", "123456",
"chat", 3306, nullptr, 0);
if (p != nullptr) {
mysql_query(_conn, "set names utf8mb4");
return true;
}
return false;
}
bool MySQL::update(string sql) {
if (mysql_query(_conn, sql.c_str())) {
LOG_ERROR << sql << " 更新失败!";
return false;
}
return true;
}
MYSQL_RES *MySQL::query(string sql) {
if (mysql_query(_conn, sql.c_str())) {
LOG_ERROR << sql << " 查询失败!";
return nullptr;
}
return mysql_use_result(_conn); // phase1 用了的这个 use_result,phase5 改成 store_result
}
**踩坑:**mysql_use_result 是流式读取,没读完就发下一条 SQL 会报 "Commands out of sync"。后面修复为mysql_store_result。
为什么会报错?
mysql_use_result 是 "只读不拿",必须读完结果才能发下一条 SQL,否则命令不同步报错。
mysql_store_result 是 "一次性全部读到内存",读完立刻释放连接,不会阻塞后续 SQL。
第 5 步:UserModel 数据操作层
文件:include/server/model/usermodel.hpp + src/server/model/usermodel.cpp

关键设计点:
- int↔string 映射:数据库 state 是 int(0=离线/1=在线),C++ 代码里用 string("offline"/"online")。
Model层做双向转换:
cpp// 写入时:string → int int state = (user.getState() == "online") ? 1 : 0; // 读取时:int → string user.setState(atoi(row[3]) == 1 ? "online" : "offline");
- escape() 防注入:
cppsprintf(sql, "insert into user(name, pwd) values('%s', '%s')", mysql.escape(name).c_str(), ...)每个用户输入都经过 mysql_real_escape_string。
- 自增 ID 回填:注册成功后 user.setId(mysql_insert_id(mysql.getConnection())) 把 MySQL 自增的主键填回对象。
第 6 步:ChatServer 网络层
文件:include/server/chatserver.hpp + src/server/chatserver.cpp
封装 Muduo 的 TcpServer,设置 4 个 IO 线程:
cpp
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);
void processMessage(const TcpConnectionPtr &conn, const string &msg, Timestamp time);
TcpServer _server;
EventLoop *_loop;
};
//Phase 1 的 onMessage(使用 \r\n 分隔符):
void ChatServer::onMessage(const TcpConnectionPtr &conn, Buffer *buf, Timestamp time) {
// 按 \n 分割消息 --- O(n) 扫描
string msg = buf->retrieveAllAsString();
// ... 简单按换行切分,发给 processMessage
}
//构造函数中绑定回调、设置线程数:
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); // 4个IO线程处理TCP事件
}
//onConnection 处理断开------客户端异常退出时清理状态:
void ChatServer::onConnection(const TcpConnectionPtr &conn) {
if (!conn->connected()) {
ChatService::instance()->clientCloseException(conn);
conn->shutdown();
}
}
第 7 步:ChatService 业务调度层(单例)
文件:include/server/chatservice.hpp + src/server/chatservice.cpp
这是整个服务端的核心------所有业务逻辑的入口和调度。
cpp
// Phase 1 只实现登录和注册:
class ChatService {
public:
static ChatService *instance(); // 单例
void login(const TcpConnectionPtr &conn, json &js, Timestamp time);
void reg(const TcpConnectionPtr &conn, json &js, Timestamp time);
void loginout(const TcpConnectionPtr &conn, json &js, Timestamp time);
void clientCloseException(const TcpConnectionPtr &conn);
MsgHandler getHandler(int msgid);
private:
ChatService(); // 构造函数注册所有handler
unordered_map<int, MsgHandler> _msgHandlerMap; // msgid → handler
unordered_map<int, TcpConnectionPtr> _userConnMap; // userid → 连接
mutex _connMutex;
UserModel _userModel;
};
// 构造函数中注册 handler(绑定 this 到成员函数):
ChatService::ChatService() {
_msgHandlerMap.insert({LOGIN_MSG, bind(&ChatService::login, this, _1, _2, _3)});
_msgHandlerMap.insert({LOGINOUT_MSG, bind(&ChatService::loginout, this, _1, _2, _3)});
_msgHandlerMap.insert({REG_MSG, bind(&ChatService::reg, this, _1, _2, _3)});
}
// Phase 1 的 login 逻辑(同步版本,无线程池,无缓存):
void ChatService::login(const TcpConnectionPtr &conn, json &js, Timestamp time) {
int id = js["id"].get<int>();
string pwd = js["password"].get<string>();
User user = _userModel.query(id); // 查MySQL
json res;
res["msgid"] = LOGIN_MSG_ACK;
if (user.getId() != -1 && user.getPwd() == pwd) {
// 检查重复登录
{
lock_guard<mutex> lock(_connMutex);
if (_userConnMap.find(id) != _userConnMap.end()) {
res["errno"] = 2;
res["errmsg"] = "该账号已经登录";
sendResponse(conn, res); // Phase 1 直接发,不分帧
return;
}
_userConnMap[id] = conn;
}
// 更新状态为 online
User u; u.setId(id); u.setState("online");
_userModel.updateState(u);
// 查好友 + 群组 + 离线消息(Phase 1 每次查MySQL,没有缓存)
vector<User> friends = _friendModel.query(id);
vector<Group> groups = _groupModel.queryGroups(id);
vector<string> offlineMsgs = _offlineMsgModel.query(id);
// ... 组装 JSON 响应
}
}
第 8 步:main.cpp 入口
文件:src/server/main.cpp

第 9 步:CMakeLists 构建文件
文件:src/CMakeLists.txt + src/server/CMakeLists.txt

第10步:编译验证
bash
cd /mnt/d/C++perject/MuduoChatServer
mkdir build && cd build
cmake .. && make -j$(nproc)
./bin/ChatServer 127.0.0.1 6000
Phase 1 可验证功能:注册用户 + 登录(每次走 MySQL,单线程串行处理)
