简介这份资源面向网络编程初学者与需要巩固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 而不是 UDP2.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.hWindows 下检查是否加了-lws2_32。3.2 启动顺序与验证方法必须先启动服务端再启动客户端。服务端启动后会打印「等待连接…」之类的日志此时它阻塞在accept()。然后另开一个终端启动客户端。# 终端 1启动服务端监听 8888 端口 ./server # 终端 2启动客户端连接本机 8888 端口 ./client验证是否跑通看三个信号服务端打印「客户端已连接」客户端打印「连接成功」双方各自发送一条消息后对方能收到并打印出来。如果服务端没反应先检查防火墙Linux 下sudo ufw statusWindows 下检查入站规则。端口没放行的话SYN 包直接被丢弃客户端会卡在connect()直到超时。3.3 用抓包确认 TCP 三次握手真的发生了想确认底层 TCP/IP 联网工作是否正常最直接的办法是抓包。Linux 下用tcpdumpWindows 下用 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.] - SYNACK # IP 127.0.0.1.54321 127.0.0.1.8888: Flags [.] - ACK这三行就是三次握手。看到它们说明你的源码在传输层已经正常工作。如果只看到 SYN 没有 SYNACK说明服务端没在监听或防火墙拦截如果看到 RST说明端口没开或被拒绝。抓包是排查网络问题最可靠的黑匣子比反复改代码有效得多。4. 避坑与常见问题那些让源码跑不起来的细节4.1 端口被占用导致 bind 失败现象服务端启动报「Address already in use」或「bind failed」。原因上一次运行的服务端进程没完全退出端口处于 TIME_WAIT 状态或者被其他程序占用。解决先查谁占了端口lsof -i :8888或netstat -tlnp | grep 8888杀掉对应进程。然后在代码里给 socket 设置SO_REUSEADDRint 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同时覆盖了对端关闭n0和出错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 下用pthreadWindows 下用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开头加这一行。这个坑不报错、不打印只让进程静默消失属于血泪经验级别的后悔药。希望帮到你。本文还有配套的精品资源点击获取