【问题标题】:Socket Programming: Error on listen()套接字编程:listen() 上的错误
【发布时间】:2017-08-04 15:38:05
【问题描述】:

我正在开发应用程序的服务器部分,但遇到了一个似乎无法解决的问题。服务器初始化函数是ConnectionManager类的一部分,如下:

int ConnectionManager::init_server() {

    // Log
    OutputDebugString(L"> Initializing server...\n");

    // Initialize winsock
    WSAData wsa;
    int code = WSAStartup(MAKEWORD(2, 2), &wsa);
    if (code != 0) {
        // Error initializing winsock
        OutputDebugString(L"> Log: WSAStartup()\n");
        output_error(code);
        return -1;
    }

    // Get server information
    struct addrinfo hints, *serverinfo, *ptr;
    SOCKET sockfd = INVALID_SOCKET;
    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_protocol = SOCK_STREAM;
    hints.ai_flags = AI_PASSIVE;
    hints.ai_family = AF_UNSPEC;

    if (getaddrinfo(NULL, PORT, &hints, &serverinfo) != 0) {
        // Error when getting server address information
        OutputDebugString(L"> Log: getaddrinfo()\n");
        output_error(WSAGetLastError()); // Call Cleanup?
        return -1;
    }

    for (ptr = serverinfo; ptr != NULL; ptr = ptr->ai_next) {
        // Create socket
        if ((sockfd = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol)) == INVALID_SOCKET) {
            // Error when creating a socket
            OutputDebugString(L"> Log: socket()\n");
            output_error(WSAGetLastError()); // Call Cleanup?
            continue;
        }

        // Set options
        const char enable = 1;
        if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)) == SOCKET_ERROR) {
            // Error when setting options
            OutputDebugString(L"> log: setsockopt()\n");
            output_error(WSAGetLastError()); // call cleanup?
            if (closesocket(sockfd) != 0) {
                output_error(WSAGetLastError());
            }
            return -1;
        }

        // Bind socket
        if (bind(sockfd, ptr->ai_addr, ptr->ai_addrlen) == SOCKET_ERROR) {
            // Error on binding
            OutputDebugString(L"> Log: bind()\n");
            output_error(WSAGetLastError()); // Call Cleanup?
            if (closesocket(sockfd) != 0) {
                output_error(WSAGetLastError());
            }
            continue;
        }    

        break;
    }
    freeaddrinfo(serverinfo);
    if (ptr == NULL) {
        OutputDebugString(L"Error: Failed to launch server.\n");
        return -1;
    }
    // Listen
    if (listen(sockfd, BACKLOG) == SOCKET_ERROR) {
        OutputDebugString(L"> Log: listen()\n");
        output_error(WSAGetLastError()); // Call Cleanup?;
        return -1;
    }
    // Accept
    struct sockaddr_storage clientinfo;
    int size = sizeof(struct sockaddr_storage);
    m_exchfd = accept(sockfd, (struct sockaddr *)&clientinfo, &size);
    if (m_exchfd = INVALID_SOCKET) {
        // Error when accepting
        OutputDebugString(L"> Log: accept()\n");
        output_error(WSAGetLastError()); // Call Cleanup?
        if (closesocket(sockfd) != 0) {
            output_error(WSAGetLastError());
        }
        return -1;
    }
    m_isConnected = true;
    return 0;
}

output_error 函数使用FormatMessage() 函数简单地打印与错误对应的消息。但是,我得到以下输出:

> Log: listen()
> ERROR: The attempted operation is not supported for the type of object referenced.

因此,错误应该是由对 listen() 的调用引起的,这令人困惑。谁能解释一下问题的原因是什么?我敢打赌它应该很容易修复,但我似乎没有看到它。

