WSA系列API函数详解:Windows网络编程从入门到实践
1. WSA系列API函数到底解决什么问题第一次接触WSA系列API函数是在一个跨平台网络工具的开发项目里。当时团队需要把一套原本跑在Linux上的网络通信模块迁移到Windows环境原本以为改改头文件、换几个函数名就能搞定结果编译直接报了几百个错误——问题就出在Windows的Socket编程接口和POSIX标准完全不是一回事。WSA系列API就是Windows平台下网络编程的基石全称是Windows Sockets API它提供了一套从套接字创建、地址绑定、连接建立到数据收发、连接关闭的完整接口体系。说白了只要你在Windows上写任何跟网络通信有关的程序——不管是客户端还是服务端不管是TCP还是UDP不管上层用的是HTTP、WebSocket还是自定义协议——底层最终都要落到WSA系列函数上。这套API最早可以追溯到Windows 3.1时代当时为了兼容BSD Socket标准微软和几家公司联合制定了Winsock规范。发展到今天Winsock 2.2已经成为Windows各版本的标准组件WSA系列函数也从最初的几十个扩展到了上百个。这篇文章适合谁看如果你正在学习Windows网络编程或者需要把Linux下的网络代码移植到Windows又或者你在调试网络程序时经常遇到WSAE开头的错误码却不知道怎么排查那这篇内容应该能帮到你。我会从整体设计思路讲到具体函数的实操细节再结合我踩过的坑把WSA系列API的完整使用链路梳理清楚。2. WSA系列API的整体架构与设计思路2.1 为什么Windows不直接用POSIX Socket很多人第一次写Windows网络程序时都会有个疑问为什么不能直接用Linux那套socket、bind、listen、accept答案在于Windows的架构和Unix有本质区别。Unix系统里socket就是一个文件描述符可以像操作文件一样用read、write来收发数据。但Windows的内核对象管理和文件系统是两套体系socket在Windows里是一个独立的句柄类型不能用ReadFile、WriteFile来操作。WSA系列API的设计思路就是在Windows的句柄体系上重新实现一套符合BSD Socket语义的接口。函数名大多以WSA开头比如WSASocket、WSAConnect、WSASend、WSARecv这些是Winsock 2.0引入的扩展版本支持重叠I/O和事件通知等高级特性。而像socket、bind、connect这些不带WSA前缀的是Winsock 1.1就有的基础版本功能相对简单。这种设计带来的一个直接后果是Windows网络编程必须显式初始化Winsock库。Linux下你直接调socket就行但Windows下必须先调WSAStartup加载Ws2_32.dll程序结束时还要调WSACleanup释放资源。我见过不少新手写的代码编译通过了但运行时报WSANOTINITIALISED错误就是因为忘了这一步。2.2 WSAStartup与WSACleanup的配对逻辑WSAStartup是整个WSA系列API的入口函数它的原型是int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);第一个参数指定你需要的Winsock版本通常用MAKEWORD(2, 2)表示请求2.2版本。第二个参数是一个输出参数系统会把实际协商到的版本信息填进去。这里有个细节如果你请求2.2但系统只支持1.1WSAStartup不会失败而是返回1.1的版本信息。所以严谨的做法是检查返回的版本号是否满足你的最低要求。WSACleanup的作用是通知系统你不再使用Winsock了系统会递减一个内部引用计数。每次WSAStartup都会增加这个计数每次WSACleanup递减只有计数归零时系统才真正释放Winsock占用的资源。这意味着你可以在多个模块里分别调用WSAStartup只要保证WSACleanup的调用次数匹配就行。注意WSAStartup和WSACleanup必须成对出现。我遇到过最隐蔽的bug是某个动态库在DllMain里调了WSAStartup但没调WSACleanup导致进程退出时资源泄漏虽然不影响功能但长时间运行会累积。2.3 阻塞与非阻塞模式的选择逻辑WSA系列API默认创建的套接字是阻塞模式的。阻塞模式下accept会一直等到有客户端连接才返回recv会一直等到有数据到达才返回。这种模式写起来简单直观但只适合单连接场景。一旦需要同时处理多个客户端阻塞模式就力不从心了。Windows提供了几种解决思路第一种是用ioctlsocket把套接字设为非阻塞模式然后配合select轮询第二种是用WSAAsyncSelect注册窗口消息通知第三种是用WSAEventSelect注册事件对象第四种是直接用重叠I/O配合IOCP完成端口。这几种方案的选择取决于你的应用场景连接数少、逻辑简单用select就够了连接数上千、追求高性能就得上IOCP。我个人的经验是如果是学习或者做小工具从阻塞模式加多线程开始最稳妥如果是生产环境的服务端直接上IOCP虽然学习曲线陡但性能天花板高。中间那些方案看起来省事实际上在连接数增长后都会遇到瓶颈。3. 核心函数逐个拆解与实操要点3.1 套接字创建socket与WSASocket的区别socket函数是创建套接字的基础接口SOCKET socket(int af, int type, int protocol);af指定地址族网络编程用AF_INETIPv4或AF_INET6IPv6。type指定套接字类型SOCK_STREAM对应TCPSOCK_DGRAM对应UDP。protocol通常填0让系统自动选择。返回值是一个SOCKET句柄失败时返回INVALID_SOCKET需要用WSAGetLastError获取具体错误码。WSASocket是它的扩展版本多了几个参数SOCKET WSASocket(int af, int type, int protocol, LPWSAPROTOCOL_INFO lpProtocolInfo, GROUP g, DWORD dwFlags);lpProtocolInfo可以指定具体的传输协议提供者一般填NULL用默认的。dwFlags是关键可以填WSA_FLAG_OVERLAPPED来创建支持重叠I/O的套接字。如果你打算用IOCP或者WSASend/WSARecv的重叠模式就必须用WSASocket并带上这个标志。实操心得即使你暂时不用重叠I/O我也建议统一用WSASocket创建套接字并加上WSA_FLAG_OVERLAPPED标志。这样后续如果要升级到高性能模式不用改创建代码。这个标志本身不会带来额外开销。3.2 地址绑定bind与sockaddr结构体bind函数把套接字和一个本地地址关联起来int bind(SOCKET s, const struct sockaddr *name, int namelen);服务端必须调bind指定监听端口客户端通常不需要系统会在connect时自动分配一个临时端口。sockaddr是一个通用地址结构实际使用时需要填充sockaddr_inIPv4或sockaddr_in6IPv6struct sockaddr_in addr; addr.sin_family AF_INET; addr.sin_port htons(8080); addr.sin_addr.s_addr htonl(INADDR_ANY); bind(sock, (struct sockaddr*)addr, sizeof(addr));这里有两个容易出错的地方一是端口和地址必须用网络字节序htons和htonl不能省二是INADDR_ANY表示监听所有网卡如果只想监听特定网卡就填对应的IP地址。常见坑bind失败返回SOCKET_ERROR错误码WSAEADDRINUSE表示端口被占用。Windows有个默认行为是TIME_WAIT状态的端口不能被立即重用调试时频繁重启服务会碰到这个问题。解决办法是设置SO_REUSEADDR选项但要注意这个选项在Windows上的语义和Linux不同Windows上它允许完全重复绑定可能导致意外的端口冲突。3.3 连接建立listen、accept与connect服务端调listen把套接字转为监听状态int listen(SOCKET s, int backlog);backlog指定等待连接队列的最大长度。这个值不是越大越好Windows上实际生效的值由系统内部决定填SOMAXCONN让系统自动选择就行。accept从监听队列里取出一个已完成连接的套接字SOCKET accept(SOCKET s, struct sockaddr *addr, int *addrlen);返回的新套接字用于和该客户端通信原来的监听套接字继续等待其他连接。addr和addrlen是输出参数返回客户端的地址信息不关心的话可以传NULL。客户端调connect发起连接int connect(SOCKET s, const struct sockaddr *name, int namelen);阻塞模式下connect会一直等到连接建立或失败。非阻塞模式下connect通常返回WSAEWOULDBLOCK需要用select或WSAEventSelect等待可写事件来判断连接是否成功。实操心得accept返回的套接字和监听套接字是独立的关闭监听套接字不会影响已建立的连接。我见过有人在accept之后把监听套接字关了结果后续连接全部失败。正确的做法是监听套接字在整个服务生命周期内保持打开。3.4 数据收发send、recv与WSASend、WSARecvsend和recv是最基础的数据收发函数int send(SOCKET s, const char *buf, int len, int flags); int recv(SOCKET s, char *buf, int len, int flags);flags通常填0。send返回实际发送的字节数可能小于len所以需要循环发送直到全部发完。recv返回实际接收的字节数返回0表示对方关闭了连接返回SOCKET_ERROR表示出错。WSASend和WSARecv是扩展版本支持重叠I/O和分散聚集int WSASend(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesSent, DWORD dwFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);lpBuffers是一个WSABUF数组可以一次发送多个不连续的内存块这叫分散聚集I/O。lpOverlapped用于异步模式传NULL就是同步调用。关键细节TCP是字节流协议没有消息边界。send一次发100字节recv可能分两次收到60和40。所以应用层必须自己定义消息边界常见做法是固定长度头加变长体头部里包含体长度字段。这个坑我踩过不止一次新手最容易在这里翻车。3.5 连接关闭closesocket与shutdownclosesocket关闭套接字并释放资源int closesocket(SOCKET s);但直接closesocket有个问题如果发送缓冲区还有数据没发完这些数据会被丢弃。优雅的关闭流程是先调shutdownint shutdown(SOCKET s, int how);how填SD_SEND表示不再发送数据但继续接收填SD_RECEIVE表示不再接收填SD_BOTH表示双向关闭。调完shutdown后再调recv会返回0表示对方可以安全关闭了。等对方也关闭后再调closesocket释放句柄。避坑技巧多线程环境下closesocket要特别小心。如果一个线程正在recv阻塞另一个线程closesocketrecv会立即返回错误。但更危险的是如果套接字句柄被复用了closesocket可能关掉一个完全不相关的连接。稳妥的做法是用引用计数管理套接字生命周期确保没有线程在使用时才真正关闭。4. 完整TCP服务端的实操流程4.1 初始化与套接字创建下面是一个最小可用的TCP服务端骨架我把它拆成几个关键步骤来说明。首先是初始化和创建监听套接字WSADATA wsaData; int ret WSAStartup(MAKEWORD(2, 2), wsaData); if (ret ! 0) { printf(WSAStartup failed: %d\n, ret); return -1; } SOCKET listenSock WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED); if (listenSock INVALID_SOCKET) { printf(WSASocket failed: %d\n, WSAGetLastError()); WSACleanup(); return -1; }这里用WSASocket而不是socket并带上WSA_FLAG_OVERLAPPED为后续可能的异步升级留余地。IPPROTO_TCP明确指定TCP协议虽然填0也能工作但显式指定更清晰。4.2 绑定、监听与接受连接接下来绑定地址并开始监听struct sockaddr_in addr; memset(addr, 0, sizeof(addr)); addr.sin_family AF_INET; addr.sin_port htons(8080); addr.sin_addr.s_addr htonl(INADDR_ANY); if (bind(listenSock, (struct sockaddr*)addr, sizeof(addr)) SOCKET_ERROR) { printf(bind failed: %d\n, WSAGetLastError()); closesocket(listenSock); WSACleanup(); return -1; } if (listen(listenSock, SOMAXCONN) SOCKET_ERROR) { printf(listen failed: %d\n, WSAGetLastError()); closesocket(listenSock); WSACleanup(); return -1; }然后进入接受连接的循环while (1) { struct sockaddr_in clientAddr; int clientAddrLen sizeof(clientAddr); SOCKET clientSock accept(listenSock, (struct sockaddr*)clientAddr, clientAddrLen); if (clientSock INVALID_SOCKET) { printf(accept failed: %d\n, WSAGetLastError()); continue; } printf(Client connected: %s:%d\n, inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port)); // 处理客户端连接 handleClient(clientSock); }这个循环是阻塞的accept会一直等到有客户端连接。如果要同时处理多个客户端需要把handleClient放到独立线程里或者改用非阻塞模式加select。4.3 数据收发与消息边界处理handleClient函数里处理具体的数据收发。前面说过TCP没有消息边界所以这里用一个简单的长度前缀协议void handleClient(SOCKET clientSock) { char header[4]; while (1) { int received recvAll(clientSock, header, 4); if (received 0) break; int msgLen ntohl(*(int*)header); if (msgLen 0 || msgLen 1024 * 1024) break; char *msg malloc(msgLen); received recvAll(clientSock, msg, msgLen); if (received 0) { free(msg); break; } // 处理消息 processMessage(msg, msgLen); free(msg); } closesocket(clientSock); } int recvAll(SOCKET s, char *buf, int len) { int total 0; while (total len) { int ret recv(s, buf total, len - total, 0); if (ret 0) return ret; total ret; } return total; }recvAll函数确保读满指定字节数这是处理TCP流式数据的标准做法。每次recv返回的字节数可能小于请求的必须循环读取。实操心得消息长度一定要做上限校验。我见过一个服务端因为没校验长度客户端发了个超大长度值导致malloc失败程序直接崩溃。上限设多少取决于业务一般1MB到10MB之间比较合理。4.4 优雅关闭与资源释放服务端退出时要按顺序释放资源shutdown(listenSock, SD_BOTH); closesocket(listenSock); WSACleanup();先shutdown通知对方再closesocket释放句柄最后WSACleanup清理Winsock库。如果是多线程服务端还要先通知所有工作线程退出并等待它们结束再执行这些清理操作。5. 常见错误码与排查技巧实录5.1 WSAE开头的错误码速查WSA系列API的错误码都以WSAE开头下面这张表是我在实际调试中整理的高频错误码错误码数值含义常见原因WSAEWOULDBLOCK10035操作无法立即完成非阻塞模式下正常返回WSAEINPROGRESS10036阻塞操作正在进行多线程同时操作同一套接字WSAENOTSOCK10038句柄不是套接字套接字已关闭或句柄被复用WSAEADDRINUSE10048地址已被占用端口被其他程序占用WSAECONNRESET10054连接被对方重置对方强制关闭或网络中断WSAETIMEDOUT10060连接超时网络不通或对方未响应WSAECONNREFUSED10061连接被拒绝目标端口没有服务监听WSAENOTINITIALISED10093Winsock未初始化忘记调WSAStartup排查错误码的第一步永远是调WSAGetLastError不要凭猜测。我见过有人看到connect失败就以为是网络问题结果错误码是WSAENOTINITIALISED纯粹是忘了初始化。5.2 连接超时与重试策略connect在阻塞模式下默认超时时间很长可能达到20秒以上。如果目标不可达用户要等很久才能得到反馈。解决办法是先把套接字设为非阻塞调connect后立即返回WSAEWOULDBLOCK然后用select等待可写事件并设置超时u_long mode 1; ioctlsocket(sock, FIONBIO, mode); int ret connect(sock, (struct sockaddr*)addr, sizeof(addr)); if (ret SOCKET_ERROR WSAGetLastError() WSAEWOULDBLOCK) { fd_set writeSet; FD_ZERO(writeSet); FD_SET(sock, writeSet); struct timeval tv {5, 0}; ret select(0, NULL, writeSet, NULL, tv); if (ret 0) { // 连接成功检查SO_ERROR确认 int err 0; int len sizeof(err); getsockopt(sock, SOL_SOCKET, SO_ERROR, (char*)err, len); if (err ! 0) { /* 连接失败 */ } } else { // 超时 } }这段代码的关键是select返回可写后还要用getsockopt检查SO_ERROR因为可写不代表连接成功也可能是连接失败后的错误状态。5.3 大量连接下的句柄管理Windows的套接字句柄是有限资源默认每个进程最多能打开一定数量的句柄。如果服务端要处理上万连接需要调整这个限制。另外每个套接字在内核里都有缓冲区连接数多了内存占用也不小。我做过一个测试单进程维持5000个空闲TCP连接内存占用大约200MBCPU几乎为零。但如果这些连接频繁收发数据CPU和内存都会显著上升。所以连接数规划要结合业务特点不能只看理论值。避坑技巧套接字句柄泄漏是服务端最常见的慢性病。表现是运行几天后新连接建立失败错误码WSAENOBUFS或WSAEMFILE。排查方法是定期打印当前打开的句柄数发现持续增长就说明有泄漏。用任务管理器的句柄数列或者性能监视器都能看到。5.4 网络异常断开与心跳机制TCP连接在正常情况下通过FIN包优雅关闭但网络中断、对方断电、防火墙超时等情况不会发送FIN。这时本地套接字还以为是连接状态直到下次发送数据才会发现对方不可达。对于长连接应用必须有心跳机制来检测死连接。心跳的实现方式有两种一是应用层定时发送心跳包对方回复确认二是开启TCP的SO_KEEPALIVE选项由系统自动探测。SO_KEEPALIVE的探测间隔默认是2小时太长了可以通过WSAIoctl调整但比较麻烦。我一般用应用层心跳间隔30秒到60秒连续3次无响应就判定连接断开。6. 从阻塞到异步的进阶路线6.1 select模型的适用场景与局限select是最简单的多路复用方案可以同时监控多个套接字的可读、可写、异常状态fd_set readSet; FD_ZERO(readSet); FD_SET(sock1, readSet); FD_SET(sock2, readSet); int ret select(0, readSet, NULL, NULL, tv);select的局限很明显一是fd_set能容纳的套接字数量有上限Windows上默认是64个二是每次调用都要把整个集合从用户态复制到内核态连接数多了开销很大三是返回后要遍历所有套接字才知道哪个有事件。所以select只适合连接数少、对性能要求不高的场景。6.2 WSAEventSelect的事件驱动模式WSAEventSelect把套接字事件和事件对象关联起来WSAEVENT event WSACreateEvent(); WSAEventSelect(sock, event, FD_READ | FD_WRITE | FD_CLOSE);然后用WSAWaitForMultipleEvents等待事件触发再用WSAEnumNetworkEvents获取具体事件类型。这种模式比select效率高因为不需要每次遍历所有套接字。但WSAWaitForMultipleEvents一次最多等64个事件连接数多了还是要分组管理。6.3 IOCP完成端口的核心思路IOCP是Windows上性能最高的网络模型核心思想是把I/O操作提交给内核内核完成后再通知应用程序。应用程序只需要维护一个线程池线程从完成队列里取结果处理就行。使用IOCP的关键步骤创建完成端口、把监听套接字关联到完成端口、创建AcceptEx接受连接、用WSARecv和WSASend提交异步收发请求。每个操作都要带一个OVERLAPPED结构完成时通过GetQueuedCompletionStatus获取。IOCP的学习曲线确实陡但一旦掌握单机支撑几万甚至几十万连接都不是问题。我建议先用阻塞模式把业务逻辑跑通再逐步替换成IOCP不要一上来就啃最难的。7. 跨平台移植的注意事项7.1 头文件与库的差异Linux下网络编程包含sys/socket.h、netinet/in.h、arpa/inet.h链接时不需要额外指定库。Windows下包含winsock2.h和ws2tcpip.h链接时要加Ws2_32.lib。注意winsock2.h和windows.h有包含顺序问题必须先包含winsock2.h再包含windows.h否则会报重定义错误。7.2 函数名与错误处理的映射Linux的close对应Windows的closesocketLinux的errno对应Windows的WSAGetLastError。Linux下EAGAIN和EWOULDBLOCK是同一个值Windows下WSAEWOULDBLOCK是独立的。写跨平台代码时建议封装一层宏#ifdef _WIN32 #define CLOSE_SOCKET closesocket #define GET_SOCKET_ERROR WSAGetLastError #define SOCKET_ERROR_CODE WSAEWOULDBLOCK #else #define CLOSE_SOCKET close #define GET_SOCKET_ERROR errno #define SOCKET_ERROR_CODE EAGAIN #endif这样业务代码不用到处写ifdef维护起来清爽很多。7.3 字节序与数据对齐网络字节序转换函数htons、htonl、ntohs、ntohl在两端都有用法一致。但结构体对齐要注意Windows默认对齐和Linux可能不同跨平台传输二进制结构体时最好手动序列化不要直接memcpy整个结构体。8. 我踩过的几个典型坑第一个坑是WSAStartup的版本参数。我一开始填的是MAKEWORD(1, 1)结果WSASocket返回失败因为1.1不支持扩展函数。后来改成MAKEWORD(2, 2)就正常了。这个错误很隐蔽因为WSAStartup本身返回成功只是后续函数不可用。第二个坑是accept的addrlen参数。它是值结果参数传入时必须初始化为sizeof(struct sockaddr_in)函数返回时会被修改为实际长度。我有次忘了初始化传了个随机值进去accept直接返回WSAEFAULT。第三个坑是recv返回0的处理。返回0表示对方优雅关闭了连接不是错误。我早期代码里把0当成错误处理导致正常的连接关闭被当成异常日志里全是误报。第四个坑是多线程下共享套接字。两个线程同时对同一个套接字调send数据可能会交错。虽然TCP保证字节流顺序但两个线程的数据块可能混在一起。解决办法是加锁或者每个线程用独立套接字。第五个坑是closesocket后继续使用句柄。Windows的句柄是复用的closesocket后句柄值可能立即被新创建的套接字占用。如果旧代码还拿着这个值操作就会影响到新套接字。这种bug极难排查表现是随机的数据错乱。稳妥做法是closesocket后立即把变量置为INVALID_SOCKET。9. 性能调优的几个实用参数套接字缓冲区大小对吞吐量影响很大。默认的发送和接收缓冲区通常是8KB在高带宽场景下可以调大到64KB甚至256KBint bufSize 256 * 1024; setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (char*)bufSize, sizeof(bufSize)); setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (char*)bufSize, sizeof(bufSize));但缓冲区不是越大越好太大会增加内存占用和延迟。我一般根据带宽延迟积来估算缓冲区大小等于带宽乘以往返延迟。比如100Mbps带宽、10ms延迟乘积是125KB左右缓冲区设128KB就够。TCP_NODELAY选项也值得关注。默认情况下TCP会启用Nagle算法把小包合并成大包发送减少网络开销但增加延迟。对于实时性要求高的应用比如游戏或交互式工具应该关闭Nagleint nodelay 1; setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (char*)nodelay, sizeof(nodelay));这个选项没有绝对的好坏取决于你的业务是吞吐量优先还是延迟优先。文件传输关不关都行实时通信建议关掉。10. 调试工具与日志策略WSA系列API的调试离不开网络抓包工具。Wireshark能看完整的TCP交互过程包括三次握手、数据传输、四次挥手。我排查连接问题时习惯先抓包看是握手阶段就失败还是数据传输阶段出问题。日志方面建议在关键函数调用前后都打日志包括函数名、参数、返回值和错误码。但日志量要控制生产环境不能每个recv都打否则日志文件会爆炸。我的做法是正常流程只记录连接建立和关闭错误流程记录完整上下文再加一个可开关的详细日志模式用于排查。错误码转文字可以用FormatMessage函数char *errMsg NULL; FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, WSAGetLastError(), 0, (LPSTR)errMsg, 0, NULL);这样日志里显示的是可读的错误描述比纯数字好排查得多。记得用完调LocalFree释放。11. 一个完整的客户端示例把前面的知识点串起来写一个完整的TCP客户端int main() { WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), wsaData) ! 0) return -1; SOCKET sock WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED); if (sock INVALID_SOCKET) { WSACleanup(); return -1; } struct sockaddr_in serverAddr; memset(serverAddr, 0, sizeof(serverAddr)); serverAddr.sin_family AF_INET; serverAddr.sin_port htons(8080); serverAddr.sin_addr.s_addr inet_addr(127.0.0.1); if (connect(sock, (struct sockaddr*)serverAddr, sizeof(serverAddr)) SOCKET_ERROR) { printf(connect failed: %d\n, WSAGetLastError()); closesocket(sock); WSACleanup(); return -1; } const char *msg hello; int msgLen strlen(msg); int netLen htonl(msgLen); send(sock, (char*)netLen, 4, 0); send(sock, msg, msgLen, 0); char buf[1024]; int ret recv(sock, buf, sizeof(buf), 0); if (ret 0) { buf[ret] \0; printf(received: %s\n, buf); } shutdown(sock, SD_BOTH); closesocket(sock); WSACleanup(); return 0; }这个客户端先发4字节长度头再发消息体和服务端的长度前缀协议对应。实际项目中还要加上错误处理、超时控制、重连逻辑等但核心骨架就是这样。12. 后续可以深入的方向WSA系列API的内容远不止这些。如果要把网络编程做深下一步可以研究IOCP的完整实现包括AcceptEx批量接受连接、WSARecv提交重叠读、完成键管理连接上下文。再往上可以看Windows过滤平台和网络驱动不过那就偏离应用层开发了。另一个方向是安全相关的扩展比如用Schannel实现TLS加密通信WSA系列API提供了相应的扩展接口。还有QoS服务质量控制、多播和广播、原始套接字等专题每个都够写一篇长文。我个人在实际项目中的体会是WSA系列API的文档虽然齐全但很多细节只有真正写过、调过、踩过坑才能理解。比如错误码的触发条件、多线程下的竞态问题、不同Windows版本的细微差异这些在官方文档里往往一笔带过。所以建议你在学习时一定要动手写代码哪怕是最简单的echo服务端亲手跑一遍比看十篇文章都管用。

