如何使用WinSock实现从服务器异步读取数据?
嘿,看起来你已经搞定了WinSock的基础TCP连接,现在要实现异步读取数据对吧?我给你梳理几个常用的方案,你可以根据自己的程序场景(GUI/控制台/高性能服务)来选择:
方案1:WSAAsyncSelect(基于窗口消息)
这个方法适合带GUI的程序,它会把Socket的事件通知以窗口消息的形式发送给你指定的窗口,非常贴合Windows的消息驱动模型。
实现步骤
- 准备一个窗口(控制台程序也可以创建隐藏窗口)
- 调用
WSAAsyncSelect注册你关心的FD_READ(可读事件)和FD_CLOSE(连接关闭事件) - 在窗口过程中处理自定义的Socket消息
- 收到
FD_READ时调用recv读取数据,读完后记得重新注册事件
代码示例
#include <winsock2.h> #include <Windows.h> #define PORT 5051 #define WM_SOCKET (WM_USER + 1) // 自定义Socket消息 LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); int main() { WSADATA WsaData; WSAStartup(MAKEWORD(2, 2), &WsaData); SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); SOCKADDR_IN sin; ZeroMemory(&sin, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_port = htons(PORT); sin.sin_addr.s_addr = inet_addr("127.0.0.1"); // 替换为你的服务器IP // 完成连接 connect(s, (SOCKADDR*)&sin, sizeof(sin)); // 创建隐藏窗口用于接收Socket消息 WNDCLASS wc = {0}; wc.lpfnWndProc = WindowProc; wc.hInstance = GetModuleHandle(NULL); wc.lpszClassName = L"SocketAsyncWindow"; RegisterClass(&wc); HWND hwnd = CreateWindow(wc.lpszClassName, L"", 0, 0, 0, 0, 0, HWND_MESSAGE, NULL, NULL, NULL); // 注册可读和关闭事件 WSAAsyncSelect(s, hwnd, WM_SOCKET, FD_READ | FD_CLOSE); // 消息循环(GUI程序已有此循环,控制台程序需要加上) MSG msg; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } // 资源清理 closesocket(s); WSACleanup(); return 0; } LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { if (uMsg == WM_SOCKET) { SOCKET s = (SOCKET)wParam; int eventType = WSAGETSELECTEVENT(lParam); int errorCode = WSAGETSELECTERROR(lParam); if (errorCode != 0) { // 处理错误,比如连接异常断开 closesocket(s); return 0; } switch (eventType) { case FD_READ: { char buffer[1024]; int bytesRead = recv(s, buffer, sizeof(buffer)-1, 0); if (bytesRead > 0) { buffer[bytesRead] = '\0'; printf("收到数据:%s\n", buffer); // 必须重新注册FD_READ,否则下次不会触发 WSAAsyncSelect(s, hwnd, WM_SOCKET, FD_READ | FD_CLOSE); } else if (bytesRead == 0) { printf("服务器已关闭连接\n"); closesocket(s); } break; } case FD_CLOSE: { printf("连接已关闭\n"); closesocket(s); break; } } return 0; } return DefWindowProc(hwnd, uMsg, wParam, lParam); }
方案2:WSAEventSelect(基于事件对象)
这个方案适合控制台程序或者不想依赖窗口的场景,它用Windows事件对象来监听Socket状态,通过等待事件触发来处理读取操作。
实现步骤
- 创建一个WSA事件对象
- 调用
WSAEventSelect将Socket与事件、目标事件类型绑定 - 用
WSAWaitForMultipleEvents等待事件触发 - 事件触发后,调用
WSAEnumNetworkEvents获取具体事件,再处理读取
代码示例
#include <winsock2.h> #include <Windows.h> #include <stdio.h> #define PORT 5051 int main() { WSADATA WsaData; WSAStartup(MAKEWORD(2, 2), &WsaData); SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); SOCKADDR_IN sin; ZeroMemory(&sin, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_port = htons(PORT); sin.sin_addr.s_addr = inet_addr("127.0.0.1"); connect(s, (SOCKADDR*)&sin, sizeof(sin)); // 创建事件对象 WSAEVENT socketEvent = WSACreateEvent(); // 绑定可读和关闭事件 WSAEventSelect(s, socketEvent, FD_READ | FD_CLOSE); while (1) { // 等待事件触发,INFINITE表示一直阻塞等待 DWORD waitResult = WSAWaitForMultipleEvents(1, &socketEvent, FALSE, INFINITE, FALSE); if (waitResult == WSA_WAIT_EVENT_0) { WSANETWORKEVENTS networkEvents; // 获取发生的具体事件 WSAEnumNetworkEvents(s, socketEvent, &networkEvents); // 处理可读事件 if (networkEvents.lNetworkEvents & FD_READ && networkEvents.iErrorCode[FD_READ_BIT] == 0) { char buffer[1024]; int bytesRead = recv(s, buffer, sizeof(buffer)-1, 0); if (bytesRead > 0) { buffer[bytesRead] = '\0'; printf("收到数据:%s\n", buffer); } else if (bytesRead == 0) { printf("服务器断开连接\n"); break; } } // 处理关闭事件 if (networkEvents.lNetworkEvents & FD_CLOSE) { printf("连接已关闭\n"); break; } // 重置事件,否则下次无法触发 WSAResetEvent(socketEvent); } } // 资源清理 WSACloseEvent(socketEvent); closesocket(s); WSACleanup(); return 0; }
方案3:重叠IO(Overlapped I/O)+ 完成例程
这个是高性能场景的首选,比如高并发服务器,它支持批量异步IO操作,效率远高于前两种方案。下面是用完成例程的简单实现:
实现步骤
- 定义包含
WSAOVERLAPPED的自定义结构体,保存Socket和缓冲区信息 - 调用
WSARecv发起异步读取,指定完成例程 - 完成例程中处理读取到的数据,并重新发起异步读取以保持监听
代码示例
#include <winsock2.h> #include <Windows.h> #include <stdio.h> #define PORT 5051 // 完成例程回调函数 void CALLBACK ReadCompletionRoutine(DWORD dwError, DWORD dwBytesTransferred, LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags); // 自定义结构体:保存Socket、缓冲区和重叠结构 typedef struct { WSAOVERLAPPED overlapped; SOCKET socket; char buffer[1024]; } PER_IO_DATA; int main() { WSADATA WsaData; WSAStartup(MAKEWORD(2, 2), &WsaData); SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); SOCKADDR_IN sin; ZeroMemory(&sin, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_port = htons(PORT); sin.sin_addr.s_addr = inet_addr("127.0.0.1"); connect(s, (SOCKADDR*)&sin, sizeof(sin)); // 分配IO数据结构 PER_IO_DATA* pIoData = (PER_IO_DATA*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(PER_IO_DATA)); pIoData->socket = s; ZeroMemory(&pIoData->overlapped, sizeof(WSAOVERLAPPED)); // 发起异步读取 WSABUF buf; buf.buf = pIoData->buffer; buf.len = sizeof(pIoData->buffer)-1; DWORD bytesRead = 0; DWORD flags = 0; WSARecv(s, &buf, 1, &bytesRead, &flags, &pIoData->overlapped, ReadCompletionRoutine); // 消息循环(完成例程需要消息循环或WSAWaitForMultipleEvents驱动) MSG msg; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } // 资源清理 HeapFree(GetProcessHeap(), 0, pIoData); closesocket(s); WSACleanup(); return 0; } void CALLBACK ReadCompletionRoutine(DWORD dwError, DWORD dwBytesTransferred, LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags) { PER_IO_DATA* pIoData = (PER_IO_DATA*)lpOverlapped; SOCKET s = pIoData->socket; if (dwError != 0 || dwBytesTransferred == 0) { // 错误或连接关闭,清理资源 HeapFree(GetProcessHeap(), 0, pIoData); closesocket(s); return; } // 处理读取到的数据 pIoData->buffer[dwBytesTransferred] = '\0'; printf("收到数据:%s\n", pIoData->buffer); // 重新发起异步读取,保持持续监听 WSABUF buf; buf.buf = pIoData->buffer; buf.len = sizeof(pIoData->buffer)-1; DWORD bytesRead = 0; DWORD flags = 0; ZeroMemory(&pIoData->overlapped, sizeof(WSAOVERLAPPED)); WSARecv(s, &buf, 1, &bytesRead, &flags, &pIoData->overlapped, ReadCompletionRoutine); }
额外注意事项
- 所有异步操作都要记得处理错误,比如
recv返回SOCKET_ERROR时,用WSAGetLastError()判断是WSAEWOULDBLOCK(异步未完成,正常情况)还是其他致命错误 - 资源清理要彻底:Socket关闭、事件释放、内存回收一个都不能少
- 多线程场景下要注意线程安全,比如共享数据的同步问题
内容的提问来源于stack exchange,提问作者KaraUL




