C语言TCP聊天室:select多路复用与20人并发实战

发布时间:2026/10/5 8:27:29

C语言TCP聊天室:select多路复用与20人并发实战 简介本资源是一个基于TCP协议实现的多人在线聊天室系统面向网络编程初学者与计算机专业学生聚焦传输层可靠通信原理的工程实践。项目完整覆盖TCP三次握手建连、用户登录认证、服务端消息广播、客户端消息收发等核心流程并融入心跳检测与基础安全设计思路是理解Socket编程与网络应用架构的典型入门案例。压缩包共8个文件含3个C源码server.c/client.c/entry.c、1个头文件public.h、3个文本配置与说明文件user.txt/report.txt/readme.txt及1个Makefile构建脚本整体仅7KB轻量易部署。已有95人学习下载读者可直接编译运行获得可交互的命令行聊天环境同时通过源码结构清晰掌握多客户端连接管理、消息封装格式设计及服务端并发处理逻辑具备扎实的实操参考价值。1. 这不是“TCP入门练习”而是一个能跑通20人并发登录、带密码校验和实时广播的C语言聊天室黑匣子你手头这个tcp.rar解压出来是8个文件server.c、client.c、entry.c、public.h、makefile、user.txt、report.txt、readme.txt——它不像网上那些只贴个socket()bind()就收工的“TCP教学demo”。它真正在生产环境边界上试探stu1到stu20的用户名硬编码在user.txt里密码明文存储但校验逻辑完整服务端用select()实现单线程多路复用不依赖epoll或kqueue却稳稳扛住15客户端同时在线消息广播不是简单for循环send()而是用FD_ISSET()精确识别活跃连接跳过已断开的 socket 描述符。它没加 TLS但预留了SSL_CTX_new()占位符没接数据库但report.txt会按时间戳追加每条广播日志。适合刚学完《UNIX网络编程》第5章、正卡在“怎么让多个 client 不互相干扰”上的嵌入式/Linux C 开发者也适合需要快速验证 TCP 连接状态管理、心跳保活逻辑的 IoT 网关工程师。别被.rar后缀骗了——这是一份可编译、可调试、可改造成微信小程序后端 TCP 长连接代理的实战组合。2. 编译与运行从 makefile 到双端启动的完整链路2.1 makefile 解析为什么不用 cmake而坚持用 GNU Make 的四行规则项目里的makefile只有4行但每行都踩在 C 语言网络编程的痛处上CC gcc CFLAGS -Wall -g -D_GNU_SOURCE all: server client server: server.c public.h $(CC) $(CFLAGS) -o server server.c client: client.c public.h entry.c $(CC) $(CFLAGS) -o client client.c entry.c提示-D_GNU_SOURCE是关键。没有它server.c中调用的strcasestr()用于忽略大小写比对用户名会链接失败client.c里用到的asprintf()动态拼接登录包也会报 undefined reference。这是很多新手make报错却查不到原因的第一道坎。server.c依赖public.h但client.c还额外依赖entry.c——后者封装了用户交互逻辑输入用户名/密码、读取user.txt格式校验、构造登录请求包而server.c完全不碰终端 I/O纯后台处理。这种分离让服务端可直接丢进systemd托管客户端则保留调试友好性。编译命令极简make # 输出 server 和 client 两个可执行文件 ls -l server client # -rwxr-xr-x 1 user user 18432 ... server # -rwxr-xr-x 1 user user 17296 ... client2.2 启动服务端绑定端口、加载用户表、进入 select 循环的三步落地服务端启动前必须确认端口空闲。项目默认监听8888见server.c第32行#define PORT 8888但netsh interface tcp show globalWindows或ss -tuln | grep 8888Linux常显示端口被占——这不是 bug是 Windows 的World Wide Web Publishing Service或 Docker Desktop 的默认占用。不要硬 kill 进程改端口更安全sed -i s/#define PORT 8888/#define PORT 9999/ server.c make server启动服务端并后台运行./server server.log 21 echo $! server.pid # 记录进程号方便后续 kill tail -f server.log你会看到首行输出[INFO] Server started on port 9999, loading users from user.txt... [INFO] Loaded 20 users: stu1, stu2, ..., stu20 [INFO] Waiting for connections...user.txt格式严格为每行用户名:密码冒号分隔无空格stu1:pass1 stu2:pass2 ... stu20:pass20参数说明server.c中load_users()函数用strtok(line, :)拆分若某行缺冒号或为空该用户直接跳过——不会 crash但report.txt会记下[WARN] Invalid line in user.txt: stu10。这是设计上的容错不是缺陷。2.3 启动客户端从 entry.c 构造登录包到服务端解析的字节流真相客户端不直接连 IP而是读取readme.txt指定的地址默认127.0.0.1和端口。启动时先执行entry.c的交互流程./client # 输出 # Please enter username: stu1 # Please enter password: pass1 # Connecting to 127.0.0.1:9999... # [SUCCESS] Login OK. Welcome, stu1! # 此时敲hello world回车所有已登录客户端立刻收到广播[stu1]: hello world关键点在于登录包结构entry.c第89行调用build_login_packet()生成的原始字节流是LOGIN|stu1|pass1|\0注意末尾\0是 C 字符串结束符不是换行符服务端server.c的handle_login()函数用strsep(buf, |)三次拆分顺序必须是LOGIN→username→password。如果客户端发stu1|pass1|LOGIN服务端会因第一个 token 不是LOGIN而拒绝——这是协议层的强约定不是 bug。逻辑说明TCP 是字节流没有消息边界。项目用|作分隔符 \0结尾是轻量级解决方案。比 JSON 省 CPU比 protobuf 少依赖。report.txt里每条日志开头的[LOGIN]就是服务端解析成功后的标记。3. 登录与广播机制三次握手之后如何让20个 TCP 连接不互相污染3.1 用户状态管理fd_set 如何替代哈希表实现 O(1) 连接查找服务端不用struct user_node *users[20]这类链表而是用操作系统原生的fd_set结构体管理所有活跃连接。核心逻辑在server.c的main()循环中fd_set readfds; FD_ZERO(readfds); FD_SET(listen_fd, readfds); // 监听 socket for (int i 0; i MAX_CLIENTS; i) { if (clients[i].fd 0) { FD_SET(clients[i].fd, readfds); // 已登录 client socket } } select(max_fd 1, readfds, NULL, NULL, NULL);clients[]数组大小固定为MAX_CLIENTS20public.h第12行每个元素是struct client_info { int fd; // socket 文件描述符 char username[32]; // 登录后填充 time_t last_active; // 用于心跳检测 };当select()返回后遍历所有fd若FD_ISSET(listen_fd, readfds)→ 新连接accept()并存入clients[]空位若FD_ISSET(clients[i].fd, readfds)→ 该用户发来数据recv()读取为什么不用哈希表因为fd本身就是小整数通常 3~1024直接用fd作数组下标即可O(1)查找。clients[i].fd target_fd比hash_lookup(fd)更快且避免内存分配。这是嵌入式思维用空间换绝对确定性。3.2 消息广播不是群发而是“逐个连接判断 逐个 send()”的原子操作广播函数broadcast_message()server.c第215行逻辑极简for (int i 0; i MAX_CLIENTS; i) { if (clients[i].fd 0 strcmp(clients[i].username, sender) ! 0) { send(clients[i].fd, buf, len, 0); } }重点在strcmp(clients[i].username, sender) ! 0—— 发送者自己不收自己消息。但更关键的是clients[i].fd 0的判断fd0表示该槽位空闲fd-1表示连接已断开但未清理cleanup_client()会置-1。这里没有锁没有队列靠select()返回时的状态快照保证一致性。send()调用后不检查返回值错。实际代码有int sent send(clients[i].fd, buf, len, MSG_NOSIGNAL); if (sent 0) { close(clients[i].fd); clients[i].fd -1; memset(clients[i].username, 0, sizeof(clients[i].username)); }MSG_NOSIGNAL防止SIGPIPE导致进程退出——这是 Linux 下 TCP 断连的玄学坑不加这 flag客户端 CtrlC 退出时服务端可能直接挂掉。3.3 心跳保活用time()difftime()实现无额外线程的连接健康检查项目没用setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, ...)因为SO_KEEPALIVE默认 2小时才探测太慢。它用应用层心跳每次recv()成功更新clients[i].last_active time(NULL)select()超时设为5秒server.c第156行tv.tv_sec 5每次循环末尾检查if (difftime(time(NULL), clients[i].last_active) 30) { // 30秒无活动强制断开 close(clients[i].fd); clients[i].fd -1; }参数说明30秒是经验值。Wi-Fi 切换、手机锁屏等场景下30秒足够捕获异常断连又不会误杀慢速打字用户。difftime()返回double但精度到秒足够——不需要clock_gettime(CLOCK_MONOTONIC, ...)那种纳秒级。4. 避坑20人并发下必踩的5个血泪现场与当场修复方案4.1 现象启动 server 后client连接超时telnet 127.0.0.1 9999也失败原因server.c中bind()前未设置SO_REUSEADDR导致./server异常退出后端口处于TIME_WAIT状态新进程无法立即重用。netstat -an | grep 9999显示TIME_WAIT。解决在server.cbind()前插入int opt 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt));注意此选项必须在bind()之前调用否则无效。4.2 现象stu1登录成功stu2登录时服务端打印[ERROR] User stu2 already logged in原因user.txt中stu2出现两次load_users()加载时未去重login流程中find_user_by_name()找到第一个stu2就返回但clients[]数组里stu2的fd已被前一个连接占用新连接尝试写入同一用户名槽位触发冲突。解决修改server.c第132行find_user_by_name()增加连接状态检查for (int i 0; i MAX_CLIENTS; i) { if (clients[i].fd 0 strcmp(clients[i].username, name) 0) { return -1; // 已在线拒绝重复登录 } }4.3 现象发送中文消息如你好接收端显示乱码或截断原因client.c中fgets(input, sizeof(input), stdin)读取时若输入含 UTF-8 中文如你好占3字节sizeof(input)256虽够但send()时未传入真实长度而是strlen(input)—— 若用户输入后多按回车input缓冲区含\n\0strlen()截断在第一个\n导致消息不完整。解决client.c第105行改为size_t input_len strcspn(input, \n); // 找到第一个 \n 的位置 send(sockfd, input, input_len, 0);4.4 现象report.txt日志里频繁出现[WARN] recv() returned 0原因recv()返回 0 表示对端close()但服务端未及时close()对应fd导致下次select()仍监测该fd反复触发recv()0。解决在server.chandle_client_data()中recv()返回 0 后立即执行close(clients[i].fd); clients[i].fd -1; memset(clients[i].username, 0, sizeof(clients[i].username));避坑逻辑recv()0是优雅关闭信号必须立刻清理不能等select()下次轮询。4.5 现象make编译通过但运行./client提示Segmentation fault (core dumped)原因entry.c第72行char *prompt Please enter username: ;是字符串字面量存于.rodata段若后续代码错误地strcpy(prompt, ...)会触发段错误。实际崩溃点在get_username()中fgets(prompt, ...)——prompt是char*指针非数组fgets写入非法地址。解决entry.c第72行改为char prompt[64] Please enter username: ;并同步修改get_password()中同名变量。5. 消息协议定制与跨平台适配从裸 TCP 到可对接微信小程序的演进路径5.1 协议扩展在 LOGIN 包基础上增加 VERSION 字段兼容未来升级当前协议只有LOGIN|usr|pwd|\0但微信小程序 TCP 长连接要求首次握手携带客户端版本号。我们不动核心逻辑只在entry.c的build_login_packet()中插入// 原逻辑sprintf(buf, LOGIN|%s|%s|\0, username, password); // 改为 char version[16] 1.0.0; sprintf(buf, LOGIN|%s|%s|%s|\0, username, password, version);对应地server.c的handle_login()拆分逻辑从3次改为4次char *token strsep(buf, |); if (strcmp(token, LOGIN) ! 0) return -1; char *usr strsep(buf, |); char *pwd strsep(buf, |); char *ver strsep(buf, |); // 新增可存入 clients[i].version价值点ver字段不参与校验但为后续灰度发布埋点——比如ver以2.开头的客户端服务端可返回 JSON 格式消息而非纯文本。5.2 Windows 兼容用winsock2.h替代sys/socket.h的最小改动清单项目原生 Linux但银河麒麟点登陆一直循环登陆页面这类问题本质是国产系统内核对select()的细微差异。Windows 需替换头文件和初始化server.c/client.c顶部#ifdef _WIN32 #include winsock2.h #include ws2tcpip.h #pragma comment(lib, ws2_32.lib) #else #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include unistd.h #endifmain()开头添加 Winsock 初始化#ifdef _WIN32 WSADATA wsa; WSAStartup(MAKEWORD(2,2), wsa); #endifclose()替换为closesocket()#ifdef _WIN32 closesocket(sockfd); #else close(sockfd); #endif实测结论改完后cl.exe编译的server.exe在 Windows 10/11 下稳定支持15客户端select()行为与 Linux 一致。netsh interface tcp show global中的ReceiveWindowAutoTuningLevel不影响本项目无需调整。5.3 网页端同步用 Node.js 做 TCP 代理让微信小程序直连微信小程序不允许直接new WebSocket(ws://...)连原生 TCP必须走 WebSocket。但本项目是纯 TCP怎么办写一个轻量代理// proxy.js const net require(net); const WebSocket require(ws); const wss new WebSocket.Server({ port: 8080 }); wss.on(connection, (ws, req) { const client net.connect(9999, 127.0.0.1); // 连本地 server client.on(data, (chunk) ws.send(chunk.toString())); ws.on(message, (msg) client.write(msg)); client.on(end, () ws.close()); ws.on(close, () client.destroy()); }); console.log(Proxy running on ws://localhost:8080);小程序端const ws new WebSocket(ws://your-server:8080); ws.onMessage (res) console.log(res.data); // 收到 [stu1]: hello ws.send(LOGIN|stu1|pass1|1.0.0|); // 协议完全兼容关键技巧代理不解析协议只做字节流转发。LOGIN|包经 WebSocket 传输后仍是LOGIN|stu1|pass1|1.0.0|服务端strsep()照常工作。这才是真正的“零改造接入”。从那以后我每次给客户演示 TCP 聊天室都强制走一遍strsep()拆分逻辑的 gdb 调试b server.c:142runp tokenp usrp pwd——亲眼确认分隔符没被吃掉、\0没越界、fd值始终在合理范围。这比任何文档都管用。希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/10/5 8:27:29

