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。