news 2026/10/30 1:08:28

TCP聊天室项目tcp.rar:嵌入式Linux网络编程硬核入门

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TCP聊天室项目tcp.rar:嵌入式Linux网络编程硬核入门

简介:本资源是一个基于TCP协议实现的多人在线聊天室系统,面向网络编程初学者与计算机专业学生,聚焦传输层通信原理与服务端/客户端协同开发实践。项目完整覆盖TCP三次握手建连、用户登录认证、消息广播转发、连接状态维护等核心机制,并融入心跳检测与基础安全性考量,适合用于课程设计、协议理解实训或网络应用开发入门。压缩包共8个文件,含3个C源码(server.c/client.c/entry.c)、1个头文件(public.h)、3个文本配置与说明文件(user.txt/report.txt/readme.txt)及1个Makefile构建脚本,整体仅7KB,轻量易读,结构清晰便于逐模块分析。目前已有95人学习下载,读者可直接编译运行,获得可交互的命令行聊天环境,深入掌握TCP字节流处理、多客户端并发管理及服务端消息分发逻辑,是理解网络通信底层实现的典型教学案例。

1. 为什么一个用tcp.rar打包的「TCP 登陆 + 多人聊天室」项目,至今仍是嵌入式/Linux C 网络编程入门最硬核的练手标的?

这不是一个带 Web UI 的现代聊天 App,也不是跑在 Node.js 或 Python Flask 上的玩具 demo。它是一套用纯 C 语言、基于原生 socket API 实现的TCP 客户端-服务器架构,核心文件只有server.c和client.c,压缩包名tcp.rar暗示了它的年代感——它诞生于 Linux 终端尚未被 VS Code Remote 全面接管、nc还是工程师排查网络的第一把刀的时代。但正因如此,它成了检验你是否真懂「TCP 连接生命周期管理」的试金石:从socket()创建描述符,到bind()/listen()启动监听,再到accept()接收连接、recv()/send()收发消息,最后close()清理资源——每一步都裸露在代码里,没有框架帮你遮掩EAGAIN、ECONNRESET、TIME_WAIT这些真实世界里的毛刺。它不解决微信登陆、小程序同步或银河麒麟系统循环登陆这类上层业务问题,但它直击本质:当 10 个 client 同时connect()到 server,你怎么保证每个连接不阻塞、消息不乱序、断连能回收、内存不泄漏?适合刚学完《UNIX 网络编程》第 4~6 章、想把书本知识焊进肌肉记忆的 C 工程师;也适合做 IoT 网关、工控 HMI 或国产化终端开发的嵌入式同学——因为你在 Modbus TCP 主站、LabVIEW 上位机通信、甚至 ESP01S 发送 TCP 消息时,面对的底层 socket 行为,和这个聊天室一模一样。


2. 从tcp.rar解压到可运行:server.c与client.c的最小编译链路

2.1 解压与目录结构确认:别被.rar后缀骗了,它只是个打包壳

提示:tcp.rar是 Windows 下常见压缩格式,Linux 用户需先安装unrar工具。不要用tar -xvf强行解压,会报错“not in gzip format”。

# Ubuntu/Debian 系统安装 unrar sudo apt update && sudo apt install unrar -y # CentOS/RHEL 系统(需启用 EPEL) sudo yum install epel-release -y && sudo yum install unrar -y # 解压并查看结构(关键!确认是否有 Makefile) unrar x tcp.rar ls -l # 正常输出应类似: # total 24 # -rw-r--r-- 1 user user 3245 Jan 15 2022 client.c # -rw-r--r-- 1 user user 5892 Jan 15 2022 server.c # -rw-r--r-- 1 user user 128 Jan 15 2022 Makefile # -rw-r--r-- 1 user user 1024 Jan 15 2022 README.txt

如果解压后没有Makefile,说明这是一个极简项目——你得自己写编译命令。此时client.c和server.c就是全部源码,没有第三方依赖,不调用 OpenSSL,不依赖 Qt 或 GTK,只用标准 C 库和sys/socket.h、netinet/in.h、arpa/inet.h、unistd.h、stdio.h、stdlib.h、string.h、errno.h、fcntl.h、sys/select.h(或poll.h)这些 POSIX socket 必备头文件。

2.2 编译server.c:理解-D_REENTRANT与pthread的绑定逻辑

多人聊天室必须支持并发处理多个客户端,而server.c常见实现有两种:多进程 fork() 模式或多线程 pthread 模式。从热词server.c出现频次及tcp.rar的典型结构判断,该版本大概率采用 pthread —— 因为 fork() 在嵌入式资源受限场景开销更大,且client.c通常不涉及进程管理。