【问题讨论】:

    标签: visual-c++ winsock


    【解决方案1】:

    问题的根源是在调用getaddrinfo() 时,您错误地填写了hints 结构。

    您将SOCK_STREAM 分配给ai_protocol 字段,并将ai_socktype 字段设置为0。SOCK_STREAM 定义为1,与IPPROTO_ICMP 的值相同,通常与SOCK_DGRAM 套接字。因此,getaddrinfo() 可能会返回 addrinfo 条目,其 ai_socktype 字段设置为 SOCK_DGRAM。您不能在数据报套接字上使用 listen(),因此您会看到 WSAEOPNOTSUPP 错误:

    如果没有发生错误,listen 返回零。否则返回值SOCKET_ERROR,调用WSAGetLastError可以得到具体的错误码。

    ...

    WSAEOPNOTSUPP
    引用的套接字不是支持listen 操作的类型。

    您需要将SOCK_STREAM 分配给hints.ai_socktype 字段,并将hints.ai_protocol 字段设置为0 或IPPROTO_TCP(最好是后者)。

    另外,getaddrinfo() 会返回错误代码,就像 WSAStartup() 一样。不要使用WSAGetLastError() 来获取其错误代码。

    除此之外,我发现您的代码还有许多其他问题。

    • SO_REUSEADDR 需要 BOOL(4 字节整数),而不是 char(1 字节)。您正在传递一个指向单个char 的指针,但告诉setsockopt() 您正在传递一个指向int 的指针。 setsockopt() 最终会尝试从您不拥有的堆栈内存中读取值。

    • 当您的循环调用closesocket() 时,您也应该将sockfd 重置为INVALID_SOCKET,然后在循环之后您应该检查该条件而不是检查ptr 是否为NULL。

    • 您应该在循环内而不是在循环之后调用listen()。仅仅因为你bind()一个套接字成功并不能保证你可以打开它分配的监听端口。您应该继续循环,直到您实际成功打开侦听端口。您还可以考虑在循环之后添加额外的日志消息,以了解哪个本地 IP/端口对实际正在侦听,这样您就可以知道哪些客户端可以 connect() 到。

    • 调用 WSAGetLastError() 时,始终在 Winsock 调用失败后立即调用它。如果您事先调用其他任何内容,则可能会重置错误代码,因为 WSAGetLastError() 只是 GetLastError() 的别名,许多 API 都使用它。

    • 调用accept() 时,在检查m_exchfd 是否等于INVALID_SOCKET 时,您使用的是= 赋值运算符而不是== 比较运算符。即使在你修复它之后,如果accept() 成功,你也会泄漏sockfd,因为你忘记了它并且不要调用closesocket()。如果您希望只有 1 个客户端连接,请在接受客户端后关闭侦听套接字。否则,将侦听套接字存储在您的类中,并在关闭接受的客户端套接字后将其关闭。

    说了这么多,试试这样的:

    void OutputWinsockError(LPCWSTR funcName, int errCode)
    {
        std::wostringstream msg;
        msg << L"> Log: " << funcName << L"()\n";
        OutputDebugStringW(msg.str().c_str());
        output_error(errCode);
    }
    
    void OutputWinsockError(LPCWSTR funcName)
    {
        OutputWinsockError(funcName, WSAGetLastError());
    }
    
    int ConnectionManager::init_server() {
    
        // Log
        OutputDebugString(L"> Initializing server...\n");
    
        // Initialize winsock
        WSAData wsa;
        int err = WSAStartup(MAKEWORD(2, 2), &wsa);
        if (err != 0) {
            // Error initializing winsock
            OutputWinsockError(L"WSAStartup", err);
            return -1;
        }
    
        // Get server information
        struct addrinfo hints, *serverinfo, *ptr;
        SOCKET sockfd = INVALID_SOCKET;
    
        memset(&hints, 0, sizeof(hints));
        hints.ai_flags = AI_PASSIVE;
        hints.ai_family = AF_UNSPEC;
        hints.ai_socktype = SOCK_STREAM;
        hints.ai_protocol = IPPROTO_TCP;
    
        err = getaddrinfo(NULL, PORT, &hints, &serverinfo);
        if (err != 0) {
            // Error when getting server address information
            OutputWinsockError(L"getaddrinfo", err);
            // Call Cleanup?
            return -1;
        }
    
        for (ptr = serverinfo; ptr != NULL; ptr = ptr->ai_next) {
            // Create socket
            sockfd = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol);
            if (sockfd == INVALID_SOCKET) {
                // Error when creating a socket
                OutputWinsockError(L"socket");
                continue;
            }
    
            // Set options
            const BOOL enable = TRUE;
            if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (char*)&enable, sizeof(BOOL)) == SOCKET_ERROR) {
                // Error when setting options
                OutputWinsockError(L"setsockopt");
                if (closesocket(sockfd) == SOCKET_ERROR) {
                    // Error when closing socket
                    OutputWinsockError(L"closesocket");
                }
                sockfd = INVALID_SOCKET;
                continue;
            }
    
            // Bind socket
            if (bind(sockfd, ptr->ai_addr, ptr->ai_addrlen) == SOCKET_ERROR) {
                // Error on binding
                OutputWinsockError(L"bind");
                if (closesocket(sockfd) == SOCKET_ERROR) {
                    // Error when closing socket
                    OutputWinsockError(L"closesocket");
                }
                sockfd = INVALID_SOCKET;
                continue;
            }    
    
            // Listen on port
            if (listen(sockfd, BACKLOG) == SOCKET_ERROR) {
                // Error on listening
                OutputWinsockError(L"listen");
                if (closesocket(sockfd) == SOCKET_ERROR) {
                    // Error when closing socket
                    OutputWinsockError(L"closesocket");
                }
                sockfd = INVALID_SOCKET;
                continue;
            }
    
            break;
        }
    
        freeaddrinfo(serverinfo);
    
        if (sockfd == INVALID_SOCKET) {
            OutputDebugString(L"Error: Failed to launch server.\n");
            // Call Cleanup?
            return -1;
        }
    
        // Accept
        struct sockaddr_storage clientinfo;
        int size = sizeof(clientinfo);
    
        m_exchfd = accept(sockfd, (struct sockaddr *)&clientinfo, &size);
        if (m_exchfd == INVALID_SOCKET) {
            // Error when accepting
            OutputWinsockError(L"accept");
            if (closesocket(sockfd) == SOCKET_ERROR) {
                OutputWinsockError(L"closesocket");
            }
            // Call Cleanup?
            return -1;
        }
    
        m_isConnected = true;
    
        // is not storing sockfd, close it
    
        // m_listenfd = sockfd;
        if (closesocket(sockfd) == SOCKET_ERROR) {
            OutputWinsockError(L"closesocket");
        }
    
        return 0;
    }
    

    【讨论】:

    • 我已尝试根据您的 cmets 修改我的程序,非常感谢他们,但我仍然收到相同的错误,现在在找到的两个地址上。另外,为什么我们在 setsockopt() 函数中使用 BOOL?按照它的原型,应该用char代替吧?
    • listen() 失败时,ptr-&gt;ai_family、ptr-&gt;ai_socktype 和 ptr-&gt;ai_protocol 的实际值是多少?如果它们分别不是AF_INET/6、SOCK_STREAM 和IPPROTO_TCP,那么就会出现问题。哦,等等,我刚刚在您的代码中看到另一个错误。我已经编辑了我的答案。
    • 阅读setsockopt() documentation。 SO_REUSEADDR 需要 BOOL。不要被setsockopt() 使用char* 指针的签名所迷惑(从逻辑上讲,它应该使用void* 代替)。这是历史时期的遗留物。它实际上可以采用任何类型的指针,它只需要进行类型转换。就像其他几个套接字 API 函数一样,最值得注意的是那些声明采用 sockaddr* 指针但实际上可以采用 sockaddr_XX* 指针的函数。
    • 将ai_protocol 更改为ai_socktype 似乎已经解决了这个问题。非常感谢!
    猜你喜欢
    • 2015-12-10
    • 2020-05-31
    • 2016-03-08
    • 1970-01-01
    • 2012-10-05
    • 1970-01-01
    • 2012-08-09
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多