简介:这份资源面向网络编程初学者与需要巩固TCP/IP基础的开发者,提供一套可直接运行的客户端与服务端通信源码,帮助理解面向连接、可靠传输的TCP协议与负责路由寻址的IP协议如何协同工作。压缩包共38个文件,约73KB,以C#源码文件为主,包含服务端与客户端核心逻辑、项目配置、资源文件及编译生成的程序集,结构紧凑,便于在Visual Studio中直接打开调试。源码完整覆盖套接字创建、地址结构体初始化、绑定、监听、接受连接、发起连接、数据收发与关闭连接等关键环节,读者可对照代码观察三次握手建立连接、四次挥手断开连接的实际流程,并在此基础上扩展自己的网络应用。目前已有1371人学习下载,适合作为课程实验、自学练手或项目起步的参考模板。
1. 从一份 TCP/IP 客户端服务端源码说起:它到底能跑通什么
很多人第一次接触网络编程,都是从「写一个能收发消息的客户端和服务端」开始的。这份 TCP/IP 创建客户端和服务端源码,核心就是一套最小可运行的 socket 通信示例:服务端监听端口、接受连接、收发数据,客户端主动连接、发送请求、读取响应。它解决的不是高深问题,而是把 TCP/IP 协议栈里最基础的一层——传输层 socket 编程——用可编译、可调试的代码固定下来。适合谁?刚学完 TCP/IP 模型各层功能详解、想动手验证三次握手和四次挥手的人;需要快速搭一个内网通信 demo 的嵌入式或后端开发者;以及做服务端接口测试时想有个干净对照实现的人。源码本身不依赖复杂框架,拿到手改几个参数就能跑,这是它最大的价值。
2. 先把 socket 通信模型立住:为什么是 TCP 而不是 UDP
2.1 TCP 与 UDP 在源码层面的分叉点
这份源码选的是 TCP,不是 UDP。原因很直接:客户端和服务端之间要保证数据按序到达、不丢包、不重复,而 TCP 协议栈本身就在内核里替你做了这些事。UDP 的 socket 代码更短,但你要自己在应用层补确认、重传、排序,对于一份教学和快速验证用的源码来说,那是给自己找麻烦。
从代码结构看,TCP 服务端的生命周期是固定的五步:socket()创建套接字、bind()绑定地址端口、listen()进入监听、accept()阻塞等待连接、recv()/send()收发数据。客户端少两步:socket()之后直接connect(),然后收发。这个差异不是随便定的,它对应 TCP 状态机:服务端必须处于 LISTEN 状态才能接受握手,客户端发起 SYN 后进入 SYN_SENT。
常见做法是服务端用SO_REUSEADDR选项,避免重启时端口被 TIME_WAIT 状态占住。这个细节很多示例代码会漏,但实际调试时非常关键——你改完代码重新编译运行,如果报「Address already in use」,八成就是没设这个选项。
2.2 阻塞与非阻塞:源码默认走哪条路
这份源码默认是阻塞模式。也就是说,accept()没连接进来就卡住,recv()没数据到就卡住。对单客户端调试来说,这反而好理解:你能清楚看到程序停在哪一步,配合打印日志就能判断是卡在等待连接还是等待数据。
但阻塞模式有个硬边界:一个服务端只能顺序处理一个客户端。第二个客户端连上来,得等第一个断开才能被accept()拿到。如果你要同时服务多个客户端,就得引入多线程、多进程或者 I/O 多路复用(select/poll/epoll)。这份源码的定位是「跑通基础流程」,不是「直接上生产」,所以它没做并发。知道这个边界,你就不会拿它去扛真实业务流量。
提示:如果你在 Windows 上编译,记得在代码开头加
WSAStartup()初始化 Winsock 库,Linux 下不需要。这是跨平台移植时第一个翻车点。
2.3 地址与端口的参数怎么定
服务端绑定地址一般写INADDR_ANY,表示监听本机所有网卡。端口选 1024 以上,比如 8888、9000,避免和系统服务冲突。客户端连接地址写服务端实际 IP:本机测试用127.0.0.1,局域网测试用服务端的内网 IP,比如192.168.1.100。
这里有个容易忽略的点:如果服务端跑在虚拟机或容器里,客户端在宿主机上,127.0.0.1是连不通的,必须用虚拟机的桥接 IP 或做端口映射。我一般会先用ping确认网络可达,再用telnet 服务端IP 端口确认端口开放,最后才跑源码。这三步能省掉大量「代码没问题但就是连不上」的玄学排查。
3. 把源码跑起来:编译、启动、验证的完整链路
3.1 编译环境与依赖确认
这份源码是标准 C/C++ socket 编程,Linux 下用 gcc/g++ 直接编译,Windows 下用 MinGW 或 Visual Studio 都行。Linux 不需要额外链接库,Windows 需要链接ws2_32.lib。
先确认编译器在位:
# Linux 下检查 gcc 版本 gcc --version # 如果没有,Debian/Ubuntu 系安装 sudo apt update sudo apt install build-essential -y编译服务端和客户端:
# 编译服务端,输出 server 可执行文件 gcc server.c -o server # 编译客户端,输出 client 可执行文件 gcc client.c -o client # Windows 下用 MinGW 编译需要加链接选项 # gcc server.c -o server.exe -lws2_32逻辑说明:-o指定输出文件名,-lws2_32是 Windows 下链接 Winsock 库的固定写法。如果编译报「undefined reference tosocket」,Linux 下检查是否漏了头文件<sys/socket.h>,Windows 下检查是否加了-lws2_32。
3.2 启动顺序与验证方法
必须先启动服务端,再启动客户端。服务端启动后会打印「等待连接…」之类的日志,此时它阻塞在accept()。然后另开一个终端启动客户端。
# 终端 1:启动服务端,监听 8888 端口 ./server # 终端 2:启动客户端,连接本机 8888 端口 ./client验证是否跑通,看三个信号:服务端打印「客户端已连接」,客户端打印「连接成功」,双方各自发送一条消息后对方能收到并打印出来。如果服务端没反应,先检查防火墙:Linux 下sudo ufw status,Windows 下检查入站规则。端口没放行的话,SYN 包直接被丢弃,客户端会卡在connect()直到超时。
3.3 用抓包确认 TCP 三次握手真的发生了
想确认底层 TCP/IP 联网工作是否正常,最直接的办法是抓包。Linux 下用tcpdump,Windows 下用 Wireshark。
# Linux 下抓取 8888 端口的包,-i any 表示所有网卡 sudo tcpdump -i any port 8888 -nn # 启动客户端后,你应该能看到类似输出: # IP 127.0.0.1.54321 > 127.0.0.1.8888: Flags [S] <- SYN # IP 127.0.0.1.8888 > 127.0.0.1.54321: Flags [S.] <- SYN+ACK # IP 127.0.0.1.54321 > 127.0.0.1.8888: Flags [.] <- ACK这三行就是三次握手。看到它们,说明你的源码在传输层已经正常工作。如果只看到 SYN 没有 SYN+ACK,说明服务端没在监听或防火墙拦截;如果看到 RST,说明端口没开或被拒绝。抓包是排查网络问题最可靠的黑匣子,比反复改代码有效得多。
4. 避坑与常见问题:那些让源码跑不起来的细节
4.1 端口被占用导致 bind 失败
现象:服务端启动报「Address already in use」或「bind failed」。
原因:上一次运行的服务端进程没完全退出,端口处于 TIME_WAIT 状态,或者被其他程序占用。
解决:先查谁占了端口,lsof -i :8888或netstat -tlnp | grep 8888,杀掉对应进程。然后在代码里给 socket 设置SO_REUSEADDR:
int opt = 1; setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));这行要放在bind()之前。参数含义:SOL_SOCKET表示套接字层选项,SO_REUSEADDR允许重用本地地址,&opt指向选项值。设了它,重启服务端就不会再被 TIME_WAIT 卡住。
4.2 客户端 connect 返回 Connection refused
现象:客户端报「Connection refused」或「无法连接」。
原因:服务端没启动,或者客户端连的 IP/端口不对,或者服务端绑定的地址不是客户端连的地址。
解决:按顺序排查——服务端进程在不在?ps aux | grep server。端口对不对?服务端打印的监听端口和客户端填的是否一致。地址对不对?服务端绑INADDR_ANY才能接受任意网卡连接,如果绑了127.0.0.1,外部机器就连不进来。我一般会在服务端启动日志里把实际绑定的地址和端口打出来,省得猜。
4.3 收发数据不完整或乱码
现象:客户端发了一长串,服务端只收到一半;或者收到的内容后面带乱码。
原因:TCP 是字节流协议,没有消息边界。send()一次发送的数据,recv()可能分多次收到;recv()缓冲区没清零,打印时会把旧数据带出来。
解决:应用层自己定协议。常见做法是固定长度头 + 变长体,或者用换行符分隔消息。接收端循环recv()直到收满预期长度。打印前把缓冲区memset清零,并且只打印实际接收到的字节数:
char buf[1024] = {0}; // 初始化为 0,避免打印脏数据 int n = recv(client_fd, buf, sizeof(buf) - 1, 0); if (n > 0) { buf[n] = '\0'; // 确保字符串结束 printf("收到 %d 字节: %s\n", n, buf); }参数说明:sizeof(buf) - 1留一个字节给结束符,n是实际收到的字节数,用它来截断字符串。这是处理 TCP 粘包和半包问题的最小实践。
4.4 Windows 下编译报错找不到 winsock2.h
现象:Windows 下编译报「fatal error: winsock2.h: No such file or directory」。
原因:头文件顺序错了。winsock2.h必须在windows.h之前包含,否则会冲突。
解决:把#include <winsock2.h>放在所有头文件最前面,然后加#include <ws2tcpip.h>。链接时加-lws2_32。如果用的是 Visual Studio,在项目属性里把Ws2_32.lib加到链接器输入里。
4.5 服务端 accept 后客户端立刻断开
现象:服务端刚打印「客户端已连接」,马上又打印「客户端断开」。
原因:客户端代码里connect()之后没等用户输入就直接close()了,或者客户端程序执行完发送逻辑就退出了。
解决:客户端在close()之前加一个等待,比如getchar()或sleep(),让连接保持住。服务端在recv()返回 0 时才认为对端关闭,返回 -1 是出错。区分这两个返回值,日志才准确。
5. 从能跑到好用:把这份源码改成你自己的调试工具
5.1 加一个循环收发,变成常驻通信
原始源码多半是收发一次就退出。实际调试时,你希望服务端一直挂着,客户端能反复发消息。改法很简单:把recv/send包在while(1)里,收到exit或空消息再break。
while (1) { memset(buf, 0, sizeof(buf)); int n = recv(client_fd, buf, sizeof(buf) - 1, 0); if (n <= 0) { printf("客户端断开或出错\n"); break; } printf("收到: %s", buf); send(client_fd, buf, n, 0); // 原样回显 }逻辑说明:n <= 0同时覆盖了对端关闭(n=0)和出错(n=-1)两种情况。回显时用n而不是strlen(buf),因为 buf 里可能有二进制数据或提前出现的\0。这个回显服务端配合telnet或nc就能当简易调试工具用。
5.2 用 iperf 对照验证吞吐和延迟
源码跑通后,如果你想量化这条 TCP 连接的吞吐和延迟,别自己写计时逻辑,直接用 iperf。它是专门做这个的,结果比手写代码可信。
# 服务端启动 iperf 监听 iperf -s # 客户端发起 10 秒测试 iperf -c 127.0.0.1 -t 10输出里的Transfer和Bandwidth就是吞吐量,Jitter和Lost/Total在 UDP 模式下看丢包。用 iperf 的结果对照你自己源码的收发速度,能判断瓶颈是在代码逻辑还是网络本身。很多人搜 iperf 操作视频,其实核心就这两条命令,参数-t控制时长,-i控制报告间隔。
5.3 把服务端改成多客户端并发的最小方案
单客户端不够用时,最省事的改法是每accept()到一个连接就开一个线程。Linux 下用pthread,Windows 下用CreateThread。
#include <pthread.h> void *handle_client(void *arg) { int fd = *(int *)arg; char buf[1024]; while (1) { memset(buf, 0, sizeof(buf)); int n = recv(fd, buf, sizeof(buf) - 1, 0); if (n <= 0) break; send(fd, buf, n, 0); } close(fd); return NULL; } // 在 accept 循环里 int *pfd = malloc(sizeof(int)); *pfd = client_fd; pthread_t tid; pthread_create(&tid, NULL, handle_client, pfd); pthread_detach(tid);参数说明:pthread_create的第四个参数传的是堆上分配的 fd 指针,不能传栈上变量的地址,否则线程还没读到就被下一轮循环覆盖了。pthread_detach让线程结束后自动回收资源,不用主线程join。这是最小并发模型,连接数上千后要换成epoll,但作为调试工具,几十个连接足够。
5.4 验证方法:用 nc 和 telnet 做对照测试
不想编译客户端时,用系统自带的nc或telnet就能测服务端。
# 用 nc 连接服务端并发送消息 nc 127.0.0.1 8888 hello # 用 telnet 连接 telnet 127.0.0.1 8888如果nc能连上并收到回显,说明服务端逻辑没问题,问题在客户端代码。如果nc也连不上,问题在服务端或网络层。这个对照法能快速定位故障在哪一侧,比两边同时改代码高效得多。
5.5 一个我常犯的错:忘了处理 SIGPIPE
服务端向一个已经断开的客户端send()数据时,Linux 默认会发 SIGPIPE 信号,直接把进程干掉。现象就是服务端莫名其妙退出,日志里什么都没有。
解决办法是在程序开头忽略 SIGPIPE:
#include <signal.h> signal(SIGPIPE, SIG_IGN);或者在send()时用MSG_NOSIGNAL标志。从那以后我每次写 socket 服务端,都强制在main开头加这一行。这个坑不报错、不打印,只让进程静默消失,属于血泪经验级别的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取