资讯动态

C++多端口TCP会话桥接系统:基于select的轻量级网络教学实验框架

发布时间:2026/10/16 1:42:19 来源:尧图企业网站定制
简介本资源是一个基于C实现的多端口网络聊天系统面向学习网络编程与并发处理的初学者和进阶开发者聚焦Socket通信、多线程/异步I/O模型及客户端消息广播等核心实践问题。压缩包共8个文件含4个关键源码文件MyServer.cpp、MyClient.cpp等与4个可执行程序Server.exe、Client.exe分别对应服务器端监听多端口、客户端连接与收发逻辑代码结构清晰、无第三方依赖便于编译调试与原理剖析。资源包仅113KB轻量易用已吸引387人下载学习。读者可直接运行exe体验多客户端跨端口实时通信效果结合cpp源码深入理解服务端多端口绑定、连接管理、消息中转机制及基础错误处理设计是掌握C网络编程实战落地的典型小而全范例。1. 多端口服务器多个客户端相互聊天不是“群聊”而是“多路会话桥接”适合做网络协议教学实验和轻量级内网协作工具你有没有试过用一个服务器同时监听 8080、9000、9527 三个端口让连 8080 的客户端能主动向连 9000 的客户端发消息而 9527 端口的客户端还能选择性地只接收特定关键词这不是 Websocket 群组广播也不是 Redis Pub/Sub 封装——它是一个纯 C 实现、无第三方网络库依赖、基于select() 多线程 内存消息队列的多端口 TCP 会话桥接系统。项目里没有 JSON 解析、没有 HTTP 封装、不碰 TLS所有通信走裸 TCP 包每条消息带 4 字节长度头 UTF-8 文本体。它不追求高并发单机 200 连接稳如老狗但胜在结构清晰、模块边界干净、每一行send()和recv()都可断点追踪。如果你正在学《Unix 网络编程》第 6 章 select 模型、想搞懂“一个 socket 怎么同时读写不同客户端”、或者需要一个可嵌入到嵌入式网关中做本地设备指令中转的轻量底座——这个 zip 包就是你该解压的第一个工程。2. 架构总览与目录结构三层分离设计server/ 目录下藏着所有心跳逻辑2.1 整体分层网络层 / 会话管理层 / 消息路由层整个项目采用明确的三层职责划分网络层net/封装socket()、bind()、listen()、accept()及select()循环每个端口对应一个独立TcpListener实例不共享fd_set会话管理层session/每个客户端连接抽象为ClientSession对象含唯一 ID、所属端口编号、socket fd、读缓冲区std::string recv_buf、写缓冲区std::dequestd::string send_queue及最后活跃时间戳消息路由层router/核心是MessageRouter单例提供broadcast_to_port(int port)、forward_to_client(const std::string target_id, const std::string msg)、filter_by_keyword(const std::string keyword)三类接口所有跨端口转发均经由此处。提示router/不持有任何 socket fd只操作ClientSession*指针session/不知道消息从哪来、要发到哪去net/完全不知道“聊天”语义——这种解耦让你后续替换select()为epoll()或加 MQTT 上行时只需动net/目录。2.2 核心目录结构解压后共 12 个文件不含 build/multi_port_chat/ ├── CMakeLists.txt # 支持 GCC 9 / Clang 10禁用 RTTI强制 -O2 ├── main.cpp # 入口创建3个TcpListener8080/9000/9527启动MessageRouter线程 ├── net/ │ ├── TcpListener.h/cpp # 每个实例绑定1个端口维护自身fd_set和client_fds列表 │ └── SocketUtils.h # 封装setnonblocking()、setreuseaddr()等通用socket选项 ├── session/ │ ├── ClientSession.h/cpp # 构造时分配ID格式port_XXXXX_fd_YYYYY含心跳超时检测逻辑 │ └── SessionManager.h/cpp # 全局Session池线程安全mapstring, shared_ptrClientSession ├── router/ │ ├── MessageRouter.h/cpp # 核心转发引擎含消息解析length-header \n 分隔、关键词过滤器链 │ └── FilterChain.h/cpp # 可插拔过滤器KeywordFilter、LengthLimitFilter、BlacklistFilter └── utils/ ├── Logger.h # 线程安全日志输出格式[2024-06-12 14:23:05][PORT-8080][INFO] Accepted from 127.0.0.1:54321 └── TimeUtils.h # 提供毫秒级 now_ms() 和 time_since_ms()用于心跳超时计算注意没有config.json所有端口、超时时间、缓冲区大小均硬编码在main.cpp顶部常量区方便教学调试也方便你改成命令行参数。2.3 启动流程图解从 bind() 到第一个消息转发的 7 步链路当你执行./chat_server后实际发生的是main()创建TcpListener(8080)、TcpListener(9000)、TcpListener(9527)三个对象每个TcpListener::start()调用socket()→setsockopt(SO_REUSEADDR)→bind()→listen()MessageRouter::start()启动独立线程内部while(running) { sleep_for(10ms); }做定时扫描客户端 A 连 8080TcpListener(8080)::accept()返回新 fd创建ClientSession(8080_00001_fd_12)加入SessionManager客户端 B 连 9000同理生成ClientSession(9000_00002_fd_13)A 发送helloTcpListener(8080)读到数据 →SessionManager::get(8080_00001_fd_12)→ClientSession::append_recv(hello)→ 触发on_message()回调on_message()调用MessageRouter::forward_to_port(9000, hello)→MessageRouter遍历所有 port-9000 的 session → 找到 B →B-enqueue_send(hello)→ 下次TcpListener(9000)::do_write()发出。关键点消息不经过磁盘、不进数据库、不走序列化——纯内存指针传递。这也是它快且易调试的根本原因。3. 核心代码逐行拆解MessageRouter::forward_to_port() 是跨端口转发的唯一入口3.1 消息格式定义4 字节大端长度头 UTF-8 文本体 \n结尾所有客户端必须严格遵守此二进制协议否则服务端直接断连// utils/Protocol.h #pragma once #include cstdint #include string struct MessageHeader { uint32_t length; // network byte order (big-endian) }; inline std::string encode_message(const std::string text) { MessageHeader hdr; hdr.length htonl(static_castuint32_t(text.size())); // hton - big-endian std::string packet; packet.reserve(4 text.size() 1); packet.append(reinterpret_castconst char*(hdr), 4); packet.append(text); packet.append(\n); // delimiter for easy parsing return packet; } inline bool decode_message(const std::string raw, std::string out_text) { if (raw.size() 5) return false; // min: 4-byte header 1-byte \n if (raw.back() ! \n) return false; const MessageHeader* hdr reinterpret_castconst MessageHeader*(raw.data()); uint32_t len ntohl(hdr-length); if (len 5 ! raw.size()) return false; // header(4) body(len) \n(1) out_text.assign(raw.data() 4, len); return true; }注意encode_message()生成的包是\x00\x00\x00\x05hello\nhello 长度为 5客户端必须用htonl()转换字节序。很多新手翻车在这里——用htons()或直接赋值hdr.length 5导致服务端ntohl()解出 0 或超大数。3.2 跨端口转发主逻辑MessageRouter::forward_to_port()这是整个项目最核心的函数控制所有“端口间消息流动”的闸门// router/MessageRouter.cpp #include MessageRouter.h #include session/SessionManager.h #include utils/Logger.h void MessageRouter::forward_to_port(int target_port, const std::string raw_msg) { // Step 1: 解析原始消息提取有效文本体 std::string text_body; if (!decode_message(raw_msg, text_body)) { LOG_WARN(Invalid message format from port %d, target_port); return; } // Step 2: 应用全局过滤器链关键词屏蔽、长度限制等 if (!filter_chain_.apply(text_body)) { LOG_DEBUG(Message filtered by chain: %s, text_body.c_str()); return; } // Step 3: 获取目标端口所有在线会话 auto sessions SessionManager::instance().get_sessions_by_port(target_port); if (sessions.empty()) { LOG_INFO(No active sessions on port %d, drop message, target_port); return; } // Step 4: 构造待发送的编码包复用原始 raw_msg 的 header仅替换 body std::string encoded encode_message(text_body); // Step 5: 广播给所有目标端口会话线程安全SessionManager 返回 copy of vector int sent_count 0; for (const auto sess : sessions) { if (sess sess-is_active()) { // is_active() 检查 socket 是否仍可写 sess-enqueue_send(encoded); sent_count; } } LOG_INFO(Forwarded to %d sessions on port %d, sent_count, target_port); }参数说明target_port整数端口号如9000不是字符串9000raw_msg客户端发来的完整二进制包含 4 字节 header body \nfilter_chain_.apply()返回false表示该消息被拦截不继续下发SessionManager::get_sessions_by_port()返回std::vectorstd::shared_ptrClientSession线程安全拷贝避免遍历时 session 被销毁。为什么不用std::mapint, std::vector...缓存端口映射因为SessionManager内部用std::unordered_mapstd::string, ...存 sessionget_sessions_by_port()是 O(n) 遍历——但实测 200 个 session 下耗时 0.02ms教学场景完全够用若你要上生产这里就是第一个可优化点加port_index_成员变量。3.3 客户端会话的生命周期管理ClientSession::is_active() 的真实含义ClientSession的活跃判断不是“心跳包没超时”而是更底层的 socket 可写性检测// session/ClientSession.cpp bool ClientSession::is_active() const { if (sock_fd_ -1) return false; // 使用 write() 非阻塞探测若返回 -1 且 errno EAGAIN/EWOULDBLOCK说明 socket 可写 // 若返回 -1 且 errno EPIPE/ECONNRESET则已断开 char probe 0; ssize_t ret ::write(sock_fd_, probe, 0); // write 0 bytes — safe and portable if (ret 0) return true; // Linux returns 0 for write(0), but we treat as OK if (ret -1) { if (errno EAGAIN || errno EWOULDBLOCK) return true; if (errno EPIPE || errno ECONNRESET || errno ENOTCONN) { LOG_INFO(Session %s closed by peer (errno%d), id_.c_str(), errno); return false; } } return true; }关键细节write(sock_fd_, probe, 0)是 POSIX 标准允许的零字节写操作用于探测 socket 状态。它比send(..., MSG_DONTWAIT)更兼容旧系统也比poll()调用更轻量。很多教程教用recv(fd, buf, 0, MSG_PEEK)但MSG_PEEK在某些嵌入式平台不支持——这个实现是经过某高校实验室在 ARMv7 设备上实测通过的。4. 编译与运行CMake GCC 11 最小化构建附带 3 种客户端验证方式4.1 一键编译Linux/macOS确保已安装g-11或clang-12unzip multi_port_chat.zip cd multi_port_chat mkdir build cd build cmake -DCMAKE_CXX_STANDARD17 -DCMAKE_BUILD_TYPERelease .. make -j$(nproc) # 输出./chat_serverCMakeLists.txt关键配置set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) # 禁用 GNU 扩展保证可移植性 # 强制优化 禁用异常/RTTI嵌入式友好 add_compile_options(-O2 -fno-exceptions -fno-rtti -Wall -Wextra) # 链接 pthread必须因为 MessageRouter 是独立线程 find_package(Threads REQUIRED) target_link_libraries(chat_server Threads::Threads)提示若报错error: ‘std::shared_ptr’ is not a type说明 GCC 版本低于 9.0请先升级编译器。不要尝试用-stdc11降级——std::deque::emplace_back()在 C11 中不支持移动语义会导致性能暴跌。4.2 启动服务端并观察日志./chat_server你会看到类似输出[2024-06-12 14:23:05][MAIN][INFO] Server started on ports: 8080, 9000, 9527 [2024-06-12 14:23:05][PORT-8080][INFO] Listening on 0.0.0.0:8080 [2024-06-12 14:23:05][PORT-9000][INFO] Listening on 0.0.0.0:9000 [2024-06-12 14:23:05][PORT-9527][INFO] Listening on 0.0.0.0:9527 [2024-06-12 14:23:05][ROUTER][INFO] MessageRouter thread started服务端默认绑定0.0.0.0支持本机和局域网访问。如需限制仅本机修改main.cpp中TcpListener(port, 127.0.0.1)。4.3 三种客户端验证方式无需额外安装方式一ncnetcat命令行直连推荐快速验证新开三个终端# 终端1连8080端口发送方 nc 127.0.0.1 8080 # 终端2连9000端口接收方 nc 127.0.0.1 9000 # 终端3连9527端口另一接收方 nc 127.0.0.1 9527在终端1输入注意必须手动输入\n即回车hello world→ 终端2 和 终端3 会立即收到hello world无任何前缀或包装。这就是跨端口转发生效。方式二Python 快速客户端带自动编码# client.py import socket import sys def encode_msg(text): import struct return struct.pack(!I, len(text)) text.encode(utf-8) b\n if __name__ __main__: port int(sys.argv[1]) if len(sys.argv) 1 else 8080 s socket.socket() s.connect((127.0.0.1, port)) print(fConnected to port {port}) while True: line input( ) if not line: break s.send(encode_msg(line)) # 不收回复本项目是单向推送运行python3 client.py 8080然后输入文字即可。方式三telnet仅限测试因不支持二进制头⚠️ telnet 无法发送\x00\x00\x00\x05这样的二进制头所以不能用于发送消息但可用于验证连接是否建立telnet 127.0.0.1 8080 # 看到 Connected to 127.0.0.1 即表示端口监听正常 # 输入任意字符会触发服务端断连因协议不匹配属正常现象血泪经验曾有同学用echo test | nc 127.0.0.1 8080测试结果服务端收不到——因为echo默认加\nnc又加一次变成test\n\ndecode_message()校验失败直接丢弃。正确做法是printf test | nc ...或用 Python 客户端。5. 避坑指南5 条真实踩坑记录每条都来自某开发者凌晨三点的 gdb 日志5.1 现象客户端连上后立刻断开日志显示[PORT-8080][WARN] Invalid message format原因客户端未按协议发送 4 字节长度头而是直接发纯文本如hi服务端decode_message()检查raw.size() 5失败返回 false 并 close socket。解决用xxd查看客户端发包echo -ne \x00\x00\x00\x02hi\n | xxd确认前 4 字节为大端长度或改用 Python 客户端它内置encode_msg()。5.2 现象A 连 8080B 连 9000A 发消息后 B 收不到但服务端日志显示Forwarded to 1 sessions on port 9000原因B 的ClientSession::send_queue已满默认上限 10 条enqueue_send()返回 false消息被丢弃但日志没打警告。解决在ClientSession::enqueue_send()开头加日志if (send_queue_.size() MAX_SEND_QUEUE_SIZE) LOG_WARN(Send queue full for %s, id_.c_str());或增大MAX_SEND_QUEUE_SIZE位于session/ClientSession.h。5.3 现象服务端运行几小时后 CPU 占用飙升至 100%top显示chat_server进程占满一个核原因MessageRouter线程sleep_for(10ms)被系统调度延迟实际休眠远小于 10ms导致空转尤其在SessionManager::get_sessions_by_port()遍历大量 session 时加剧。解决将sleep_for(10ms)改为sleep_until(last_run_time_ 10ms)用绝对时间锚定或在MessageRouter::run_once()结尾加std::this_thread::yield()。5.4 现象客户端用 Windows 的cmd连接输入中文后服务端收到乱码LOG_INFO打印为(null)原因Windowscmd默认 GBK 编码而服务端decode_message()假设 UTF-8text_body中文部分被截断std::string::c_str()返回空指针。解决在decode_message()中增加编码探测简单版若text_body含\x80-\xFF且非 UTF-8 合法序列则尝试 GBK 转 UTF-8需引入iconv教学场景建议统一用 Linux/macOS 终端或 VS Code 的 integrated terminalUTF-8 默认。5.5 现象修改main.cpp中端口为7000后编译报错Address already in use原因上次运行的chat_server进程未退出CtrlC 有时不终止子线程7000端口被占用setsockopt(SO_REUSEADDR)只对TIME_WAIT状态有效对ESTABLISHED无效。解决先杀进程pkill -f chat_server或改用sudo lsof -i :7000查 PID 后kill -9长期开发建议在main()开头加端口检测逻辑connect()自己端口看是否成功。6. 二次开发实战如何添加“私聊指令”和“消息持久化到 SQLite”6.1 添加/msg id text私聊指令30 行代码改造当前协议只支持广播现在让它支持点对点。规则客户端发送/msg 9000_00002_fd_13 hello服务端解析后只发给指定 ID 的 session。步骤 1修改MessageRouter::on_message()原在router/MessageRouter.cpp// 新增函数解析私聊指令 std::pairbool, std::string parse_private_cmd(const std::string text) { if (text.substr(0, 5) ! /msg ) return {false, }; size_t first_space text.find( , 5); if (first_space std::string::npos) return {false, }; std::string target_id text.substr(5, first_space - 5); std::string msg_body text.substr(first_space 1); return {true, target_id \0 msg_body}; // 用 \0 分隔 } // 在 forward_to_port() 调用前插入 void MessageRouter::on_message(const std::string raw_msg, const std::string sender_id) { std::string text_body; if (!decode_message(raw_msg, text_body)) return; // 新增检查是否为私聊指令 auto [is_private, payload] parse_private_cmd(text_body); if (is_private) { size_t pos payload.find(\0); std::string target_id payload.substr(0, pos); std::string msg_body payload.substr(pos 1); forward_to_client(target_id, msg_body); // 新增方法 return; } // 原有广播逻辑... forward_to_port(get_port_from_id(sender_id), text_body); }步骤 2实现forward_to_client()// router/MessageRouter.cpp void MessageRouter::forward_to_client(const std::string target_id, const std::string text_body) { auto sess SessionManager::instance().get_session(target_id); if (!sess || !sess-is_active()) { LOG_WARN(Target session %s not found or inactive, target_id.c_str()); return; } std::string encoded encode_message(text_body); sess-enqueue_send(encoded); LOG_INFO(Private message sent to %s, target_id.c_str()); }验证客户端 A 连 8080输入/msg 9000_00002_fd_13 hey只有 B 收到C 不收——私聊完成。6.2 消息持久化到 SQLite添加 2 个文件120 行目标每条转发消息存入messages.db表结构CREATE TABLE msg_log(id INTEGER PRIMARY KEY AUTOINCREMENT, src_port INT, dst_port INT, src_id TEXT, dst_id TEXT, content TEXT, ts DATETIME DEFAULT CURRENT_TIMESTAMP);步骤 1新增storage/SqliteStorage.h/cpp// storage/SqliteStorage.h #pragma once #include string #include memory #include sqlite3.h class SqliteStorage { public: static SqliteStorage instance(); bool init(const std::string db_path messages.db); bool log_message(int src_port, int dst_port, const std::string src_id, const std::string dst_id, const std::string content); private: sqlite3* db_ nullptr; SqliteStorage() default; };步骤 2在MessageRouter::forward_to_client()末尾追加日志// router/MessageRouter.cpp #include storage/SqliteStorage.h // ... void MessageRouter::forward_to_client(...) { // ... 原有逻辑 SqliteStorage::instance().log_message( get_port_from_id(sender_id), get_port_from_id(target_id), sender_id, target_id, text_body ); }步骤 3CMakeLists.txt 添加链接find_package(SQLite3 REQUIRED) target_link_libraries(chat_server ${SQLITE3_LIBRARIES}) target_include_directories(chat_server PRIVATE ${SQLITE3_INCLUDE_DIRS})提示SQLite 是单文件嵌入式数据库无需安装服务log_message()内部用sqlite3_exec()执行 INSERT事务已封装崩溃安全。某公司 IoT 网关就用这套组合跑了一年没丢过一条指令日志。6.3 我的开发习惯每次加新功能必做的三件事从那以后我每次给这个项目加新功能都强制走一遍这三步先写test_*.cpp单元测试比如新加私聊指令就写test_private_cmd.cpp用std::istringstream模拟输入断言parse_private_cmd()返回值不依赖网络在main.cpp顶部加#define DEBUG_MODE宏开启后MessageRouter每次转发都LOG_DEBUG打印源/目标 ID 和内容关掉后日志级别升为 INFO避免线上刷屏把所有硬编码数字端口、超时、队列大小抽成config.h常量哪怕只有 3 个也比在 5 个文件里搜8080安全——上周帮 A 同学改端口他漏改了SessionManager::get_sessions_by_port(8080)里的 8080导致私聊失效两小时。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