相关新闻

(论文速读)RT-DETRv4:借助视觉基础模型,无额外推理开销地提升实时目标检测

(论文速读)RT-DETRv4:借助视觉基础模型,无额外推理开销地提升实时目标检测

论文题目: RT-DETRv4: Painlessly Furthering Real-Time Object Detection with Vision Foundation Models(RT-DETRv4:利用视觉基础模型,无痛地进一步提升实时目标检测) 论文版本: arXiv:2510.25257v2&…

2026/10/18 5:31:41 阅读更多 →
现代C++进阶实战:移动语义、智能指针与零成本抽象

现代C++进阶实战:移动语义、智能指针与零成本抽象

从手写new/delete到完全依赖 RAII,从到处拷贝到熟练使用移动语义,这个过程几乎每个 C 开发者都会经历。这一部是《C 小程序编写系列》的收官之作,我不想再堆砌某个具体特性的语法细节,而是想把“现代 C 进阶实战——从灵活到高效”…

2026/10/18 5:31:41 阅读更多 →
用一套仓库替换 Harbor、npm 私服和 Nexus:AngusRepo vs Nexus vs Artifactory

用一套仓库替换 Harbor、npm 私服和 Nexus:AngusRepo vs Nexus vs Artifactory

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 5:30:41 阅读更多 →