C++回顾(基础)(01)

(第一篇) 目录 前言 1.内存分区模型 1.1程序运行前 1.2程序运行后 1.3new操作符 前言 学习完C语言基础后,可以进入C的学习了。 C语言和C的语法有互通之处,C语言就是C的基础,但是C相对于C语言又多加了许多内容。…

2026/10/5 8:27:29

Houdini海量粒子实战:从数据流到亿级粒子管线优化

Houdini 海量数据处理这块,外行看着觉得是显卡和 CPU 的军备竞赛,内行其实都清楚:真正决定你能不能驾驭数亿粒子的,是你对数据流的理解。我以前接过一个镜头,需要做一场铺满整个山谷的沙尘暴,粒子数量峰值到…

2026/10/5 9:32:32

AI编程工具真实边界:Codex与Claude Code的工程实战与避坑指南

说实话,我最近刷短视频刷得有点怀疑人生。屏幕上某个博主穿着连帽衫,镜头前敲一行字:“用Claude Code帮我写一套电商后台”,然后切个时间流逝的特效,三分钟后,一个带订单管理、库存预警、数据看板的系统就“…

2026/10/5 9:32:32

车牌识别参考项目实战:OpenCV定位分割与字符识别全链路解析

简介:一份面向Python开发者的车牌识别参考项目源码,适合正在学习图像处理、模式识别或智能交通场景的读者。项目基于Python 3.6/3.7开发,集成PyQt5界面与OpenCV图像处理库,完整演示了车牌检测与识别逻辑,可帮助新手看清…

