【问题标题】:Winsock non-blocking overlapped IO still blocksWinsock 非阻塞重叠 IO 仍然阻塞
【发布时间】:2014-09-28 04:46:43
【问题描述】:

我正在尝试编写一个简单的winsock 客户端,将数据发送到 使用 非阻塞 重叠 IO的winsock服务器。问题是WSASend 调用被阻塞。客户端代码如下所示

// Initialize Winsock
WSAStartup(MAKEWORD(2, 2), &wsaData);

// Create a SOCKET for connecting to server
ConnectSocket = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);

// Connect to server.
WSAConnect(ConnectSocket, ai_addr, ai_addrlen, 0, 0, 0, 0);

// Make the socket non-blocking
u_long iMode = 1;
ioctlsocket(ConnectSocket, FIONBIO, &iMode);

// Send the data
WSAOVERLAPPED SendOverlapped{};
SendOverlapped.hEvent = WSACreateEvent();
WSASend(ConnectSocket, &DataBuf, 1, &SendBytes, 0, &SendOverlapped, 0);

我已经通过ioctlsocket 函数使套接字变为非阻塞,并且我已经为WSASocket 函数提供了WSA_FLAG_OVERLAPPED 标志。我还为WSASend 函数提供了lpOverlapped 参数。但是,对WSASend 的调用仍处于阻塞状态。我在这里遗漏了什么吗?

请原谅上面代码中缺少错误检查,这只是为了这个问题的目的而保持简单。

澄清:在我看来调用被阻塞的原因是——我在一个循环中调用了WSASend 函数 2560 次,每次循环发送 4MB。将所有数据传输到服务器后,循环在 16 秒内完成。如果它是非阻塞的,我会期望循环更快地完成。正如预期的那样,WSASend 函数确实返回 ERROR_IO_PENDING

这是完整的客户端代码

// Client.cpp
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif

#include <stdio.h>
#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <iostream>
#include <ctime>

#pragma comment (lib, "Ws2_32.lib")
#pragma comment (lib, "Mswsock.lib")
#pragma comment (lib, "AdvApi32.lib")

#define DEFAULT_BUFLEN 4 * 1024 * 1024
#define DEFAULT_PORT "27015"

int __cdecl main(int argc, char **argv)
{
    // Initialize Winsock
    WSADATA wsaData;
    int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (iResult != 0) {
        printf("WSAStartup failed with error: %d\n", iResult);
        return 1;
    }

    // Resolve the server address and port
    addrinfo *result = NULL;
    addrinfo hints{};
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;
    iResult = getaddrinfo("127.0.0.1", DEFAULT_PORT, &hints, &result);
    if (iResult != 0) {
        printf("getaddrinfo failed with error: %d\n", iResult);
        WSACleanup();
        return 1;
    }

    // Create a SOCKET for connecting to server
    SOCKET ConnectSocket = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
    if (ConnectSocket == INVALID_SOCKET) {
        printf("socket failed with error: %ld\n", WSAGetLastError());
        WSACleanup();
        return 1;
    }

    // Connect to server.
    iResult = WSAConnect(ConnectSocket, result->ai_addr, result->ai_addrlen, 0, 0, 0, 0);
    freeaddrinfo(result);
    if (ConnectSocket == INVALID_SOCKET) {
        printf("Unable to connect to server!\n");
        WSACleanup();
        return 1;
    }

    // Make the socket non-blocking
    u_long iMode = 1;
    iResult = ioctlsocket(ConnectSocket, FIONBIO, &iMode);
    if (iResult != NO_ERROR) {
        printf("ioctlsocket failed with error: %ld\n", iResult);
        WSACleanup();
        return 1;
    }

    // Prepare the buffer
    char *sendbuf = new char[DEFAULT_BUFLEN];
    for (int i = 0; i < DEFAULT_BUFLEN; ++i)
        sendbuf[i] = 'a';
    WSABUF DataBuf;
    DWORD SendBytes = 0;
    DataBuf.buf = sendbuf;
    DataBuf.len = DEFAULT_BUFLEN;

    // Send the buffer in a loop
    int loopCount = 2560;
    WSAOVERLAPPED* SendOverlapped = (WSAOVERLAPPED*)calloc(loopCount, sizeof(WSAOVERLAPPED));
    clock_t start = clock();
    for (int i = 0; i < loopCount; ++i)
    {
        SendOverlapped[i].hEvent = WSACreateEvent();
        iResult = WSASend(ConnectSocket, &DataBuf, 1, &SendBytes, 0, SendOverlapped + i, 0);
        if (iResult == SOCKET_ERROR)
        {
            if (ERROR_IO_PENDING == WSAGetLastError())
            {
                continue;
            }           

            printf("send failed with error: %d\n", WSAGetLastError());
            closesocket(ConnectSocket);
            WSACleanup();
            return 1;
        }
    }

    std::cout << "initiating send data took " << clock() - start << " ms" << std::endl;

    // Wait for all the events to be signalled
    for (int i = 0; i < loopCount; ++i)
    {
        iResult = WSAWaitForMultipleEvents(1, &SendOverlapped[i].hEvent, TRUE, INFINITE, TRUE);
        if (iResult == WSA_WAIT_FAILED) {
            printf("WSAWaitForMultipleEvents failed with error: %d\n", WSAGetLastError());
            closesocket(ConnectSocket);
            WSACleanup();
            return 1;
        }

        DWORD Flags = 0;
        BOOL result = WSAGetOverlappedResult(ConnectSocket, SendOverlapped + i, &SendBytes, FALSE, &Flags);
        if (result == FALSE) {
            printf("WSASend failed with error: %d\n", WSAGetLastError());
            break;
        }
    }

    std::cout << "actual send data took " << clock() - start << " ms";

    // shutdown the connection since no more data will be sent
    iResult = shutdown(ConnectSocket, SD_SEND);
    if (iResult == SOCKET_ERROR) {
        printf("shutdown failed with error: %d\n", WSAGetLastError());
        closesocket(ConnectSocket);
        WSACleanup();
        return 1;
    }

    // cleanup
    closesocket(ConnectSocket);
    WSACleanup();
    ////free(SendOverlapped);
    return 0;
}

