【问题标题】:Handling 'Non-blocking' socket connects处理“非阻塞”套接字连接
【发布时间】:2016-03-04 10:48:05
【问题描述】:

我正在尝试编写一个使用非阻塞套接字连接的客户端,但我对应该检查什么以及检查顺序感到困惑。我查看了Non blocking socket - how to check if a connection was successful 问题并试图在没有运气的情况下实施它。这是 Windows,而不是 Linux。我更喜欢使用 Posix 方法,所以以后可以移植它。

问题是在 Windows 中,即使服务器不存在,我也会看到 EWOULDBLOCK。一旦服务器出现,我就会在服务器上看到多个连接,因此我没有正确处理尚未完成的“阻塞”连接。

连接代码是(并且在循环中调用,如果它不能立即连接,直到 100 次尝试或它连接):

bool IPV4Socket::Connect( std::string hostname
                        , unsigned short remotePort
                        , TimeoutValue *timeout )
{
    AddrInfo getResults;
    AddrInfo getaddrinfoHints;
    int connReturn = 0;
    SockAddr_In *addrData;
    std::string service = std::to_string( remotePort );
    int errorCode = 0;

    getaddrinfoHints.ai_family = AddressFamily_inet;
    getaddrinfoHints.ai_socktype = SockType_stream;

    if ( m_socketAdaptor->getaddrinfo( hostname
                                     , service
                                     , &getaddrinfoHints
                                     , &getResults ) != 0 )
    {
        return false;
    }

    addrData = (SockAddr_In *)&( *getResults.ai_addr.begin() );

    connReturn = m_socketAdaptor->connect( m_socket
                                         , (const Sockaddr *)addrData
                                         , (int)getResults.ai_addrlen );

    static int staticLastErr = 0;
    static int staticConnStat = -2048;

    if ( connReturn == 0 )
    {
        m_isConnected = true;
        return true;
    }

    if (connReturn != staticConnStat)
    {
        std::cout << "[DEBUG] IPV4Socket::Connect() ::Connect() returned : " << connReturn << std::endl;
        staticConnStat = connReturn;
    }

    //  Check if the error is fatal - e.g. not blocking related!
    if ( connReturn == SocketError )
    {
        errorCode = m_socketAdaptor->GetLastError();

        //  Check for fatal connection error
#ifdef LIBSSL_OS_WIN32
        if ( errorCode != SockErr_EWOULDBLOCK )
#else
        if ( errorCode != SockErr_EINPROGRESS )
#endif
        {
            Close();
            return false;
        }
    }

    SocketSet writeFDS;
    SocketSet exceptFDS;
    int selectReturn = 0;

    //  Clear all the socket FDS structures
    SocketSet_ZERO( &writeFDS );
    SocketSet_ZERO( &exceptFDS );

    //  Put the socket into the FDS structures
    SocketSet_SET( m_socket, &writeFDS );
    SocketSet_SET( m_socket, &exceptFDS );

    selectReturn = m_socketAdaptor->select( m_socket + 1
                                          , NULL
                                          , &writeFDS
                                          , &exceptFDS
                                          , timeout );

    //  select() failed or timed out, connection wasn't successful!
    if ( ( selectReturn == SocketError ) || ( selectReturn == 0) )
    {
        if ( selectReturn != 0 ) std::cout << "[DEBUG] m_socketAdaptor->select() returned : " << selectReturn << std::endl;
        Close();
        return false;
    }

    //  Check for error (exception) first
    if ( m_socketAdaptor->SocketSet_ISSET( m_socket, &exceptFDS ) )
    {
        std::cout << "[DEBUG] ::Connect() found excetion on 'm_socketAdaptor->SocketSet_ISSET'" << std::endl;
        Close();
        return false;
    }

    if ( m_socketAdaptor->SocketSet_ISSET( m_socket, &writeFDS ) )
    {
        std::cout << "[DEBUG] ::Connect() m_socketAdaptor->SocketSet_ISSET( m_socket, &writeFDS )     [FOUND]" << std::endl;
        m_isConnected = true;
        return true;
    }

    Close();
    return false;
}

关闭函数:

