phase1:基础框架——编译 + MySQL + 登录/注册

目标:服务端能跑起来,用户能注册和登录,单线程处理一切。

第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 即可判断是否查到。

为什么要这么设计?

  1. 避免空指针崩溃

如果查询不到用户,返回 nullptr 或者空引用:

  • 调用方一不小心没判断,直接 user->getId()
  • 程序直接段错误(Segmentation fault)崩溃

返回一个 id=-1默认空对象

  • 调用方怎么访问都不会崩
  • 安全、健壮、不会导致服务宕机

2. 接口统一,不用处理两种返回类型

不管查没查到,返回的都是 User 对象 调用方永远只需要写一种逻辑:

复制代码
User user = query();
if (user.getId() != -1) {
    // 查到了
} else {
    // 没查到
}

代码干净、统一、不易出错

  1. 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

关键设计点:

  1. 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");
  1. escape() 防注入:
cpp 复制代码
sprintf(sql, "insert into user(name, pwd) values('%s', '%s')", mysql.escape(name).c_str(), ...)

每个用户输入都经过 mysql_real_escape_string。

  1. 自增 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,单线程串行处理)

相关推荐
小蜗子1 小时前
Windows 11 + RTX 5060 + WSL2 Ubuntu + NVIDIA DGL 容器
linux·运维·ubuntu
特种加菲猫1 小时前
C++11核心特性深度解析:从列表初始化到lambda与包装器
开发语言·c++
枕星而眠2 小时前
C++ 面向对象核心机制深度解析:多态性、虚函数、虚继承与 final 类
运维·开发语言·c++·后端
着迷不白2 小时前
八、shell脚本
linux·运维
智者知已应修善业2 小时前
【51单片机8个LED,已经使用了D1D2,怎么样在不动D1D2的前提下实现D6~D8的流水灯】2024-1-19
c++·经验分享·笔记·算法·51单片机
坚果派·白晓明2 小时前
鸿蒙PC适配实战:simdjson 三方库移植攻略与 AtomCode Skills 提效之道
c++·harmonyos·三方库·skills·atomcode·c/c++三方库·c/c++三方库适配
爱装代码的小瓶子2 小时前
3. 设计buffer模块
linux·服务器·开发语言·c++·php
郝学胜-神的一滴2 小时前
Qt 高级开发 027: QTabWidget自定义样式表美化实战
开发语言·c++·qt·程序人生·软件构建·用户界面
双河子思2 小时前
《代码整洁之道》——读书笔记(持续更新)
开发语言·c++·c#