这是服务器端代码

// Server.cpp
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif

#include <stdio.h>
#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <iostream>

#pragma comment (lib, "Ws2_32.lib")

#define DEFAULT_BUFLEN 4 * 1024 * 1024
#define DEFAULT_PORT "27015"

int __cdecl main(void)
{
    // Initialize Winsock
    WSADATA wsaData;
    int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (iResult != 0) {
        printf("WSAStartup failed with error: %d\n", iResult);
        return 1;
    }

    // Resolve the server address and port
    addrinfo hints{};
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;
    hints.ai_flags = AI_PASSIVE;
    addrinfo *result = NULL;
    iResult = getaddrinfo(NULL, DEFAULT_PORT, &hints, &result);
    if (iResult != 0) {
        printf("getaddrinfo failed with error: %d\n", iResult);
        WSACleanup();
        return 1;
    }

    // Create a SOCKET for connecting to server
    SOCKET ListenSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
    if (ListenSocket == INVALID_SOCKET) {
        printf("socket failed with error: %ld\n", WSAGetLastError());
        freeaddrinfo(result);
        WSACleanup();
        return 1;
    }

    // Setup the TCP listening socket
    iResult = bind(ListenSocket, result->ai_addr, (int)result->ai_addrlen);
    if (iResult == SOCKET_ERROR) {
        printf("bind failed with error: %d\n", WSAGetLastError());
        freeaddrinfo(result);
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }

    freeaddrinfo(result);

    iResult = listen(ListenSocket, SOMAXCONN);
    if (iResult == SOCKET_ERROR) {
        printf("listen failed with error: %d\n", WSAGetLastError());
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }

    // Accept a client socket
    SOCKET ClientSocket = accept(ListenSocket, NULL, NULL);
    if (ClientSocket == INVALID_SOCKET) {
        printf("accept failed with error: %d\n", WSAGetLastError());
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }

    // No longer need server socket
    closesocket(ListenSocket);

    // Receive until the peer shuts down the connection
    char* recvbuf = new char[DEFAULT_BUFLEN];
    do {
        iResult = recv(ClientSocket, recvbuf, DEFAULT_BUFLEN, 0);
        if (iResult > 0) {
            printf("Bytes received: %d\n", iResult);
        }
        else if (iResult == 0)
            printf("Connection closing...\n");
        else  {
            printf("recv failed with error: %d\n", WSAGetLastError());
            closesocket(ClientSocket);
            WSACleanup();
            return 1;
        }

    } while (iResult > 0);

    // shutdown the connection since we're done
    iResult = shutdown(ClientSocket, SD_SEND);
    if (iResult == SOCKET_ERROR) {
        printf("shutdown failed with error: %d\n", WSAGetLastError());
        closesocket(ClientSocket);
        WSACleanup();
        return 1;
    }

    // cleanup
    closesocket(ClientSocket);
    WSACleanup();

    return 0;
}

【问题讨论】:

  • “呼叫被阻塞”到底是什么意思?即使您指定应使用重叠 IO,API 也可能能够在返回之前完成调用。正如文档所说,“如果重叠操作立即完成,WSASend 将返回零值,并且 lpNumberOfBytesSent 参数将使用发送的字节数进行更新”。
  • 此代码中的错误检查绝对为零。你不能写这样的代码。
  • @EJP,我知道没有错误检查,那只是为了让这个问题保持整洁。
  • 单次非阻塞调用表示:WSASend立即返回,实际I/O操作完成需要等待重叠事件。仅此而已。您对异步调用的期望过高。
  • @EJP,请在否决问题之前考虑一下。如果您需要更多信息,您可以随时询问。失去一个人所拥有的极少数观点并且不鼓励在 * 上提问是很痛苦的。

标签: sockets network-programming winsock2


【解决方案1】:

WSASend 调用被阻塞,因为它完成得太快了。有人会认为 4MB 的传输足够大,可以调用异步处理,但事实并非如此。当我在服务器端插入 1 毫秒的睡眠时,我可以看到客户端异步处理,并且循环在数据传输之前就结束了。

【讨论】:

  • WSASend() 调用根本没有阻塞。你误解了症状。它正在返回。 I/O 异步发生。您似乎不了解自己的代码。请参阅上面@AlexFarber 的评论。
  • 如果一个 for 循环运行 2560 次需要 16 秒才能完成,对我来说这就是阻塞。
  • 代码在我的机器上的表现比你描述的要糟糕得多。客户端永远不会完成,第二次运行后我什至无法终止客户端进程——我最终不得不重新启动我的机器。我怀疑我的反恶意软件防火墙可能会导致一些问题。有没有其他人觉得反恶意软件造成的麻烦几乎与它应该阻止的恶意软件一样多?
  • @Michael,您可以尝试将循环计数减少到 256 而不是 2560。我在另一台计算机上遇到了与您描述的相同的问题。
  • @EJP,您可以进行测试以确认呼叫被阻塞。在评论别人对代码的理解之前,请先做好功课。