最新新闻

Selenium八大定位器与显式等待:从原理到实战的稳定自动化方案

Selenium八大定位器与显式等待:从原理到实战的稳定自动化方案

做Web自动化这几年,前前后后看过不少脚本,也帮团队整理过很多次用例。问得最多、踩坑最深的往往不在框架选型上,而在最基础的两件事:元素定位和等待策略。什么八大定位器、显式等待,概念大家都会背,一上手就…

2026/10/18 6:18:49 阅读更多 →
开源教程被AI推荐背后:内容结构如何影响大模型引用

开源教程被AI推荐背后:内容结构如何影响大模型引用

1. 一个教程项目被大模型推荐,意味着什么先交代背景。我维护了一套面向零基础开发者的 AI 编程入门教程,纯开源、纯文档,没有花哨的官网,也没有做任何付费投放。前段时间有读者截图给我,说在某个国产 AI 助手里问“AI …

2026/10/18 6:18:49 阅读更多 →
配电网故障恢复重构与孤岛划分统一建模及Matlab实现

配电网故障恢复重构与孤岛划分统一建模及Matlab实现

搞配电网故障恢复的人大概都有同感:重构和孤岛划分这俩问题,分开弄各有各的成熟办法,一旦要放一起同时优化,就各种打架。前段时间我把这两块重新整理,塞进同一个优化模型,用Matlab重新实现了一个升级版本。…

