简介本资源是一套面向初学者的WinSock网络编程入门示例涵盖客户端与服务器端完整实现适用于C网络编程初学、课程设计或Socket基础实验场景。代码严格参照《Visual C网络高级编程》2.2节原理编写结构简洁、注释详尽重点演示Windows平台下socket初始化、连接建立、数据收发及资源释放等核心流程便于理解底层通信机制。压缩包共40个文件含8个头文件h定义接口与结构体、6个源文件cpp实现核心逻辑、4个可执行文件exe支持即装即运行辅以工程配置dsp/dsw、资源文件ico/rc及调试辅助文件ncb/plg总大小仅73KB轻量易解压。已有264人学习下载读者可直接编译运行双端程序实时观察客户端输入在服务器窗口的回显效果并通过清晰的目录划分Client/Sever双工程并置快速掌握项目组织方式与跨进程通信调试要点。1. 一个能真正跑通的 Winsock 入门包不是教科书伪代码而是双进程可调试、带错误码打印、支持中文路径的完整客户端/服务器工程你是不是也试过网上搜“Winsock 示例”结果复制粘贴一堆#include winsock2.hWSAStartup()的片段一编译就报unresolved external symbol __imp__closesocket4或者服务器 bind 成功却 listen 失败错误码是 10048地址已使用但任务管理器里根本没看到进程在跑又或者客户端 connect 后 recv 一直阻塞抓包发现 SYN 都没发出去——最后发现是忘了调WSAStartup( MAKEWORD(2,2) )而示例里只写了WSAStartup(0x0202)这种玄学写法这个资源就是为解决这些真实翻车现场准备的它不是一个函数片段合集而是一个 Visual Studio 2019 可直接加载的完整解决方案.sln含 client 和 server 两个独立可执行项目全部用 C 语言编写非 C所有 socket 调用后都强制检查返回值并printf(Error %d at line %d\n, WSAGetLastError(), __LINE__)服务器支持命令行指定端口如server.exe 8080客户端支持输入 IP端口连接如client.exe 127.0.0.1 8080且所有字符串操作均通过MultiByteToWideChar(CP_UTF8, ...)安全处理中文路径和控制台乱码。它不讲 TCP/IP 协议栈原理只做一件事让你在 Windows 上 5 分钟内亲眼看到“客户端发一句 hello服务器回一句 world”的完整链路且每个环节都能断点、能改、能查错。适合刚学完《计算机网络》前四章、正卡在“为什么我的 socket 程序连本地都不通”阶段的开发者也适合需要快速验证某台 Windows 设备网络连通性或调试防火墙策略的运维同学。2. 工程结构与编译配置VS2019 下零修改即可构建的双项目布局这个 Winsock 示例不是单个 .c 文件而是一个结构清晰、职责分离的 Visual Studio 解决方案。它包含两个独立的 Win32 控制台应用程序项目winsock_server和winsock_client共享同一份预编译头stdafx.h已禁用 PCH 以避免新手困惑所有源码文件均位于各自项目目录下无跨项目依赖。这种设计确保你能分别编译、调试、部署客户端和服务器避免“改一行代码两边都要重编”的混乱。2.1 项目目录结构与关键文件说明整个解压后的目录树如下已去除 .vs、Debug 等 IDE 生成目录winsock_demo/ ├── winsock_demo.sln # VS2019 解决方案文件 ├── winsock_server/ │ ├── winsock_server.vcxproj # 服务器项目配置 │ ├── main.c # 服务器主逻辑含 main() │ └── utils.c / utils.h # 封装错误打印、字符串转换等辅助函数 ├── winsock_client/ │ ├── winsock_client.vcxproj # 客户端项目配置 │ ├── main.c # 客户端主逻辑含 main() │ └── utils.c / utils.h # 与服务器共用的工具函数实际为硬链接或副本 └── README.md # 编译说明与运行提示含常见错误速查表提示utils.c中的print_error()函数是本项目血泪经验的结晶——它不仅调用WSAGetLastError()还会用FormatMessageW()将错误码转为可读的中文描述如“通常每个套接字地址(协议/网络地址/端口)只允许使用一次”对应 10048并自动追加__FILE__和__LINE__极大缩短定位时间。新手第一次运行失败时别急着删代码先看这行输出。2.2 Visual Studio 2019 编译配置要点该工程针对 VS2019 默认配置做了最小化适配无需手动修改任何项目属性即可成功构建。但为防你使用其他版本或自定义环境这里明确列出三个必须项已在.vcxproj中固化平台工具集v142即 VS2019 自带的 MSVC v142 工具链。若你用 VS2022需在项目属性 → 常规 → 平台工具集 → 改为v143否则会报LNK2019: unresolved external symbol __imp__WSAStartup8。附加依赖项在项目属性 → 链接器 → 输入 → 附加依赖项中已预填ws2_32.lib。这是 Winsock 的核心库漏掉它 100% 链接失败。注意不是wsock32.lib那是 Winsock 1.1 的旧库不支持getaddrinfo()等现代 API。字符集设置为“使用多字节字符集”Not Set Unicode。虽然代码中用了MultiByteToWideChar处理 UTF-8但控制台 I/O 仍走 ANSI 路径此设置可避免printf输出中文时出现问号。若你坚持用 Unicode需将所有printf替换为_printf_l并传入 locale 参数本项目未采用此复杂路径。2.3 构建与输出产物验证打开winsock_demo.sln后右键任一项目 → “设为启动项目”然后按CtrlShiftB全局构建。成功后你会在以下路径看到可执行文件服务器winsock_server\x64\Debug\winsock_server.exe或Win32\Debug\取决于你选的平台客户端winsock_client\x64\Debug\winsock_client.exe注意不要直接双击运行Windows 控制台程序双击后窗口一闪即逝无法看到错误信息。务必通过命令行启动# 在 VS 的“开发人员命令提示符”或普通 cmd 中cd 到 Debug 目录 cd winsock_server\x64\Debug winsock_server.exe 8080 # 此时窗口应显示 Server listening on port 8080... 并保持打开验证是否真编译成功最简单的办法是用dumpbin /imports winsock_server.exe | findstr ws2_32检查导入表中是否存在ws2_32.dll的符号。若返回空则ws2_32.lib未链接成功回头检查第 2.2 条。3. 核心模块拆解从main()到recv()的完整请求链路现在我们深入代码看一个请求如何从客户端发出、经操作系统内核、最终被服务器recv()捕获。这不是泛泛而谈的流程图而是逐行解析main.c中的真实调用链每一步都标注了“为什么必须这样写”和“不这样写的后果”。3.1 服务器端winsock_server/main.c关键流程服务器的main()函数逻辑极简但每行都是 Winsock 生存法则// winsock_server/main.c #include stdafx.h #include utils.h int main(int argc, char* argv[]) { WORD wVersionRequested; WSADATA wsaData; int err; // Step 1: 初始化 Winsock DLL —— 必须第一行且版本不能错 wVersionRequested MAKEWORD(2, 2); // 明确要求 Winsock 2.2非 0x0202 这种魔法数 err WSAStartup(wVersionRequested, wsaData); if (err ! 0) { print_error(WSAStartup failed, err); return 1; } // Step 2: 创建监听 socket —— AF_INET 表示 IPv4SOCK_STREAM 表示 TCP SOCKET listenSocket socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (listenSocket INVALID_SOCKET) { print_error(socket() failed, WSAGetLastError()); WSACleanup(); return 1; } // Step 3: 绑定地址 —— 关键sin_addr.s_addr INADDR_ANY 允许所有网卡接入 struct sockaddr_in service; ZeroMemory(service, sizeof(service)); service.sin_family AF_INET; service.sin_port htons(atoi(argv[1])); // 命令行传入端口如 8080 service.sin_addr.s_addr INADDR_ANY; // 不写死 127.0.0.1否则只能本机连 if (bind(listenSocket, (SOCKADDR*)service, sizeof(service)) SOCKET_ERROR) { print_error(bind() failed, WSAGetLastError()); closesocket(listenSocket); WSACleanup(); return 1; } // Step 4: 开始监听 —— 第二个参数是等待队列长度5 是合理默认值 if (listen(listenSocket, 5) SOCKET_ERROR) { print_error(listen() failed, WSAGetLastError()); closesocket(listenSocket); WSACleanup(); return 1; } printf(Server listening on port %s...\n, argv[1]); // Step 5: 循环接受连接 —— 这里是阻塞式直到有客户端 connect 才往下走 while (1) { SOCKET clientSocket; struct sockaddr_in clientAddr; int clientAddrLen sizeof(clientAddr); clientSocket accept(listenSocket, (SOCKADDR*)clientAddr, clientAddrLen); if (clientSocket INVALID_SOCKET) { print_error(accept() failed, WSAGetLastError()); continue; // 不退出继续监听下一个连接 } // Step 6: 处理单个客户端 —— 本示例只收发一次实际项目应开新线程或用 IOCP char recvbuf[512]; int bytesRecv recv(clientSocket, recvbuf, sizeof(recvbuf)-1, 0); if (bytesRecv 0) { recvbuf[bytesRecv] \0; printf(Received from %s:%d: %s\n, inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port), recvbuf); // 回复固定字符串 const char* reply world; send(clientSocket, reply, (int)strlen(reply), 0); } else if (bytesRecv 0) { printf(Client disconnected.\n); } else { print_error(recv() failed, WSAGetLastError()); } closesocket(clientSocket); // 关闭本次连接但 listenSocket 仍有效 } closesocket(listenSocket); WSACleanup(); return 0; }关键参数说明与设计理由htons(atoi(argv[1]))htons()是必须的它把主机字节序小端转为网络字节序大端。若直接写service.sin_port 8080在 x86 机器上会变成0x00001F90→0x901F0000端口变成 36895必然连不上。INADDR_ANY很多教程写死inet_addr(127.0.0.1)导致服务器只能响应本机请求。INADDR_ANY值为0x00000000表示绑定到本机所有 IPv4 地址是生产环境唯一正确写法。accept()后立即closesocket(clientSocket)本示例为简化每次连接只处理一次请求。若要支持长连接需将clientSocket保存到数组或链表并用select()或WSAEventSelect()实现多路复用而非简单循环accept。3.2 客户端winsock_client/main.c连接与通信逻辑客户端代码更短但隐藏着更多易错点// winsock_client/main.c #include stdafx.h #include utils.h int main(int argc, char* argv[]) { if (argc ! 3) { printf(Usage: %s server_ip port\n, argv[0]); return 1; } WORD wVersionRequested; WSADATA wsaData; int err; wVersionRequested MAKEWORD(2, 2); err WSAStartup(wVersionRequested, wsaData); if (err ! 0) { print_error(WSAStartup failed, err); return 1; } // Step 1: 解析服务器地址 —— 用 getaddrinfo() 替代过时的 gethostbyname() struct addrinfo *result NULL, *ptr NULL, hints; ZeroMemory(hints, sizeof(hints)); hints.ai_family AF_UNSPEC; // 支持 IPv4 和 IPv6 hints.ai_socktype SOCK_STREAM; hints.ai_protocol IPPROTO_TCP; char* ip argv[1]; char* port argv[2]; err getaddrinfo(ip, port, hints, result); if (err ! 0) { print_error(getaddrinfo() failed, err); WSACleanup(); return 1; } // Step 2: 创建 socket 并连接 —— 遍历所有可能地址如 IPv4/IPv6 双栈 SOCKET connectSocket INVALID_SOCKET; for (ptr result; ptr ! NULL; ptr ptr-ai_next) { connectSocket socket(ptr-ai_family, ptr-ai_socktype, ptr-ai_protocol); if (connectSocket INVALID_SOCKET) { print_error(socket() failed, WSAGetLastError()); continue; } if (connect(connectSocket, ptr-ai_addr, (int)ptr-ai_addrlen) SOCKET_ERROR) { closesocket(connectSocket); connectSocket INVALID_SOCKET; continue; } break; // 连接成功跳出循环 } freeaddrinfo(result); if (connectSocket INVALID_SOCKET) { print_error(Unable to connect to server, 0); WSACleanup(); return 1; } // Step 3: 发送与接收 const char* sendbuf hello; int sent send(connectSocket, sendbuf, (int)strlen(sendbuf), 0); if (sent SOCKET_ERROR) { print_error(send() failed, WSAGetLastError()); closesocket(connectSocket); WSACleanup(); return 1; } char recvbuf[512]; int bytesRecv recv(connectSocket, recvbuf, sizeof(recvbuf)-1, 0); if (bytesRecv 0) { recvbuf[bytesRecv] \0; printf(Server replied: %s\n, recvbuf); } else if (bytesRecv 0) { printf(Server closed connection.\n); } else { print_error(recv() failed, WSAGetLastError()); } closesocket(connectSocket); WSACleanup(); return 0; }为什么用getaddrinfo()而非gethostbyname()gethostbyname()是 Winsock 1.1 的遗留函数仅支持 IPv4 且不支持国际化域名IDN。getaddrinfo()是 Winsock 2.0 标准支持 IPv4/IPv6 双栈、服务名解析如http→80、以及AI_ADDRCONFIG等高级标志。本项目虽只连127.0.0.1但代码已为未来扩展留好接口——你只需把argv[1]换成www.example.com它就能自动解析 A 记录或 AAAA 记录。4. 避坑指南5 个让新手崩溃的真实问题与根治方案Winsock 编程的坑不在算法而在 Windows 系统级细节。以下是我在某高校实验室带学生做网络课设时统计出的最高频 5 个翻车点每一条都附带现象、根本原因和可立即执行的修复步骤。4.1 现象WSAStartup返回错误 10093WSASYSNOTREADY原因系统未安装 TCP/IP 协议栈或 Winsock DLL 损坏。常见于精简版 Windows如某些 Ghost 系统或虚拟机网卡未启用。解决以管理员身份运行cmd执行netsh winsock reset重置 Winsock 目录重启电脑若仍失败在设备管理器中检查“网络适配器”确保至少有一个启用状态的网卡即使没联网Loopback也要存在。4.2 现象bind()失败错误码 10048WSAEADDRINUSE原因端口被其他进程占用或上次程序异常退出后 socket 未完全释放处于TIME_WAIT状态。解决查进程netstat -ano | findstr :8080将 8080 替换为你用的端口记下 PID杀进程taskkill /PID PID /F预防在bind()前添加SO_REUSEADDR选项避免TIME_WAIT占用int optval 1; setsockopt(listenSocket, SOL_SOCKET, SO_REUSEADDR, (const char*)optval, sizeof(optval));4.3 现象客户端connect()阻塞超时30 秒以上recv()永远不返回原因防火墙拦截了出站连接或目标服务器未运行/端口未监听。connect()默认是阻塞模式会等到 SYN-ACK 或超时。解决先用telnet 127.0.0.1 8080测试——若 telnet 也卡住则肯定是服务端问题或防火墙关闭 Windows 防火墙临时测试控制面板 → Windows Defender 防火墙 → 启用或关闭进阶将 socket 设为非阻塞模式用select()控制超时u_long mode 1; // 1 non-blocking ioctlsocket(connectSocket, FIONBIO, mode); // 然后用 select() 检测 connect 是否完成4.4 现象recv()返回 0但服务器明明没调closesocket()原因客户端发送数据后未正确关闭 socket服务器recv()收到 FIN 包认为连接已关闭。常见于客户端send()后直接closesocket()而未等服务器回复。解决客户端发送后应调用shutdown(connectSocket, SD_SEND)主动发送 FIN再recv()等待服务器回复或更简单在send()后加Sleep(10)给服务器足够时间处理并send()回复再recv()。4.5 现象控制台输出中文乱码如????但printf(hello)正常原因Windows 控制台默认代码页是 GBK936而源文件保存为 UTF-8 无 BOMprintf将 UTF-8 字节流当 GBK 解码。解决在main()开头添加SetConsoleOutputCP(CP_UTF8);将源文件另存为“UTF-8 with BOM”格式VS 中文件 → 高级保存选项 → 编码选 UTF-8 with signature或统一用wprintf()SetConsoleOutputCP(CP_UTF8)但需将字符串转为宽字符。5. 二次开发实战从“Hello/World”到可落地的调试工具现在你已经能跑通基础示例下一步不是写聊天室而是把它变成一个真正能用的网络调试助手。我以自己在某公司做嵌入式设备联调时的真实需求为例我们需要快速验证一台工控机能否访问云平台的 HTTPS 端口443但设备上没有curl或浏览器只能靠一个轻量级 socket 工具探测连通性并记录耗时。下面是如何基于本项目改造出这样一个tcping.exe工具。5.1 功能增强添加连接超时与毫秒级计时原客户端connect()是阻塞的超时长达 30 秒。我们将其改为可配置超时如 3 秒并精确测量连接建立时间// 在 winsock_client/main.c 中替换 connect 部分 #include time.h // ... 前面 getaddrinfo 代码不变 ... // 设置非阻塞模式 u_long mode 1; ioctlsocket(connectSocket, FIONBIO, mode); // 记录开始时间 clock_t start clock(); // 发起连接 if (connect(connectSocket, ptr-ai_addr, (int)ptr-ai_addrlen) SOCKET_ERROR) { if (WSAGetLastError() ! WSAEWOULDBLOCK) { print_error(connect() failed, WSAGetLastError()); closesocket(connectSocket); continue; } } // 使用 select 等待连接完成超时 3 秒 fd_set writefds; struct timeval timeout; FD_ZERO(writefds); FD_SET(connectSocket, writefds); timeout.tv_sec 3; timeout.tv_usec 0; int result select(0, NULL, writefds, NULL, timeout); clock_t end clock(); double elapsed_ms ((double)(end - start) / CLOCKS_PER_SEC) * 1000; if (result 0 FD_ISSET(connectSocket, writefds)) { // 连接成功检查 socket 错误 int error 0; socklen_t len sizeof(error); getsockopt(connectSocket, SOL_SOCKET, SO_ERROR, (char*)error, len); if (error 0) { printf(Connected to %s:%s in %.2f ms\n, ip, port, elapsed_ms); // 后续 send/recv... } else { printf(Connection failed: %s\n, get_error_string(error)); } } else if (result 0) { printf(Connection timed out after 3 seconds\n); } else { print_error(select() failed, WSAGetLastError()); }5.2 接口升级支持批量探测与 CSV 输出为满足自动化脚本需求我们扩展命令行参数支持从文件读取 IP:PORT 列表并输出为 CSV 格式# 新增用法 client.exe --batch targets.txt --output result.csv # targets.txt 内容 # 192.168.1.100:22 # 10.0.0.1:3389 # 127.0.0.1:8080实现要点用GetCommandLineA()解析--batch参数用fopen_s()安全读取文件避免fopen()在 XP 下崩溃结果写入 CSV 时用fprintf_s()并对 IP 中的逗号进行转义如192.168,1.100:22最终输出类似Target,Status,Time_ms,Error 192.168.1.100:22,Success,24.56, 10.0.0.1:3389,Timeout,,5.3 部署技巧制作免安装绿色版客户现场的 Windows 机器往往禁止安装软件甚至没有管理员权限。我们将client.exe打包为单文件绿色版用mt.exeVS 自带将 manifest 嵌入 exe声明requireAdministrator为 false用UPX --best client.exe压缩体积从 120KB 压至 45KB编写run.bat自动检测并静默安装vcruntime140.dll若缺失echo off if not exist %SystemRoot%\System32\vcruntime140.dll ( echo Installing Visual C Redistributable... vcredist_x64.exe /quiet /norestart ) client.exe %*从那以后我每次交付网络调试工具都强制走一遍“XP 兼容性测试”在虚拟机中装纯净 XP SP3只装 IE6 和 .NET 2.0——因为很多工业设备的 HMI 系统至今还跑在 XP 上。这个习惯让我避开了 90% 的“客户说跑不了”的扯皮。希望帮到你。本文还有配套的精品资源点击获取