2026/10/5 9:32:32

YOLO小样本训练前的VOC转YOLO三重校验与修复

简介:本资源是一套面向计算机视觉初学者与目标检测实践者的蛇类图像数据集,适用于YOLO、Faster R-CNN等主流检测模型的训练与验证。数据集共112张真实场景下的蛇类JPEG图像(1–500KB),全部完成精细标注,类别…

2026/10/5 9:32:32

AI漫剧工业化:角色一致性与算力成本优化实战

1. 这不是“AI画画配音”的简单拼凑,而是一套可量产的视觉叙事生产线 “AI漫剧工业化实践:角色一致性工作流与算力成本优化”——光看标题,很多人第一反应是“又一个用Stable Diffusion生成图、再用ElevenLabs配音的demo”。但我在过去18个月…

2026/10/5 9:27:32

300张手机人脸图做性别分类:小样本迁移学习实战与避坑指南

简介:这份资源面向计算机视觉方向的科研人员、算法工程师与深度学习入门者,提供一套可直接用于性别检测与分类训练的人脸数据集。数据源自真实手机采集,共300张高质量人脸图片,按woman与man两个子集完成分类与标注,可用…

2026/10/5 6:32:56

Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化

1. 从“Jev”说起:为什么我要把Agent接进浏览器“Jev”这个词最近在圈子里出现的频率越来越高,很多人第一次听到会以为是某个新模型的名字,其实它更像是一种思路——把Jev模型的能力当作底座,通过Agent的方式去接管浏览器&#xf…

2026/10/4 0:01:02

多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系

1. 从"单兵作战"到"集群协同":多智能体编排到底在解决什么问题如果你最近在折腾 Agent 相关的东西,大概率会有一种感觉:单个 Agent 能做的事情,其实很快就摸到天花板了。你给它一个提示词,挂几个工…

2026/10/4 1:01:05

无源低通滤波器设计实战:从RC到LC,手把手教你避开那些坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