可靠性疑问

TCP 是可靠的传输协议,不会丢包、乱序,其在理论上是非常可靠的,但在实际应用中需要区分场景。

  • 发送方能不能知道已发送的数据对方是不是都收到了?或者收到多少?不能。
  • 如果怀疑对方没收到,有没有办法可以确认对方没有收到?不能。
  • 需要发送 123,对方会不会却收到 1223?会的。

第一个问题

众所周知 TCP 拥有 ACK,ACK 就是用来确认对方接收到了多少字节。但是 ACK 是 OS 的操作,OS 收到之后并不会通知用户程序。发送的流程如下:

  1. 应用程序把待发送的数据交给操作系统
  2. 操作系统把数据接收到自己的 buffer 里,接收完成后通知应用程序发送完成
  3. 操作系统进行实际的发送操作
  4. 操作系统收到对方的 ACK

如果在执行第二步之后,网络出现了暂时性故障,TCP 断开了连接,会发生什么?如果是网络游戏则很简单,可以将用户踢下线,让其重新登录。但如果是比较严肃的场景,当然希望能够支持 TCP 重连,但是重连后如何知道哪些数据已发送、哪些数据已丢失。

以Windows I/O completion ports举个例子。一般的网络库实现是这样的:在调用WSASend之前,malloc一个WSABuffer,把待发送数据填进去。等到收到操作系统的发送成功的通知后,把buffer释放掉(或者转给下一个Send用)。在这样的设计下,就意味着一旦遇上网络故障,丢失的数据就再也找不回来了。你可以reconnect,但是你没办法resend,因为buffer已经被释放掉了。所以这种管理buffer的方式是一个很失败的设计,释放buffer应当是在收到response之后。

方案:不要依赖于操作系统的发送成功通知,也不要依赖于TCP的ACK,如果你希望保证对方能收到,那就在应用层设计一个答复消息。再或者说,one-way RPC都是不可靠的,无论传输层是TCP还是UDP,都有可能会丢。

第二个问题

这是设计应用层协议的人很需要考虑的,简单来说,”成功一定意味着成功,而失败则未必意味着失败“。比如正在通过网银转账,这是出现“网络超时,转账操作可能失败”,这时并不能确定是否转账成功。即“失败”的定义可以包含多个层次。

方案:采用positioned write。即在客户端发给服务器的请求里加上文件偏移量(offset)。缺点是:若你想要多个客户端同时追加写入同一个文件,那几乎是不可能的。

第三个问题

方案:在应用层给每个message标记一个id,让接收者去重即可。

如何正确关闭连接

简单来说,谁是收到最后一条消息的人,谁来主动关闭 TCP 连接。另一方在 recv 返回 0 字节之后 close,千万不要主动 close。

在协议设计上,分两种情况:

  • 协议是一问一答,类似于 HTTP,且发问的总是同一方。一方只问,另一方只答;
  • 有显示 EOF 的消息通知对方 shutdown。

如果不满足以上两点的任何一点,那么就没有任何一方能够判断它收到的消息是不是最后一条。