IT技术博客大学习 共学习 共进步
全部 移动开发 后端 数据库 AI 算法 安全 DevOps 前端 设计 开发者

关于TCP可靠性的一点思考,借此浅谈应用层协议设计

Changming's Blog 2017-02-06 23:18:08 累计浏览 3,238 次
本机暂存

本文主要讨论如何设计一个可靠的RPC协议。TCP是可靠的传输协议,不会丢包,不会乱序,这是课本上讲述了无数遍的道理。基于TCP的传输理论上来说都是可靠的,但是实际这也得看场景。当我做网络游戏的时候也是一直把它当一个可靠的传输协议来用,从没考虑过TCP丢包的问题。直到当我面临像网络存储、机器学习这样领域时,我发现TCP变得“不可靠”了。

具体来说:

  1. 发送方能不能知道已发送的数据对方是不是都收到了?或者,收到了多少?答:不能

  2. 如果怀疑对方没收到,有没有办法可以确认对方没有收到? 答:不能

  3. 我想发送的是“123”,对方收到的会不会是“1223”? 答:是的,会这样,而且无法避免。

第一个问题看起来很傻,众所周知TCP有ACK啊,ACK就是用来对方通知接收到了多少个字节的。可是,实际情况是,ACK是操作系统的事儿,它收到ACK后并不会通知用户态的程序。发送的流程是这样的:
  1. 应用程序把待发送的数据交给操作系统

  2. 操作系统把数据接收到自己的buffer里,接收完成后通知应用程序发送完成

  3. 操作系统进行实际的发送操作

  4. 操作系统收到对方的ACK

问题来了,假如在执行完第二步之后,网络出现了暂时性故障,TCP连接断了,你该怎么办?如果是网络游戏,这很简单,把用户踢下线,让他重新登录去,活该他网不好。但是如果比较严肃的场合,你当然希望能支持TCP重连。那么问题就来了,应用程序并不知道哪些数据发丢了。
以Windows I/O completion ports举个例子。一般的网络库实现是这样的:在调用WSASend之前,malloc一个WSABuffer,把待发送数据填进去。等到收到操作系统的发送成功的通知后,把buffer释放掉(或者转给下一个Send用)。在这样的设计下,就意味着一旦遇上网络故障,丢失的数据就再也找不回来了。你可以reconnect,但是你没办法resend,因为buffer已经被释放掉了。所以这种管理buffer的方式是一个很失败的设计,释放buffer应当是在收到response之后。
Solution:不要依赖于操作系统的发送成功通知,也不要依赖于TCP的ACK,如果你希望保证对方能收到,那就在应用层设计一个答复消息。再或者说,one-way RPC都是不可靠的,无论传输层是TCP还是UDP,都有可能会丢。
第二个问题,是设计应用层协议的人很需要考虑的,简单来说,“成功一定是成功但失败不一定是失败”。我想举个例子。假如你现在正在通过网银给房东转账交房租,然后网银客户端说:“网络超时,转账操作可能失败”。你敢重新再转一次吗?我打赌你不敢。
再举个例子,假设你设计了一个分布式文件存储服务。这个服务只有一条“Append”协议:
  1. 客户端向服务器发送文件名和二进制data。

  2. 服务器把文件打开(不存在则创建),写入数据,然后返回“OK”。中途遇到任何错误则返回“FAIL”

假设你现在有一个20TB的文件,你按照1 GB、1 GB的方式往上传。每发送1 GB,收到OK后,继续发送下1 GB。然后不幸的是中途遇到一个FAIL,你该怎么办?能断点续传吗?NO。因为服务器有可能在写入成功的情况下也返回FAIL(或者网络超时,没有任何回复)。所以你不能重发送未完成的请求。如果你选择从头传,而文件又特别大,那么你可能永远都不会成功。
Solution:采用positioned write。即在客户端发给服务器的请求里加上文件偏移量(offset)。缺点是:若你想要多个客户端同时追加写入同一个文件,那几乎是不可能的。
第三个问题:我想发送的是“123”,对方收到的会不会是“1223”?你想要支持重连、重试,那么你得容忍这种情况发生。
Solution:在应用层给每个message标记一个id,让接收者去重即可。
接下来讨论下如何关闭连接。简单来说:谁是收到最后一条消息的人,谁来主动关闭tcp 连接。另一方在recv返回0字节之后close,千万不要主动的close。
在协议设计上,分两种情况:
  1. 协议是一问一答(类似于HTTP),且发“问”(request)的总是同一方。一方只问,另一方只答

  2. 有显式的EOF消息通知对方shutdown。

如果不满足以上两点的任何一点,那么就没有任何一方能判断它收到的消息是不是最后一条,那协议设计有问题,要改!
(p.s. Windows上还有一种方法,就是用半关连接shutdown(SD_SEND)来标志结束,但是操作起来比较复杂,还不如改协议来的快,容易debug)

同分类推荐文章

  1. 等了十年的 Go 链式管道,终于来了:seq 让你像写 Scala 一样写 Go (2026-06-25 18:38:18)
  2. Go 实验特性详解 (2026-06-21 10:05:27)
  3. amd64 微架构级别对 Go 程序性能提升多少? (2026-06-21 09:38:49)

查看更多 后端 文章 →

建议继续学习

  1. gen_tcp发送进程被挂起起因分析及对策 (累计阅读 37,821)
  2. Linux大棚版Thrift入门教程 (累计阅读 24,498)
  3. TCP 的那些事儿(上) (累计阅读 22,696)
  4. 从输入 URL 到页面加载完成的过程中都发生了什么事情? (累计阅读 15,932)
  5. 自建DNS以防止GFW干扰 (累计阅读 13,123)
  6. 浅谈TCP优化 (累计阅读 11,079)
  7. 推荐一些socket工具,TCP、UDP调试、抓包工具 (累计阅读 10,842)
  8. 查看 Apache并发请求数及其TCP连接状态 (累计阅读 10,067)
  9. 推荐一些socket工具,TCP、UDP调试、抓包工具 (累计阅读 8,839)
  10. websocket 连接 C Server的尝试 (累计阅读 7,922)