2

我在非阻塞模式下使用套接字,有时 WSAConnect 函数返回 WSAEINVAL 错误。我调查了一个问题并发现,如果 WSAConnect 函数调用之间没有暂停(或者非常小),就会发生这种情况。有谁知道如何避免这种情况?您可以在下面找到重现问题的源代码。如果我将睡眠功能中的参数值增加到 50 或更大 - 问题就会消失。
PS 这个问题只在 Windows XP 上重现,在 Win7 上运行良好。

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

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

    static int getError(SOCKET sock)
    {
        DWORD error = WSAGetLastError();
        return error;
    }

    void main()
    {
        SOCKET sock;
        WSADATA wsaData;
        if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
            fprintf(stderr, "Socket Initialization Error. Program aborted\n");
            return;
        }

        for (int i = 0; i < 1000; ++i) {
            struct addrinfo hints;
            struct addrinfo *res = NULL;
            memset(&hints, 0, sizeof(hints));
            hints.ai_flags = AI_PASSIVE;
            hints.ai_socktype = SOCK_STREAM;
            hints.ai_family = AF_INET;
            hints.ai_protocol = IPPROTO_TCP;

            if (0 != getaddrinfo("172.20.1.59", "8091", &hints, &res)) {
                fprintf(stderr, "GetAddrInfo Error. Program aborted\n");
                closesocket(sock);
                WSACleanup();
                return;
            }

            struct addrinfo *ptr = 0;
            for (ptr=res; ptr != NULL ;ptr=ptr->ai_next) {
                sock = WSASocket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol, NULL, 0, NULL);    // 

                if (sock == INVALID_SOCKET) 
                    int err = getError(sock);
                else {
                    u_long noblock = 1;
                    if (ioctlsocket(sock, FIONBIO, &noblock) == SOCKET_ERROR) {
                        int err = getError(sock);
                        closesocket(sock);
                        sock = INVALID_SOCKET;
                    }   
                    break;
                }
            }
            int ret;

            do {
                ret = WSAConnect(sock, ptr->ai_addr, (int)ptr->ai_addrlen, NULL, NULL, NULL, NULL);

                if (ret == SOCKET_ERROR) {
                    int error = getError(sock);

                    if (error == WSAEWOULDBLOCK) {
                        Sleep(5);
                        continue;
                    }
                    else if (error == WSAEISCONN) {
                        fprintf(stderr, "+");
                        closesocket(sock);
                        sock = SOCKET_ERROR;
                        break;
                    }
                    else if (error == 10037) {
                        fprintf(stderr, "-");
                        closesocket(sock);
                        sock = SOCKET_ERROR;
                        break;
                    }
                    else {
                        fprintf(stderr, "Connect Error. [%d]\n", error);
                        closesocket(sock);
                        sock = SOCKET_ERROR;
                        break;
                    }
                }
                else {
                    int one = 1;
                    setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (char*)&one, sizeof(one));
                    fprintf(stderr, "OK\n");
                    break;
                }
            }
            while (1);
        }

        std::cout<<"end";
        char ch;
        std::cin >> ch;
    }
4

1 回答 1

4

这里有一大堆错误以及可疑的设计和编码决策。我将不得不将它们分成两组:

彻底的错误

我希望如果您修复本节中的所有项目,您的症状就会消失,但我不想推测哪个是关键修复:

  • connect()在单个套接字上调用循环是完全错误的。

    如果您的意思是建立一个连接,将其断开,然后重新建立 1000 次,则需要closesocket()在每个循环结束时调用,然后socket()再次调用以获取新的套接字。您不能继续重新连接同一个套接字。把它想象成一个电源插头:如果你想插入两次,你必须在两次closesocket()之间拔掉( )。

    相反,如果您的意思是建立 1000 个同时连接,则需要socket()在每次迭代时分配一个新的套接字connect(),然后再返回以获取另一个套接字。它与前一种情况基本相同,除了没有closesocket()调用。

    请注意,由于 XP 是 Windows 的客户端版本,因此它并未针对处理数千个同时套接字进行优化。

  • connect()再次调用不是正确的响应WSAEWOULDBLOCK

    if (error == WSAEWOULDBLOCK) {
        Sleep(5);
        continue; /// WRONG!
    }
    

    continue代码有效地提交了与上述相同的错误,但更糟糕的是,如果您只修复先前的错误并保留此错误,那么这种用法将使您的代码开始泄漏套接字。

    WSAEWOULDBLOCK不是错误。在非阻塞套接字上之后,这意味着connect()连接没有立即建立。堆栈会在它发生时通知您的程序。

    select()您可以通过调用、WSAEventSelect()或之一来获得该通知WSAAsyncSelect()。如果使用select(),则在建立连接时,套接字将被标记为可写。对于其他两个,您将FD_CONNECT在建立连接时收到一个事件。

    调用这三个 API 中的哪一个取决于您首先需要非阻塞套接字的原因,以及程序的其余部分会是什么样子。到目前为止我所看到的根本不需要非阻塞套接字,但我想你有一些未来的计划可以为你的决定提供信息。我写了一篇文章,我应该使用哪种 I/O 策略Winsock 程序员常见问题解答的一部分),它将帮助您决定使用哪些选项;相反,它可能会引导您完全选择另一个选项。

  • 您不应该在同一个套接字上使用AI_PASSIVE和。connect()您使用AI_PASSIVEwithgetaddrinfo()告诉堆栈您打算使用此套接字来接受传入连接。然后你去使用那个套接字来建立一个传出连接。

    你在这里基本上对堆栈撒谎了。当您对计算机撒谎时,计算机会找到报复的方法。

  • Sleep()永远不是解决 Winsock 问题的正确方法。您的程序可以看到堆栈中的内置延迟,例如TIME_WAITNagle算法,但Sleep()也不是处理这些的正确方法。

有问题的编码/设计决策

本节适用于我不希望让您的症状消失的事情,但无论如何您都应该考虑修复它们:

  • getaddrinfo()与较旧的、更简单的功能相比,使用的主要原因inet_addr()是如果您必须支持 IPv6。这与您支持 XP 的愿望相冲突,因为 XP 的 IPv6 堆栈在 XP 是 Windows 的当前版本期间几乎没有其 IPv4 堆栈那样经过严格测试。我希望 XP 的 IPv6 堆栈仍然存在错误,即使您已经安装了所有补丁。

    如果您真的不需要 IPv6 支持,那么使用旧方法可能会使您的症状消失。您最终可能需要一个仅适用于 XP 的 IPv4 版本。

  • 这段代码:

    for (int i = 0; i < 1000; ++i) {
        // ...
        if (0 != getaddrinfo("172.20.1.59", "8091", &hints, &res)) {
    

    ...效率低下。没有理由需要res在每个循环上继续重新初始化。

    即使我没有看到某些原因,您也不会调用freeaddrinfo().res

    您应该在进入循环之前初始化此数据结构一次,然后在每次迭代中重用它。

  • else if (error == 10037) {

    你为什么不在这里使用WSAEALREADY

  • 你不需要在WSAConnect()这里使用。您正在使用 Winsock 与 BSD 套接字共享的 3 参数子集。你不妨在connect()这里改用。

    让你的代码比它必须的更复杂是没有意义的。

  • 你为什么不switch为此使用声明?

    if (error == WSAEWOULDBLOCK) {
        // ...
    }
    else if (error == WSAEISCONN) {
        // ...
    }
    // etc.
    
  • 您不应该禁用 Nagle 算法:

    setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, ...);
    
于 2013-03-14T23:19:34.960 回答