在关闭之前等待 NetworkStream 完成发送数据

Wait for NetworkStream to Finish Sending Data before Closing It

我正在编写一个 Minecraft Classic 服务器,但在实施踢球数据包时遇到了一些问题。服务器应该发送踢球数据包,然后自行处理。主要问题是因为在“发送”之后写入网络流 returns(这是一个超级模糊的定义),玩家会话将尝试自行处理它的 tcp 客户端,可能会中断流踢数据包的数据到客户端 - 这正是目前正在发生的事情。

我想提一下,不可能将 the protocol specifications 更改为要求在应用程序端进行某种确认。

这就是数据包的发送方式。通常没有任何问题,但是 disconnecting/disposing 发送数据包后会中断网络流上的数据流。

public bool SendPacket(Packet packet)
{
  try
  {
    packet.Send(networkStream); //this simply writes stuff to the network stream
    return true;
  }
  catch
  {
    return false;
  }
}

会话是这样处理的:

public void Dispose(bool disposing)
{
    if (!disposed)
    {
        if (disposing)
        {
            if (currentWorld != null)
                currentWorld.LeaveWorld(this);
            if (account != null)
                server.AccountManager.Logout(account);
            networkStream.Close();
            client.Close();
            Disconnected = true;
        }
        disposed = true;
    }
}

我的临时解决方案是发送kick数据包后等待100ms:

public void Kick(string reason)
{
  SendPacket(new DisconnectPlayerPacket(reason));
  Thread.Sleep(100);
  Dispose();
}

它适用于一些测试用例,但并非始终如此。客户端没有收到所有数据。我知道这一点是因为我已经调试了 ClassiCube 客户端(一个 Minecraft Classic 客户端),我知道它在 100% 的时间里都能正常工作。

无论哪种方式等待都不是可行的解决方案,因为我的服务器会话是在单个线程中处理的 - 这在大多数情况下都有效,因为 Minecraft Classic 数据包相对较短且易于处理。但是,每等待一秒,就会浪费一秒,因为无法处理其他数据包。

我想要一些方法来指示 tcp 客户端是否已完成发送数据 - 在这种情况下,这意味着客户端已成功接收到 kick 数据包。本质上,我想要 dispose 方法,本质上是在关闭连接之前等待接收到最后一个数据包。我有点了解 TCP 协议,我知道客户端应该在之后发送一个 ACK​​ 信号,但据我所知,TcpClient 将这些东西抽象出来。

完整的 github 回购 is here for reference, but please note that all of this mostly takes place in PlayerSession.cs

总之,如何判断网络流是否已发送完所有数据?

当您调用NetworkStream.Write时,您正在排队发送数据。 OS 会在幕后为您发送。要知道对方是否真的收到并处理了您的数据,唯一的方法就是让他们向您发送某种确认数据包。

当您调用 NetworkStream.Dispose() 时,它会做两件事。首先,它会关闭两个方向的套接字,如果一切顺利,最终会导致远程方在收到您的所有数据后看到 EOF。

其次,它会关闭您的套接字句柄。但是,这只会关闭 user-mode 句柄。在幕后,OS 将使套接字保持活动状态以刷新数据。

您可以通过设置套接字的延迟选项来稍微控制这里的行为,它控制超时以及 Close() 是否会在等待超时时阻塞。

这种行为有时是合适的,但许多网络协议将依赖于 NetworkStream.Dispose 无法提供的行为,因此您需要在 Socket 上自行管理这些内容。这里的常见操作顺序(如果需要,交换 client/server)看起来像这样:

  1. 客户端调用 Shutdown(Send)。
  2. 客户端继续循环接收,但不发送。
  3. 由于客户端关闭,服务器看到 EOF。
  4. 服务器写入任何最终状态并调用 Shutdown(Send)。
  5. 服务器关闭他们的套接字。
  6. 客户端看到任何最终状态,后跟由于服务器关闭而导致的 EOF。
  7. 客户端关闭了他们的套接字。 (如果他们不在乎,客户可以自由地提前关闭他们的套接字)