int IPV4Socket::Close()
{
    int errNo = -1;

    if ( m_socket >= 0 )
    {
        errNo = m_socketAdaptor->shutdown(m_socket, ShutdownFlag_Both);
        if ( errNo < 0 )
        {
            int lastError = m_socketAdaptor->GetLastError();
            if ( lastError != SockErr_ENOTCONN && lastError != SockErr_EINVAL ) return lastError;
        }

        errNo = m_socketAdaptor->closesocket(m_socket);
        if (errNo < 0) return m_socketAdaptor->GetLastError();
    }

    return 0;
}

使用 cmets 更新连接:

bool IPV4Socket::Connect( std::string hostname
                        , unsigned short remotePort
                        , TimeoutValue *timeout )
{
    bool connectReturn = false;

    if ( m_incompleteConnect == false )
    {
        connectReturn = PerformConnect(hostname, remotePort );
    }

    //  If connect failed (returned false) then abort!
    if ( connectReturn == false ) return false;

    //  If Connect() returned success, but didn't connect, it is because of a
    //  blocking IO not completing in time and needs to be retried, otherwise a
    //  connection was successful and just return success.
    if ( connectReturn && m_isConnected ) return true;

    m_incompleteConnect = true;

    fd_set writeFDS;
    fd_set exceptFDS;

    //  Clear all the socket FDS structures
    FD_ZERO( &writeFDS );
    FD_ZERO( &exceptFDS );

    //  Put the socket into the FDS structures
    FD_SET( m_socket, &writeFDS );
    FD_SET( m_socket, &exceptFDS );

    int selectReturn = ::select( m_socket + 1
                               , NULL
                               , &writeFDS
                               , &exceptFDS
                               , (const timeval *)timeout);

    //  Check if ::select() has timed out, if so, connection wasn't successful!
    if ( selectReturn == 0 )
    {
        m_incompleteConnect = false;
        return false;
    }

    if ( FD_ISSET( m_socket, &writeFDS ) )
    {
        m_isConnected = true;
        m_incompleteConnect = false;
    }

    //  Check for error (exception)
    if ( FD_ISSET( m_socket, &exceptFDS ) )
    {
        m_incompleteConnect = false;
        return false;
    }

    return m_isConnected;
}


bool IPV4Socket::PerformConnect( std::string hostname, int port )
{
    addrinfo *results;
    addrinfo hints;
    int connReturn = 0;
    std::string service = std::to_string( port );
    int errorCode = 0;
    bool returnValue = false;

    memset( &hints, 0, sizeof hints );
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;

    if (::getaddrinfo( hostname.c_str(), service.c_str(), &hints, &results) != 0 )
    {
        return false;
    }

    //  Attempt the connection...
    connReturn = ::connect( m_socket, results->ai_addr, results->ai_addrlen );

    //  If connect returned error (SOCKET_ERROR), check that it's not fatal -
    //  e.g. EWOULDBLOCK, if it is then connect can check until complete!
    if (connReturn == SOCKET_ERROR)
    {
        int errorCode = WSAGetLastError();
        returnValue = (errorCode == WSAEWOULDBLOCK) ? true : false;
    }
    else
    {
        m_isConnected = true;
        returnValue = true;
    }

    return returnValue;
}

谢谢你:)

【问题讨论】:

  • “没有运气”不是问题描述。
  • @EJP 添加了额外的问题“描述”。
  • 无论目标是否存在,您总是会首先看到 EWOILDBLOCK。在connect() 返回之后发生的所有事情就是 SYN 已排队等待传递。正是以下select() 和错误检查告诉您连接是否成功、失败、超时等。
  • @EJP 谢谢,我不明白为什么服务器端看到多个连接(例如 10-15),是什么让我认为 connect() 工作不正常.
  • 你没有关闭任何东西。

标签: c++ sockets network-programming


【解决方案1】:

connect from msdn(仔细阅读返回值)

所以当你第一次尝试在你的循环中连接时,你会得到 SOCKET_ERROR 和 errno=WSAEWOULDBLOCK; (这意味着一切正常!!)

如果你继续循环(再次连接)你会得到 SOCKET_ERROR 和 errno=WSAEINPROGRESS ,所以不要再循环了!

所以在得到 WSAEWOULDBLOCK 之后,正如解释的那样,你可以,

使用select通过检查socket是否可写来判断连接请求是否完成。

fd_set wr_set;
struct timeval timeout={10,1};

