我正在将一个旧式嵌入式应用程序移植到 Linux。此应用程序使用专有协议通过 TCP 连接与远程服务器通信。除了应用程序特定的消息之外,此协议还实现了允许 TCP 和 UDP 流量通过 TCP 套接字进行隧道传输的消息,以便应用程序可以为第三方客户端(例如嵌入式 Web 服务器)提供服务。这实际上是一种定制的 VPN 解决方案。
由于原始应用程序在裸机系统上运行,因此只能将所有附加服务作为应用程序的一部分来实现。
作为移植工作的一部分,我决定使用 Linux 内核提供的 TUN/TAP 驱动程序,并将所有封装的 TCP 和 UDP 帧路由到 TUN 接口。然而,这会导致在 TCP 上运行 TCP带来所有相关的问题. 原始实现不存在此问题,因为隧道没有封装完整的 TCIP/IP 堆栈。
所以我的问题是,是否可以配置 TUN 接口,以便通过它运行的 TCP/IP 堆栈不执行任何排队和重新传输?还是我只能使用我继承的定制实现?我只知道 TCP over TCP 的重新传输问题。对于这样的解决方案,我还需要注意其他问题吗?
答案1
这是一个棘手的情况。我希望您最终能够用设计更好的东西替换整个应用程序。
在 TUN/TAP 级别上你能做的事情不多,因为它是堆栈中太低的层,无法理解重传。
但是,您可以针对 IP over TCP 实现做一些事情,以最大程度地缓解重传问题。请注意,我自己还不需要实现这样的事情,因此可能存在我尚未意识到的问题。但是,我可以从理论上解释这些想法是如何运作的。
问题是,一旦外部 TCP 连接丢失任何单个数据包,接收方将被阻塞,直到丢失的数据包被重新传输。这将导致内部数据包延迟,内层可能会检测到数据包丢失,从而导致内部层也重新传输,这将不必要地消耗额外的带宽。
在接收方
关于如何处理这个问题,我最好的想法是调整接收端以部分绕过内核 TCP 堆栈。您仍然使用内核 TCP 实现设置 TCP 连接,就像在正常情况下一样。但在接收端,您实际上并不使用从 TCP 套接字接收的数据。相反,您将有一个线程或进程不断从 TCP 套接字读取并丢弃所有接收到的数据。
为了将数据包传送到 TUN/TAP 接口,您可以使用原始套接字来接收线路上看到的 TCP 段。此进程可以使用内核中的过滤器来仅查看它关心的数据包,如果内核无法足够准确地进行过滤,则忽略任何多余的数据包。您的进程必须自行进行足够的 TCP 重组,以便提取内部数据包,然后将其传送到 TUN/TAP 接口。
这里重要的是,当外部数据包丢失时,只有受其影响的内部数据包会丢失或延迟。您的进程可以在丢失数据包后继续重组数据包,以便提取内部数据包并将其传送到 TUN/TAP 接口。内部 TCP 堆栈可能仍会重新传输一些数据包,但远不及外部 TCP 连接停滞时那么多。
有几点需要指出的注意事项可能很明显,也可能不很明显:
- 如果接收窗口或拥塞窗口填满,发送端的 TCP 将停止。您无法阻止这种情况,但可以通过确保外部 TCP 连接支持选择性确认 (SACK) 来降低风险。
- 根据隧道协议的具体情况,在数据包丢失后准确识别数据包边界可能很困难甚至不可能。如果您需要实施的协议确实存在这种情况,那么您可能运气不佳。我建议修改协议,但我知道这对您来说不是一个选择。
在发送方
接收端能够绕过这一限制对于发送端的数据包而言还不够。当数据包丢失时,您无法阻止外部 TCP 连接在接收端停滞。
相反,最好的办法是尽量避免内部连接上不必要的重传。如果可能的话,您可以调整内部 TCP 连接上的重传计时器。您需要等待至少 2 次往返时间,内部 TCP 连接才会重传数据包。
完全禁用内部 TCP 连接上的重新传输并不是一个好主意,因为数据包可能会在隧道之前或之后丢失,在这种情况下外部 TCP 连接将无法重新传输。
一个理论上可行的但可能需要大量工作才能实现的方法是使用上面提到的原始套接字来监听 ACK 数据包。这样您就可以推断出哪些内部数据包仍在传输中。然后必须将每个内部 TCP 数据包与传输中的数据包进行检查,如果它是外部 TCP 连接尚未确认的数据包的重新传输,您将默默地丢弃内部 TCP 连接的重新传输。
忽视问题
当前应用程序很可能不执行任何这些操作。它可能只执行 TCP over TCP 部分并希望获得最佳效果。如果到目前为止这对您来说不是问题,那么一旦您将连接的一端替换为相同协议的新实现,它可能就不会成为问题。
因此,尝试使用已知的次优协议并仅在发现它导致实际问题时才进行修复可能会更有效。这当然取决于部署重新实现并在以后遇到问题会产生什么后果。