一、常见踩坑点
| 现象 | 原因 | 解决方法 |
|---|---|---|
编译时 undefined reference to ... |
未链接必要的库(如 -lts 对于 tslib,但 TCP 编程无需额外库) |
TCP 编程一般只需 gcc 直接编译,若使用 pthread 等需加 -lpthread |
运行客户端时提示 Usage: ./client <server_ip> |
未提供 IP 地址参数 | 运行:./client 127.0.0.1 |
服务器 bind error: Address already in use |
端口被占用,可能是上一个服务器进程未退出,或处于 TIME_WAIT 状态 |
1. 用 pkill server 杀掉旧进程 2. 使用 setsockopt 设置 SO_REUSEADDR 选项 |
客户端 connect error |
服务器未运行,或 IP 地址错误,或防火墙阻止 | 确保服务器已启动,IP 正确,尝试 ping 测试连通性 |
服务器子进程变成僵尸(<defunct>) |
父进程未回收子进程资源 | 在服务器开头添加 signal(SIGCHLD, SIG_IGN); 让系统自动回收 |
客户端输入 exit 后服务器没反应 |
客户端发送的字符串包含换行符 \n,而服务器用 strcmp 比较时未去除 |
在客户端用 strcspn 或手动去掉换行符;或在服务器接收后也去掉换行符 |
| 服务器打印乱码或额外字符 | 接收到的数据未以 \0 结尾,直接打印 |
在 recv 后加上 ucRecvBuf[iRecvLen] = '\0'; |
| 第二个客户端无法连接/通信 | 服务器未使用并发(fork/线程),主进程阻塞在第一个客户端的 recv 中 |
使用 fork 创建子进程处理每个客户端,或用 select/poll 实现 I/O 多路复用 |
客户端正常退出,但服务器端 recv 未返回 0 |
客户端未正确关闭 socket(如只 exit 未调用 close) |
在客户端 break 前调用 close(iSocketClient); |
| 服务器端打印的客户端编号始终是 0 或 -1 | 忘记在每次 accept 后递增 iClientNum,或未正确传递 |
在 accept 成功后 iClientNum++,子进程中应使用该编号(注意子进程复制了父进程变量) |
二、核心概念总结
1. TCP 编程基本步骤
服务器端
-
socket()-- 创建监听 socket -
bind()-- 绑定 IP 和端口 -
listen()-- 开始监听 -
accept()-- 等待客户端连接,返回新的通信 socket -
recv()/send()-- 与客户端通信 -
close()-- 关闭 socket
客户端
-
socket()-- 创建 socket -
connect()-- 连接服务器 -
send()/recv()-- 通信 -
close()-- 关闭
2. 并发处理
-
使用
fork()为每个客户端创建一个子进程,父进程继续等待新连接。 -
子进程处理完通信后必须退出(
return或exit),避免成为僵尸。 -
使用
signal(SIGCHLD, SIG_IGN)让系统自动回收子进程资源,避免僵尸进程。
3. 优雅关闭连接
-
应用层可以约定特殊命令(如
exit)作为断开信号。 -
发送
exit后,客户端主动关闭 socket。 -
服务器端收到
exit或recv返回 0,也应关闭 socket 并退出子进程。
4. 字符串协议注意事项
-
网络传输的是字节流,没有自动的字符串结束符。
-
接收后必须手动添加
\0才能作为 C 字符串处理。 -
使用
fgets时会保留换行符,需去除后再比较(或用strncmp指定长度)。
5. 阻塞 I/O 的影响
-
默认情况下,
accept、recv、send等函数会阻塞直到操作完成。 -
单进程服务器如果阻塞在一个客户端的
recv上,将无法接受新连接或处理其他客户端。 -
解决方案:多进程、多线程、非阻塞 I/O、I/O 多路复用(select/poll/epoll)。
三、学习收获
通过本次学习,你掌握了:
-
TCP 套接字编程的核心函数和流程。
-
如何使用
fork实现简单的并发服务器。 -
如何自定义应用层协议(如退出命令)实现优雅关闭。
-
常见错误的排查方法(端口占用、僵尸进程、字符串处理等)。
-
通过修改代码、观察现象加深了对阻塞、并发等概念的理解。
这些知识是后续学习更高级网络编程(如 epoll、多线程、异步 I/O)的基础,也是开发实际网络应用(聊天室、文件传输、远程控制等)的必备技能。