简介本资源是一份面向初学者的C网络编程基础PDF文档内容涵盖OSI七层网络模型、TCP/IP协议簇、C/S编程模型等核心知识点并介绍了MFC套接字类与Windows API两种开发方式适合正在学习Visual C网络编程、需要建立网络通信基础的读者查阅。文档包含1个PDF文件资料包大小约114KB内容精炼、便于速览。目前已有564人学习下载。除了概念讲解文中还说明了流式套接字与数据报套接字的区别、网络字节顺序以及CAsyncSocket类的使用步骤可帮助读者在动手编写Sockets程序前厘清关键原理。1. 一份 C 网络编程实例 PDF真正该教会你的是这三件事不少人下过《c网络编程实例.pdf》这类资料翻完目录就记住了 Socket、select、多线程一上手写代码还是满头问号。这类文档表面上是给你一堆能跑的代码段实际核心是逼你把 TCP/UDP 的调用顺序、缓冲区生命周期、错误码处理这三件事变成肌肉记忆。适合正在从 C 语法转向网络库、或者准备面试手写 Socket 的从业者。我一般建议把它当成“最小可运行的骨架”来读再对着抓包工具改参数验证而不是把代码抄一遍就完事。原因很简单网络编程的出问题点不在语法而在系统调用状态和内核缓冲区的行为这些只有亲手跑坏几次才能理解。这篇按我的落地路径展开先讲模型选型再给能编译的实例最后把常见的坑逐个拆开。2. 看懂实例前先选对模型阻塞、非阻塞与多路复用到底在解决什么2.1 Socket 生命周期从 socket 到 close 的六次调用C 网络编程实例里最常出现的图是三次握手但真正落实成代码你要记的是另外六个函数socket、bind、listen、accept、recv/send、close。服务端每一步都有返回值socket 返回文件描述符bind 在端口被占时返回 -1listen 决定内核的未完成连接队列大小accept 从已完成连接队列里取一个取不到就阻塞。很多新手只盯着 accept 的返回值忽略 socket 本身的超时属性这是后面一堆坑的源头。我一般建议先在 Linux 上用 POSIX API 入门因为 Windows 的 Winsock 本质是同一套模型只是多了 WSAStartup 和链接 ws2_32。实例 PDF 如果只贴跨平台代码而不解释平台差异读者很容易在 Windows 上卡在链接错误。这里不展开平台移植先把六次调用顺一遍。参数说明socket 的 domain 用 AF_INETtype 用 SOCK_STREAMprotocol 写 0。bind 的第二参数要强转成 sockaddr 指针这是历史遗留因为 sockaddr_in 和 sockaddr 的内存布局前两字节是对齐的。listen 的第二参数是未完成连接队列长度不是最大连接数这是常见误解后面会专门说到。2.2 为什么阻塞模型最容易入门也最容易翻车阻塞模型就是调用 recv 时内核没有数据就让线程睡着。优点是逻辑简单accept 一次处理一个连接循环里再套一个 recv 循环。缺点是客户端一直不发数据时你的程序永远卡在 recv 上客户端断开后recv 返回 0你得主动 break。实例 PDF 里常用“回显服务器”做演示恰好掩盖了多客户端场景下的问题。真正的网络程序很少只有一个连接。你可以给 recv 设 SO_RCVTIMEO把阻塞变成“带超时的阻塞”也可以切到非阻塞加 select/poll。我的习惯是第一步按阻塞模型跑通再把 accept 和 recv 分别搬进线程最后才用 epoll。这样每一步出问题都能定位到是哪一层。多路复用不是让程序更快只是不让线程空等。select 的 fd_set 位图默认上限是 1024超出后 FD_SET 会越界poll 没有上限但返回后要遍历所有 fdepoll 适合大量长连接但代码复杂度和理解成本也高。很多 C 面试题会追问你“什么场景用 select什么场景用 epoll”如果你连阻塞模型都没跑通过这个问题答起来就是背八股。先做对模型再谈性能优化。2.3 字节序与 sockaddr_in实例 PDF 里从不解释的两个细节端口号 8080 在内存里是小端还是大端x86 是小端网络字节序是大端。所以 bind 之前必须 htons(8080)否则你在本机连自己都会失败。IP 地址同理127.0.0.1 要 inet_addr 或 inet_pton 转成网络字节序。实例 PDF 里如果只写 sin_port 8080那这段代码在 Linux 上就是错的。另外 sockaddr_in 要初始化清零或者用sockaddr_in addr{};让 C 帮你清零。不初始化就设置 sin_family、sin_port后面未初始化的字节会参与内核比较connect 失败时报错会特别莫名其妙。Wireshark 里看到源端口和目的端口对不上多半就是字节序反转。这类问题排查起来不是看逻辑而是看内存布局属于典型的“看起来代码没错但就是连不上”的玄学现场。3. 用 vscode 配好 C/C 环境跑通最小 TCP 服务端的最小命令3.1 最小 TCP 服务端能编译、能 accept、能退出的完整骨架下面是能直接保存为 server.cpp 的最小服务端。它只接受一次连接、收一条消息、回一个 ok 就退出但覆盖了服务端全部关键调用// server.cpp - 最小 TCP 服务端 #include cstdio #include cstring #include unistd.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h int main() { int fd socket(AF_INET, SOCK_STREAM, 0); if (fd 0) { perror(socket); return 1; } int opt 1; // SO_REUSEADDR 必须在 bind 之前设置否则 TIME_WAIT 会挡住重启 setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt)); sockaddr_in addr{}; addr.sin_family AF_INET; addr.sin_port htons(8080); addr.sin_addr.s_addr htonl(INADDR_ANY); // 监听所有网卡 if (bind(fd, (sockaddr*)addr, sizeof(addr)) 0) { perror(bind); return 1; } if (listen(fd, 8) 0) { perror(listen); return 1; } sockaddr_in peer{}; socklen_t len sizeof(peer); // 阻塞等待连接没有客户端来时进程会停在这里 int client accept(fd, (sockaddr*)peer, len); if (client 0) { perror(accept); return 1; } char buf[1024]; int n recv(client, buf, sizeof(buf) - 1, 0); if (n 0) { buf[n] 0; // 手动补上字符串结束符别依赖 recv 帮你做 printf(recv: %s\n, buf); send(client, ok, 2, 0); } else { printf(recv returned %d\n, n); } close(client); close(fd); return 0; }代码复制到本地后先别急着加功能。你需要确认每一步的返回值socket 失败多是因为文件描述符耗尽bind 失败多半是端口被占accept 失败要注意 EINTR 信号打断。我这里故意没做 accept 的 EINTR 重试是想让你先看到它真实出错的样子。参数说明sizeof(buf) - 1是防止 recv 把缓冲区填满后没有地方放结束符htonl(INADDR_ANY)里的 INADDR_ANY 是 0主机字节序和网络字节序都是 0写成 htonl 是为了语义统一。listen(fd, 8)的 8 是待处理连接队列长度不是并发上限同一时间能建立的连接数还受内核 somaxconn 参数限制。3.2 配一个不依赖 IDE 的编译命令g 参数与 -pthread 的教训g -stdc17 -O0 -g -Wall -Wextra server.cpp -o server ./server用 VSCode 配置 C/C 环境时我习惯先在终端里把这条命令跑通再把它写进 tasks.json。-O0是为了断点调试时变量不被优化掉-g保留调试符号-Wall -Wextra能把未初始化结构体这类问题提前暴露。等确认代码逻辑正确后再去掉-O0改成-O2。注意 socket 代码本身不依赖 pthread但一旦你把 accept 或 recv 搬进 std::thread链接时就必须加-pthread否则编译能过运行时直接崩溃。VSCode 的 tasks.json 里别把-stdc17 -pthread拼成一个字符串参数g 会把整串当成输入文件名报 “No such file or directory”。每个参数独立占一个数组项这是 Windows 和 Linux 下都很容易踩的配置坑。3.3 客户端实例recv 返回 0 不等于出错// client.cpp - 最小 TCP 客户端 #include cstdio #include cstring #include unistd.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h int main() { int fd socket(AF_INET, SOCK_STREAM, 0); if (fd 0) { perror(socket); return 1; } sockaddr_in addr{}; addr.sin_family AF_INET; addr.sin_port htons(8080); inet_pton(AF_INET, 127.0.0.1, addr.sin_addr); if (connect(fd, (sockaddr*)addr, sizeof(addr)) 0) { perror(connect); return 1; } send(fd, ping, 4, 0); char buf[16]; int n recv(fd, buf, sizeof(buf) - 1, 0); if (n 0) { buf[n] 0; printf(server: %s\n, buf); } else { printf(recv returned %d\n, n); } close(fd); return 0; }客户端先 connect再 send再 recv。connect 返回成功只代表三次握手完成不代表服务端已经执行到 recv。所以 send 之后立刻 recv 时数据可能还在路上阻塞 recv 会等你自己的服务端回包。这里服务端回的是 “ok”实际项目里要在协议层设计好“请求-响应”或者“心跳”否则客户端和服务端会互相等死。参数说明inet_pton比inet_addr更安全它要求传入点分十进制的字符串和 AF_INET返回值 1 表示成功0 表示字符串不是合法地址-1 表示地址族不支持。我在上面没检查返回值因为127.0.0.1是固定的但真实代码里必须检查否则连到错误地址时 errno 会被后面的 connect 覆盖排查起来很绕。4. 从 TCP 实例延伸到 UDP 与 select看清 PDF 里没写全的边界4.1 UDP 实例不需要 listen 的“发完就完”以及丢包怎么读UDP 和 TCP 最大的差异是没有连接。服务端只要 socket bind然后 recvfrom/sendto不需要 listen也不需要 accept。客户端可以直接 sendto 到目标地址不需要 connect。这个“不需要连接”听起来简单但真实场景里你会遇到丢包、乱序、重复包实例 PDF 里如果只演示局域网里的本机回环这些现象全看不到。// udp_server.cpp - 最小 UDP 收包 #include cstdio #include cstring #include unistd.h #include sys/socket.h #include netinet/in.h int main() { int fd socket(AF_INET, SOCK_DGRAM, 0); sockaddr_in addr{}; addr.sin_family AF_INET; addr.sin_port htons(9000); addr.sin_addr.s_addr htonl(INADDR_ANY); if (bind(fd, (sockaddr*)addr, sizeof(addr)) 0) { perror(bind); return 1; } char buf[2048]; sockaddr_in peer{}; socklen_t len sizeof(peer); for (int i 0; i 5; i) { int n recvfrom(fd, buf, sizeof(buf) - 1, 0, (sockaddr*)peer, len); if (n 0) { perror(recvfrom); break; } buf[n] 0; printf(packet %d from %s:%d: %s\n, i, inet_ntoa(peer.sin_addr), ntohs(peer.sin_port), buf); } close(fd); return 0; }recvfrom 的返回值是单包长度。TCP 的 recv 可能只返回半条消息因为你不知道对方什么时候发完UDP 的 recvfrom 一次返回一个完整数据报前提是你的缓冲区比包大。缓冲区给 2048 而实际包更大时内核会截断并丢掉尾部你收到的还是 2048 字节这不会报错只能通过对比包长度来发现。经验取值是给 65507也就是 UDP 负载上限省得排查截断问题。4.2 用 select 改造成多路复用timeout 参数是重点阻塞模型里一个线程对应一个连接连接多了线程跟着多。select 的思想是让一个线程同时盯着多个 fd哪一个可读了就去处理哪一个。下面这段是 select 的核心用法它同时监听监听 socket 和已连接 socket// select 关键片段同时监听 listener 和 client fd_set rfds; FD_ZERO(rfds); FD_SET(listener, rfds); FD_SET(client, rfds); int maxfd (listener client ? listener : client) 1; timeval tv{}; tv.tv_sec 2; tv.tv_usec 0; int ret select(maxfd, rfds, nullptr, nullptr, tv); if (ret 0) { if (FD_ISSET(listener, rfds)) { int c accept(listener, nullptr, nullptr); // 这里先不处理 c 的收发只保存起来 } if (FD_ISSET(client, rfds)) { char buf[1024]; int n recv(client, buf, sizeof(buf), 0); // 根据 n 的值判断是数据还是对端关闭 } } else if (ret 0) { // 超时2 秒内没有活动 } else { // 出错多半是 EINTR 信号打断重试而不是退出 }逻辑说明select 会修改传入的 fd_set 和 timeval所以循环里每次调用前都要重新 FD_ZERO、FD_SET并重新赋 tv。很多人把 tv 初始化放在循环外结果第二次 select 定时器变成 0立刻返回超时这是 select 最经典的坑。第一个参数是“最大 fd 编号 1”不是 fd 个数写成 maxfd 就是为了这个 1。参数说明fd_set 的容量由 FD_SETSIZE 决定默认 1024。当你的连接 fd 超过 1024 时FD_SET 会越界写内存程序可能在神秘的地方崩溃。解决方法是换 poll 或 epoll或者调大 FD_SETSIZE 重新编译但后者治标不治本。实例 PDF 里如果教你 select几乎不会提这个上限你自己要意识到它只适合中小连接数场景。4.3 recv 返回值与 errno 对照判断连接状态比看状态码可靠新手经常纠结“连接是否断开”最简单可靠的判据是 recv 的返回值和 errno。我列一张对照表你可以贴到调试代码旁边recv 结果errno真实含义处理动作正数无收到 n 字节数据处理这 n 字节继续循环0无对端正常关闭FIN 已收到关闭 fd清理资源-1EAGAIN / EWOULDBLOCK非阻塞下暂时无数据等待下一轮 select/poll-1EINTR信号打断重试 recv不是错误-1ECONNRESET对端强行关闭收到 RST关闭 fd记录一次异常断开-1ENOTCONN连接已断开但 fd 未关闭先 close 再重连看到这里你应该明白不要用if (n 0)一把梭处理所有错误。正数、0、-1 分别代表三种不同状态而 -1 里还要继续看 errno。很多实例只在程序里打印perror(recv)打印完就退出这在真实服务里会把 EINTR 这种可恢复错误当致命错误处理。 我习惯写一个safe_recv包装函数把 EINTR 和 EAGAIN 单独拎出来这样主逻辑不会被信号和超时打断。5. 网络编程实例常见问题排查五个让新手崩溃的现象与对应处理5.1 connect 失败却报 Operation now in progress现象把 socket 设为非阻塞后connect 返回 -1打印 errno 是 EINPROGRESS。新手第一反应是连接失败直接 return。原因非阻塞 connect 不会等三次握手完成它只是把连接请求交给内核然后立刻返回。EINPROGRESS 表示“正在连接中”不是失败。解决对同一个 fd 调用 select 或 poll把时间设为 3 秒。返回可写时再用 getsockopt 取 SO_ERROR值 0 表示连接成功非 0 表示失败原因。这是标准做法。想省事就把 connect 改回阻塞模式但线程会卡在连接阶段无法响应超时。5.2 recv 阻塞不返回程序关不掉现象客户端连接后一直不发数据服务端线程卡在 recv。CtrlC 杀不掉因为主线程也被 join 卡住。原因阻塞模式没有超时recv 在没有数据时会让线程睡眠shutdown 和 close 都不一定能唤醒它。解决三种方案选一个。第一给 fd 设置 SO_RCVTIMEO超时后 recv 返回 -1errno 是 EAGAIN。第二把 recv 放子线程用 detach 让它自己陪你到天荒地老主线程只做 accept。第三需要优雅退出时在别的线程调用 shutdown(fd, SHUT_RDWR)recv 会立刻返回 0。别指望 close 能唤醒阻塞 recv在某些系统上 close 只是减少引用计数fd 还被 recv 攥着。5.3 bind 报 Address already in use 不是代码写错是 TIME_WAIT现象服务端退出后立刻重启bind 报 Address already in use。新手以为上一个进程没死干净用 ps 查完发现进程确实没了。原因主动关闭连接的一方会进入 TIME_WAIT 状态持续 2MSL默认约 60 秒。端口还被内核占用bind 自然失败。解决在 bind 前加 setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, ...)。这个选项允许 TIME_WAIT 状态的端口被重新绑定。注意它只对 TCP 生效明显UDP 行为不同。如果是监听大量短连接的服务一定要加如果客户端频繁 reconnect 也是主动关闭方客户端同样要设置否则 60 秒内不能重连同端口。5.4 重连后数据对不上你忘了清零缓冲区现象第二次连接收到的字符串后面多了一段上一次的残留比如第一次收 “ping”第二次收 “pong” 变成了 “ponging”。原因char buf[1024] 在栈上是不清零的recv 只覆盖实际收到的字节后面的旧数据还在。你打印时没有手动补结束符就把整块缓冲区当字符串输出了。解决recv 后立刻buf[n] 0;或者接收前memset(buf, 0, sizeof(buf));。我推荐前者因为 memset 整个 1024 的开销在热点路径上不划算。更彻底的做法是用 std::vector resize 到实际接收长度。这条属于网络编程里最常见的低智坑但面试里就是有人答不上来。5.5 WSAStartup 是 Windows 上第一行代码不是可选项现象同一份代码Linux 编译运行没问题放到 Windows 上链接失败报 unresolved external symbol WSAStartup。原因Winsock 库不是自动加载的必须调用 WSAStartup 初始化并且要链接 ws2_32.lib。VSCode 配置 C/C 环境时如果 tasks.json 或 CMakeLists 里没加 ws2_32链接器就找不符号。解决代码开头加 WSAStartup 和 WSACleanup这是封装里的一部分。VSCode 的链接参数写成g main.cpp -o main -lws2_32。如果还报错检查是 32 位还是 64 位 toolchain库路径要匹配。WSAStartup 第二个参数要传(LPWSADATA)wsaData不检查返回值的话初始化失败后面全部白搭。这条对只用 Linux 的人来说可能用不上但只要你想把 C 网络编程用到 Windows 环境这是绕不开的第一步。6. 抓包验证的三种手法把实例从“能跑”变成“确认对”6.1 回环抓包、断点看结构体、日志打点三者配合代码能跑通不代表行为正确尤其网络编程里“通了”和“符合协议”是两回事。第一种手法是 Wireshark 抓回环包在 localhost 上测试时选 lo 接口过滤器写tcp.port 8080。你会看到 SYN、SYN-ACK、ACK 三次握手以及后面的 PSH 数据包。如果只看到握手没有数据说明 recv 或 send 的缓冲区没被正确消费。第二种手法是断点直接看内存。gdb 里x/8bx addr能显示 sockaddr_in 的 8 个字节对照 htons 后的端口值是否成字节序。之前 Wireshark 里显示端口和代码对不上用这招一眼定位是不是字节序反转。第三种手法是日志打点。我习惯在 recv 后打印收到长度和时间戳在 send 前打印要发送的长度。把日志和 Wireshark 时间线对齐能立刻看出数据是不是在你日志里待了一会儿才发出去这通常是 Nagle 算法和延迟 ACK 互相等待的经典踩坑现场。我最后悔的几次排错都是直接改代码而不是先抓包验证。说到底把实例里的小服务端改造成一个“echo 日志”程序再配一个会自动发包的客户端这套组合能用去重、超时、乱序三个维度同时验证你的网络模型。希望帮到你。本文还有配套的精品资源点击获取