套接字编程:listen() 出错

Socket Programming: Error on listen()

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

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()的调用引起的,这是令人困惑的。谁能向我解释问题的原因是什么?我敢打赌它应该很容易修复,但我似乎没有看到它。

问题的根源在于调用 getaddrinfo() 时,您填写的 hints 结构不正确。

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

If no error occurs, listen returns zero. Otherwise, a value of SOCKET_ERROR is returned, and a specific error code can be retrieved by calling WSAGetLastError.

...

WSAEOPNOTSUPP
The referenced socket is not of a type that supports the listen operation.

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

此外,getaddrinfo() returns 一个错误代码,就像 WSAStartup() 一样。不要使用 WSAGetLastError() 来获取它的错误代码。

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

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

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

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

  • 调用 WSAGetLastError() 时,总是在 Winsock 调用失败后立即调用它。如果您事先调用了其他任何东西,您就有重置错误代码的风险,因为 WSAGetLastError() 只是 GetLastError() 的一个别名,许多 API 使用它。

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

综上所述,试试这样的东西:

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;
}