2026/10/18 6:18:49 阅读更多 →
一维人工势场法Matlab完整实现与调参避坑指南

一维人工势场法Matlab完整实现与调参避坑指南

最近我在重新整理路径规划算法的基础实现,把人工势场法(APF,Artificial Potential Field)用Matlab从头到尾完整过了一遍。网上关于APF的资料一搜一大把,绝大多数一上来就是二维栅格图、彩色势场曲面和一堆箭头&#xf…

2026/10/18 6:18:49 阅读更多 →
Windows下VS2019编译OSQP求解器:从CMake到C/C++接入全指南

Windows下VS2019编译OSQP求解器:从CMake到C/C++接入全指南

做控制、做机器人、做运筹优化的人,早晚要跟二次规划(QP)打交道。OSQP(Operator Splitting QP)是这一卦里口碑不错的开源求解器——用ADMM迭代解稀疏二次规划,对大规模、结构稀疏的问题内存占用低、速度可观…

2026/10/18 6:18:49 阅读更多 →
智能家居全国交付中异地高并发管理带宽的系统架构与AI半自动化实现

智能家居全国交付中异地高并发管理带宽的系统架构与AI半自动化实现

一、背景/痛点分析 做全国交付,最难的不是没有订单,是订单来了管不住。一个运营经理盯十个项目可控,盯三十个开始漏项,盯五十个开始出错,盯一百个就崩。这不是能力问题,是管理带宽到极限。 异地高并发比本地…

2026/10/18 6:17:49 阅读更多 →

日新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 0:55:02 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 14:23:36 阅读更多 →