You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何使用WinSock实现从服务器异步读取数据?

嘿,看起来你已经搞定了WinSock的基础TCP连接,现在要实现异步读取数据对吧?我给你梳理几个常用的方案,你可以根据自己的程序场景(GUI/控制台/高性能服务)来选择:


方案1:WSAAsyncSelect(基于窗口消息)

这个方法适合带GUI的程序,它会把Socket的事件通知以窗口消息的形式发送给你指定的窗口,非常贴合Windows的消息驱动模型。

实现步骤

  1. 准备一个窗口(控制台程序也可以创建隐藏窗口)
  2. 调用WSAAsyncSelect注册你关心的FD_READ(可读事件)和FD_CLOSE(连接关闭事件)
  3. 在窗口过程中处理自定义的Socket消息
  4. 收到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状态,通过等待事件触发来处理读取操作。

实现步骤

  1. 创建一个WSA事件对象
  2. 调用WSAEventSelect将Socket与事件、目标事件类型绑定
  3. WSAWaitForMultipleEvents等待事件触发
  4. 事件触发后,调用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操作,效率远高于前两种方案。下面是用完成例程的简单实现:

实现步骤

  1. 定义包含WSAOVERLAPPED的自定义结构体,保存Socket和缓冲区信息
  2. 调用WSARecv发起异步读取,指定完成例程
  3. 完成例程中处理读取到的数据,并重新发起异步读取以保持监听

代码示例

#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

火山引擎 最新活动