理想的工作流程是下面这个样子:

+--------+      +--------+      +--------+
| Client | ---> | Agent  | ---> | Server |
+--------+      +--------+      +--------+

+--------+      +--------+      +--------+
| Client | <--- | Agent  | <--- | Server |
+--------+      +--------+      +--------+

完成回话之后的关闭顺序可能是:

Client --X-- Agent
Agent  --X-- Server
Server --X-- Agent
Agent  --X-- Client

总之就是大多数情况下,如果不是因为网络错误都是没有问题的。我们通常将转发通道分为两个,接收和发送通道。
在一个连接中,关闭一段的发送通道,另外一端也还是可以继续发送。当我们将两个通道分开处理的时候,如果出现
下面的情况就会卡住,导致 fd 没有正确释放。

+--------+       +--------+       +--------+
| Client | ----> | Agent  | --!-> | Server |
+--------+       +--------+       +--------+

+--------+       +--------+       +--------+
| Client | <---- | Agent  | <---- | Server |
+--------+       +--------+       +--------+

假设 Client 和 Agent 都是在本地,那么基本上是能保证数据传输不会出现错误,但是在 Agent 到 Server 段出现网络错误,
C->A->S 的这个通道就会关闭,对于 Client 来说已经认为数据发送出去了,对于 Server 来说就是等待数据的状态。所以
Server 不会主动关闭 S->A 的这个连接。这个时候就会导致 S->A->C 的这个通道悬挂着。解决的办法也很简单,我在使用
Rust Tokio 库拷贝流量的时候,使用try_join!代替join!,前者需要等待所有异步表达式返回,或者某个表达式出错
也会提前返回。join! 则是等待所有异步表达式返回。