Rust in Everything
基于 embassy-net 0.9.1

connect 一直不返回;超时返回 ConnectionReset 而不是 TimedOut;drop 不发 FIN

主线位置:网络栈的定时器主线,Runner 按 smoltcp 的 poll_at 定下一次 poll 的时刻。connect、set_timeout 和 Drop 的实现与抓包见 TCP 与 UDP 套接字,Runner 的循环见 Stack 与 Runner。

stateDiagram-v2
  [*] --> SynSent: connect
  SynSent --> SynSent: 只发 ARP
  SynSent --> Closed: set_timeout 到期
  Closed --> [*]: Err ConnectionReset
  SynSent --> Established: 对方回 SYN-ACK
  Established --> Removed: 直接 drop
  Established --> FinWait: close 加 flush

实验 crate exp-embassy/pitb-net,bin p25_connect,五种模式(platform-std,release);两个 embassy-net 栈(10.0.0.1 和 10.0.0.2)用进程内的假驱动接成一根线;M2 Max、macOS 27、rustc 1.99.0。

现象

连一个不存在的地址(同网段的 10.0.0.9),什么超时都不设(hang):

let mut c = TcpSocket::new(sa, leak([0u8; 1024]), leak([0u8; 1024]));
let absent = (Ipv4Address::new(10, 0, 0, 9), 7);
match select(c.connect(absent), Timer::after_secs(10)).await {
  Either::First(r) => println!("connect -> {:?} after {} ms", r, ms() - t0),
  Either::Second(()) => println!("connect still pending after {} ms, state {:?}", ms() - t0, c.state()),
}
connect still pending after 10005 ms, state SynSent

加上 set_timeout(Some(2 s))(set_timeout):

set_timeout(2 s): connect -> Err(ConnectionReset) after 2008 ms, state Closed

超时了,返回的却是「连接被重置」。按错误类型分支的代码(TimedOut 就重试,ConnectionReset 就报警)在这里走错路。

连上以后直接 drop(drop,B 端 accept 后用 500 ms 超时读):

[  12 ms] A: connected
[  18 ms] A: dropped; frames A sent after connect: 0
[ 516 ms] B: read -> Err(TimeoutError), state Established

A 一个包都没发,B 不知道连接已经没了,还是 Established。

出处