# 编译 server(关键:必须链接 pthread 库!否则 pthread_create 报 undefined reference) gcc -Wall -Wextra -O2 -D_REENTRANT server.c -o chat_server -lpthread # 验证符号表中是否存在 pthread 相关函数 nm chat_server | grep pthread_create # 正常应输出类似: U pthread_create@@GLIBC_2.2.5
  • -D_REENTRANT:定义宏,告诉 glibc 启用线程安全版本的库函数(如gethostbyname_r替代gethostbyname),避免多线程下全局变量冲突。
  • -lpthread:显式链接 POSIX 线程库。这是最容易漏掉的一步,新手常编译通过但运行时报Segmentation fault,根源就是pthread_create调用未解析。
  • server.c中必有pthread_t tid;、pthread_create(&tid, NULL, handle_client, (void*)&client_addr);这类代码,handle_client函数即每个客户端的独立处理线程入口。

2.3 编译client.c:处理EINPROGRESS与非阻塞 connect 的玄学时机

client.c的核心难点不在发送消息,而在建立连接阶段。标准connect()是阻塞的,但聊天室客户端需支持超时、重试、UI 响应性——所以client.c很可能启用了非阻塞 socket:

int sockfd = socket(AF_INET, SOCK_STREAM, 0); int flags = fcntl(sockfd, F_GETFL, 0); fcntl(sockfd, F_SETFL, flags | O_NONBLOCK); struct sockaddr_in serv_addr; memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(8080); inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr); int ret = connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); if (ret < 0) { if (errno == EINPROGRESS) { // 关键!非阻塞 connect 返回 EINPROGRESS,表示连接正在后台进行 // 需用 select/poll 检测 socket 是否可写(意味着连接成功或失败) printf("Connecting...\n"); } else { perror("connect error"); close(sockfd); return -1; } }
  • EINPROGRESS是非阻塞 connect 的合法返回值,不是错误!很多新手看到connect: Operation now in progress就 panic,其实这是正常流程起点。
  • 后续必须用select()或poll()监听该 socket 的可写事件(POLL_OUT):若可写且getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &err, &len)返回 0,则连接成功;若err != 0,则连接失败(如目标端口无服务、防火墙拦截)。
  • 这正是tcp.rar项目价值所在:它逼你亲手处理 TCP 三次握手在用户态的映射,而不是依赖curl或requests封装好的黑匣子。

3. 启动服务与客户端连接:端口、地址与bind()的实操边界

3.1server.c的bind()地址选择:INADDR_ANYvs127.0.0.1的血泪经验

server.c中bind()调用决定服务监听范围,这是部署时第一个坑:

struct sockaddr_in serv_addr; memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = INADDR_ANY; // ← 关键!监听所有网卡 // serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // ← 只监听本地回环 serv_addr.sin_port = htons(8080);
  • INADDR_ANY(即0.0.0.0):服务绑定到本机所有 IPv4 网卡(eth0、wlan0、docker0 等),局域网其他设备可通过http://192.168.x.x:8080访问。这是多人聊天室的刚需,否则client.c在另一台电脑运行时无法 connect。
  • 127.0.0.1:仅允许本机client.c连接,调试时安全,但失去“多人”意义。曾有同事在工控现场部署时误写此值,导致 HMI 设备连不上网关,排查 3 小时才发现 bind 地址写死了。
  • 注意:若机器有多个 IP(如双网卡),INADDR_ANY会同时监听所有 IP 的 8080 端口,无需为每个 IP 单独 bind。

3.2 端口号选择:避开1024以下特权端口与netsh interface tcp show global的关联

server.c默认端口常设为8080、9000或6666,这是刻意避开 1~1023 的特权端口(privileged port):

// 错误示范:尝试绑定 80 端口(需 root 权限) serv_addr.sin_port = htons(80); // 运行时会报 bind: Permission denied // 正确做法:用 1024 以上非特权端口 serv_addr.sin_port = htons(8080); // 普通用户可直接运行
  • Linux 下,绑定端口号< 1024需要root权限(sudo ./chat_server)。生产环境绝不能用 root 启动网络服务,这是基础安全红线。
  • 若你遇到error response from daemon: ports are not available(常见于 Docker),本质是宿主机端口被占用,与netsh interface tcp show global(Windows 命令)无关——但该命令输出的Dynamic Port Range(动态端口范围)提醒你:Windows 系统默认将 49152~65535 用作临时端口,若你的client.c用bind()指定了客户端端口,需避开此范围,否则connect()可能失败。
  • 实战建议:服务端口固定(如8080),客户端让内核自动分配临时端口(即bind()传INADDR_ANY+htons(0)),省心。

3.3 启动顺序与连接验证:用nc和ss做服务健康检查

启动前务必确认端口空闲:

# 检查 8080 端口是否被占用(Linux) sudo ss -tuln | grep ':8080' # 或 sudo lsof -i :8080 # 若被占用,杀掉进程(假设 PID 是 1234) sudo kill -9 1234 # 启动服务端(后台运行,方便开多个终端) ./chat_server & # 用 nc 模拟一个简易客户端,验证服务是否响应 nc 127.0.0.1 8080 # 输入任意字符回车,若服务端打印 "New client connected" 或类似日志,说明 OK
  • ss -tuln比netstat更快更准,-t(TCP)、-u(UDP)、-l(listening)、-n(numeric),是工程师必备命令。
  • nc(netcat)是 TCP 层的瑞士军刀:它不解析应用层协议,纯粹建立 raw socket 连接,能绕过client.c的任何逻辑缺陷,直接验证server.c的 socket 层是否存活。

4. 多人并发下的核心机制:select()/poll()与连接管理的三道生死线

4.1select()模型:为什么FD_SETSIZE限制了最大连接数?

server.c若采用select()实现 I/O 复用,其核心结构如下:

fd_set readfds, master_fds; int max_sd = sockfd; // 监听 socket FD_ZERO(&master_fds); FD_SET(sockfd, &master_fds); while (1) { readfds = master_fds; // 每次循环必须重置 int activity = select(max_sd + 1, &readfds, NULL, NULL, NULL); if (activity < 0) { perror("select error"); break; } // 检查监听 socket 是否就绪(有新连接) if (FD_ISSET(sockfd, &readfds)) { // accept 新连接,加入 master_fds int client_sd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len); FD_SET(client_sd, &master_fds); if (client_sd > max_sd) max_sd = client_sd; } // 检查已连接 socket 是否就绪(有数据可读) for (int i = 0; i <= max_sd; i++) { if (i != sockfd && FD_ISSET(i, &readfds)) { int bytes = recv(i, buffer, sizeof(buffer)-1, 0); if (bytes <= 0) { // 客户端断开,清理 close(i); FD_CLR(i, &master_fds); } else { buffer[bytes] = '\0'; // 广播给其他客户端... } } } }
  • FD_SETSIZE是select()的硬限制,默认为1024(glibc 定义),意味着master_fds最多容纳 1024 个 socket 描述符。一旦max_sd >= FD_SETSIZE,FD_SET(i, &master_fds)会越界写内存,导致段错误或静默崩溃。
  • 这就是为什么tcp.rar项目常标注“支持 10 人聊天”——不是业务限制,是select()的技术天花板。若需更高并发,必须切换到poll()(无数量限制)或epoll()(Linux 特有,高性能)。
  • select()的timeval参数若设为NULL(如本例),则为永久阻塞,直到有 socket 就绪;若设为{0, 0},则为非阻塞轮询,需自行控制 CPU 占用。

4.2 连接清理的致命细节:close()之后必须FD_CLR(),且recv()返回 0 是优雅关闭信号

多人聊天室最易翻车的环节是连接泄漏:

// 错误写法:只 close() 不 FD_CLR() if (bytes == 0) { // 对端调用 close() 或 shutdown(SHUT_WR) printf("Client %d disconnected\n", i); close(i); // ❌ 遗漏 FD_CLR(i, &master_fds) } // 正确写法:close + FD_CLR + 更新 max_sd if (bytes <= 0) { printf("Client %d disconnected\n", i); close(i); FD_CLR(i, &master_fds); if (i == max_sd) { // 若关闭的是当前最大 fd,需重新扫描 max_sd while (max_sd >= 0 && !FD_ISSET(max_sd, &master_fds)) max_sd--; } }
  • recv()返回0表示对端已关闭连接(TCP FIN 包到达),这是优雅关闭(graceful shutdown)的标志,不是错误,不应perror。
  • recv()返回-1且errno == ECONNRESET表示对端异常断开(如 kill -9 client 进程),此时也需清理。
  • FD_CLR()必须在close()之后立即执行,否则下次select()仍会检测该已关闭 fd,recv()返回-1且errno == EBADF,造成逻辑混乱。

4.3 消息广播的原子性陷阱:send()不保证一次发完,write()也不

server.c中向所有客户端广播消息的代码,常被简化为:

// 危险!未检查 send 返回值 for (int i = 0; i <= max_sd; i++) { if (i != sockfd && FD_ISSET(i, &master_fds)) { send(i, buffer, strlen(buffer), 0); // ❌ 可能只发部分字节! } }
  • send()和write()是尽力而为(best-effort),返回值是实际发送字节数,可能小于strlen(buffer)(尤其在网络拥塞或接收方缓冲区满时)。
  • 正确做法是循环发送直到全部发出:
ssize_t sent = 0; size_t len = strlen(buffer); while (sent < len) { ssize_t n = send(i, buffer + sent, len - sent, 0); if (n < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { // 非阻塞 socket 缓冲区满,稍后重试 break; } else { // 真正错误,关闭连接 close(i); FD_CLR(i, &master_fds); break; } } sent += n; }
  • 这就是为什么tcp.rar项目里client.c常有while (recv(...)>0)循环读取——因为send()分片,recv()也可能分片到达,应用层需自行拼包。TCP 保证字节流有序交付,不保证消息边界。

5. 避坑:tcp.rar项目中 4 个高频翻车点与根因修复

5.1 现象:client.c编译通过,运行后卡在connect(),strace显示connect(3, {sa_family=AF_INET, sin_port=htons(8080), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress),但后续无响应

原因:client.c启用了非阻塞 connect,但未用select()/poll()检测连接结果,也未设置超时,导致程序在connect()后无限等待。
解决:在connect()返回EINPROGRESS后,必须调用select()监听该 socket 的可写事件,并设置timeval超时(如 5 秒),超时则close()并报错。

5.2 现象:启动chat_server后,ss -tuln | grep 8080显示端口监听,但nc 127.0.0.1 8080连接立即关闭,server.c无日志输出

原因:server.c的listen()调用缺失或backlog参数过小(如设为 0),导致新连接请求被内核直接拒绝。listen(sockfd, 1)是最低有效值,1表示等待队列长度为 1。
解决:确保listen(sockfd, SOMAXCONN),SOMAXCONN是系统最大值(Linux 通常 128 或 4096),或至少设为10。backlog过小会导致高并发下连接丢失。

5.3 现象:两个client.c同时连接,server.c日志显示 “New client connected”,但其中一个客户端发消息,另一个收不到,server.c也无广播日志

原因:server.c的广播循环中,for (int i = 0; i <= max_sd; i++)的max_sd未动态更新。当第一个 client 断开后,max_sd仍为旧值,导致FD_ISSET(i, &master_fds)检查到已关闭的 fd,send()返回-1且errno == EBADF,但代码未处理,后续i循环跳过有效 fd。
解决:每次close()客户端 socket 后,必须FD_CLR(),并重新扫描max_sd(如 4.2 节所示),或改用poll()结构,天然规避此问题。

5.4 现象:client.c发送中文消息(如 “你好”),server.crecv()收到乱码,printf("%s", buffer)输出异常字符

原因:recv()读取的是原始字节流,未以\0结尾,且buffer未初始化。printf("%s")会一直打印直到遇到\0,若buffer末尾恰好是随机垃圾值,就会越界读取。
解决:recv()后手动添加字符串结尾,并确保buffer初始化:

char buffer[1024] = {0}; // 初始化为 0 int bytes = recv(sockfd, buffer, sizeof(buffer)-1, 0); if (bytes > 0) { buffer[bytes] = '\0'; // 强制结尾 printf("Received: %s", buffer); }

6. 进阶验证与国产化适配:用strace、tcpdump和银河麒麟实测要点

6.1 用strace追踪 socket 系统调用:看清三次握手与四次挥手的每一帧

strace是诊断 TCP 行为的终极武器,比Wireshark更贴近内核:

# 追踪 server.c 的所有 socket 相关系统调用 strace -e trace=socket,bind,listen,accept,connect,sendto,recvfrom,close -f ./chat_server 2>&1 | grep -E "(socket|connect|accept|send|recv|close)" # 追踪 client.c 的 connect 流程(重点关注 EINPROGRESS 和后续可写事件) strace -e trace=socket,connect,select,poll,sendto,recvfrom,close -f ./chat_client 127.0.0.1 8080 2>&1
  • 输出中你会看到:
    • socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)→ 创建 socket
    • connect(3, {sa_family=AF_INET, ...}, 16) = -1 EINPROGRESS→ 三次握手开始
    • select(4, [3], [], [], {5, 0}) = 1 (in [3], left {4, 999999})→ 检测到 socket 可写,连接成功
    • sendto(3, "hello\0", 6, MSG_NOSIGNAL, NULL, 0) = 6→ 发送数据
    • recvfrom(3, "world\0", 1024, MSG_WAITALL, NULL, NULL) = 6→ 接收数据
    • close(3)→ 发送 FIN,启动四次挥手
  • strace能让你确认server.c是否真的调用了accept(),client.c是否真的完成了connect(),避免代码逻辑与实际行为脱节。

6.2 用tcpdump抓包分析:验证TIME_WAIT与FIN包的真实流向

在服务端机器抓包,过滤 8080 端口:

# 抓取 8080 端口的 TCP 包,保存为 pcap 文件 sudo tcpdump -i any -w chat.pcap port 8080 # 或实时打印(加 -nn 不解析域名/IP,-vv 详细信息) sudo tcpdump -i any -nn -vv port 8080
  • 启动server.c和两个client.c,发送几条消息后关闭 client。
  • 在 Wireshark 中打开chat.pcap,过滤tcp.port == 8080,你会看到:
    • SYN→SYN-ACK→ACK:三次握手建立连接
    • PSH, ACK:携带数据的包(send()触发)
    • FIN, ACK→ACK→FIN, ACK→ACK:四次挥手断开连接
    • 关闭 client 后,服务端 socket 进入TIME_WAIT状态(持续 2MSL,Linux 默认 60 秒),期间ss -tan | grep 8080会显示127.0.0.1:8080处于TIME-WAIT。
  • 若发现大量TIME-WAIT占用端口,可在server.c的socket()后添加setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)),允许bind()重用处于TIME_WAIT的地址端口。

6.3 银河麒麟系统适配要点:glibc版本、pthread和libunrar的兼容性

银河麒麟(Kylin)基于 Ubuntu 或 Debian,但内核和 glibc 版本可能较旧:

  • 检查 glibc 版本:ldd --version,若低于 2.27,-D_REENTRANT可能不生效,需在server.c中显式调用pthread_mutex_init()等函数。
  • unrar兼容性:银河麒麟 V10 自带unrar,但若tcp.rar是老版 RAR 格式(RAR5),需升级unrar:sudo apt install unrar-free或从官网下载新版。
  • pthread编译:麒麟系统gcc默认链接pthread,但为保险起见,仍建议显式加-lpthread,避免undefined reference to 'pthread_create'。
  • ss命令替代:若ss不可用,用netstat -tuln | grep 8080,但netstat已被标记为废弃,ss是未来标准。

我当年在某电力自动化项目里,用这套tcp.rar改写 Modbus TCP 主站,把server.c的handle_client()换成解析0x03读保持寄存器请求,把client.c的send()换成构造 Modbus RTU 帧——底层 socket 逻辑完全复用,一周就跑通了与西门子 PLC 的通信。TCP 协议栈的稳定性和可预测性,远胜于任何上层框架的炫技。当你在cursor登陆成功却 app 不显示登录、或ensppro无需账号登陆版本卡在启动页时,不妨回到tcp.rar,敲一遍gcc -lpthread server.c -o s && ./s,看着ss -tuln里那个静静监听的8080,你会重新相信:网络的本质,就是socket、bind、listen、accept、send、recv、close这七个词。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/30 1:08:26

STM32F1+DHT11温湿度采集实战:从选型到联调排错全记录

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

作者头像 李华
网站建设 2026/10/30 1:08:04

硬件I2C与软件I2C选型实战:从OLED点不亮到稳定通信的避坑指南

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

作者头像 李华
网站建设 2026/10/30 1:07:46

PyTorch DDP多卡训练实战:从单卡到4卡DDP避坑指南

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

作者头像 李华
网站建设 2026/10/30 1:06:50

基于STM32F103C8T6的实验室巡检遥控小车设计与实现

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

作者头像 李华
网站建设 2026/10/30 1:06:40

全人类AI发展路径:技术民主化与公平性建模

1. 项目背景与问题定义2026年美赛F题直指人工智能发展中最具争议性的命题——是否应该以及如何发展"全人类人工智能"(Humanity-wide AI)。这个看似简单的问题背后&#xff0c;涉及技术伦理、社会公平、经济影响和全球治理等多维度复杂因素。作为数学建模竞赛题目&…

作者头像 李华
网站建设 2026/10/30 1:04:44

TIA Portal中LAD/FBD/STL/SCL注释技巧与避坑指南

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

作者头像 李华