int err=WSAConnect(s,(struct sockaddr*)&addr,len,0,0,0,0);
printf("connect: %d %d\n",err,WSAGetLastError());


FD_ZERO(&wr_set);
FD_SET(s, &wr_set);

printf("%d\n",wr_set.fd_count);
select(s,0,&wr_set,0,&timeout); <- blocks (10s) only if can't get socket writable
printf("%d\n",wr_set.fd_count);

【讨论】:

    【解决方案2】:

    您必须在致电connect 后立即致电GetLastError。这两个函数之间的所有代码都可以清除或更改错误代码。

    您的代码将输出写入这两个调用之间的流。如果该写入没有产生错误,GetLastError 将不会从正在进行的connect 中得到错误。

    errorCode = m_socketAdaptor-&gt;GetLastError(); 行移到connect 之后。

    【讨论】:

      【解决方案3】:

      我不确定这是否有帮助,但我发现在 Windows 平台中,当您使用 connect() 函数时,它会返回一个 WSAEWOULDBLOCK(我认为它与 EWOULDBLOCK 相同)。而且您必须稍后确定连接是否实际成功。如果您愿意,可以查看https://msdn.microsoft.com/en-us/library/windows/desktop/ms737625%28v=vs.85%29.aspx(我指的部分是“返回值”)

      【讨论】:

      • 感谢您,不幸的是,如果您查看问题中的代码,我会使用 select() 检查可写性,但不幸的是,这并没有帮助。
      • 您发布的代码的哪一行出现了错误?我的意思是 EWOULDBLOCK 错误。
      • 我不是 100% 发生关闭失败的地方。
      • 好的,这是另一件事(我已经有一段时间没有接触过套接字了,所以我可能错了,我只是想通过谷歌搜索来帮助我找到任何东西)。 select() 第一个参数应该是 max(writeFD​​s,exceptionFDs) + 1 - 如果您要使用读取文件描述符,您也应该考虑这些 - 但在您的情况下,作为第一个参数,您传递 m_socket + 1 并作为据我所知,如果调用 connect() 获取的文件描述符是 m_socket。老实说,我不认为这是导致错误的原因,但更改它不会有什么坏处。
      • 没有区别,我写了一小段测试代码,它们返回相同的值——不过提醒一下最大值很有用;)
      【解决方案4】:

      问题是在 Windows 中,即使服务器不存在,我也会看到 EWOULDBLOCK。

      由于套接字处于非阻塞模式,因此 connect() 在实际连接到服务器之前立即返回。因此,预计会获得 EINPROGRESS。

      如果没有 m_socketAdaptor 的代码,我可以重现这个,但我很确定问题出在这个:

      连接代码是(并且在循环中调用,如果它不能立即连接,直到 100 次尝试或它连接):

      如果您已经调用了一次 connect() 并获得了 (WSA)EINPROGRESS,则不应再次调用 connect,但您应该只在以下迭代中执行 select() 部分。此外,您不应在检查一次后关闭()。

      所以你应该这样做:

      • 第一次通过 - 或者如果连接完全失败:

      getaddrinfo(); connect(); select();

      • 第 2-100 遍或直到选择返回成功:

      select()

      • 如果之前的调用失败

      close() go back to 1st step

      【讨论】:

      • 在 Windows 上,无论您是否已连接,您似乎总是得到 EINPROGRESS?
      • 正确。不仅在 Windows 上,在 Linux 上也是如此。这是预期的非阻塞套接字行为。 MS-Windows 正确地模拟了非阻塞套接字的传统 Berkeley 套接字 API 行为。这就是它应该如何工作的方式。
      • @SamVarshavchik 我已经修改了我的代码(见问题),但我仍然看到多个连接......好像仍然没有正确处理连接
      • @SamVarshavchik 快速说明:我现在已将代码更改为直接调用套接字 api。
      • 新代码似乎正确。尝试在调用 ::connect 的行之后放置一个 printf 和一个计数器(或断点或记录到文件)。我相信,每当您在服务器端有多个连接时,那是因为由于某种原因 ::connect 被多次调用。
      猜你喜欢
      • 2013-07-20
      • 2017-04-18
      • 1970-01-01
      • 2010-10-31
      • 2013-10-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-08-19
      相关资源
      最近更新 更多