connect 调 smoltcp 的 connect 后只等状态离开 SynSent / SynReceived:

        poll_fn(|cx| {
            self.io.with_mut(|s, _| match s.state() {
                tcp::State::Closed | tcp::State::TimeWait => Poll::Ready(Err(ConnectError::ConnectionReset)),
                tcp::State::Listen => unreachable!(),
                tcp::State::SynSent | tcp::State::SynReceived => {
                    s.register_send_waker(cx.waker());
                    Poll::Pending
                }
  • 为什么一直挂着:smoltcp 在拿到对方 MAC 之前不发这个套接字的任何包,只每秒发一次 ARP 请求,SYN 的重传定时器根本没启动;默认没有超时,状态永远是 SynSent。
  • 为什么是 ConnectionReset:set_timeout 交给 smoltcp,smoltcp 超时后把套接字关成 Closed,上面那一支把 Closed 一律映射成 ConnectionReset。ConnectError 有 TimedOut 变体,但 tcp.rs 里除了 embedded_io_async::Error 的映射以外没有地方构造它。main 分支上(已从 smoltcp 换到 xarxa,未发版)TimedOut 也仍然只出现在定义和映射里。这里 2 s 正好落在 ARP 的整秒节拍上;设 2500 ms 时要到 3030 ms 才返回,原因见 TCP 与 UDP 套接字。
  • 为什么 drop 不发 FIN:Drop 只有一行 i.sockets.remove(self.io.handle)。套接字从 SocketSet 里删掉,Runner 下一次 poll 时它已经不在了,没有机会发 FIN 或 RST。B 要等到自己发数据、A 回 RST 时才知道。

修法

给 connect 一个准确的超时,并在超时后自己收尾(with_timeout):

let r = with_timeout(Duration::from_secs(2), c.connect(absent)).await;
println!("with_timeout(2 s): connect -> {:?} after {} ms, state {:?}", r, ms() - t0, c.state());
c.abort();
println!("after abort(): state {:?}, remote {:?}", c.state(), c.remote_endpoint());
let f = with_timeout(Duration::from_secs(5), c.flush()).await;
println!("flush() after abort -> {:?} after {} ms, remote {:?}", f, ms() - t0, c.remote_endpoint());
with_timeout(2 s): connect -> Err(TimeoutError) after 2005 ms, state SynSent
after abort(): state Closed, remote Some(Endpoint { addr: Ipv4(10.0.0.9), port: 7 })
flush() after abort -> Err(TimeoutError) after 7010 ms, remote Some(Endpoint { addr: Ipv4(10.0.0.9), port: 7 })

embassy_time::with_timeout 的到期时刻由 embassy 的定时器决定,不受 ARP 节拍影响,错误类型也明确是超时。connect 被取消后套接字还在 SynSent,abort 把它关成 Closed。

abort 的文档要求「wait for a flush() call to complete before dropping or reusing the socket」,可在这个场景里 flush() 永远不会完成:上面给了它 5 s,仍然超时。embassy-net 的 flush 把「Closed 且 remote_endpoint() 还在」当作 RST 待发(rst_pending),smoltcp 要等 RST 真的发出去才清掉远端地址;而对方的 MAC 一直没解析出来,smoltcp 不会给这个套接字发任何包,于是 RST 永远待发。所以 connect 没连上时,abort() 之后直接 drop,不要 flush;或者也给 flush 包一层 with_timeout。这是 0.9.1 上的一个上游问题候选(复现就是上面这段)。

set_timeout 仍然有用,但它的语义是「这么久没收到对方任何东西就关闭」,适合检测已建立连接上的对端失联,那时得到的 ConnectionReset 要按「可能是超时」处理。

关连接用 close 再 flush,然后才 drop(close):

c.close();
let r = c.flush().await;
drop(c);
[  11 ms] A: connected
[  11 ms] B: read -> Ok(Ok(0)), state CloseWait
[  11 ms] A: close() + flush() -> Ok(()), state FinWait2
[  17 ms] A: dropped; frames A sent after connect: 1

A 发出了 FIN(1 帧),B 的 read 立刻得到 Ok(0)(对端关闭),状态 CloseWait。flush 等到发送缓冲区里的数据和 FIN 都被对方确认,close 本身只改状态。想立刻断开就 abort() 加 flush(),发的是 RST。tcp::client::TcpClient 的连接在 drop 时也只调了 close() 没有等,同样不发 FIN(TCP 与 UDP 套接字)。

怎么确认没中招

  • 搜所有 .connect(:外面没有 with_timeout 的,在网线拔掉或对端关机时会永远挂住。
  • 搜 ConnectError::TimedOut:在 embassy-net 0.9.1 上这个分支永远不会走到,超时实际落在 ConnectionReset 里。
  • 对端日志里大量连接停在 ESTABLISHED、直到下一次写才被 RST:本端在直接 drop 套接字。每个 drop 之前应有 close() 或 abort() 加 flush().await。

小结

  • 对方不在时 connect 只发 ARP、永远 SynSent;set_timeout 超时返回 ConnectionReset,ConnectError::TimedOut 从不构造(main 上也一样)。
  • TcpSocket 的 Drop 只从 SocketSet 删掉套接字,不发 FIN 也不发 RST。
  • 修法:with_timeout 包住 connect,超时后 abort() 再 drop(此时 flush() 不会返回);关已建立的连接用 close()(或 abort())加 flush().await 再 drop。

💬 关联讨论

发起讨论
加载中...