
简介本资源是一套面向初学者的WinSock网络编程入门示例聚焦Windows平台下基于C的Socket通信基础实践适用于高校计算机专业学生、自学网络编程的开发者及备考网络方向实训项目的初阶学习者。压缩包共40个文件73KB包含8个头文件.h定义接口与结构体、6个源码文件.cpp实现客户端与服务器核心逻辑、4个可执行文件.exe支持即点即运行验证另有工程配置文件.dsp/.dsw、资源文件.rc/.ico及调试辅助文件.ncb/.plg整体目录结构完整便于理解VC6.0环境下WinSock项目组织方式。已有262人学习下载。资源严格参照《Visual C网络高级编程》第2.2节编写服务端能实时回显客户端发送内容代码简洁、注释详尽关键步骤如WSAStartup初始化、socket创建、bind/listen/accept与send/recv调用均有清晰说明特别适合动手调试、理解TCP连接建立与数据收发全流程。1. 为什么 Winsock 编程至今仍是 Windows 网络开发的“地基课”一个能编译、能调试、能抓包验证的客户机/服务器最小闭环你可能在查“客户机操作系统已禁用 cpu”这类报错时顺手点进某个虚拟化文档结果被一堆 Hyper-V、WSL2、CPU 指令集兼容性绕晕也可能正为“wamp服务器局域网访问不了”焦头烂额反复检查防火墙、IP、端口却漏掉了最底层——那个连socket()都没成功返回句柄的 Winsock 初始化。这不是过时技术而是 Windows 上所有 TCP/IP 网络行为的必经入口Winsock 不是库是 Windows 内核网络栈与用户态程序之间的唯一契约接口。它不依赖 .NET 或 Python 解释器不靠 Docker 容器封装只要ws2_32.lib链上、WSAStartup()调对哪怕用纯 C 写个 20 行的echo服务也能在 Win7 到 Win11 的任何桌面系统上跑通。本文不讲抽象协议栈只给你一套可粘贴、可断点、可 Wireshark 抓到 SYN 包的完整客户机/服务器示例——用最简代码暴露 Winsock 的真实逻辑如何注册协议族、如何绑定地址、如何处理阻塞与非阻塞、为什么closesocket()比close()多一步、以及那些让新手卡死在WSAEADDRINUSE的隐藏陷阱。适合刚学完 C 语言想碰网络、或正在排查“服务器启动失败但无日志”问题的开发者。2. 从零构建 Winsock 客户机用标准 C 实现可调试的 TCP 连接与数据收发Winsock 客户机不是“写个connect()就完事”。它是一套状态机初始化 → 创建套接字 → 解析目标地址 → 连接 → 收发 → 清理。每一步失败都返回特定错误码而这些错误码直接对应网络层的真实状态。下面代码是经过 VS2019 / MinGW-w64 双环境实测的最小可行客户机client.c关键处带调试注释#include winsock2.h #include ws2tcpip.h #include stdio.h #include stdlib.h #pragma comment(lib, ws2_32.lib) // 必须链接此库否则链接失败 int main(int argc, char* argv[]) { WSADATA wsaData; SOCKET sock; struct addrinfo hints, *result NULL; int status; // 1. 初始化 Winsock必须调用且版本号不能乱填 if (WSAStartup(MAKEWORD(2, 2), wsaData) ! 0) { printf(WSAStartup failed: %d\n, WSAGetLastError()); return 1; } // 2. 构造地址查询参数明确指定 IPv4、TCP、主动连接 ZeroMemory(hints, sizeof(hints)); hints.ai_family AF_INET; // 强制 IPv4避免 IPv6 地址解析失败 hints.ai_socktype SOCK_STREAM; // TCP 流式套接字 hints.ai_protocol IPPROTO_TCP; // 3. 解析服务器地址支持域名和 IP 字符串比 inet_addr() 更健壮 status getaddrinfo(127.0.0.1, 8080, hints, result); if (status ! 0) { printf(getaddrinfo failed: %s\n, gai_strerror(status)); WSACleanup(); return 1; } // 4. 创建套接字AF_INET SOCK_STREAM IPPROTO_TCP 是 TCP 客户机黄金组合 sock socket(result-ai_family, result-ai_socktype, result-ai_protocol); if (sock INVALID_SOCKET) { printf(socket() failed: %d\n, WSAGetLastError()); freeaddrinfo(result); WSACleanup(); return 1; } // 5. 连接服务器阻塞模式下会等待直到连接建立或超时 if (connect(sock, result-ai_addr, (int)result-ai_addrlen) SOCKET_ERROR) { printf(connect() failed: %d\n, WSAGetLastError()); closesocket(sock); freeaddrinfo(result); WSACleanup(); return 1; } // 6. 发送与接收用 send()/recv() 替代 write()/read()这是 Winsock 强制约定 const char* msg Hello from Winsock Client\r\n; if (send(sock, msg, (int)strlen(msg), 0) SOCKET_ERROR) { printf(send() failed: %d\n, WSAGetLastError()); } else { printf(Sent: %s, msg); } char buffer[1024] {0}; int bytes_received recv(sock, buffer, sizeof(buffer)-1, 0); if (bytes_received 0) { printf(Received: %s, buffer); } else if (bytes_received 0) { printf(Server closed connection\n); } else { printf(recv() failed: %d\n, WSAGetLastError()); } // 7. 清理资源顺序不能错先关套接字再释放地址最后卸载 Winsock closesocket(sock); freeaddrinfo(result); WSACleanup(); return 0; }关键逻辑说明#pragma comment(lib, ws2_32.lib)是 Windows 平台特有语法告诉链接器必须链接 Winsock 2 库。若遗漏LNK2019错误会直接卡死编译。MAKEWORD(2,2)指定 Winsock 2.2 版本。填MAKEWORD(1,1)会因 API 过时导致getaddrinfo等函数不可用填MAKEWORD(3,3)则因系统不支持而初始化失败。getaddrinfo()替代老旧的inet_addr()和gethostbyname()它能同时处理 IPv4/IPv6、域名解析、端口字符串转换如http→80且线程安全。send()/recv()是 Winsock 专用 I/O 函数其第 4 个参数flags可控制行为MSG_NOSIGNALWindows 不需要、MSG_DONTWAIT设为非阻塞等。编译命令以 MinGW-w64 为例x86_64-w64-mingw32-gcc client.c -o client.exe -lws2_32VS 用户需在项目属性 → 链接器 → 输入 → 附加依赖项中添加ws2_32.lib。3. 从零构建 Winsock 服务器实现多客户端并发的阻塞式 echo 服务服务器比客户机复杂在连接管理。一个listen()套接字只能接受连接真正通信需为每个客户端创建新套接字。下面是最小可运行服务器server.c采用单线程阻塞模型便于调试和理解连接生命周期#include winsock2.h #include ws2tcpip.h #include stdio.h #include stdlib.h #pragma comment(lib, ws2_32.lib) int main() { WSADATA wsaData; SOCKET listen_sock, client_sock; struct sockaddr_in server_addr, client_addr; int client_len sizeof(client_addr); char buffer[1024]; // 1. 初始化 Winsock if (WSAStartup(MAKEWORD(2, 2), wsaData) ! 0) { printf(WSAStartup failed: %d\n, WSAGetLastError()); return 1; } // 2. 创建监听套接字 listen_sock socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (listen_sock INVALID_SOCKET) { printf(socket() failed: %d\n, WSAGetLastError()); WSACleanup(); return 1; } // 3. 绑定地址INADDR_ANY 允许所有本地 IP 接入端口 8080 ZeroMemory(server_addr, sizeof(server_addr)); server_addr.sin_family AF_INET; server_addr.sin_addr.s_addr INADDR_ANY; // 监听本机所有 IPv4 地址 server_addr.sin_port htons(8080); // 端口必须用网络字节序 if (bind(listen_sock, (struct sockaddr*)server_addr, sizeof(server_addr)) SOCKET_ERROR) { printf(bind() failed: %d\n, WSAGetLastError()); closesocket(listen_sock); WSACleanup(); return 1; } // 4. 开始监听第二个参数是等待连接队列长度积压队列 if (listen(listen_sock, SOMAXCONN) SOCKET_ERROR) { printf(listen() failed: %d\n, WSAGetLastError()); closesocket(listen_sock); WSACleanup(); return 1; } printf(Server listening on port 8080...\n); // 5. 主循环接受连接 → 收发数据 → 关闭 → 继续接受 while (1) { // accept() 阻塞直到有新连接返回新套接字用于通信 client_sock accept(listen_sock, (struct sockaddr*)client_addr, client_len); if (client_sock INVALID_SOCKET) { printf(accept() failed: %d\n, WSAGetLastError()); continue; // 不退出继续尝试接受 } // 打印客户端信息IP 和端口注意 ntohs() 转回主机字节序 char client_ip[INET_ADDRSTRLEN]; inet_ntop(AF_INET, client_addr.sin_addr, client_ip, sizeof(client_ip)); printf(Client connected: %s:%d\n, client_ip, ntohs(client_addr.sin_port)); // 6. 读取客户端消息并原样回显echo int bytes_received recv(client_sock, buffer, sizeof(buffer)-1, 0); if (bytes_received 0) { buffer[bytes_received] \0; printf(Received from %s: %s, client_ip, buffer); // 回复客户端 if (send(client_sock, buffer, bytes_received, 0) SOCKET_ERROR) { printf(send() to client failed: %d\n, WSAGetLastError()); } } else if (bytes_received 0) { printf(Client %s disconnected gracefully\n, client_ip); } else { printf(recv() from client failed: %d\n, WSAGetLastError()); } // 7. 关闭当前客户端连接套接字监听套接字保持打开 closesocket(client_sock); } // 8. 清理此处不会执行因上面是死循环仅作示意 closesocket(listen_sock); WSACleanup(); return 0; }参数与行为说明htons(8080)将主机字节序小端端口号转为网络字节序大端。若直接写8080在 x86 系统上会导致端口值错乱如变成33280连接必然失败。SOMAXCONN系统定义的最大积压连接数通常为200。它不是并发数而是已完成三次握手但尚未被accept()取走的连接队列长度。设太小会导致高并发时连接被拒绝WSAECONNREFUSED。accept()返回的是全新套接字与listen_sock完全独立。listen_sock持续监听client_sock专用于该客户端通信。这是 Winsock 多路复用的基础设计。此服务器为单线程阻塞模型一次只能服务一个客户端。若需并发需用select()、WSAEventSelect()或 I/O 完成端口IOCP但那是进阶主题本文聚焦“能跑通”的最小闭环。4. Winsock 客户机与服务器联调抓包验证、端口占用排查与常见错误定位光有代码不够必须验证数据真正在网络上传输。Wireshark 是 Windows 下最直观的抓包工具配合本示例可清晰看到 TCP 三次握手、HTTP-like 数据交互全过程。以下是联调四步法4.1 用 Wireshark 抓包验证连接真实性启动 Wireshark选择Loopback: Microsoft KM-TEST Loopback Adapter若无则选Ethernet或Wi-Fi设置过滤器tcp.port 8080只看本例端口先运行server.exe观察 Wireshark 是否捕获到TCP 8080 → LISTEN状态无数据包但端口已就绪再运行client.exe立即看到客户端发出SYN源端口随机目标8080服务器回复SYN-ACK客户端回复ACK三次握手完成紧接着PSH-ACK包载荷为Hello from Winsock Client\r\n服务器回复PSH-ACK载荷为相同字符串若看不到SYN说明客户端根本没发出连接请求——检查connect()是否被跳过或失败未打印日志。4.2 端口被占用用命令行精准定位WSAEADDRINUSE10048是服务器启动失败最常见错误。不要盲目重启电脑用以下命令秒级定位# 查看所有监听 8080 端口的进程 netstat -ano | findstr :8080 # 根据 PID 查进程名假设 PID 是 1234 tasklist | findstr 1234 # 强制结束谨慎使用 taskkill /PID 1234 /F注意netstat -ano输出中State列为LISTENING才是真占端口TIME_WAIT状态是正常关闭后的残留持续约 2 分钟无需处理。4.3 客户机连不上服务器的三大高频原因现象原因解决方案connect() failed: 10061WSAECONNREFUSED服务器未运行或bind()地址不对如绑127.0.0.1却从另一台机器连用telnet 127.0.0.1 8080测试本地连通性服务器bind()改用INADDR_ANYgetaddrinfo failed: No such host is known客户机传入的服务器域名无法解析如localhost在 hosts 文件被注释改用127.0.0.1测试或检查C:\Windows\System32\drivers\etc\hosts中127.0.0.1 localhost是否生效recv() failed: 10054WSAECONNRESET服务器进程崩溃或强制关闭了连接套接字而客户机还在读在服务器closesocket(client_sock)前确保recv()已返回0或错误客户机需检查recv()返回值而非假设总成功4.4 防火墙拦截临时放行验证Windows 防火墙默认阻止入站连接。若服务器在物理机运行客户机在虚拟机如 VMware、Hyper-V必须放行控制面板 → Windows Defender 防火墙 → 高级设置入站规则 → 新建规则 → 端口 → TCP → 特定本地端口8080→ 允许连接 → 域/专用/公用全选 → 命名Winsock Test保存后重试。切勿关闭整个防火墙这会带来安全风险。5. Winsock 编程避坑指南5 条血泪经验总结含现象、原因、解决Winsock 的坑不在语法而在 Windows 网络栈与 C 运行时的隐式耦合。以下 5 条是我在嵌入式设备通信、工业网关调试、内网代理开发中踩出的硬核教训每条都附可复现场景5.1 现象WSAStartup()成功但socket()返回INVALID_SOCKETWSAGetLastError()为10093WSANOTINITIALISED原因WSAStartup()调用后若后续代码发生未处理的结构化异常SEH如访问违规可能导致 Winsock 内部状态损坏即使WSAStartup()返回 0后续 API 仍失效。常见于指针野指针、数组越界后未崩溃却破坏了 Winsock 的 TLS 存储。解决在WSAStartup()后立即加一句测试SOCKET test_sock socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (test_sock INVALID_SOCKET) { printf(Winsock init corrupted! Error: %d\n, WSAGetLastError()); // 此时必须 WSACleanup() 并重新 WSAStartup() }5.2 现象服务器bind()成功listen()成功但客户机connect()永远超时10060原因bind()时sin_addr.s_addr填了inet_addr(192.168.1.100)但该 IP 不属于本机任何网卡如填错子网、或网卡被禁用。bind()不校验 IP 归属只检查端口是否空闲导致套接字绑定到“不存在”的地址。解决永远用INADDR_ANY0x00000000绑定让系统自动选择可用网卡若需指定网卡用getaddrinfo()查询本机所有 IPv4 地址遍历bind()直到成功。5.3 现象客户机发送数据后服务器recv()返回0但客户机未调用shutdown()或closesocket()原因Windows TCP 栈对“半关闭”敏感。客户机若只closesocket()而不先shutdown(SD_SEND)TCP 层可能不发送 FIN 包服务器recv()无法感知连接结束一直阻塞。解决客户机发送完毕后显式调用shutdown(sock, SD_SEND); // 发送 FIN // 此时可继续 recv() 服务器响应 closesocket(sock);5.4 现象多线程服务器中closesocket()后其他线程对同一套接字调用send()程序崩溃而非返回错误原因Winsock 套接字句柄是进程内全局的closesocket()立即释放内核资源但句柄值整数可能被其他线程复用。若 A 线程closesocket(s)B 线程立刻send(s, ...)实际操作的是新分配的套接字导致不可预测行为。解决必须用线程安全的句柄管理。最简方案——closesocket()后立即将句柄置为INVALID_SOCKET并在每次send()/recv()前检查if (sock INVALID_SOCKET) return; // 避免野指针调用5.5 现象服务器在 Windows Server 2019 上运行正常在 Win10 21H2 上accept()随机失败错误码10038WSAENOTSOCK原因accept()返回的client_sock被误存为int类型如int client_fd accept(...)而 Winsock 套接字是SOCKETunsigned int。在 64 位系统上高位被截断导致句柄值错误。解决严格使用SOCKET类型声明所有套接字变量SOCKET listen_sock, client_sock; // ✅ 正确 // int sock; // ❌ 危险尤其在 64 位编译时6. 进阶技巧用select()实现单线程多客户端管理非阻塞 I/O 的轻量替代阻塞式服务器一次只能服务一个客户端但select()能让单线程同时监控多个套接字的状态变化是学习异步 I/O 的最佳跳板。它不依赖线程或事件纯靠内核通知资源开销极低特别适合嵌入式 Windows 设备或轻量网关。6.1select()的核心逻辑三组描述符集合select()监控三类事件readfds套接字是否有数据可读包括新连接到达writefds套接字是否可写连接建立成功后首次可写exceptfds套接字是否有异常如带外数据 OOB对服务器而言只需关注readfds监听套接字就绪 → 有新连接客户端套接字就绪 → 有数据到达。6.2 改写服务器支持 5 个并发客户端的select()版本以下代码server_select.c在原服务器基础上增加select()循环移除阻塞accept()和recv()#include winsock2.h #include ws2tcpip.h #include stdio.h #include stdlib.h #include time.h #pragma comment(lib, ws2_32.lib) #define MAX_CLIENTS 5 int main() { WSADATA wsaData; SOCKET listen_sock; struct sockaddr_in server_addr; fd_set read_fds, master_fds; struct timeval timeout; int i, max_sd, activity, client_count 0; SOCKET client_socks[MAX_CLIENTS]; if (WSAStartup(MAKEWORD(2, 2), wsaData) ! 0) { printf(WSAStartup failed\n); return 1; } // 初始化监听套接字同前 listen_sock socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (listen_sock INVALID_SOCKET) { printf(socket failed\n); goto cleanup; } ZeroMemory(server_addr, sizeof(server_addr)); server_addr.sin_family AF_INET; server_addr.sin_addr.s_addr INADDR_ANY; server_addr.sin_port htons(8080); if (bind(listen_sock, (struct sockaddr*)server_addr, sizeof(server_addr)) SOCKET_ERROR) { printf(bind failed\n); goto cleanup; } if (listen(listen_sock, SOMAXCONN) SOCKET_ERROR) { printf(listen failed\n); goto cleanup; } printf(Select server listening on port 8080...\n); // 初始化 master_fds加入监听套接字 FD_ZERO(master_fds); FD_SET(listen_sock, master_fds); max_sd listen_sock; // 初始化客户端套接字数组 for (i 0; i MAX_CLIENTS; i) { client_socks[i] INVALID_SOCKET; } // 主循环select 监控所有套接字 while (1) { read_fds master_fds; // 每次调用前重置 timeout.tv_sec 5; // 5秒超时避免永久阻塞 timeout.tv_usec 0; activity select(max_sd 1, read_fds, NULL, NULL, timeout); if (activity 0) { printf(select error\n); break; } if (activity 0) { continue; // 超时继续下一轮 } // 检查监听套接字是否就绪有新连接 if (FD_ISSET(listen_sock, read_fds)) { struct sockaddr_in client_addr; int client_len sizeof(client_addr); SOCKET new_sock accept(listen_sock, (struct sockaddr*)client_addr, client_len); if (new_sock INVALID_SOCKET) { printf(accept failed\n); } else { // 将新套接字加入 master_fds FD_SET(new_sock, master_fds); if (new_sock max_sd) max_sd new_sock; // 记录到数组以便后续管理 for (i 0; i MAX_CLIENTS; i) { if (client_socks[i] INVALID_SOCKET) { client_socks[i] new_sock; client_count; printf(New client %d, total: %d\n, i1, client_count); break; } } } } // 检查所有客户端套接字是否有数据 for (i 0; i MAX_CLIENTS; i) { if (client_socks[i] ! INVALID_SOCKET FD_ISSET(client_socks[i], read_fds)) { char buffer[1024] {0}; int bytes_received recv(client_socks[i], buffer, sizeof(buffer)-1, 0); if (bytes_received 0) { printf(Client %d: %s, i1, buffer); send(client_socks[i], buffer, bytes_received, 0); } else if (bytes_received 0) { // 客户端关闭连接 printf(Client %d disconnected\n, i1); closesocket(client_socks[i]); FD_CLR(client_socks[i], master_fds); client_socks[i] INVALID_SOCKET; client_count--; } else { printf(recv from client %d failed\n, i1); closesocket(client_socks[i]); FD_CLR(client_socks[i], master_fds); client_socks[i] INVALID_SOCKET; client_count--; } } } } cleanup: closesocket(listen_sock); for (i 0; i MAX_CLIENTS; i) { if (client_socks[i] ! INVALID_SOCKET) closesocket(client_socks[i]); } WSACleanup(); return 0; }关键参数与行为说明max_sdselect()第一个参数是最大套接字描述符值加 1。必须动态更新因为accept()返回的新套接字 ID 可能大于listen_sock。FD_SET()/FD_ISSET()Winsock 提供的宏用于操作fd_set结构体。fd_set是位图大小固定默认 64 个套接字故MAX_CLIENTS不能超过 64。timeout必须设置否则select()永久阻塞无法响应 CtrlC。5 秒超时足够平衡响应与 CPU 占用。FD_CLR()客户端断开后必须从master_fds中清除否则下次select()仍会报告该套接字就绪导致recv()失败。6.3 性能对比阻塞 vsselect()指标阻塞服务器select()服务器最大并发1≤64fd_set限制CPU 占用低accept()阻塞低select()阻塞超时唤醒内存占用极低无额外结构低fd_set固定大小编程复杂度极简中等需管理套接字集合适用场景学习、调试、单客户端工具轻量网关、嵌入式设备、教学演示我坚持在所有 Winsock 教学项目中优先用select()因为它不引入线程同步复杂度又能真实体现“一个线程管多个连接”的网络编程本质。当你在 Wireshark 里看到 5 个客户端同时发包、服务器依次响应那种掌控感是任何高级框架都给不了的原始快感。希望帮到你。本文还有配套的精品资源点击获取