为什么从 Winsock 开始
Windows 上的网络编程绕不开 Winsock。它是 Windows 对 BSD Socket 的扩展实现,绝大多数上层库(包括跨平台框架)最终都落到这套接口上。理解 Winsock 的初始化流程,是排查“连接失败但代码没错”类问题的前提。
关键一步是 WSAStartup,它负责加载 winsock 动态库并协商版本。忘记调用或版本不匹配,后续所有 socket 调用都会失败。程序退出前对应调用 WSACleanup。这两步看似模板化,却常被遗漏。
阻塞与非阻塞的选择
默认创建的 socket 是阻塞模式:accept、recv 在无数据时会让线程挂起。单连接、低并发的工具程序用阻塞模式最省事,代码直白,调试容易。
一旦连接数上升,阻塞模式就需要“一连接一线程”,线程切换和内存开销迅速放大。此时应转向非阻塞模式,配合 select、WSAEventSelect 或 WSAAsyncSelect 做事件驱动。select 兼容性好、逻辑直观,但有 64 个句柄的上限(FD_SETSIZE),且每次调用都要重新填充集合,高并发下效率有限。
重叠 I/O 与完成端口
真正面向高并发的是重叠 I/O(Overlapped I/O)。提交 WSARecv、WSASend 时传入 OVERLAPPED 结构,调用立即返回,操作完成后再通知结果。通知方式有两种:事件对象和完成例程。
在此之上是 I/O 完成端口(IOCP)。它把完成通知集中到一个队列,由固定数量的工作线程消费,避免线程数与连接数绑定。典型做法是创建 N 个工作者线程(常取 CPU 核数附近),每个线程循环调用 GetQueuedCompletionStatus 取结果并处理。IOCP 是 Windows 上构建高性能服务器的首选模型,代价是状态管理和缓冲区生命周期更复杂,需要谨慎处理“半包”“粘包”和内存回收。
几个容易踩的坑
第一,注意字节序。网络传输用大端,htons、htonl 系列函数负责转换,漏用会导致端口或长度解析错误。
第二,recv 返回值要区分三种情况:大于零是实际字节数,等于零表示对端正常关闭,小于零需查 WSAGetLastError,其中 WSAEWOULDBLOCK 在非阻塞场景下是正常现象而非错误。
第三,资源释放要成对。socket 用 closesocket,事件对象用 CloseHandle,重叠结构中的缓冲区要在操作真正完成后才能复用或释放。
选型建议
小工具和原型优先阻塞模式,代码量最少;中等并发可用 select 或事件模型过渡;面向生产的高并发服务直接上 IOCP。无论选哪种,先把错误码处理和连接生命周期理清楚,比追求模型先进更实际。