本阶段目标
在 Echo Server 的基础上,设计并实现自定义通信协议,解决 TCP 粘包/拆包问题,让服务端能区分不同类型的消息(登录、聊天、心跳)。
一、为什么需要自定义协议?
回顾:TCP 的天然缺陷
TCP 是字节流,没有消息边界。客户端连续发送两条消息:
1 | 发送1: "hello" |
服务端可能收到:
1 | 情况A(正常): "hello" "world" ← 分开 |
如果服务端分不清消息边界,聊天应用根本没法用。
解决方案:给每条消息加”信封”
1 | ┌──────────────┬──────────────┬────────────────────┐ |
读取流程:
- 先读 4 字节 → 知道消息体长度是 N
- 再读 4 字节 → 知道消息类型(登录?聊天?心跳?)
- 再读 N 字节 → 正好读完一条完整消息
- 重复步骤 1 → 处理下一条
二、消息类型设计
| 类型值 | 枚举名 | 方向 | 说明 | JSON示例 |
|---|---|---|---|---|
| 1 | LOGIN_REQ |
客户端→服务端 | 登录请求 | "tom" |
| 2 | LOGIN_RESP |
服务端→客户端 | 登录响应 | {"success":true,"username":"tom"} |
| 3 | CHAT_MSG |
双向 | 聊天消息 | {"from":"tom","text":"你好"} |
| 4 | HEARTBEAT |
双向 | 心跳保活 | {"pong":true} |
| 5 | ERROR_MSG |
服务端→客户端 | 错误提示 | {"code":400,"msg":"xxx"} |
三、核心代码实现
3.1 协议头文件 protocol.h
定义三个核心东西:
① 消息类型枚举
1 | enum class MsgType : uint32_t { |
② 打包函数 —— 把消息装进信封
1 | std::string pack_message(MsgType type, const std::string& json_body) { |
htonl()是什么? — Host TO Network Long,把主机字节序转成网络字节序(大端)。因为不同 CPU 存储多字节数据的方式不同,统一成大端序才能跨平台通信。
③ 缓冲区类 —— 解决粘包/拆包的核心
1 | class MessageBuffer { |
关键思路: 缓冲区就像一个”积木盒”,每次 read() 收到新的字节就丢进去,然后不断尝试从里面”拼出”完整消息。
3.2 服务端 chat_server.cpp
相比 Echo Server 的三大升级:
| Echo Server | chat_server |
|---|---|
| 阻塞 I/O,一次只能服务一个客户端 | epoll 非阻塞,同时管理多个客户端 |
| 原样回显,不解析内容 | 按协议解包,区分消息类型 |
| 没有状态管理 | 维护在线用户表,支持广播 |
epoll 事件循环的核心结构:
1 | while (true) { |
3.3 客户端 chat_client.cpp
双线程设计:
- 主线程:读取用户键盘输入 → 封装协议 → 发送
- 接收线程:不断
read()→MessageBuffer解析 → 打印消息
1 | ┌─────────────────┐ ┌─────────────────┐ |
四、编译与测试
编译
1 | cd backend |
启动测试
终端1(服务端):
1 | ./chat_server |
输出:
1 | ======================================== |
终端2(客户端A - tom):
1 | ./chat_client |
终端3(客户端B - jerry):
1 | ./chat_client |
此时客户端A(tom)会收到:
1 | [消息] {"from":"jerry","text":"你好啊"} |
五、和上一阶段的对比
| Echo Server | 聊天服务端 | |
|---|---|---|
| 代码行数 | ~140 行 | ~250 行 |
| 并发能力 | 1 个客户端 | 多客户端(epoll) |
| 消息解析 | 原始字符串 | 自定义协议解包 |
| 消息类型 | 无区分 | 登录/聊天/心跳/错误 |
| 业务逻辑 | 回显 | 登录 + 广播 + 心跳 |
六、本节小结
学到了什么
| 知识点 | 关键理解 |
|---|---|
| 自定义协议 | 长度头 + 类型头 → 解决粘包/拆包,区分消息类型 |
| 字节序转换 | htonl() / ntohl() → 保证跨平台数据一致性 |
| 缓冲区设计 | MessageBuffer 累积字节 → 逐条解包 |
| epoll 多路复用 | 一个线程管理多个连接,谁有数据就处理谁 |
| 双线程客户端 | 主线程发消息 + 接收线程收消息 |
下一步
加入 JSON 解析库(nlohmann/json),让消息体不再手动拼接字符串,支持结构化的 JSON 数据处理。
上一篇:
聊天项目-01-TCP底层通信原理
下一篇:聊天项目-03-JSON序列化与数据库