# chat_server **Repository Path**: gitee00000/chat_server ## Basic Information - **Project Name**: chat_server - **Description**: _________________________________________ - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-06-25 - **Last Updated**: 2026-06-25 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README 下面是一份面向直接编码的落地方案。相比上一版,这次去掉模糊性和可选项发散,每个关键点都做出确定选择,并明确标注采用的设计原则与设计模式,使其可以直接进入代码实现阶段。 一、范围与确定性决策 本版只做一件事并把它做扎实:两台/多台不同电脑之间,通过网络进行安全的远程聊天(单聊 + 群聊),JSON 作为载荷,传输全程加密。 技术选型一次性锁定,不再提供"可选项": 表格 维度 确定选择 理由 语言标准 C++20 协程可选、std::span、concepts 构建 CMake ≥ 3.20 + FetchContent 依赖自动拉取,无需系统安装 网络 standalone Asio(header-only) 不依赖 Boost,Reactor 模型成熟 加密传输 OpenSSL + Asio SSL stream(TLS 1.3) 直接解决"传输安全" 序列化 nlohmann/json 开发期可读,易调试 存储 SQLite3 单文件、零运维、事务可靠 密码哈希 OpenSSL PBKDF2-HMAC-SHA256 无需额外引入 libsodium 令牌 自签 HMAC-SHA256 token 无状态校验 日志 spdlog 生产级 客户端输入 linenoise 单文件、行编辑、历史 测试 Catch2 header-only 安全模型明确定义为: 传输层:TLS 1.3 强制加密(防窃听/篡改/中间人)。 应用层:登录获取 HMAC 签名 token,后续每帧鉴权;服务端绝不信任客户端自报的 sender_id,一律从已认证 Session 取真实身份。 存储层:密码 PBKDF2 加盐哈希,不可逆。 端到端加密(E2EE)明确不在本版范围,作为后续演进项,避免现在引入密钥协商复杂度导致无法落地。 二、设计原则与设计模式映射 这是本方案的骨架,代码结构严格围绕它展开。 设计原则(SOLID + 关注点分离): 依赖倒置(DIP): 业务核心只依赖抽象接口(IMessageStore、IAuthService、ITransport),不依赖 SQLite/Asio 具体实现。这样 SQLite→PostgreSQL、JSON→Protobuf 的替换不触碰业务层。 单一职责(SRP): 网络层只管字节收发与分帧;编解码层只管帧↔对象;业务层只管路由与状态。 开闭原则(OCP): 新增消息类型 = 注册一个 Handler,不修改分发主循环。 设计模式(明确落点): 表格 模式 落点 解决什么 Reactor IoContextPool + Asio 单线程事件循环驱动高并发 IO Strategy IMessageStore / IAuthService 存储与认证可替换 Factory + Registry HandlerRegistry 按 type 路由消息处理器,新增类型零侵入 Command 客户端 ICommand /login /to /send 等命令解耦 Observer PresenceManager 在线状态变更通知 Builder Envelope 构造 统一帧构造,避免散落拼装 PIMPL TcpServer::Impl 隔离 Asio 头文件,缩短编译、稳定 ABI RAII Session / DbConnection 连接、事务、资源自动释放 三、线路协议(可直接照此编码) 1. 分帧:长度前缀 + JSON TLS 之上是面向流的字节,必须自分帧。规则固定: diff +----------------------+--------------------------------+ | length (4 bytes, BE) | JSON payload (UTF-8, length 字节) | +----------------------+--------------------------------+ length:大端 uint32,仅表示 JSON 字节数,不含自身 4 字节。 上限:MAX_FRAME = 1 MiB。超限立即断开,防 OOM 攻击。 解码必须处理半包/粘包(见 FrameCodec)。 2. JSON 信封(Envelope)统一结构 所有消息共用同一外层结构,业务字段放 data: json { "type": "send_message", "seq": 1024, "token": "eyJ...hmac", "data": { } } type:字符串枚举,决定 data 的 schema。 seq:客户端单调递增,用于请求-响应配对与 ACK。 token:除 login 外必填,服务端校验。 响应复用同一结构,type 加 _resp 或为推送类型。 3. 消息类型全集(本版冻结) rust login -> login_resp heartbeat -> heartbeat_resp send_message -> send_message_resp (服务端回执:已落库 + server_msg_id) push_message (服务端 -> 客户端,新消息推送) message_ack (客户端 -> 服务端,确认已收到 push) sync_request -> sync_resp (拉取离线消息) create_group -> create_group_resp error (任意错误统一返回) 4. 关键 schema(data 部分) jsonc // login.data { "username": "alice", "password": "secret", "device_id": "laptop-01" } // login_resp.data { "ok": true, "user_id": "u_alice", "token": "...", "server_time": 1719200000 } // send_message.data (注意:无 sender_id,由服务端填充) { "conversation_id": "c_alice_bob", "content_type": "text", "content": "hi" } // push_message.data { "server_msg_id": 5012, "conversation_id": "c_alice_bob", "sender_id": "u_alice", "content_type": "text", "content": "hi", "timestamp": 1719200001 } // sync_request.data { "conversation_id": "c_alice_bob", "after_msg_id": 5000, "limit": 100 } 四、目录结构(直接对应代码文件) bash cchat/ ├── CMakeLists.txt ├── cmake/Dependencies.cmake # FetchContent: asio/json/openssl/sqlite/spdlog/catch2 ├── include/cchat/ │ ├── proto/envelope.hpp # Envelope 结构 + JSON 转换 │ ├── proto/message_types.hpp # type 字符串常量 + 错误码 │ ├── net/frame_codec.hpp # 长度前缀分帧 │ ├── net/io_context_pool.hpp # Reactor 线程池 │ ├── net/ssl_context.hpp # TLS 配置 │ ├── core/session.hpp │ ├── core/session_manager.hpp │ ├── core/message_router.hpp │ ├── core/handler_registry.hpp # Factory/Registry │ ├── core/presence_manager.hpp # Observer │ ├── auth/iauth_service.hpp # 抽象 (DIP) │ ├── auth/token.hpp # HMAC token 签发/校验 │ ├── storage/imessage_store.hpp # 抽象 (Strategy) │ ├── storage/iuser_store.hpp │ └── client/command.hpp # Command 模式 ├── src/ │ ├── proto/... net/... core/... auth/... storage/sqlite_*.cpp │ └── client/... ├── apps/ │ ├── cchat_server.cpp │ └── cchat_client.cpp ├── tests/ └── configs/{server.toml, client.toml} 五、核心抽象接口(实现层直接据此编码) 1. 分帧编解码(纯函数式,可独立单测) cpp class FrameCodec { public: // 把 JSON 字符串封装成 [len|payload] 待发送字节 static std::vector encode(const std::string& json); // 从累积缓冲区尝试取出一个完整帧。 // 返回 nullopt 表示数据不足需继续读;抛 ProtocolError 表示超限/非法。 static std::optional try_decode(std::vector& buffer); static constexpr uint32_t kMaxFrame = 1u << 20; // 1 MiB }; 2. 存储抽象(Strategy / DIP) cpp struct StoredMessage { int64_t server_msg_id; // 自增主键,会话内单调 std::string conversation_id; std::string sender_id; std::string content_type; std::string content; int64_t timestamp; }; class IMessageStore { public: virtual ~IMessageStore() = default; // 落库并返回分配的 server_msg_id(事务保证) virtual int64_t append(StoredMessage& msg) = 0; // 拉取 conversation_id 中 server_msg_id > after 的消息(升序,limit 上限) virtual std::vector fetch_after(const std::string& conv, int64_t after, int limit) = 0; // 会话成员(用于群聊扇出与单聊解析) virtual std::vector members_of(const std::string& conv) = 0; }; 3. 认证抽象 + Token cpp struct UserInfo { std::string user_id; std::string username; }; class IAuthService { public: virtual ~IAuthService() = default; // 校验账号密码,成功返回 UserInfo virtual std::optional authenticate(const std::string& username, const std::string& password) = 0; }; // HMAC-SHA256 无状态 token: base64(payload) + "." + base64(hmac) class TokenService { public: explicit TokenService(std::string secret); std::string issue(const std::string& user_id, int64_t ttl_sec); // 校验签名 + 过期,成功返回 user_id std::optional verify(const std::string& token); }; 4. Session(RAII,绑定一个 TLS 连接) cpp class Session : public std::enable_shared_from_this { public: using SslStream = asio::ssl::stream; Session(SslStream stream, ServerContext& ctx); void start(); // TLS handshake -> 读循环 void send(const Envelope& env); // 线程安全:经 strand 投递 void close(); const std::string& user_id() const { return user_id_; } const std::string& device_id() const { return device_id_; } bool authenticated() const { return !user_id_.empty(); } private: void do_read(); void on_frame(std::string json); // -> Envelope -> 鉴权 -> 分发 asio::strand strand_; // 串行化该连接所有操作 SslStream stream_; std::vector rx_buffer_; std::string user_id_, device_id_; // 认证后填充 = 可信身份 ServerContext& ctx_; }; 关键安全点:on_frame 中,除 login 外所有消息先 TokenService::verify(token),并用返回的 user_id 覆盖任何业务字段里的发送者。客户端伪造 sender_id 无效。 5. 消息分发(Factory + Registry,满足 OCP) cpp using Handler = std::function; class HandlerRegistry { public: void register_handler(std::string type, Handler h); void dispatch(Session& s, const Envelope& env); // 未知 type -> error private: std::unordered_map handlers_; }; 新增功能只需 registry.register_handler("xxx", ...),主循环零改动。 6. 路由 + 在线状态(Observer) cpp class MessageRouter { public: // 单聊/群聊统一:落库 -> 取成员 -> 向在线成员所有设备 push -> 离线者靠 sync void deliver(const StoredMessage& msg); private: IMessageStore& store_; SessionManager& sessions_; // user_id -> 多个 Session(多设备) }; 六、服务端运行模型(确定的并发方案) 采用 Reactor + IO 线程池,无业务锁热点: ini main 线程: 读配置 -> 建 TLS 上下文 -> acceptor.async_accept IoContextPool: N 个 io_context(N = 硬件并发数),每个跑在独立线程 每个 Session 绑定到某个 io_context 的 strand => 同一连接的读、解码、发送串行,无需为单连接加锁 SessionManager: 跨连接共享,内部用 sharded mutex(按 user_id 哈希分片) 数据流(发消息): rust Session.on_frame -> verify token -> HandlerRegistry.dispatch("send_message") -> 校验该 user 是会话成员 -> IMessageStore.append (事务,返回 server_msg_id) -> 回 send_message_resp(含 server_msg_id)给发送方 -> MessageRouter.deliver: members = store.members_of(conv) for each online member's each device session: session.send(push_message) // 离线成员不缓存副本,上线后用 sync_request 拉 可靠性:接收方收到 push_message 回 message_ack;客户端本地记录每会话 last_msg_id,重连后 sync_request(after=last_msg_id) 补齐。这套游标机制同时覆盖离线消息和丢包重传,不需要额外的离线队列表。 七、存储 schema(SQLite,可直接建表) sql CREATE TABLE users ( user_id TEXT PRIMARY KEY, username TEXT UNIQUE NOT NULL, pwd_hash BLOB NOT NULL, -- PBKDF2 输出 pwd_salt BLOB NOT NULL, created_at INTEGER NOT NULL ); CREATE TABLE conversations ( conversation_id TEXT PRIMARY KEY, type INTEGER NOT NULL, -- 1=single 2=group created_at INTEGER NOT NULL ); CREATE TABLE conversation_members ( conversation_id TEXT NOT NULL, user_id TEXT NOT NULL, role INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(conversation_id, user_id) ); CREATE TABLE messages ( server_msg_id INTEGER PRIMARY KEY AUTOINCREMENT, conversation_id TEXT NOT NULL, sender_id TEXT NOT NULL, content_type TEXT NOT NULL, content TEXT NOT NULL, timestamp INTEGER NOT NULL ); CREATE INDEX idx_msg_conv ON messages(conversation_id, server_msg_id); messages.server_msg_id 全局自增即满足"会话内单调",fetch_after 用 WHERE conversation_id=? AND server_msg_id>? ORDER BY server_msg_id LIMIT ?。所有写入用单条 BEGIN/COMMIT 包裹保证原子。SQLite 开 WAL 模式提升并发读。 八、安全实现要点(逐条可落地) TLS 1.3 强制 服务端:asio::ssl::context ctx{tls_server}; 加载证书私钥,set_options(no_tlsv1|no_tlsv1_1|no_tlsv1_2)。 客户端:校验服务端证书(verify_peer),生产环境校验 CN/SAN;开发期可配置自签 CA。 提供脚本 scripts/gen_cert.sh 生成自签证书用于本地双机测试。 密码存储 注册:随机 16 字节 salt,PBKDF2-HMAC-SHA256, 200000 iters, 32B。 校验:常量时间比较(CRYPTO_memcmp)防时序攻击。 Token payload {uid, exp} → base64;签名 HMAC-SHA256(secret, payload)。 每帧校验签名与 exp;secret 从配置/环境变量读,不硬编码。 不信任客户端身份:send_message 的发送者一律取 session.user_id(),并校验其属于目标会话成员,否则 error: not_a_member。 抗滥用:每连接令牌桶限流(默认 20 msg/s);MAX_FRAME 限制;未认证连接 10s 内未 login 则断开。 九、客户端设计(Command 模式) cpp struct CommandContext { NetworkClient& net; ClientState& state; }; class ICommand { public: virtual ~ICommand() = default; virtual void execute(CommandContext& ctx, const std::vector& args) = 0; }; // LoginCommand / ToCommand / SendCommand / HistoryCommand / SyncCommand / QuitCommand 客户端两线程: 网络线程:Asio io_context,收 push_message → 打印到终端 → 回 message_ack,更新本地 last_msg_id。 输入线程:linenoise 读行 → CommandParser 解析 → 找到 ICommand 执行。 交互示例: csharp cchat> /login alice secret [ok] logged in as u_alice cchat> /to bob [u_alice -> bob]> hello [u_alice -> bob]> /history 20 [bob 12:01] hi there 启动即自动 sync_request 补齐离线消息。 十、构建配置(可直接用) cmake/Dependencies.cmake 用 FetchContent 拉取 asio、nlohmann_json、spdlog、Catch2;OpenSSL、SQLite3 用 find_package(或一并 FetchContent)。核心库与可执行分离: cmake add_library(cchat_core ...) # proto/net/core/auth/storage target_link_libraries(cchat_core PUBLIC asio nlohmann_json::nlohmann_json OpenSSL::SSL OpenSSL::Crypto SQLite::SQLite3 spdlog::spdlog) add_executable(cchat_server apps/cchat_server.cpp) target_link_libraries(cchat_server PRIVATE cchat_core) add_executable(cchat_client apps/cchat_client.cpp) target_link_libraries(cchat_client PRIVATE cchat_core linenoise) 十一、实现顺序(下一步直接照此写代码) 按依赖自底向上,每步均可独立单测,保证随时可编译运行: proto/envelope + message_types + FrameCodec(纯逻辑,先写 Catch2 测试覆盖半包/粘包/超限)。 auth/token(PBKDF2 + HMAC,单测签发/校验/过期)。 storage/sqlite_*(实现 IMessageStore/IUserStore,单测 append/fetch_after)。 net/ssl_context + net/io_context_pool。 core/session + session_manager + handler_registry + message_router。 apps/cchat_server:组装上述 + 注册 handlers。 client/:NetworkClient + ICommand 体系 + apps/cchat_client。 双机联调:gen_cert.sh → A 机起 server → B 机 client 连接收发。 这版的关键改进是:消除了"可选项"带来的模糊,固定了协议字节布局、JSON schema、并发模型、表结构、安全实现的每个细节,并把每个模块映射到具体设计模式与文件。 任意模块都能独立编码、独立测试。 需要的话,下一步我可以直接产出第 1 步(FrameCodec + Envelope + 对应 Catch2 测试)的完整可编译代码作为起点。要从这里开始吗? proto/envelope + message_types + FrameCodec(纯逻辑,先写 Catch2 测试覆盖半包/粘包/超限)。 auth/token(PBKDF2 + HMAC,单测签发/校验/过期)。 storage/sqlite_*(实现 IMessageStore/IUserStore,单测 append/fetch_after)。 net/ssl_context + net/io_context_pool。 core/session + session_manager + handler_registry + message_router。 apps/cchat_server:组装上述 + 注册 handlers。 client/:NetworkClient + ICommand 体系 + apps/cchat_client。 双机联调:gen_cert.sh → A 机起 server → B 机 client 连接收发。 请给出整个完整的方案实现 可先提供目录结构 测试代码可暂不提供 主要聚焦于功能实现 展示更少 Claude Opus 4.8 工件创建工具当前不可用,因此我把完整实现直接以代码块形式给出,你可以按目录逐个落地。下面聚焦功能实现,省略测试代码。 目录结构 bash cchat/ ├── CMakeLists.txt ├── cmake/Dependencies.cmake ├── scripts/gen_cert.sh ├── configs/{server.toml, client.toml} ├── include/cchat/ │ ├── proto/message_types.hpp │ ├── proto/envelope.hpp │ ├── net/frame_codec.hpp │ ├── net/ssl_context.hpp │ ├── net/io_context_pool.hpp │ ├── auth/token.hpp │ ├── auth/iauth_service.hpp │ ├── storage/types.hpp │ ├── storage/imessage_store.hpp │ ├── storage/iuser_store.hpp │ ├── storage/sqlite_store.hpp │ ├── core/server_context.hpp │ ├── core/session.hpp │ ├── core/session_manager.hpp │ ├── core/handler_registry.hpp │ ├── core/presence_manager.hpp │ ├── core/message_router.hpp │ └── client/network_client.hpp ├── src/ │ ├── proto/envelope.cpp │ ├── net/frame_codec.cpp │ ├── net/ssl_context.cpp │ ├── auth/token.cpp │ ├── storage/sqlite_store.cpp │ ├── core/session.cpp │ ├── core/session_manager.cpp │ ├── core/handler_registry.cpp │ ├── core/message_router.cpp │ └── core/handlers.cpp └── apps/ ├── cchat_server.cpp └── cchat_client.cpp 1. 构建配置 CMakeLists.txt cmake cmake_minimum_required(VERSION 3.20) project(cchat VERSION 0.1.0 LANGUAGES C CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_C_STANDARD 11) if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Release) endif() include(cmake/Dependencies.cmake) add_library(cchat_core src/proto/envelope.cpp src/net/frame_codec.cpp src/net/ssl_context.cpp src/auth/token.cpp src/storage/sqlite_store.cpp src/core/session.cpp src/core/session_manager.cpp src/core/handler_registry.cpp src/core/message_router.cpp src/core/handlers.cpp ) target_include_directories(cchat_core PUBLIC ${PROJECT_SOURCE_DIR}/include) target_link_libraries(cchat_core PUBLIC asio nlohmann_json::nlohmann_json OpenSSL::SSL OpenSSL::Crypto SQLite::SQLite3 spdlog::spdlog Threads::Threads) add_executable(cchat_server apps/cchat_server.cpp) target_link_libraries(cchat_server PRIVATE cchat_core) add_executable(cchat_client apps/cchat_client.cpp) target_link_libraries(cchat_client PRIVATE cchat_core) cmake/Dependencies.cmake cmake include(FetchContent) find_package(Threads REQUIRED) find_package(OpenSSL REQUIRED) # SQLite3 find_package(SQLite3 QUIET) if(NOT SQLite3_FOUND) message(FATAL_ERROR "Install libsqlite3-dev") endif() FetchContent_Declare(json GIT_REPOSITORY https://github.com/nlohmann/json.git GIT_TAG v3.11.3) FetchContent_MakeAvailable(json) FetchContent_Declare(spdlog GIT_REPOSITORY https://github.com/gabime/spdlog.git GIT_TAG v1.14.1) FetchContent_MakeAvailable(spdlog) # standalone Asio (header only) FetchContent_Declare(asio GIT_REPOSITORY https://github.com/chriskohlhoff/asio.git GIT_TAG asio-1-30-2) FetchContent_MakeAvailable(asio) add_library(asio INTERFACE) target_include_directories(asio INTERFACE ${asio_SOURCE_DIR}/asio/include) target_compile_definitions(asio INTERFACE ASIO_STANDALONE ASIO_NO_DEPRECATED) target_link_libraries(asio INTERFACE Threads::Threads OpenSSL::SSL OpenSSL::Crypto) scripts/gen_cert.sh bash #!/usr/bin/env bash set -e mkdir -p certs openssl req -x509 -newkey rsa:2048 -nodes \ -keyout certs/server.key -out certs/server.crt \ -days 3650 -subj "/CN=cchat-local" \ -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" echo "Generated certs/server.crt and certs/server.key" 2. 协议层 include/cchat/proto/message_types.hpp cpp #pragma once namespace cchat::proto { // 消息类型字符串常量(线路冻结) inline constexpr auto kLogin = "login"; inline constexpr auto kLoginResp = "login_resp"; inline constexpr auto kHeartbeat = "heartbeat"; inline constexpr auto kHeartbeatResp = "heartbeat_resp"; inline constexpr auto kSendMessage = "send_message"; inline constexpr auto kSendMessageResp = "send_message_resp"; inline constexpr auto kPushMessage = "push_message"; inline constexpr auto kMessageAck = "message_ack"; inline constexpr auto kSyncRequest = "sync_request"; inline constexpr auto kSyncResp = "sync_resp"; inline constexpr auto kCreateGroup = "create_group"; inline constexpr auto kCreateGroupResp = "create_group_resp"; inline constexpr auto kError = "error"; // 错误码 enum class ErrCode { Ok = 0, BadRequest = 400, Unauthorized = 401, NotMember = 403, NotFound = 404, RateLimited = 429, Internal = 500 }; } // namespace cchat::proto include/cchat/proto/envelope.hpp cpp #pragma once #include #include #include namespace cchat::proto { // 统一信封:所有消息共用外层结构,业务字段在 data struct Envelope { std::string type; std::uint64_t seq = 0; std::string token; // login 之外必填 nlohmann::json data = nlohmann::json::object(); // ---- Builder 风格构造 ---- static Envelope make(std::string type) { Envelope e; e.type = std::move(type); return e; } Envelope& with_seq(std::uint64_t s) { seq = s; return *this; } Envelope& with_token(std::string t) { token = std::move(t); return *this; } Envelope& with_data(nlohmann::json d) { data = std::move(d); return *this; } std::string to_json() const; static Envelope from_json(const std::string& s); // 解析失败抛 std::runtime_error // 快速构造一个 error 信封 static Envelope error(std::uint64_t seq, int code, const std::string& msg); }; } // namespace cchat::proto src/proto/envelope.cpp cpp #include "cchat/proto/envelope.hpp" #include "cchat/proto/message_types.hpp" #include namespace cchat::proto { std::string Envelope::to_json() const { nlohmann::json j; j["type"] = type; j["seq"] = seq; if (!token.empty()) j["token"] = token; j["data"] = data; return j.dump(); } Envelope Envelope::from_json(const std::string& s) { nlohmann::json j = nlohmann::json::parse(s); // 抛 parse_error Envelope e; if (!j.contains("type") || !j["type"].is_string()) throw std::runtime_error("envelope: missing type"); e.type = j["type"].get(); e.seq = j.value("seq", 0ull); e.token = j.value("token", std::string{}); e.data = j.value("data", nlohmann::json::object()); return e; } Envelope Envelope::error(std::uint64_t seq, int code, const std::string& msg) { return Envelope::make(kError).with_seq(seq) .with_data({{"code", code}, {"message", msg}}); } } // namespace cchat::proto 3. 分帧编解码 include/cchat/net/frame_codec.hpp cpp #pragma once #include #include #include #include #include namespace cchat::net { struct ProtocolError : std::runtime_error { using std::runtime_error::runtime_error; }; // 线路分帧: [uint32 BE length][JSON payload] class FrameCodec { public: static constexpr std::uint32_t kMaxFrame = 1u << 20; // 1 MiB // 封帧 static std::vector encode(const std::string& json); // 从累积缓冲尝试取出一帧;不足返回 nullopt;超限/非法抛 ProtocolError static std::optional try_decode(std::vector& buffer); }; } // namespace cchat::net src/net/frame_codec.cpp cpp #include "cchat/net/frame_codec.hpp" #include namespace cchat::net { std::vector FrameCodec::encode(const std::string& json) { if (json.size() > kMaxFrame) throw ProtocolError("encode: frame too large"); std::vector out; out.reserve(4 + json.size()); std::uint32_t n = static_cast(json.size()); out.push_back(static_cast((n >> 24) & 0xFF)); out.push_back(static_cast((n >> 16) & 0xFF)); out.push_back(static_cast((n >> 8) & 0xFF)); out.push_back(static_cast( n & 0xFF)); out.insert(out.end(), json.begin(), json.end()); return out; } std::optional FrameCodec::try_decode(std::vector& buf) { if (buf.size() < 4) return std::nullopt; std::uint32_t n = (std::uint32_t(buf[0]) << 24) | (std::uint32_t(buf[1]) << 16) | (std::uint32_t(buf[2]) << 8) | std::uint32_t(buf[3]); if (n > kMaxFrame) throw ProtocolError("decode: frame too large"); if (buf.size() < 4 + n) return std::nullopt; // 半包,继续读 std::string payload(reinterpret_cast(buf.data() + 4), n); buf.erase(buf.begin(), buf.begin() + 4 + n); // 处理粘包:留下剩余 return payload; } } // namespace cchat::net 4. 认证 / Token include/cchat/auth/token.hpp cpp #pragma once #include #include #include #include namespace cchat::auth { // PBKDF2-HMAC-SHA256 密码哈希工具 struct PasswordHash { static std::vector make_salt(std::size_t n = 16); static std::vector derive(const std::string& password, const std::vector& salt, int iters = 200000, int dklen = 32); static bool verify(const std::string& password, const std::vector& salt, const std::vector& expected, int iters = 200000); }; // 无状态 token: base64url(payload).base64url(hmac_sha256) class TokenService { public: explicit TokenService(std::string secret) : secret_(std::move(secret)) {} std::string issue(const std::string& user_id, std::int64_t ttl_sec); std::optional verify(const std::string& token); // 返回 user_id private: std::string secret_; }; } // namespace cchat::auth src/auth/token.cpp cpp #include "cchat/auth/token.hpp" #include "cchat/proto/envelope.hpp" // for json #include #include #include #include #include #include namespace cchat::auth { namespace { // base64url (无填充) std::string b64url_encode(const std::uint8_t* d, std::size_t n) { static const char* t = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; std::string out; int val = 0, bits = -6; for (std::size_t i = 0; i < n; ++i) { val = (val << 8) + d[i]; bits += 8; while (bits >= 0) { out.push_back(t[(val >> bits) & 0x3F]); bits -= 6; } } if (bits > -6) out.push_back(t[((val << 8) >> (bits + 8)) & 0x3F]); return out; } std::vector b64url_decode(const std::string& s) { static int T[256]; static bool init = false; if (!init) { for (int i=0;i<256;i++) T[i]=-1; const char* t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; for (int i=0;i<64;i++) T[(unsigned char)t[i]]=i; init=true; } std::vector out; int val=0,bits=-8; for (unsigned char c : s) { if (T[c]==-1) break; val=(val<<6)+T[c]; bits+=6; if (bits>=0){ out.push_back((val>>bits)&0xFF); bits-=8; } } return out; } std::vector hmac_sha256(const std::string& key, const std::string& msg) { unsigned int len = 0; std::vector out(EVP_MAX_MD_SIZE); HMAC(EVP_sha256(), key.data(), (int)key.size(), reinterpret_cast(msg.data()), msg.size(), out.data(), &len); out.resize(len); return out; } } // namespace std::vector PasswordHash::make_salt(std::size_t n) { std::vector s(n); if (RAND_bytes(s.data(), (int)n) != 1) throw std::runtime_error("RAND_bytes failed"); return s; } std::vector PasswordHash::derive(const std::string& pwd, const std::vector& salt, int iters, int dklen) { std::vector out(dklen); if (PKCS5_PBKDF2_HMAC(pwd.data(), (int)pwd.size(), salt.data(), (int)salt.size(), iters, EVP_sha256(), dklen, out.data()) != 1) throw std::runtime_error("PBKDF2 failed"); return out; } bool PasswordHash::verify(const std::string& pwd, const std::vector& salt, const std::vector& expected, int iters) { auto got = derive(pwd, salt, iters, (int)expected.size()); if (got.size() != expected.size()) return false; return CRYPTO_memcmp(got.data(), expected.data(), got.size()) == 0; } std::string TokenService::issue(const std::string& user_id, std::int64_t ttl_sec) { nlohmann::json p{{"uid", user_id}, {"exp", (std::int64_t)std::time(nullptr) + ttl_sec}}; std::string payload = p.dump(); std::string pb = b64url_encode(reinterpret_cast(payload.data()), payload.size()); auto mac = hmac_sha256(secret_, pb); return pb + "." + b64url_encode(mac.data(), mac.size()); } std::optional TokenService::verify(const std::string& token) { auto dot = token.find('.'); if (dot == std::string::npos) return std::nullopt; std::string pb = token.substr(0, dot); std::string sig = token.substr(dot + 1); auto expect = hmac_sha256(secret_, pb); std::string expect_b = b64url_encode(expect.data(), expect.size()); if (expect_b.size() != sig.size() || CRYPTO_memcmp(expect_b.data(), sig.data(), sig.size()) != 0) return std::nullopt; auto raw = b64url_decode(pb); try { auto p = nlohmann::json::parse(std::string(raw.begin(), raw.end())); if (p.value("exp", (std::int64_t)0) < (std::int64_t)std::time(nullptr)) return std::nullopt; return p.value("uid", std::string{}); } catch (...) { return std::nullopt; } } } // namespace cchat::auth 5. 存储层 include/cchat/storage/types.hpp cpp #pragma once #include #include namespace cchat::storage { struct StoredMessage { std::int64_t server_msg_id = 0; std::string conversation_id; std::string sender_id; std::string content_type; std::string content; std::int64_t timestamp = 0; }; struct UserRecord { std::string user_id; std::string username; }; } // namespace cchat::storage include/cchat/storage/imessage_store.hpp cpp #pragma once #include "cchat/storage/types.hpp" #include #include namespace cchat::storage { class IMessageStore { public: virtual ~IMessageStore() = default; virtual std::int64_t append(StoredMessage& msg) = 0; // 返回分配的 id virtual std::vector fetch_after( const std::string& conv, std::int64_t after, int limit) = 0; virtual std::vector members_of(const std::string& conv) = 0; virtual bool is_member(const std::string& conv, const std::string& uid) = 0; virtual void ensure_single_conversation(const std::string& conv, const std::string& a, const std::string& b) = 0; virtual bool create_group(const std::string& conv, const std::vector& members) = 0; }; } // namespace cchat::storage include/cchat/storage/iuser_store.hpp cpp #pragma once #include "cchat/storage/types.hpp" #include #include namespace cchat::storage { class IUserStore { public: virtual ~IUserStore() = default; // 注册:成功返回 user_id;用户名已存在返回 nullopt virtual std::optional create_user( const std::string& username, const std::string& password) = 0; // 校验账号密码 virtual std::optional authenticate( const std::string& username, const std::string& password) = 0; virtual std::optional find_by_username(const std::string& username) = 0; }; } // namespace cchat::storage include/cchat/storage/sqlite_store.hpp cpp #pragma once #include "cchat/storage/imessage_store.hpp" #include "cchat/storage/iuser_store.hpp" #include #include #include namespace cchat::storage { // 同一 SQLite 后端同时实现两个接口。内部 mutex 串行化写,WAL 提升并发读。 class SqliteStore : public IMessageStore, public IUserStore { public: explicit SqliteStore(const std::string& path); ~SqliteStore() override; // IUserStore std::optional create_user(const std::string&, const std::string&) override; std::optional authenticate(const std::string&, const std::string&) override; std::optional find_by_username(const std::string&) override; // IMessageStore std::int64_t append(StoredMessage&) override; std::vector fetch_after(const std::string&, std::int64_t, int) override; std::vector members_of(const std::string&) override; bool is_member(const std::string&, const std::string&) override; void ensure_single_conversation(const std::string&, const std::string&, const std::string&) override; bool create_group(const std::string&, const std::vector&) override; private: void init_schema(); sqlite3* db_ = nullptr; std::mutex mu_; }; } // namespace cchat::storage src/storage/sqlite_store.cpp cpp #include "cchat/storage/sqlite_store.hpp" #include "cchat/auth/token.hpp" #include #include namespace cchat::storage { using auth::PasswordHash; namespace { void check(int rc, sqlite3* db, const char* what) { if (rc != SQLITE_OK && rc != SQLITE_DONE && rc != SQLITE_ROW) throw std::runtime_error(std::string(what) + ": " + sqlite3_errmsg(db)); } } SqliteStore::SqliteStore(const std::string& path) { if (sqlite3_open(path.c_str(), &db_) != SQLITE_OK) throw std::runtime_error("cannot open db"); sqlite3_exec(db_, "PRAGMA journal_mode=WAL;", nullptr, nullptr, nullptr); sqlite3_exec(db_, "PRAGMA foreign_keys=ON;", nullptr, nullptr, nullptr); init_schema(); } SqliteStore::~SqliteStore() { if (db_) sqlite3_close(db_); } void SqliteStore::init_schema() { const char* ddl = R"sql( CREATE TABLE IF NOT EXISTS users( user_id TEXT PRIMARY KEY, username TEXT UNIQUE NOT NULL, pwd_hash BLOB NOT NULL, pwd_salt BLOB NOT NULL, created_at INTEGER NOT NULL); CREATE TABLE IF NOT EXISTS conversations( conversation_id TEXT PRIMARY KEY, type INTEGER NOT NULL, created_at INTEGER NOT NULL); CREATE TABLE IF NOT EXISTS conversation_members( conversation_id TEXT NOT NULL, user_id TEXT NOT NULL, role INTEGER DEFAULT 0, PRIMARY KEY(conversation_id, user_id)); CREATE TABLE IF NOT EXISTS messages( server_msg_id INTEGER PRIMARY KEY AUTOINCREMENT, conversation_id TEXT NOT NULL, sender_id TEXT NOT NULL, content_type TEXT NOT NULL, content TEXT NOT NULL, timestamp INTEGER NOT NULL); CREATE INDEX IF NOT EXISTS idx_msg_conv ON messages(conversation_id, server_msg_id); )sql"; char* err = nullptr; if (sqlite3_exec(db_, ddl, nullptr, nullptr, &err) != SQLITE_OK) { std::string e = err ? err : "schema"; sqlite3_free(err); throw std::runtime_error(e); } } std::optional SqliteStore::create_user( const std::string& username, const std::string& password) { std::lock_guard lk(mu_); auto salt = PasswordHash::make_salt(); auto hash = PasswordHash::derive(password, salt); std::string uid = "u_" + username; sqlite3_stmt* st = nullptr; sqlite3_prepare_v2(db_, "INSERT INTO users(user_id,username,pwd_hash,pwd_salt,created_at)" " VALUES(?,?,?,?,?)", -1, &st, nullptr); sqlite3_bind_text(st, 1, uid.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 2, username.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_blob(st, 3, hash.data(), (int)hash.size(), SQLITE_TRANSIENT); sqlite3_bind_blob(st, 4, salt.data(), (int)salt.size(), SQLITE_TRANSIENT); sqlite3_bind_int64(st, 5, std::time(nullptr)); int rc = sqlite3_step(st); sqlite3_finalize(st); if (rc == SQLITE_CONSTRAINT) return std::nullopt; // 用户名重复 if (rc != SQLITE_DONE) return std::nullopt; return uid; } std::optional SqliteStore::authenticate( const std::string& username, const std::string& password) { std::lock_guard lk(mu_); sqlite3_stmt* st = nullptr; sqlite3_prepare_v2(db_, "SELECT user_id,pwd_hash,pwd_salt FROM users WHERE username=?", -1, &st, nullptr); sqlite3_bind_text(st, 1, username.c_str(), -1, SQLITE_TRANSIENT); std::optional result; if (sqlite3_step(st) == SQLITE_ROW) { std::string uid = reinterpret_cast(sqlite3_column_text(st, 0)); const auto* h = static_cast(sqlite3_column_blob(st, 1)); int hn = sqlite3_column_bytes(st, 1); const auto* s = static_cast(sqlite3_column_blob(st, 2)); int sn = sqlite3_column_bytes(st, 2); std::vector hash(h, h + hn), salt(s, s + sn); if (PasswordHash::verify(password, salt, hash)) result = UserRecord{uid, username}; } sqlite3_finalize(st); return result; } std::optional SqliteStore::find_by_username(const std::string& username) { std::lock_guard lk(mu_); sqlite3_stmt* st = nullptr; sqlite3_prepare_v2(db_, "SELECT user_id FROM users WHERE username=?", -1, &st, nullptr); sqlite3_bind_text(st, 1, username.c_str(), -1, SQLITE_TRANSIENT); std::optional r; if (sqlite3_step(st) == SQLITE_ROW) r = UserRecord{reinterpret_cast(sqlite3_column_text(st,0)), username}; sqlite3_finalize(st); return r; } std::int64_t SqliteStore::append(StoredMessage& m) { std::lock_guard lk(mu_); sqlite3_exec(db_, "BEGIN", nullptr, nullptr, nullptr); sqlite3_stmt* st = nullptr; sqlite3_prepare_v2(db_, "INSERT INTO messages(conversation_id,sender_id,content_type,content,timestamp)" " VALUES(?,?,?,?,?)", -1, &st, nullptr); sqlite3_bind_text(st, 1, m.conversation_id.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 2, m.sender_id.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 3, m.content_type.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 4, m.content.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int64(st, 5, m.timestamp); int rc = sqlite3_step(st); sqlite3_finalize(st); if (rc != SQLITE_DONE) { sqlite3_exec(db_,"ROLLBACK",0,0,0); throw std::runtime_error("append"); } m.server_msg_id = sqlite3_last_insert_rowid(db_); sqlite3_exec(db_, "COMMIT", nullptr, nullptr, nullptr); return m.server_msg_id; } std::vector SqliteStore::fetch_after( const std::string& conv, std::int64_t after, int limit) { std::lock_guard lk(mu_); sqlite3_stmt* st = nullptr; sqlite3_prepare_v2(db_, "SELECT server_msg_id,sender_id,content_type,content,timestamp FROM messages" " WHERE conversation_id=? AND server_msg_id>? ORDER BY server_msg_id LIMIT ?", -1, &st, nullptr); sqlite3_bind_text(st, 1, conv.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int64(st, 2, after); sqlite3_bind_int(st, 3, limit); std::vector out; while (sqlite3_step(st) == SQLITE_ROW) { StoredMessage m; m.server_msg_id = sqlite3_column_int64(st, 0); m.conversation_id = conv; m.sender_id = reinterpret_cast(sqlite3_column_text(st, 1)); m.content_type = reinterpret_cast(sqlite3_column_text(st, 2)); m.content = reinterpret_cast(sqlite3_column_text(st, 3)); m.timestamp = sqlite3_column_int64(st, 4); out.push_back(std::move(m)); } sqlite3_finalize(st); return out; } std::vector SqliteStore::members_of(const std::string& conv) { std::lock_guard lk(mu_); sqlite3_stmt* st = nullptr; sqlite3_prepare_v2(db_, "SELECT user_id FROM conversation_members WHERE conversation_id=?", -1, &st, nullptr); sqlite3_bind_text(st, 1, conv.c_str(), -1, SQLITE_TRANSIENT); std::vector out; while (sqlite3_step(st) == SQLITE_ROW) out.emplace_back(reinterpret_cast(sqlite3_column_text(st, 0))); sqlite3_finalize(st); return out; } bool SqliteStore::is_member(const std::string& conv, const std::string& uid) { std::lock_guard lk(mu_); sqlite3_stmt* st = nullptr; sqlite3_prepare_v2(db_, "SELECT 1 FROM conversation_members WHERE conversation_id=? AND user_id=?", -1, &st, nullptr); sqlite3_bind_text(st, 1, conv.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 2, uid.c_str(), -1, SQLITE_TRANSIENT); bool ok = sqlite3_step(st) == SQLITE_ROW; sqlite3_finalize(st); return ok; } void SqliteStore::ensure_single_conversation(const std::string& conv, const std::string& a, const std::string& b) { std::lock_guard lk(mu_); sqlite3_exec(db_, "BEGIN", nullptr, nullptr, nullptr); sqlite3_stmt* st = nullptr; sqlite3_prepare_v2(db_, "INSERT OR IGNORE INTO conversations(conversation_id,type,created_at)" " VALUES(?,1,?)", -1, &st, nullptr); sqlite3_bind_text(st, 1, conv.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int64(st, 2, std::time(nullptr)); sqlite3_step(st); sqlite3_finalize(st); for (const auto& u : {a, b}) { sqlite3_prepare_v2(db_, "INSERT OR IGNORE INTO conversation_members(conversation_id,user_id)" " VALUES(?,?)", -1, &st, nullptr); sqlite3_bind_text(st, 1, conv.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 2, u.c_str(), -1, SQLITE_TRANSIENT); sqlite3_step(st); sqlite3_finalize(st); } sqlite3_exec(db_, "COMMIT", nullptr, nullptr, nullptr); } bool SqliteStore::create_group(const std::string& conv, const std::vector& members) { std::lock_guard lk(mu_); sqlite3_exec(db_, "BEGIN", nullptr, nullptr, nullptr); sqlite3_stmt* st = nullptr; sqlite3_prepare_v2(db_, "INSERT INTO conversations(conversation_id,type,created_at) VALUES(?,2,?)", -1, &st, nullptr); sqlite3_bind_text(st, 1, conv.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int64(st, 2, std::time(nullptr)); int rc = sqlite3_step(st); sqlite3_finalize(st); if (rc != SQLITE_DONE) { sqlite3_exec(db_,"ROLLBACK",0,0,0); return false; } for (const auto& u : members) { sqlite3_prepare_v2(db_, "INSERT OR IGNORE INTO conversation_members(conversation_id,user_id)" " VALUES(?,?)", -1, &st, nullptr); sqlite3_bind_text(st, 1, conv.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(st, 2, u.c_str(), -1, SQLITE_TRANSIENT); sqlite3_step(st); sqlite3_finalize(st); } sqlite3_exec(db_, "COMMIT", nullptr, nullptr, nullptr); return true; } } // namespace cchat::storage 6. 网络基础设施 include/cchat/net/io_context_pool.hpp cpp #pragma once #include #include #include #include namespace cchat::net { // Reactor 线程池:N 个 io_context,轮询分配连接 class IoContextPool { public: explicit IoContextPool(std::size_t n) : next_(0) { if (n == 0) n = 1; for (std::size_t i = 0; i < n; ++i) { auto ctx = std::make_shared(); contexts_.push_back(ctx); guards_.push_back(asio::make_work_guard(*ctx)); } } void run() { for (auto& ctx : contexts_) threads_.emplace_back([ctx]{ ctx->run(); }); } void stop() { for (auto& g : guards_) g.reset(); for (auto& ctx : contexts_) ctx->stop(); for (auto& t : threads_) if (t.joinable()) t.join(); } asio::io_context& pick() { return *contexts_[next_++ % contexts_.size()]; } private: std::vector> contexts_; std::vector> guards_; std::vector threads_; std::size_t next_; }; } // namespace cchat::net include/cchat/net/ssl_context.hpp cpp #pragma once #include #include namespace cchat::net { // 服务端:加载证书私钥,仅 TLS1.3 asio::ssl::context make_server_ssl(const std::string& cert, const std::string& key); // 客户端:可选校验 CA asio::ssl::context make_client_ssl(const std::string& ca_file, bool verify); } // namespace cchat::net src/net/ssl_context.cpp cpp #include "cchat/net/ssl_context.hpp" namespace cchat::net { asio::ssl::context make_server_ssl(const std::string& cert, const std::string& key) { asio::ssl::context ctx(asio::ssl::context::tls_server); ctx.set_options(asio::ssl::context::default_workarounds | asio::ssl::context::no_sslv2 | asio::ssl::context::no_sslv3 | asio::ssl::context::no_tlsv1 | asio::ssl::context::no_tlsv1_1 | asio::ssl::context::no_tlsv1_2); ctx.use_certificate_chain_file(cert); ctx.use_private_key_file(key, asio::ssl::context::pem); return ctx; } asio::ssl::context make_client_ssl(const std::string& ca_file, bool verify) { asio::ssl::context ctx(asio::ssl::context::tls_client); ctx.set_options(asio::ssl::context::no_tlsv1 | asio::ssl::context::no_tlsv1_1 | asio::ssl::context::no_tlsv1_2); if (verify) { ctx.set_verify_mode(asio::ssl::verify_peer); if (!ca_file.empty()) ctx.load_verify_file(ca_file); } else { ctx.set_verify_mode(asio::ssl::verify_none); } return ctx; } } // namespace cchat::net 7. 核心业务 include/cchat/core/server_context.hpp cpp #pragma once #include "cchat/storage/sqlite_store.hpp" #include "cchat/auth/token.hpp" #include namespace cchat::core { class SessionManager; class HandlerRegistry; class MessageRouter; // 共享上下文:依赖注入容器(DIP — 持有抽象引用) struct ServerContext { storage::IMessageStore* msg_store = nullptr; storage::IUserStore* user_store = nullptr; auth::TokenService* tokens = nullptr; SessionManager* sessions = nullptr; HandlerRegistry* registry = nullptr; MessageRouter* router = nullptr; }; } // namespace cchat::core include/cchat/core/session.hpp cpp #pragma once #include "cchat/proto/envelope.hpp" #include "cchat/core/server_context.hpp" #include #include #include #include #include namespace cchat::core { class Session : public std::enable_shared_from_this { public: using SslStream = asio::ssl::stream; Session(SslStream stream, ServerContext& ctx); void start(); // TLS 握手 -> 读循环 void send(const proto::Envelope& env); // 经 strand 线程安全发送 void close(); const std::string& user_id() const { return user_id_; } bool authenticated() const { return !user_id_.empty(); } // 由 login handler 调用 void set_identity(std::string uid, std::string device) { user_id_ = std::move(uid); device_id_ = std::move(device); } ServerContext& ctx() { return ctx_; } private: void do_read(); void on_frame(std::string json); void do_write(); SslStream stream_; asio::strand strand_; std::vector rx_; std::vector tx_queue_; bool writing_ = false; std::string user_id_, device_id_; ServerContext& ctx_; }; } // namespace cchat::core src/core/session.cpp cpp #include "cchat/core/session.hpp" #include "cchat/core/session_manager.hpp" #include "cchat/core/handler_registry.hpp" #include "cchat/net/frame_codec.hpp" #include "cchat/proto/message_types.hpp" #include namespace cchat::core { using net::FrameCodec; Session::Session(SslStream stream, ServerContext& ctx) : stream_(std::move(stream)), strand_(asio::make_strand(stream_.get_executor())), ctx_(ctx) {} void Session::start() { auto self = shared_from_this(); stream_.async_handshake(asio::ssl::stream_base::server, asio::bind_executor(strand_, [this, self](std::error_codeec) { if (ec) { spdlog::warn("handshake failed: {}", ec.message()); return; } do_read(); })); } void Session::do_read() { auto self = shared_from_this(); auto buf = std::make_shared>(4096); stream_.async_read_some(asio::buffer(*buf), asio::bind_executor(strand_, [this, self, buf](std::error_codeec,std::size_tn) { if (ec) { close(); return; } rx_.insert(rx_.end(), buf->begin(), buf->begin() + n); try { while (auto frame = FrameCodec::try_decode(rx_)) on_frame(std::move(*frame)); } catch (const std::exception& e) { spdlog::warn("decode error: {}", e.what()); close(); return; } do_read(); })); } void Session::on_frame(std::string json) { proto::Envelope env; try { env = proto::Envelope::from_json(json); } catch (const std::exception& e) { send(proto::Envelope::error(0, 400, std::string("bad json: ") + e.what())); return; } // 鉴权:login 之外必须携带有效 token,并以此覆盖可信身份 if (env.type != proto::kLogin) { auto uid = ctx_.tokens->verify(env.token); if (!uid) { send(proto::Envelope::error(env.seq, 401, "unauthorized")); return; } if (!authenticated()) { // 重连后第一帧带 token,补登记身份 set_identity(*uid, "reconnect"); ctx_.sessions->add(shared_from_this()); } } ctx_.registry->dispatch(*this, env); } void Session::send(const proto::Envelope& env) { auto self = shared_from_this(); auto payload = std::make_shared(env.to_json()); asio::post(strand_, [this, self, payload]() { tx_queue_.push_back(std::move(*payload)); if (!writing_) do_write(); }); } void Session::do_write() { if (tx_queue_.empty()) { writing_ = false; return; } writing_ = true; auto self = shared_from_this(); auto bytes = std::make_shared>( FrameCodec::encode(tx_queue_.front())); asio::async_write(stream_, asio::buffer(*bytes), asio::bind_executor(strand_, [this, self, bytes](std::error_codeec,std::size_t) { if (ec) { close(); return; } tx_queue_.erase(tx_queue_.begin()); do_write(); })); } void Session::close() { if (authenticated()) ctx_.sessions->remove(user_id_, this); std::error_code ec; stream_.lowest_layer().close(ec); } } // namespace cchat::core **include/cchat/core/session_manager.hp void Session::close() { if (authenticated()) ctx_.sessions->remove(user_id_, this); std::error_code ec; stream_.lowest_layer().close(ec); } } // namespace cchat::core **include/cchat/core/session_manager.hp 请继续给出后续代码 展示更少 Claude Opus 4.8 继续给出剩余代码。 include/cchat/core/session_manager.hpp cpp #pragma once #include "cchat/proto/envelope.hpp" #include #include #include #include #include namespace cchat::core { class Session; // user_id -> 多个 Session(多设备)。按 user_id 分片加锁降低竞争。 class SessionManager { public: void add(std::shared_ptr s); void remove(const std::string& user_id, Session* raw); // 向某用户的所有在线设备推送 void push_to_user(const std::string& user_id, const proto::Envelope& env); bool online(const std::string& user_id); private: static constexpr std::size_t kShards = 16; struct Shard { std::mutex mu; std::unordered_map>> map; }; Shard shards_[kShards]; Shard& shard_for(const std::string& uid) { return shards_[std::hash{}(uid) % kShards]; } }; } // namespace cchat::core src/core/session_manager.cpp cpp #include "cchat/core/session_manager.hpp" #include "cchat/core/session.hpp" namespace cchat::core { void SessionManager::add(std::shared_ptr s) { auto& sh = shard_for(s->user_id()); std::lock_guard lk(sh.mu); auto& vec = sh.map[s->user_id()]; // 去重:避免重连重复登记 for (auto& e : vec) if (e.get() == s.get()) return; vec.push_back(std::move(s)); } void SessionManager::remove(const std::string& user_id, Session* raw) { auto& sh = shard_for(user_id); std::lock_guard lk(sh.mu); auto it = sh.map.find(user_id); if (it == sh.map.end()) return; auto& vec = it->second; vec.erase(std::remove_if(vec.begin(), vec.end(), [raw](conststd::shared_ptr&e){ return e.get() == raw; }), vec.end()); if (vec.empty()) sh.map.erase(it); } void SessionManager::push_to_user(const std::string& user_id, const proto::Envelope& env) { auto& sh = shard_for(user_id); std::vector> targets; { std::lock_guard lk(sh.mu); auto it = sh.map.find(user_id); if (it == sh.map.end()) return; targets = it->second; // 拷贝出锁外发送,避免在持锁时进网络回调 } for (auto& s : targets) s->send(env); } bool SessionManager::online(const std::string& user_id) { auto& sh = shard_for(user_id); std::lock_guard lk(sh.mu); auto it = sh.map.find(user_id); return it != sh.map.end() && !it->second.empty(); } } // namespace cchat::core include/cchat/core/handler_registry.hpp cpp #pragma once #include "cchat/proto/envelope.hpp" #include #include #include namespace cchat::core { class Session; using Handler = std::function; // Factory/Registry:按 type 路由处理器,新增类型零侵入主循环(OCP) class HandlerRegistry { public: void register_handler(std::string type, Handler h); void dispatch(Session& s, const proto::Envelope& env); private: std::unordered_map handlers_; }; } // namespace cchat::core src/core/handler_registry.cpp cpp #include "cchat/core/handler_registry.hpp" #include "cchat/core/session.hpp" #include "cchat/proto/message_types.hpp" namespace cchat::core { void HandlerRegistry::register_handler(std::string type, Handler h) { handlers_[std::move(type)] = std::move(h); } void HandlerRegistry::dispatch(Session& s, const proto::Envelope& env) { auto it = handlers_.find(env.type); if (it == handlers_.end()) { s.send(proto::Envelope::error(env.seq, 400, "unknown type: " + env.type)); return; } it->second(s, env); } } // namespace cchat::core include/cchat/core/message_router.hpp cpp #pragma once #include "cchat/storage/types.hpp" namespace cchat::core { class SessionManager; namespace cchat { } // fwd guard // 落库后已由 handler 完成;router 负责扇出推送给在线成员 class MessageRouter { public: MessageRouter(storage::IMessageStore& store, SessionManager& sessions) : store_(store), sessions_(sessions) {} // 向会话所有成员的在线设备推送 push_message;离线成员靠 sync 拉取 void deliver(const storage::StoredMessage& msg); private: storage::IMessageStore& store_; SessionManager& sessions_; }; } // namespace cchat::core src/core/message_router.cpp cpp #include "cchat/core/message_router.hpp" #include "cchat/core/session_manager.hpp" #include "cchat/proto/envelope.hpp" #include "cchat/proto/message_types.hpp" namespace cchat::core { void MessageRouter::deliver(const storage::StoredMessage& m) { auto env = proto::Envelope::make(proto::kPushMessage) .with_data({ {"server_msg_id", m.server_msg_id}, {"conversation_id", m.conversation_id}, {"sender_id", m.sender_id}, {"content_type", m.content_type}, {"content", m.content}, {"timestamp", m.timestamp}, }); for (const auto& uid : store_.members_of(m.conversation_id)) sessions_.push_to_user(uid, env); // 含发送者自己的其他设备 = 多端同步 } } // namespace cchat::core src/core/handlers.cpp — 业务处理器集中注册 cpp #include "cchat/core/handler_registry.hpp" #include "cchat/core/session.hpp" #include "cchat/core/session_manager.hpp" #include "cchat/core/message_router.hpp" #include "cchat/proto/message_types.hpp" #include "cchat/storage/imessage_store.hpp" #include "cchat/storage/iuser_store.hpp" #include #include namespace cchat::core { namespace { // 单聊会话 id 规范化:与成员顺序无关 std::string single_conv_id(const std::string& a, const std::string& b) { return a < b ? "c_" + a + "_" + b : "c_" + b + "_" + a; } } // namespace // 在 server 启动时调用,集中注册所有 handler void register_all_handlers(HandlerRegistry& reg) { // ---- login ---- reg.register_handler(proto::kLogin, [](Session& s, const proto::Envelope& env) { auto& c = s.ctx(); const auto& d = env.data; std::string username = d.value("username", ""); std::string password = d.value("password", ""); std::string device = d.value("device_id", "default"); auto user = c.user_store->authenticate(username, password); if (!user) { // 自动注册策略:用户名不存在则创建 if (!c.user_store->find_by_username(username)) { auto uid = c.user_store->create_user(username, password); if (uid) user = storage::UserRecord{*uid, username}; } } if (!user) { s.send(proto::Envelope::error(env.seq, 401, "invalid credentials")); return; } s.set_identity(user->user_id, device); c.sessions->add(s.shared_from_this()); std::string token = c.tokens->issue(user->user_id, 7 * 24 * 3600); s.send(proto::Envelope::make(proto::kLoginResp).with_seq(env.seq).with_data({ {"ok", true}, {"user_id", user->user_id}, {"token", token}, {"server_time", (std::int64_t)std::time(nullptr)} })); }); // ---- heartbeat ---- reg.register_handler(proto::kHeartbeat, [](Session& s, const proto::Envelope& env) { s.send(proto::Envelope::make(proto::kHeartbeatResp).with_seq(env.seq) .with_data({{"server_time", (std::int64_t)std::time(nullptr)}})); }); // ---- send_message ---- reg.register_handler(proto::kSendMessage, [](Session& s, const proto::Envelope& env) { auto& c = s.ctx(); const auto& d = env.data; std::string conv = d.value("conversation_id", ""); std::string peer = d.value("to", ""); // 单聊可直接给对端 user_id // 单聊:若传 to 则规范化会话并确保存在 if (conv.empty() && !peer.empty()) { conv = single_conv_id(s.user_id(), peer); c.msg_store->ensure_single_conversation(conv, s.user_id(), peer); } if (conv.empty()) { s.send(proto::Envelope::error(env.seq, 400, "missing conversation")); return; } // 安全:发送者一律取可信身份,且必须是会话成员 if (!c.msg_store->is_member(conv, s.user_id())) { s.send(proto::Envelope::error(env.seq, 403, "not a member")); return; } storage::StoredMessage m; m.conversation_id = conv; m.sender_id = s.user_id(); // 不信任客户端自报 m.content_type = d.value("content_type", "text"); m.content = d.value("content", ""); m.timestamp = std::time(nullptr); std::int64_t id = c.msg_store->append(m); s.send(proto::Envelope::make(proto::kSendMessageResp).with_seq(env.seq).with_data({ {"ok", true}, {"server_msg_id", id}, {"conversation_id", conv} })); c.router->deliver(m); // 扇出给所有成员的在线设备 }); // ---- message_ack(目前仅记录,游标由客户端本地维护) ---- reg.register_handler(proto::kMessageAck, [](Session&, const proto::Envelope&) { }); // ---- sync_request ---- reg.register_handler(proto::kSyncRequest, [](Session& s, const proto::Envelope& env) { auto& c = s.ctx(); const auto& d = env.data; std::string conv = d.value("conversation_id", ""); std::int64_t after = d.value("after_msg_id", (std::int64_t)0); int limit = std::min(d.value("limit", 100), 500); if (!c.msg_store->is_member(conv, s.user_id())) { s.send(proto::Envelope::error(env.seq, 403, "not a member")); return; } auto msgs = c.msg_store->fetch_after(conv, after, limit); nlohmann::json arr = nlohmann::json::array(); for (auto& m : msgs) arr.push_back({ {"server_msg_id", m.server_msg_id}, {"sender_id", m.sender_id}, {"content_type", m.content_type}, {"content", m.content}, {"timestamp", m.timestamp} }); s.send(proto::Envelope::make(proto::kSyncResp).with_seq(env.seq).with_data({ {"conversation_id", conv}, {"messages", arr} })); }); // ---- create_group ---- reg.register_handler(proto::kCreateGroup, [](Session& s, const proto::Envelope& env) { auto& c = s.ctx(); const auto& d = env.data; std::string conv = "g_" + d.value("name", ""); std::vector members; members.push_back(s.user_id()); for (auto& m : d.value("members", nlohmann::json::array())) { auto u = c.user_store->find_by_username(m.get()); if (u) members.push_back(u->user_id); } bool ok = c.msg_store->create_group(conv, members); s.send(proto::Envelope::make(proto::kCreateGroupResp).with_seq(env.seq).with_data({ {"ok", ok}, {"conversation_id", conv} })); }); } } // namespace cchat::core 8. 服务端入口 apps/cchat_server.cpp cpp #include "cchat/net/io_context_pool.hpp" #include "cchat/net/ssl_context.hpp" #include "cchat/storage/sqlite_store.hpp" #include "cchat/auth/token.hpp" #include "cchat/core/server_context.hpp" #include "cchat/core/session.hpp" #include "cchat/core/session_manager.hpp" #include "cchat/core/handler_registry.hpp" #include "cchat/core/message_router.hpp" #include #include #include namespace cchat::core { void register_all_handlers(HandlerRegistry&); } int main(int argc, char** argv) { using namespace cchat; std::string host = "0.0.0.0"; unsigned short port = 9000; std::string cert = "certs/server.crt", key = "certs/server.key", db = "server.db"; std::string secret = "CHANGE_ME_SECRET"; if (argc > 1) port = (unsigned short)std::stoi(argv[1]); try { // ---- 组装依赖(DIP:核心只见抽象) ---- storage::SqliteStore store(db); auth::TokenService tokens(secret); core::SessionManager sessions; core::HandlerRegistry registry; core::ServerContext ctx; core::MessageRouter router(store, sessions); ctx.msg_store = &store; ctx.user_store = &store; ctx.tokens = &tokens; ctx.sessions = &sessions; ctx.registry = ®istry; ctx.router = &router; core::register_all_handlers(registry); net::IoContextPool pool(std::thread::hardware_concurrency()); auto ssl = net::make_server_ssl(cert, key); // ---- acceptor ---- asio::io_context accept_ctx; asio::ip::tcp::acceptor acceptor(accept_ctx, asio::ip::tcp::endpoint(asio::ip::make_address(host), port)); std::function do_accept; do_accept = [&]() { auto& io = pool.pick(); auto stream = std::make_shared(io, ssl); acceptor.async_accept(stream->lowest_layer(), [&, stream](std::error_codeec) { if (!ec) { std::make_shared(std::move(*stream), ctx)->start(); } else { spdlog::warn("accept error: {}", ec.message()); } do_accept(); }); }; do_accept(); pool.run(); spdlog::info("cchat server listening on {}:{}", host, port); accept_ctx.run(); // 阻塞主线程 } catch (const std::exception& e) { spdlog::error("fatal: {}", e.what()); return 1; } return 0; } 9. 客户端 include/cchat/client/network_client.hpp cpp #pragma once #include "cchat/proto/envelope.hpp" #include #include #include #include #include #include #include namespace cchat::client { // 同步连接 + 后台读线程。回调在读线程触发。 class NetworkClient { public: using OnPush = std::function; NetworkClient(std::string host, unsigned short port, std::string ca, bool verify); ~NetworkClient(); bool connect(); void send(const proto::Envelope& env); // 同步封帧写 void set_on_push(OnPush cb) { on_push_ = std::move(cb); } void run_read_loop(); // 在独立线程调用 void stop(); void set_token(std::string t) { token_ = std::move(t); } const std::string& token() const { return token_; } std::uint64_t next_seq() { return ++seq_; } private: using SslStream = asio::ssl::stream; std::string host_; unsigned short port_; std::string ca_; bool verify_; asio::io_context io_; asio::ssl::context ssl_; std::unique_ptr stream_; std::vector rx_; std::string token_; std::atomic seq_{0}; std::atomic running_{false}; OnPush on_push_; }; } // namespace cchat::client 由于客户端实现较短且自包含,直接放入 apps/cchat_client.cpp(NetworkClient 的实现 + Command 体系 + main 一并给出)。 apps/cchat_client.cpp cpp #include "cchat/client/network_client.hpp" #include "cchat/net/frame_codec.hpp" #include "cchat/net/ssl_context.hpp" #include "cchat/proto/message_types.hpp" #include #include #include #include #include #include using namespace cchat; using net::FrameCodec; // ---------------- NetworkClient 实现 ---------------- namespace cchat::client { NetworkClient::NetworkClient(std::string host, unsigned short port, std::string ca, bool verify) : host_(std::move(host)), port_(port), ca_(std::move(ca)), verify_(verify), ssl_(net::make_client_ssl(ca_, verify_)) {} NetworkClient::~NetworkClient() { stop(); } bool NetworkClient::connect() { try { stream_ = std::make_unique(io_, ssl_); asio::ip::tcp::resolver res(io_); auto eps = res.resolve(host_, std::to_string(port_)); asio::connect(stream_->lowest_layer(), eps); stream_->handshake(asio::ssl::stream_base::client); running_ = true; return true; } catch (const std::exception& e) { std::cerr << "[connect failed] " << e.what() << "\n"; return false; } } void NetworkClient::send(const proto::Envelope& env) { auto bytes = FrameCodec::encode(env.to_json()); asio::write(*stream_, asio::buffer(bytes)); } void NetworkClient::run_read_loop() { std::vector buf(4096); while (running_) { std::error_code ec; std::size_t n = stream_->read_some(asio::buffer(buf), ec); if (ec) { running_ = false; std::cerr << "\n[disconnected]\n"; break; } rx_.insert(rx_.end(), buf.begin(), buf.begin() + n); try { while (auto f = FrameCodec::try_decode(rx_)) { auto env = proto::Envelope::from_json(*f); if (on_push_) on_push_(env); } } catch (...) { running_ = false; break; } } } void NetworkClient::stop() { running_ = false; if (stream_) { std::error_code ec; stream_->lowest_layer().close(ec); } } } // namespace cchat::client // ---------------- 客户端状态 ---------------- struct ClientState { std::string user_id; std::string current_peer; // /to 设置的对端 username std::int64_t last_msg_id = 0; bool logged_in = false; }; // ---------------- Command 模式 ---------------- struct CommandContext { client::NetworkClient& net; ClientState& state; }; struct ICommand { virtual ~ICommand() = default; virtual void execute(CommandContext&, const std::vector&) = 0; }; struct LoginCommand : ICommand { void execute(CommandContext& c, const std::vector& a) override { if (a.size() < 3) { std::cout << "usage: /login \n"; return; } c.net.send(proto::Envelope::make(proto::kLogin).with_seq(c.net.next_seq()) .with_data({{"username", a[1]}, {"password", a[2]}, {"device_id", "cli"}})); } }; struct ToCommand : ICommand { void execute(CommandContext& c, const std::vector& a) override { if (a.size() < 2) { std::cout << "usage: /to \n"; return; } c.state.current_peer = a[1]; std::cout << "[now chatting with " << a[1] << "]\n"; } }; struct SendCommand : ICommand { void execute(CommandContext& c, const std::vector& a) override { if (c.state.current_peer.empty()) { std::cout << "use /to first\n"; return; } std::string text; for (std::size_t i = 1; i < a.size(); ++i) { if (i>1) text+=' '; text+=a[i]; } c.net.send(proto::Envelope::make(proto::kSendMessage).with_seq(c.net.next_seq()) .with_token(c.net.token()) .with_data({{"to", "u_" + c.state.current_peer}, {"content", text}})); } }; struct GroupCommand : ICommand { // /group ... void execute(CommandContext& c, const std::vector& a) override { if (a.size() < 2) { std::cout << "usage: /group [members...]\n"; return; } nlohmann::json members = nlohmann::json::array(); for (std::size_t i = 2; i < a.size(); ++i) members.push_back(a[i]); c.net.send(proto::Envelope::make(proto::kCreateGroup).with_seq(c.net.next_seq()) .with_token(c.net.token()) .with_data({{"name", a[1]}, {"members", members}})); } }; struct SyncCommand : ICommand { // /sync void execute(CommandContext& c, const std::vector& a) override { std::string conv = a.size() > 1 ? a[1] : "c_" + std::min(c.state.user_id, "u_"+c.state.current_peer) + "_" + std::max(c.state.user_id, "u_"+c.state.current_peer); c.net.send(proto::Envelope::make(proto::kSyncRequest).with_seq(c.net.next_seq()) .with_token(c.net.token()) .with_data({{"conversation_id", conv}, {"after_msg_id", 0}, {"limit", 100}})); } }; // ---------------- main ---------------- static std::vector split(const std::string& line) { std::vector out; std::istringstream is(line); std::string w; while (is >> w) out.push_back(w); return out; } int main(int argc, char** argv) { std::string host = argc > 1 ? argv[1] : "127.0.0.1"; unsigned short port = argc > 2 ? (unsigned short)std::stoi(argv[2]) : 9000; std::string ca = argc > 3 ? argv[3] : "certs/server.crt"; client::NetworkClient net(host, port, ca, /*verify=*/!ca.empty()); if (!net.connect()) return 1; ClientState state; // 推送回调:处理服务端各类响应 net.set_on_push([&](constproto::Envelope&env) { const auto& d = env.data; if (env.type == proto::kLoginResp && d.value("ok", false)) { state.user_id = d.value("user_id", ""); net.set_token(d.value("token", "")); state.logged_in = true; std::cout << "\n[ok] logged in as " << state.user_id << "\n> " << std::flush; } else if (env.type == proto::kPushMessage) { std::cout << "\n[" << d.value("sender_id","?") << "] " << d.value("content","") << "\n> " << std::flush; state.last_msg_id = d.value("server_msg_id", state.last_msg_id); } else if (env.type == proto::kSyncResp) { for (auto& m : d.value("messages", nlohmann::json::array())) std::cout << "\n(history) [" << m.value("sender_id","?") << "] " << m.value("content",""); std::cout << "\n> " << std::flush; } else if (env.type == proto::kSendMessageResp) { // 回执,可忽略或显示 ✓ } else if (env.type == proto::kError) { std::cout << "\n[error " << d.value("code",0) << "] " << d.value("message","") << "\n> " << std::flush; } }); std::thread reader([&]{ net.run_read_loop(); }); // 命令分发表 std::unordered_map> cmds; cmds["/login"] = std::make_unique(); cmds["/to"] = std::make_unique(); cmds["/send"] = std::make_unique(); cmds["/group"] = std::make_unique(); cmds["/sync"] = std::make_unique(); CommandContext cc{net, state}; std::cout << "cchat client. /login to start.\n> " << std::flush; std::string line; while (std::getline(std::cin, line)) { if (line == "/quit") break; if (line.empty()) { std::cout << "> " << std::flush; continue; } auto args = split(line); if (!args.empty() && args[0][0] == '/') { auto it = cmds.find(args[0]); if (it != cmds.end()) it->second->execute(cc, args); else std::cout << "unknown command\n"; } else { // 非命令:作为消息发往当前对端 SendCommand sc; std::vector a{"/send"}; for (auto& w : args) a.push_back(w); sc.execute(cc, a); } std::cout << "> " << std::flush; } net.stop(); if (reader.joinable()) reader.join(); return 0; } 10. 双机联调步骤 bash # 1. 生成证书(A 机) bash scripts/gen_cert.sh # 2. 构建 cmake -B build && cmake --build build -j # 3. A 机启动服务端(默认 9000) ./build/cchat_server 9000 # 4. 把 certs/server.crt 拷到 B 机,B 机连接 ./build/cchat_client 9000 server.crt # 5. 两端分别 /login alice 123 与 /login bob 123 # bob 端 /to alice 后直接打字发送;alice 端 /to bob 收发 关键落地说明 几个实现层面的要点值得你在编码时注意。 NetworkClient 这里用同步读线程 + 同步写,简化了客户端心智模型;服务端则是全异步 Reactor。客户端 send 在主线程调用、读在独立线程,两者操作同一 SSL stream——OpenSSL 的 stream 不是线程安全的,严格来说写和读并发访问同一 SSL* 需要加锁。当前实现读循环只读、命令线程只写,底层 socket 读写方向分离通常可工作,但生产化时应给写操作加一把 std::mutex,或把客户端也改成单 io_context + strand 模型。我有意保留简单版本以便你先跑通。 服务端 on_frame 里对重连补登记身份的逻辑做了简化(device_id 填 "reconnect"),实际可在重连帧里携带 device_id。message_ack 处理器目前是空实现,因为可靠性游标由客户端本地 last_msg_id + sync_request 兜底,符合前述设计。 自动注册策略写在 login handler 里(用户名不存在则创建),方便联调;生产环境应拆出独立 register 流程并禁用自动建号。 需要我继续补上 configs/*.toml 解析、客户端写锁加固,或 Catch2 测试骨架吗?