boost::asio 清除连接

38
有时使用boost::asio时,它似乎在我想要断开连接之前就已经断开了,即在服务器正确处理断开连接之前。 我不确定这是如何可能的,因为客户端似乎认为它已完全发送了消息,但当服务器发出错误时,它甚至没有读取消息头... 在测试期间,这种情况只会发生大约五分之一次数,服务器接收到客户端关闭消息,并干净地断开客户端连接。
错误信息:"远程主机强制关闭了一个现有的连接"
客户端断开连接:
void disconnect()
{
    boost::system::error_code error;
    //just creates a simple buffer with a shutdown header
    boost::uint8_t *packet = createPacket(PC_SHUTDOWN,0);
    //sends it
    if(!sendBlocking(socket,packet,&error))
    {
        //didnt get here in my tests, so its not that the write failed...
        logWrite(LOG_ERROR,"server",
            std::string("Error sending shutdown message.\n")
            + boost::system::system_error(error).what());
    }

    //actaully disconnect
    socket.close();
    ioService.stop();
}
bool sendBlocking(boost::asio::ip::tcp::socket &socket,
    boost::uint8_t *data, boost::system::error_code* error)
{
    //get the length section from the message
    boost::uint16_t len = *(boost::uint16_t*)(data - 3);
    //send it
    asio::write(socket, asio::buffer(data-3,len+3),
        asio::transfer_all(), *error);
    deletePacket(data);
    return !(*error);
}

服务器:

void Client::clientShutdown()
{
    //not getting here in problem cases
    disconnect();
}
void Client::packetHandler(boost::uint8_t type, boost::uint8_t *data,
    boost::uint16_t len, const boost::system::error_code& error)
{
    if(error)
    {
        //error handled here
        delete[] data;
        std::stringstream ss;
        ss << "Error recieving packet.\n";
        ss << logInfo() << "\n";
        ss << "Error: " << boost::system::system_error(error).what();
        logWrite(LOG_ERROR,"Client",ss.str());

        disconnect();
    }
    else
    {
        //call handlers based on type, most will then call startRead when
        //done to get the next packet. Note however, that clientShutdown
        //does not
        ...
    }
}



void startRead(boost::asio::ip::tcp::socket &socket, PacketHandler handler)
{
    boost::uint8_t *header = new boost::uint8_t[3];
    boost::asio::async_read(socket,boost::asio::buffer(header,3),
        boost::bind(&handleReadHeader,&socket,handler,header, 
        boost::asio::placeholders::bytes_transferred,boost::asio::placeholders::error));
}
void handleReadHeader(boost::asio::ip::tcp::socket *socket, PacketHandler handler,
    boost::uint8_t *header, size_t len, const boost::system::error_code& error)
{
    if(error)
    {
        //error "thrown" here, len always = 0 in problem cases...
        delete[] header;
        handler(0,0,0,error);
    }
    else
    {
        assert(len == 3);
        boost::uint16_t payLoadLen  = *((boost::uint16_t*)(header + 0));
        boost::uint8_t  type        = *((boost::uint8_t*) (header + 2));
        delete[] header;
        boost::uint8_t *payLoad = new boost::uint8_t[payLoadLen];

        boost::asio::async_read(*socket,boost::asio::buffer(payLoad,payLoadLen),
            boost::bind(&handleReadBody,socket,handler,
            type,payLoad,payLoadLen,
            boost::asio::placeholders::bytes_transferred,boost::asio::placeholders::error));
    }
}
void handleReadBody(ip::tcp::socket *socket, PacketHandler handler,
    boost::uint8_t type, boost::uint8_t *payLoad, boost::uint16_t len,
    size_t readLen, const boost::system::error_code& error)
{
    if(error)
    {
        delete[] payLoad;
        handler(0,0,0,error);
    }
    else
    {
        assert(len == readLen);
        handler(type,payLoad,len,error);
        //delete[] payLoad;
    }
}

1
你有没有找到这个问题的答案? - GrahamS
5个回答

37

我认为在调用socket.close()之前,你可能应该调用socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec)

basic_stream_socket::close的boost::asio文档中指出:

为了在关闭套接字时平稳地处理连接套接字,需要在关闭套接字之前调用 shutdown()。

这样可以确保在调用 socket.close 之前,任何挂起的套接字操作都被正确取消并且所有缓冲区都被刷新。


1
我遇到了和Fire Lancer完全相同的问题,这个解决方案对我很有帮助,谢谢。这可能应该被接受为答案。 - Silverlan

13

我已经尝试过使用close()方法和shutdown()方法来实现这一点

socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec)

关闭方法是两种方法中最好的。然而,我发现使用ASIO套接字的析构函数是一种干净的方式,因为ASIO会为您处理所有事情。所以您的目标就是让套接字超出范围。现在,您可以使用shared_ptr轻松地完成这个操作,并将shared_ptr重置为新的套接字或null。这将调用ASIO套接字的析构函数,生活很美好。


使用 shared_ptr 做得非常好! - nabroyan
5
这是具有误导性的。析构函数只起到与 socket.close(ec) 相同的作用,详情请参见 https://dev59.com/RlkS5IYBdhLWcg3wdmrC#39823756。因此,您必须手动调用 socket.shutdown 才能正确关闭连接。我可以证实,在没有调用 socket.shutdown 的情况下,缓冲区(在调用 close 之前)可能无法被刷新。 - scinart

5
也许这就是发生的情况:
  • 客户端发送断开连接数据包
  • 客户端关闭套接字
  • 服务器读处理程序被调用,但由于套接字已经关闭,关闭数据包存在错误。

我在你的读处理程序中看到,如果有错误,你从未检查过是否存在关闭数据包。也许存在。 基本上我想说的是,也许你的客户端有时能够在服务器有机会单独处理它们之前发送关闭和关闭数据包。


//在这里抛出了“错误”,在问题情况下len总是等于0...因此它始终读取0字节的头,即它还没有读取任何数据包...并且在客户端上添加Sleep(500)或其他操作不是一种好的解决方案,因为在较慢的网络上仍然可能不够快,并且会有明显的延迟。 - Fire Lancer
也许可以等待来自服务器的OK包,或者让服务器断开连接? - Macke
但是,如果客户端等待服务器发送断开连接消息(作为对客户端断开连接消息的响应),那么我最终会遇到服务器在客户端上断开连接的相反问题... - Fire Lancer
你能够单独使用“disconnect error”,或者你真的需要“disconnect packet”吗? - Chris H

4

使用async_write()函数,并将socket.close()放在write处理程序中。这样可以确保数据包被Boost Asio正常处理,而不会在处理过程中被忽略(因为close()调用)。


4
我有一个非常相似的问题。我认为这与Windows回收连接有关。以下内容是否熟悉?
- 在启动程序后立即出现此错误,但在建立连接后不会出现? - 如果您在重新启动应用程序之前等待超过4分钟,则不会发生错误?
tcp规范指定默认情况下,在关闭tcp连接时应等待四分钟进行最终确认。您可以使用netstat查看这些处于FIN_WAIT状态的连接。当Windows操作系统检测到您尝试连接到完全相同的系统并且回收这些部分关闭的连接时,您第二次调用程序会得到第一次运行留下的“关闭”连接。它获得下一个确认,然后真正关闭。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接