前言「用 C 写一个聊天程序」听起来像一个大工程其实核心只有三件事建立 TCP 连接、在连接上收发字节、把一条客户端发来的消息转发给其他客户端。真正需要设计的是消息边界、连接管理和错误处理而不是语言语法。本文实现一个只在本机运行的群聊小工具服务器监听127.0.0.1:9000多个客户端连上后任何一个人发送的一行文字会转发给其余所有人。代码使用 POSIX socket API适用于 Linux、macOS 以及带 POSIX 层的环境Windows 原生 socket 需要winsock2.h、WSAStartup和closesocket接口形状类似但细节不同。文中不涉及任何公网扫描或攻击行为所有地址都限定在本机回环。先说明一个关键概念TCP 是字节流byte stream不是消息流。一次send的内容可能被拆成多次recv多次send也可能被合并成一次recv。如果直接「收到什么就打印什么」短消息通常没事网络一慢或消息一大就会出现半句话。因此必须自己定义消息边界这就是后文的「分帧」。一、协议与消息分帧分帧framing要回答一个问题接收方如何知道一条消息到哪里结束。常见方案有两种。方案做法优点缺点换行分隔每条消息以\n结尾简单、可读、方便手工测试正文不能含换行需处理\r\n长度前缀先发 4 字节长度再发正文二进制安全、边界明确需要处理字节序和最大长度本文选换行分隔因为聊天文本天然按行输入。服务器为每个连接维护一个接收缓冲区收到字节后追加进去循环查找\n每找到一条就切出来转发剩下的半条留在缓冲区等下一次数据。这样无论 TCP 怎么拆分消息边界都由应用层恢复。协议还要约定编码。示例按 UTF-8 字节原样转发服务器不解析字符因此中文不会被破坏但如果客户端做「按字符截断」之类的处理就要注意 UTF-8 是多字节编码不能按单个char切。二、单线程 select 事件循环最直接的并发模型是「一个客户端一个线程」但线程数量随连接数增长同步和退出都更麻烦。聊天室这种 I/O 密集型场景用单线程多路复用更简单把所有 socket 放进select的读集合哪个可读就处理哪个。select的调用形式是select(maxFd 1, readfds, nullptr, nullptr, nullptr)。第一个参数是「最大文件描述符加一」不是连接数fd_set是位图FD_SET把描述符加入集合FD_ISSET检查某个描述符是否就绪。每次调用前都要重新填充集合因为select会修改它。模型适用规模特点select少量连接接口古老、跨平台性好受FD_SETSIZE限制poll中等规模用数组代替位图没有固定上限epollLinux大量连接事件驱动适合高并发每连接一线程少量连接代码直观线程开销和同步成本高select的可移植性最好适合教学和本机小工具。它有两个现实限制文件描述符编号必须小于FD_SETSIZE常见实现是 1024以及每次都要把整个集合从用户态拷进内核。连接数上去后应换poll或epoll。三、完整服务器实现下面的服务器做了这些事创建监听 socket、绑定回环地址、接受连接、用select等待可读事件、按行切分消息、广播给其他客户端、在连接断开时清理。发送部分用循环处理「只写出去一部分」的情况。// 适用于 C17POSIX 平台Linux/macOS#include arpa/inet.h#include sys/select.h#include sys/socket.h#include unistd.h#include cerrno#include csignal#include cstddef#include cstdio#include cstring#include map#include string#include vectornamespace {constexpr int kPort 9000;constexpr std::size_t kBufferSize 1024;constexpr std::size_t kMaxPending 64 * 1024;bool sendAll(int fd, const char* data, std::size_t size) {std::size_t sent 0;while (sent size) {const ssize_t n ::send(fd, data sent, size - sent, 0);if (n 0 errno EINTR) {continue;}if (n 0) {return false;}sent static_caststd::size_t(n);}return true;}void removeClient(std::vectorint clients,std::mapint, std::string pending,std::size_t index) {const int fd clients[index];::close(fd);pending.erase(fd);clients.erase(clients.begin() static_caststd::ptrdiff_t(index));}} // namespaceint main() {// 向已关闭的连接写数据会触发 SIGPIPE默认动作是终止进程std::signal(SIGPIPE, SIG_IGN);const int listenFd ::socket(AF_INET, SOCK_STREAM, 0);if (listenFd 0) {std::perror(socket);return 1;}int yes 1;if (::setsockopt(listenFd, SOL_SOCKET, SO_REUSEADDR,yes, sizeof yes) 0) {std::perror(setsockopt);::close(listenFd);return 1;}sockaddr_in addr{};addr.sin_family AF_INET;addr.sin_addr.s_addr htonl(INADDR_LOOPBACK); // 仅本机addr.sin_port htons(kPort);if (::bind(listenFd, reinterpret_castsockaddr*(addr), sizeof addr) 0) {std::perror(bind);::close(listenFd);return 1;}if (::listen(listenFd, 16) 0) {std::perror(listen);::close(listenFd);return 1;}std::printf(listening on 127.0.0.1:%d\n, kPort);std::vectorint clients;std::mapint, std::string pending;for (;;) {fd_set readfds;FD_ZERO(readfds);FD_SET(listenFd, readfds);int maxFd listenFd;for (int fd : clients) {FD_SET(fd, readfds);if (fd maxFd) {maxFd fd;}}const int ready ::select(maxFd 1, readfds, nullptr, nullptr, nullptr);if (ready 0) {if (errno EINTR) {continue;}std::perror(select);break;}if (FD_ISSET(listenFd, readfds)) {const int fd ::accept(listenFd, nullptr, nullptr);if (fd 0) {if (fd FD_SETSIZE) { // 超出 select 能表示的范围::close(fd);} else {clients.push_back(fd);pending.emplace(fd, std::string{});}}}for (std::size_t i 0; i clients.size();) {const int fd clients[i];if (!FD_ISSET(fd, readfds)) {i;continue;}char buffer[kBufferSize];const ssize_t n ::recv(fd, buffer, sizeof buffer, 0);if (n 0) {removeClient(clients, pending, i);continue;}std::string inbox pending[fd];inbox.append(buffer, static_caststd::size_t(n));std::size_t pos 0;while ((pos inbox.find(\n)) ! std::string::npos) {std::string line inbox.substr(0, pos);inbox.erase(0, pos 1);if (!line.empty() line.back() \r) {line.pop_back();}if (line.empty()) {continue;}line.push_back(\n);for (int out : clients) {if (out ! fd) {// 简化处理发送失败留给后续 recv 清理(void)sendAll(out, line.data(), line.size());}}}if (inbox.size() kMaxPending) {removeClient(clients, pending, i);continue;}i;}}for (int fd : clients) {::close(fd);}::close(listenFd);return 0;}代码里有几个设计取舍。pending用std::mapint, std::string而不是vectorstring是为了在clients删除元素后仍能按 fd 找到缓冲区。广播是同步send如果某个客户端一直不读服务器的这次发送可能阻塞进而拖住所有人小规模本机聊天可以接受正式服务应改成非阻塞 socket 加输出缓冲区或使用epoll的EPOLLOUT。kMaxPending限制单个连接未成行的数据量防止恶意客户端只发半行把内存撑爆。四、客户端与工程注意事项客户端需要同时做两件事读键盘、收服务器消息。用std::thread起一个读线程主线程负责发送是最短的写法。// 适用于 C17POSIX 平台链接时加 -pthread#include arpa/inet.h#include sys/socket.h#include unistd.h#include cstdio#include iostream#include string#include threadint main() {const int fd ::socket(AF_INET, SOCK_STREAM, 0);if (fd 0) {std::perror(socket);return 1;}sockaddr_in addr{};addr.sin_family AF_INET;addr.sin_port htons(9000);if (::inet_pton(AF_INET, 127.0.0.1, addr.sin_addr) ! 1) {std::perror(inet_pton);::close(fd);return 1;}if (::connect(fd, reinterpret_castsockaddr*(addr), sizeof addr) 0) {std::perror(connect);::close(fd);return 1;}std::thread reader([fd] {char buffer[1024];for (;;) {const ssize_t n ::recv(fd, buffer, sizeof buffer, 0);if (n 0) {break;}std::cout.write(buffer, n);std::cout.flush();}});std::string line;while (std::getline(std::cin, line)) {line.push_back(\n);std::size_t sent 0;while (sent line.size()) {const ssize_t n ::send(fd, line.data() sent,line.size() - sent, 0);if (n 0) {break;}sent static_caststd::size_t(n);}if (sent line.size()) {break;}}::shutdown(fd, SHUT_RDWR);reader.join();::close(fd);return 0;}编译服务器时用g -stdc17 -Wall -Wextra server.cpp -o server客户端再加-pthread。运行顺序是先启动服务器再开两个终端分别运行客户端。服务器绑定的是127.0.0.1外部主机无法连接如果确实需要局域网访问要显式绑定网卡地址并同时考虑认证与加密。安全方面这个示例没有任何身份验证任何人连上就能发言消息也是明文。用于学习足够放到共享网络前至少应加上连接数上限、消息长度上限、空闲超时、TLS例如用 OpenSSL 或成熟的网络库以及日志脱敏。不要把「语法上成立」等同于「可以上线」。常见坑点❌ 把一次recv的返回值当成一条完整消息。✅ TCP 是字节流必须用换行或长度前缀分帧并保留未成行的剩余数据。❌send返回的字节数少于请求长度时认为发送失败并丢弃剩余部分。✅ 用循环发送直到全部写完或遇到真正的错误。❌ 不处理EINTR信号一到select或recv就退出循环。✅ 对errno EINTR重试。❌ 向已关闭的连接send进程被SIGPIPE直接终止。✅ 忽略SIGPIPE或使用MSG_NOSIGNALLinux按平台选择。❌ 关闭 fd 后忘记从clients中删除后续select使用已关闭甚至被复用的 fd。✅ 关闭的同时 erase并同步清理该连接的接收缓冲区。❌ 客户端数超过FD_SETSIZE仍继续FD_SET造成越界。✅ 限制最大连接数或改用poll/epoll。❌ 服务器绑定INADDR_ANY本意只是本机测试却暴露到所有网卡。✅ 明确绑定INADDR_LOOPBACK需要对外时再显式配置并加防护。❌ 认为聊天室不需要认证直接把明文端口开到公网。✅ 生产环境加身份验证、TLS、限流和审计示例只用于本机学习。总结要点做法连接socket、bind、listen、accept地址限定127.0.0.1多路复用select读集合每次重新填充参数是最大 fd 加一消息边界换行分帧每连接维护接收缓冲区发送循环send直到写完处理EINTR与SIGPIPE清理close后立即从容器删除避免使用已关闭的 fd客户端读线程收消息主线程读键盘并发送退出时shutdown再join聊天程序的价值在于把网络编程的几个基本问题集中暴露出来字节流没有边界、发送可能只完成一部分、连接随时会断、慢客户端会拖累广播。把分帧、循环发送和连接清理这三件事写扎实再往上加昵称、私聊、房间和 TLS才不会在细节上反复返工。
企业数字化 ERP 产品动态
相关推荐
Flutter代码覆盖率工具在鸿蒙平台的适配实践 1. 为什么需要将Flutter code_coverage适配到鸿蒙平台在跨平台开发领域,Flutter因其高效的渲染性能和"一次编写,多端运行"的特性,已经成为移动应用开发的主流选择之一。而随着鸿蒙操作系统的快速崛起,越来越多的开发者开… · 2026/11/5 2:24:19
智能家居技术瓶颈与未来发展趋势 1. 智能家居的现状与理想差距早上被咖啡机飘散的香气唤醒,窗帘随着日出自动拉开,空调在你起床前已将室温调节到最舒适状态——这样的智能家居场景正在从科幻走向现实。但当我们真正使用市面上的智能家居产品时,常常遇到设备联动不顺畅、语音识… · 2026/11/8 16:23:46
外卖平台菜品管理模块开发实战与优化策略 1. 项目背景与核心价值"苍穹外卖"这个命名很有意思,听起来像是要打造一个覆盖范围极广的外卖平台。作为从业十年的餐饮系统开发者,我参与过多个外卖平台的后台开发,深知菜品管理模块在整个系统中的关键地位。day03这个编号说明这是… · 2026/11/6 23:17:49
齐全的特种工考证流程怎么考 全齐的特种工考证流程怎么考?全国通用证书怎么拿不被坑 你是不是也遇到过这种情况?想考个特种作业操作证,结果一打听报名点,全是各种中介,收费高低不一,证又真假难辨。最让人头疼的是,证书 全国通用… · 2026/11/10 21:28:43
焦作劳务班组负责人怎么拿证规的特种作业操作证?内部消息教你避坑 焦作劳务班组负责人怎么拿证规的特种作业操作证?内部消息教你避坑 你不知道去哪报名,怕被中介坑,结果花了冤枉钱还没拿到证?焦作这边,很多班组负责人在招人、带人考证时都踩过坑,尤其是证规的特种作业操作证,很多人以为只要花钱就能搞定,结果证书不靠… · 2026/11/10 21:28:43
介休弱电电工证怎么报名?工地太忙根本没时间复习考试 介休弱电电工证怎么报名?工地太忙根本没时间复习考试 工地太忙根本没时间复习考试,这几乎是每个特种作业从业者都面临的现实问题。尤其是对于刚毕业的应届生,像洛阳涧西区的工程类毕业生,刚步入社会就面对高强度的工作节奏,一边要适应岗位,一边还要为特… · 2026/11/10 21:28:43
成都电工证在哪办值不值得考?怕被中介坑的你必须知道这些 成都电工证在哪办值不值得考?怕被中介坑的你必须知道这些 你是不是也这样,想考个电工证,但又不知道在成都哪报名?怕被那些打着“包过”旗号的中介坑了钱还拿不到证?别急,今天就来帮你解决这个头疼的问题,告诉你【成都电工证在哪办】到底值不值得考。… · 2026/11/10 21:28:43
襄阳电工证考试费用上岗必须要这个证吗 襁褓电工证考试费用是多少?内部消息曝光报名避坑指南 不知道去哪报名怕被中介坑?襄阳电工证考试费用多少?现在市面上各种培训机构鱼龙混杂,一不小心就可能多花冤枉钱,甚至拿不到正规证书。今天就来给你说说襄阳电工证考试费用到底多少,以及怎么避开那些… · 2026/11/10 21:28:43
电工证丢失还能补吗?千万别踩坑,丢了证等于失业 电工证丢失还能补吗?千万别踩坑,丢了证等于失业 你正在找工作,但手里的电工证突然丢了,这下怎么办?别急,先别慌, 电工证丢失还能补吗 ?答案是肯定的,但 千万别踩坑 ,补办流程如果不了解,可能会耽误你上岗、甚至影响到你的收入。… · 2026/11/10 2:16:10
电力电缆作业证直播报名入口与办理流程良心建议 电力电缆作业证直播报名入口与办理流程良心建议 你是不是也担心学历不高报不了名?别急,今天就给你讲讲 电力电缆作业证直播 的报名流程和那些你必须知道的干货, 良心建议 在这里,别错过。 电力电缆作业证直播报名需要什么条件?… · 2026/11/10 17:46:39
英德电工证报考费多少钱,怕考不过白交培训费?看完这篇别再踩坑 英德电工证报考费多少钱,怕考不过白交培训费?看完这篇别再踩坑 怕考不过白交培训费?英德电工证报考费多少钱,你真的了解吗?现在市面上培训机构五花八门,价格参差不齐,很多人一不小心就花了冤枉钱,结果还考不过。别急,今天就来给你说清楚英德电工证的… · 2026/11/10 17:11:43
丹寨电工证怎么考?内部消息透露报考条件和流程 丹寨电工证怎么考?内部消息透露报考条件和流程 急需特种作业证才能上岗找工作,这是很多电工朋友最真实的焦虑。尤其是像丹寨电工证这样的证书,直接影响你的职业发展和薪资待遇。但你知道吗?很多人都在报考过程中踩坑,有的甚至花了几千块最后白忙一场。今… · 2026/11/10 0:00:13
成都建筑电工证办理郑州报考避坑指南 成都建筑电工证办理郑州报考避坑指南 证书过期了不知道怎么复审延期?别慌!这篇文章手把手带你解决成都建筑电工证办理的难题,顺便避掉郑州报考的那些坑。不管你是刚拿到证的新手,还是准备复审的老电工,这波干货都别错过。… · 2026/11/10 0:00:13
梅州低压电工证怎么考?怕被中介坑?手把手教你全国通用流程 梅州低压电工证怎么考?怕被中介坑?手把手教你全国通用流程 你是不是正为梅州低压电工证发愁?不知道去哪报名,怕被中介骗钱,又担心证书是不是全国通用?其实,这些顾虑都是正常人会有的。今天我就从一个在开封房建工程一线干了几年的电工角度,手把手教你… · 2026/11/10 0:00:13
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/10 20:19:29
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/10 19:22:45
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/10 17:00:47