Skip to main content

futu_backend/
conn.rs

1// 后端 TCP 连接管理(直连富途后端服务器)
2//
3// 加密方式:
4// - 登录命令(1001/6001/26001): 不加密
5// - 其他命令: AES-128 加密,body = encrypt(sec_data(4B BE) + proto_data)
6
7use std::collections::HashMap;
8use std::sync::Arc;
9use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
10use std::time::SystemTime;
11
12use arc_swap::ArcSwapOption;
13use bytes::Bytes;
14use futures::{SinkExt, StreamExt};
15use parking_lot::Mutex;
16use tokio::net::TcpStream;
17#[cfg(test)]
18use tokio::sync::oneshot;
19use tokio::sync::{Mutex as AsyncMutex, mpsc, watch};
20use tokio_util::codec::Framed;
21
22use futu_core::error::{FutuError, Result};
23use futu_core::log_redact::endpoint_log_fingerprint;
24
25#[cfg(test)]
26use crate::nn_codec::NNCodec;
27use crate::nn_codec::NNFrame;
28
29mod diagnostics;
30mod drop_impl;
31mod inbound;
32mod lifecycle;
33mod outbound;
34mod protocol_identity;
35mod request;
36#[cfg(feature = "test-util")]
37mod test_util;
38
39#[cfg(test)]
40use diagnostics::InboundFrameStage;
41use diagnostics::{
42    EndpointFingerprint, InboundProgress, ObservedNNCodec, PendingFailureKind,
43    TcpLoginTransportStage, trace_tcp_login_transport_stage,
44};
45#[cfg(test)]
46use diagnostics::{PendingFailureFacts, PendingRegistrationIdentity, PendingResponseEntry};
47
48use inbound::{InboundFrameDecision, decode_inbound_frame_body, is_ex_head_error};
49#[cfg(any(test, feature = "test-util"))]
50pub use lifecycle::LifecyclePauseHook;
51#[cfg(test)]
52use lifecycle::PendingRegistrationGuard;
53use lifecycle::{
54    ConnectionLifecycle, ConnectionTerminationFacts, DiagnosticSink, PendingResponses,
55    claim_termination_and_fail_all_pending, emit_diagnostic, mark_disconnected, no_diagnostic_sink,
56    pending_failure_facts,
57};
58pub use protocol_identity::BackendProtocolIdentity;
59pub(crate) use request::{BackendWriterAdmission, RequestTimeoutPolicy, WriterAdmissionObserver};
60
61static NEXT_CONNECTION_GENERATION: AtomicU64 = AtomicU64::new(1);
62
63/// 后端连接
64pub struct BackendConn {
65    serial_no: AtomicU32,
66    sec_data: AtomicU32,
67    /// Serialize secure-number allocation through writer-queue admission.
68    ///
69    /// C++ holds `FTChannelImpl::channel_mutex_` across `GetSecureNum()`,
70    /// encryption and TCP send/queue admission. Without the same boundary,
71    /// concurrent Rust callers can put a lower `sec_data` frame on the wire
72    /// after a higher one and trigger backend `-106 security number invalid`.
73    /// The guard is released before waiting for the response, so requests
74    /// remain concurrently in flight.
75    ///
76    /// Ref: `f3c/FTNet/Src/ftnet/channel/impl/channel.cpp:907-910,1002-1086`.
77    outbound_order: AsyncMutex<()>,
78    connected: Arc<std::sync::atomic::AtomicBool>,
79    connected_tx: watch::Sender<bool>,
80    /// 共享的 session key — 接收任务和发送方共用同一个 Arc。
81    /// 对齐 C++ `Logger::session_key_` 是 `std::string`(`logger.h:152`),
82    /// 长度由服务端下发决定,Platform 通常 16 字节(AES-128),Broker 可能是
83    /// 32 字节(AES-256)。v1.4.7 之前固定 `[u8; 16]` 对 broker session_key
84    /// 截断会导致服务端 `CONN decrypt failed`。
85    session_key: SharedSessionKey,
86    /// Previous key retained across an in-place session-key rotation. C++ tries
87    /// current then previous when an encrypted response races the refresh.
88    previous_session_key: SharedSessionKey,
89    cmd_tx: mpsc::Sender<BackendCmd>,
90    pending: PendingResponses,
91    termination_lifecycle: Arc<ConnectionLifecycle>,
92    endpoint_fingerprint: EndpointFingerprint,
93    connection_generation: u64,
94    inbound_progress: Arc<InboundProgress>,
95    diagnostic_sink: DiagnosticSink,
96    shutdown_tx: watch::Sender<bool>,
97    pub user_id: AtomicU32,
98    /// RspEncryptData.client_ip(field 14), set after TCP login.
99    ///
100    /// C++ `logger.cpp:511-516` stores this server-observed public IP on the
101    /// working TCP client, then `logger.cpp:1122-1125` echoes it in CMD20147.
102    client_ip: Mutex<String>,
103    protocol_identity: BackendProtocolIdentity,
104    pub client_ver: u16,
105}
106
107enum BackendCmd {
108    Send {
109        frame: NNFrame,
110        writer_admitted: Option<Arc<AtomicBool>>,
111    },
112}
113type SharedSessionKey = Arc<ArcSwapOption<Vec<u8>>>;
114
115/// 后端推送回调
116/// Backend push callback with the producing connection generation attached.
117///
118/// The generation is part of the delivery identity: callers must not infer a
119/// push source by probing whichever connection happens to be current after the
120/// frame was received.
121pub type PushCallback = Arc<dyn Fn(u64, u16, Bytes, SystemTime) + Send + Sync + 'static>;
122
123impl BackendConn {
124    /// 连接 TCP 的超时时间。
125    ///
126    /// Linux 默认 `tcp_syn_retries=6` 时 `TcpStream::connect` 等到 `ETIMEDOUT`
127    /// 需要约 127 秒——用户启动 opend 时如果选到一个不通的 IP 就卡 2 分钟后
128    /// 才报错 offline mode(某位 Rocky Linux 用户踩过)。加 10s 超时快速失败,
129    /// 让上层(`bridge.rs` 的 connect 循环)有机会 fallback 到下一个候选 IP。
130    pub const CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
131
132    /// Backend 侧可识别的 Rust OpenD 固定客户端版本号。
133    ///
134    /// C++ 同时把 `AppConfig::GetClientVersion()` 写入 protocol header 与
135    /// `ReqEncryptData.client_ex_ver`;为便于后台识别,Rust OpenD 于 2026-07-14
136    /// 将识别版本提升为 1031。该身份在首次建连和鉴权请求发出前必须已知,
137    /// 不能从服务端动态配置取得;尚未识别 1031 的 backend 环境可能拒绝登录。
138    /// 若后台重新分配版本,
139    /// 只更新本常量,并由契约测试保证所有 wire/HTTP 出口同步变化。
140    /// Ref: `f3c/FTNet/Src/ftnet/channel/impl/protocol_header.cpp:25-32` and
141    /// `f3c/FTlogin/Src/ftlogin/login/logger.cpp:304-310`.
142    pub const CLIENT_VER_FTGTW: u16 = 1031;
143
144    /// 建立底层 TcpStream —— 带超时 + set_nodelay。不 spawn 任何 task。
145    async fn establish_stream(addr: &str, timeout: std::time::Duration) -> Result<TcpStream> {
146        let addr_fp = endpoint_log_fingerprint(addr);
147        let stream = match tokio::time::timeout(timeout, TcpStream::connect(addr)).await {
148            Ok(Ok(s)) => s,
149            Ok(Err(error)) => {
150                return Err(FutuError::Network(std::io::Error::new(
151                    error.kind(),
152                    format!("backend_connect_failed endpoint_fingerprint={addr_fp}"),
153                )));
154            }
155            Err(_elapsed) => {
156                return Err(FutuError::Network(std::io::Error::new(
157                    std::io::ErrorKind::TimedOut,
158                    format!("backend_connect_timeout endpoint_fingerprint={addr_fp}"),
159                )));
160            }
161        };
162        stream.set_nodelay(true).map_err(|error| {
163            FutuError::Network(std::io::Error::new(
164                error.kind(),
165                format!("backend_socket_config_failed endpoint_fingerprint={addr_fp}"),
166            ))
167        })?;
168        Ok(stream)
169    }
170
171    /// 连接到后端服务器(带 10s 超时)
172    pub async fn connect(
173        addr: &str,
174        push_callback: PushCallback,
175        protocol_identity: BackendProtocolIdentity,
176    ) -> Result<Self> {
177        let stream = Self::establish_stream(addr, Self::CONNECT_TIMEOUT).await?;
178        tracing::info!(
179            addr_fp = %endpoint_log_fingerprint(addr),
180            "connected to backend"
181        );
182        Ok(Self::from_stream(
183            stream,
184            push_callback,
185            EndpointFingerprint::from_endpoint(addr),
186            protocol_identity,
187        ))
188    }
189
190    /// 并发连接多个候选地址,谁先通用谁(对齐 C++ `connector.cpp:175-189`
191    /// `ConnectStrategyAddr` 的 concurrency_ip 语义)。
192    ///
193    /// - 每个候选独立带 `CONNECT_TIMEOUT`(10s)超时
194    /// - 第一个 `Ok(stream)` 胜出,其余 pending task drop 时会关闭半连接
195    /// - 全部失败返回最后一个错误
196    ///
197    /// 返回 `(BackendConn, winner_addr)`,调用方用 winner_addr 做登录协议里的
198    /// host_ip/host_port 字段。
199    pub async fn connect_race(
200        addrs: &[String],
201        push_callback: PushCallback,
202        protocol_identity: BackendProtocolIdentity,
203    ) -> Result<(Self, String)> {
204        use futures::stream::{FuturesUnordered, StreamExt};
205
206        if addrs.is_empty() {
207            return Err(FutuError::Network(std::io::Error::new(
208                std::io::ErrorKind::InvalidInput,
209                "connect_race: empty address list",
210            )));
211        }
212
213        tracing::info!(
214            candidates = addrs.len(),
215            candidate_fps = ?addrs
216                .iter()
217                .map(|addr| endpoint_log_fingerprint(addr))
218                .collect::<Vec<_>>(),
219            "racing parallel connects"
220        );
221
222        let mut attempts: FuturesUnordered<_> = addrs
223            .iter()
224            .cloned()
225            .map(|addr| async move {
226                let result = Self::establish_stream(&addr, Self::CONNECT_TIMEOUT).await;
227                (addr, result)
228            })
229            .collect();
230
231        let mut last_err: Option<FutuError> = None;
232        while let Some((addr, result)) = attempts.next().await {
233            match result {
234                Ok(stream) => {
235                    tracing::info!(
236                        addr_fp = %endpoint_log_fingerprint(&addr),
237                        remaining_losers = attempts.len(),
238                        "connect race winner"
239                    );
240                    drop(attempts); // 其他 FuturesUnordered 里的 future drop 即取消
241                    let conn = Self::from_stream(
242                        stream,
243                        push_callback,
244                        EndpointFingerprint::from_endpoint(&addr),
245                        protocol_identity,
246                    );
247                    return Ok((conn, addr));
248                }
249                Err(e) => {
250                    tracing::debug!(
251                        addr_fp = %endpoint_log_fingerprint(&addr),
252                        error = %e,
253                        "candidate failed"
254                    );
255                    last_err = Some(e);
256                }
257            }
258        }
259
260        Err(last_err.unwrap_or_else(|| {
261            FutuError::Network(std::io::Error::other("connect_race: all candidates failed"))
262        }))
263    }
264
265    /// v1.4.70 D1: test-only 从 `tokio::io::DuplexStream` 构造 BackendConn
266    ///
267    /// 用于 integration tests(`crates/futu-gateway/tests/common/mock_backend.rs`)
268    /// 替代真 `TcpStream`。生产路径通过 `connect()` → `from_stream()` 不变。
269    #[cfg(feature = "test-util")]
270    pub fn from_duplex(
271        stream: tokio::io::DuplexStream,
272        push_callback: PushCallback,
273        protocol_identity: BackendProtocolIdentity,
274    ) -> Self {
275        Self::from_stream_inner(stream, push_callback, protocol_identity)
276    }
277
278    /// 从已建立的 TcpStream 构造 BackendConn(spawn recv/send task)
279    fn from_stream(
280        stream: TcpStream,
281        push_callback: PushCallback,
282        endpoint_fingerprint: EndpointFingerprint,
283        protocol_identity: BackendProtocolIdentity,
284    ) -> Self {
285        Self::from_stream_with_connection_diagnostics(
286            stream,
287            push_callback,
288            endpoint_fingerprint,
289            protocol_identity,
290            no_diagnostic_sink(),
291        )
292    }
293
294    /// v1.4.70 D1: 泛型化的 stream → BackendConn 构造(内部实现),
295    /// 生产路径用 `TcpStream`,test 用 `DuplexStream`。
296    #[cfg(any(test, feature = "test-util"))]
297    pub(crate) fn from_stream_inner<S>(
298        stream: S,
299        push_callback: PushCallback,
300        protocol_identity: BackendProtocolIdentity,
301    ) -> Self
302    where
303        S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + Unpin + 'static,
304    {
305        Self::from_stream_with_connection_diagnostics(
306            stream,
307            push_callback,
308            EndpointFingerprint::from_endpoint("test-peer"),
309            protocol_identity,
310            no_diagnostic_sink(),
311        )
312    }
313
314    #[cfg(test)]
315    pub(crate) fn from_stream_inner_default_futunn_chs_for_test<S>(
316        stream: S,
317        push_callback: PushCallback,
318    ) -> Self
319    where
320        S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + Unpin + 'static,
321    {
322        Self::from_stream_inner(stream, push_callback, BackendProtocolIdentity::new(40, 0))
323    }
324
325    /// Construct a production connection around an already-established
326    /// transport while immediately sealing its endpoint behind the sole
327    /// redaction boundary. The raw endpoint is never stored on `BackendConn`.
328    pub(crate) fn from_stream_with_endpoint<S>(
329        stream: S,
330        push_callback: PushCallback,
331        endpoint: &str,
332        protocol_identity: BackendProtocolIdentity,
333    ) -> Self
334    where
335        S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + Unpin + 'static,
336    {
337        Self::from_stream_with_connection_diagnostics(
338            stream,
339            push_callback,
340            EndpointFingerprint::from_endpoint(endpoint),
341            protocol_identity,
342            no_diagnostic_sink(),
343        )
344    }
345
346    #[cfg(test)]
347    pub(crate) fn from_stream_inner_with_diagnostics_default_futunn_chs_for_test<S>(
348        stream: S,
349        push_callback: PushCallback,
350    ) -> (Self, mpsc::Receiver<PendingFailureFacts>)
351    where
352        S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + Unpin + 'static,
353    {
354        const DIAGNOSTIC_CAPACITY: usize = 16;
355        let (diagnostic_tx, diagnostic_rx) = mpsc::channel(DIAGNOSTIC_CAPACITY);
356        let conn = Self::from_stream_with_connection_diagnostics(
357            stream,
358            push_callback,
359            EndpointFingerprint::from_endpoint("test-peer"),
360            BackendProtocolIdentity::new(40, 0),
361            Some(diagnostic_tx),
362        );
363        (conn, diagnostic_rx)
364    }
365
366    fn from_stream_with_connection_diagnostics<S>(
367        stream: S,
368        push_callback: PushCallback,
369        endpoint_fingerprint: EndpointFingerprint,
370        protocol_identity: BackendProtocolIdentity,
371        diagnostic_sink: DiagnosticSink,
372    ) -> Self
373    where
374        S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + Unpin + 'static,
375    {
376        let connection_generation = NEXT_CONNECTION_GENERATION.fetch_add(1, Ordering::Relaxed);
377        let codec = ObservedNNCodec::new_with_connection_identity(
378            endpoint_fingerprint.clone(),
379            connection_generation,
380        );
381        let inbound_progress = codec.inbound_progress();
382        let framed = Framed::new(stream, codec);
383        let (mut sink, mut stream_rx) = framed.split();
384
385        let (cmd_tx, mut cmd_rx) = mpsc::channel::<BackendCmd>(256);
386
387        let pending: PendingResponses = Arc::new(Mutex::new(HashMap::new()));
388        let pending_recv = pending.clone();
389        let pending_send = pending.clone();
390        let termination_lifecycle = Arc::new(ConnectionLifecycle::new());
391        let termination_lifecycle_recv = Arc::clone(&termination_lifecycle);
392        let termination_lifecycle_send = Arc::clone(&termination_lifecycle);
393        let endpoint_fingerprint_recv = endpoint_fingerprint.clone();
394        let endpoint_fingerprint_send = endpoint_fingerprint.clone();
395        let inbound_progress_recv = Arc::clone(&inbound_progress);
396        let inbound_progress_send = Arc::clone(&inbound_progress);
397        let diagnostic_sink_recv = diagnostic_sink.clone();
398        let diagnostic_sink_send = diagnostic_sink.clone();
399        let connected = Arc::new(std::sync::atomic::AtomicBool::new(true));
400        let (connected_tx, _) = watch::channel(true);
401        let (shutdown_tx, mut shutdown_rx_recv) = watch::channel(false);
402        let mut shutdown_rx_send = shutdown_tx.subscribe();
403        let connected_recv = Arc::clone(&connected);
404        let connected_send = Arc::clone(&connected);
405        let connected_tx_recv = connected_tx.clone();
406        let connected_tx_send = connected_tx.clone();
407        let shutdown_tx_recv = shutdown_tx.clone();
408        let shutdown_tx_send = shutdown_tx.clone();
409        let session_key: SharedSessionKey = Arc::new(ArcSwapOption::empty());
410        let session_key_for_recv = session_key.clone();
411        let previous_session_key: SharedSessionKey = Arc::new(ArcSwapOption::empty());
412        let previous_session_key_for_recv = previous_session_key.clone();
413
414        // 接收任务
415        tokio::spawn(async move {
416            let termination = loop {
417                let next_frame = tokio::select! {
418                    _ = shutdown_rx_recv.changed() => {
419                        break ConnectionTerminationFacts {
420                            kind: PendingFailureKind::Shutdown,
421                            endpoint_fingerprint: endpoint_fingerprint_recv.clone(),
422                            connection_generation,
423                            progress: inbound_progress_recv.snapshot(),
424                            diagnostic_sink: diagnostic_sink_recv.clone(),
425                        };
426                    }
427                    next = stream_rx.next() => next,
428                };
429                let mut frame = match next_frame {
430                    Some(Ok(frame)) => frame,
431                    Some(Err(_error)) => {
432                        break ConnectionTerminationFacts {
433                            kind: PendingFailureKind::CodecError,
434                            endpoint_fingerprint: endpoint_fingerprint_recv.clone(),
435                            connection_generation,
436                            progress: inbound_progress_recv.snapshot(),
437                            diagnostic_sink: diagnostic_sink_recv.clone(),
438                        };
439                    }
440                    None => {
441                        break ConnectionTerminationFacts {
442                            kind: PendingFailureKind::PeerEof,
443                            endpoint_fingerprint: endpoint_fingerprint_recv.clone(),
444                            connection_generation,
445                            progress: inbound_progress_recv.snapshot(),
446                            diagnostic_sink: diagnostic_sink_recv.clone(),
447                        };
448                    }
449                };
450
451                trace_tcp_login_transport_stage(
452                    frame.header.cmd_id,
453                    frame.header.serial_no,
454                    TcpLoginTransportStage::FrameComplete,
455                );
456
457                // 解密(非登录命令)
458                tracing::debug!(
459                    cmd_id = frame.header.cmd_id,
460                    serial_no = frame.header.serial_no,
461                    is_push = frame.header.is_push,
462                    is_compressed = frame.header.is_compressed,
463                    ex_head_len = frame.header.ex_head_len,
464                    body_len = frame.header.body_len,
465                    actual_body_len = frame.body.len(),
466                    "recv frame"
467                );
468
469                // 如果 ex_head 里有业务错误(err_info.cmd_result != 0),
470                // 把它 log 出来 —— 服务端 body 空时会通过 ex_head 返回错误。
471                //
472                // 软失败特判:某些返回 code 是正常的"无此服务/无数据"信号(例如
473                // 某些账号在 Futu HK broker 上没有授权账户时 CMD 2298 会收到
474                // `code=-102 CONN can not find command service`)。这类日志降到
475                // DEBUG 避免噪声;其他错误保持 WARN。
476                if let Some(err) = frame.parse_ex_head_error()
477                    && is_ex_head_error(&err)
478                {
479                    // `code=-102` 是服务端的软失败信号:"此账户/通道不支持该 cmd"。
480                    // 服务端实际把 "CONN can not find command service" 放在 `source`
481                    // 字段里,`message` 为空(和 C++ ErrorInfo 字段 2/4 定义略有偏差)。
482                    // 例如:账户在 Futu HK broker 通道不认 CMD 2298 / CMD 1003 heartbeat
483                    // 都会走到这里。降级到 debug 避免日志噪声;其他 code 保持 warn。
484                    let is_soft_fail = err.code == -102;
485                    if is_soft_fail {
486                        tracing::debug!(
487                            cmd_id = frame.header.cmd_id,
488                            code = err.code,
489                            source = %err.source,
490                            message = %err.message,
491                            "server: cmd not available on this channel (soft-fail)"
492                        );
493                    } else {
494                        tracing::warn!(
495                            cmd_id = frame.header.cmd_id,
496                            cmd_result = err.cmd_result,
497                            code = err.code,
498                            source = %err.source,
499                            message = %err.message,
500                            "server returned err_info in ex_head"
501                        );
502                    }
503                }
504
505                // C++ NNTCPConnBase first consumes the serial and then dispatches
506                // the actual response cmd (NNTCPConnBase.cpp:327-335, 356-364).
507                // This Rust pending entry owns one request-specific parser, so a
508                // different cmd cannot safely be delivered through that oneshot.
509                // Reject the identity mismatch before `serial_matched` instead of
510                // mislabelling the request cmd or parsing the body as another API.
511                if !frame.header.is_push {
512                    let mismatched_entry = {
513                        let mut pending = pending_recv.lock();
514                        match pending.get(&frame.header.serial_no) {
515                            Some(entry) if entry.cmd_id != frame.header.cmd_id => {
516                                pending.remove(&frame.header.serial_no)
517                            }
518                            _ => None,
519                        }
520                    };
521                    if let Some(entry) = mismatched_entry {
522                        let mut progress = inbound_progress_recv.snapshot();
523                        progress.codec_category = Some("response_cmd_mismatch");
524                        let facts = pending_failure_facts(
525                            PendingFailureKind::CodecError,
526                            entry.cmd_id,
527                            entry.serial_no,
528                            entry.writer_admitted.load(Ordering::Acquire),
529                            &endpoint_fingerprint_recv,
530                            connection_generation,
531                            &progress,
532                        )
533                        .with_response_cmd_id(frame.header.cmd_id);
534                        emit_diagnostic(&diagnostic_sink_recv, &facts);
535                        if entry.tx.send(Err(facts)).is_err() {
536                            tracing::debug!(
537                                cmd_id = entry.cmd_id,
538                                response_cmd_id = frame.header.cmd_id,
539                                serial_no = entry.serial_no,
540                                "pending response receiver dropped before cmd mismatch delivery"
541                            );
542                        }
543                        continue;
544                    }
545                }
546
547                let session_key = session_key_for_recv.load_full();
548                let previous_session_key = previous_session_key_for_recv.load_full();
549                if decode_inbound_frame_body(
550                    &mut frame,
551                    session_key.as_deref().map(Vec::as_slice),
552                    previous_session_key.as_deref().map(Vec::as_slice),
553                ) == InboundFrameDecision::Drop
554                {
555                    if !frame.header.is_push {
556                        let mut progress = inbound_progress_recv.snapshot();
557                        progress.codec_category = Some("decode_error");
558                        if let Some(entry) = pending_recv.lock().remove(&frame.header.serial_no) {
559                            trace_tcp_login_transport_stage(
560                                entry.cmd_id,
561                                entry.serial_no,
562                                TcpLoginTransportStage::SerialMatched,
563                            );
564                            let facts = pending_failure_facts(
565                                PendingFailureKind::CodecError,
566                                entry.cmd_id,
567                                entry.serial_no,
568                                entry.writer_admitted.load(Ordering::Acquire),
569                                &endpoint_fingerprint_recv,
570                                connection_generation,
571                                &progress,
572                            );
573                            emit_diagnostic(&diagnostic_sink_recv, &facts);
574                            if entry.tx.send(Err(facts)).is_err() {
575                                tracing::debug!(
576                                    cmd_id = entry.cmd_id,
577                                    serial_no = entry.serial_no,
578                                    "pending response receiver dropped before inbound decode failure delivery"
579                                );
580                            }
581                        } else {
582                            let facts = pending_failure_facts(
583                                PendingFailureKind::UnmatchedSerial,
584                                frame.header.cmd_id,
585                                frame.header.serial_no,
586                                false,
587                                &endpoint_fingerprint_recv,
588                                connection_generation,
589                                &progress,
590                            );
591                            emit_diagnostic(&diagnostic_sink_recv, &facts);
592                        }
593                    }
594                    continue;
595                }
596
597                // 判断是否为推送帧:
598                // 1. flags.push_ == 1 (标准推送)
599                // 2. flags.push_ == 0 但 serial_no == 0 (后端首次订阅快照,
600                //    以 Reply 形式发送, 如 CMD6212 的初始摆盘/报价)
601                let is_push = frame.header.is_push
602                    || (frame.header.serial_no == 0 && !pending_recv.lock().contains_key(&0));
603                if is_push {
604                    tracing::debug!(
605                        cmd_id = frame.header.cmd_id,
606                        body_len = frame.body.len(),
607                        is_push = frame.header.is_push,
608                        is_compressed = frame.header.is_compressed,
609                        reserved = ?frame.header.reserved,
610                        "backend push received"
611                    );
612                    push_callback(
613                        connection_generation,
614                        frame.header.cmd_id,
615                        frame.body,
616                        SystemTime::now(),
617                    );
618                } else {
619                    let entry = pending_recv.lock().remove(&frame.header.serial_no);
620                    if let Some(entry) = entry {
621                        trace_tcp_login_transport_stage(
622                            entry.cmd_id,
623                            entry.serial_no,
624                            TcpLoginTransportStage::SerialMatched,
625                        );
626                        if let Err(Ok(_frame)) = entry.tx.send(Ok(frame)) {
627                            tracing::debug!(
628                                cmd_id = entry.cmd_id,
629                                serial_no = entry.serial_no,
630                                "backend response receiver dropped before frame delivery"
631                            );
632                        }
633                    } else {
634                        let progress = inbound_progress_recv.snapshot();
635                        let facts = pending_failure_facts(
636                            PendingFailureKind::UnmatchedSerial,
637                            frame.header.cmd_id,
638                            frame.header.serial_no,
639                            false,
640                            &endpoint_fingerprint_recv,
641                            connection_generation,
642                            &progress,
643                        );
644                        emit_diagnostic(&diagnostic_sink_recv, &facts);
645                    }
646                }
647            };
648            // 连接断开 — 主动回复所有 pending 请求 (C++ OnDisConnectRelpyAll)
649            claim_termination_and_fail_all_pending(
650                &termination_lifecycle_recv,
651                &connected_recv,
652                &connected_tx_recv,
653                &shutdown_tx_recv,
654                &pending_recv,
655                &termination,
656            );
657        });
658
659        // 发送任务
660        tokio::spawn(async move {
661            loop {
662                if termination_lifecycle_send.is_terminated() {
663                    break;
664                }
665                let next_cmd = tokio::select! {
666                    biased;
667                    _ = shutdown_rx_send.changed() => {
668                        break;
669                    }
670                    cmd = cmd_rx.recv() => cmd,
671                };
672                let Some(cmd) = next_cmd else {
673                    break;
674                };
675                let Some(writer_activity) = termination_lifecycle_send.try_begin_writer() else {
676                    break;
677                };
678
679                match cmd {
680                    BackendCmd::Send {
681                        frame,
682                        writer_admitted,
683                    } => {
684                        // Receiving the command proves queue admission completed. Set the
685                        // shared fact before touching the sink so a same-poll send failure
686                        // cannot race the request task's post-send store.
687                        if let Some(writer_admitted) = writer_admitted {
688                            writer_admitted.store(true, Ordering::Release);
689                            trace_tcp_login_transport_stage(
690                                frame.header.cmd_id,
691                                frame.header.serial_no,
692                                TcpLoginTransportStage::WriterAdmitted,
693                            );
694                        }
695                        if sink.send(frame).await.is_err() {
696                            drop(writer_activity);
697                            let termination = ConnectionTerminationFacts {
698                                kind: PendingFailureKind::SendError,
699                                endpoint_fingerprint: endpoint_fingerprint_send.clone(),
700                                connection_generation,
701                                progress: inbound_progress_send.snapshot(),
702                                diagnostic_sink: diagnostic_sink_send.clone(),
703                            };
704                            claim_termination_and_fail_all_pending(
705                                &termination_lifecycle_send,
706                                &connected_send,
707                                &connected_tx_send,
708                                &shutdown_tx_send,
709                                &pending_send,
710                                &termination,
711                            );
712                            break;
713                        }
714                    }
715                }
716            }
717        });
718
719        Self {
720            serial_no: AtomicU32::new(0),
721            sec_data: AtomicU32::new(1),
722            outbound_order: AsyncMutex::new(()),
723            connected,
724            connected_tx,
725            session_key, // 与接收任务共享同一个 Arc
726            previous_session_key,
727            cmd_tx,
728            pending,
729            termination_lifecycle,
730            endpoint_fingerprint,
731            connection_generation,
732            inbound_progress,
733            diagnostic_sink,
734            shutdown_tx,
735            user_id: AtomicU32::new(0),
736            client_ip: Mutex::new(String::new()),
737            protocol_identity,
738            client_ver: Self::CLIENT_VER_FTGTW,
739        }
740    }
741
742    pub const fn protocol_identity(&self) -> BackendProtocolIdentity {
743        self.protocol_identity
744    }
745
746    /// 设置 session key(登录成功后调用)。接受变长字节,16/24/32 分别对应
747    /// AES-128/192/256。对齐 C++ `Logger::session_key_` 是 `std::string`,
748    /// 长度取决于服务端下发的 `RspEncryptData.session_key` 字段原始长度。
749    pub fn set_session_key(&self, key: Vec<u8>) {
750        let previous = self.session_key.swap(Some(Arc::new(key)));
751        self.previous_session_key.store(previous);
752    }
753
754    /// 设置 sec_data 初始值(登录成功后调用)
755    pub fn set_sec_data(&self, val: u32) {
756        self.sec_data.store(val, Ordering::Relaxed);
757    }
758
759    /// 应用登录响应里的 sec_data。
760    ///
761    /// C++ 只有在 `RspEncryptData.has_sec_data()` 时更新 `secure_num_`;
762    /// 字段缺失时保留当前计数,避免 reconnect/login 响应裁剪时把安全数回退。
763    ///
764    /// Ref: `f3c/FTlogin/Src/ftlogin/login/logger.cpp:553-555`.
765    pub fn apply_login_sec_data(&self, val: Option<u32>) {
766        if let Some(val) = val {
767            self.set_sec_data(val);
768        }
769    }
770
771    /// 设置登录响应里的客户端外网 IP。
772    pub fn set_client_ip(&self, ip: String) {
773        *self.client_ip.lock() = ip;
774    }
775
776    /// 读取登录响应里的客户端外网 IP。
777    pub fn client_ip(&self) -> String {
778        self.client_ip.lock().clone()
779    }
780
781    /// Immutable identity of this transport instance, included with every
782    /// backend push so consumers can reject delivery from a retired route.
783    pub fn connection_generation(&self) -> u64 {
784        self.connection_generation
785    }
786
787    #[cfg(any(test, feature = "test-util"))]
788    pub fn pause_writer_admission_for_test(&self) -> LifecyclePauseHook {
789        let hook = LifecyclePauseHook::default();
790        *self.termination_lifecycle.admission_publish_hook.lock() = Some(hook.clone());
791        hook
792    }
793
794    pub fn is_connected(&self) -> bool {
795        self.connected.load(Ordering::Acquire)
796    }
797
798    pub(crate) fn has_session_key(&self) -> bool {
799        self.session_key
800            .load_full()
801            .is_some_and(|key| !key.is_empty())
802    }
803
804    /// Subscribe to low-level connection liveness changes.
805    ///
806    /// C++ FTLogin publishes a connection-closed event immediately
807    /// (`GTWCmdAndPushReply::OMEvProc_ConnClosed`) and lets the gateway enter
808    /// reconnecting state without waiting for a later business request. Rust
809    /// uses this watch channel to wake the reconnect monitor as soon as the
810    /// recv/send task marks the TCP channel disconnected.
811    pub fn subscribe_connection_state(&self) -> watch::Receiver<bool> {
812        self.connected_tx.subscribe()
813    }
814
815    /// Mark this connection disconnected so owner-side reconnect loops stop
816    /// routing business requests through a stale session.
817    ///
818    /// Used when a higher-level channel event invalidates the session semantics
819    /// even if the TCP socket has not closed yet. C++ channel state transitions
820    /// publish disconnect/re-establish events; Rust exposes the same liveness
821    /// edge through the existing connection-state watch channel.
822    pub fn mark_disconnected_for_reconnect(&self, reason: &'static str) {
823        tracing::warn!(
824            reason,
825            "marking backend connection disconnected to trigger reconnect"
826        );
827        let termination = ConnectionTerminationFacts {
828            // This is a local lifecycle invalidation, not peer EOF, codec
829            // failure, or writer failure. It shares the approved local
830            // shutdown reason with `shutdown_tx` termination.
831            kind: PendingFailureKind::Shutdown,
832            endpoint_fingerprint: self.endpoint_fingerprint.clone(),
833            connection_generation: self.connection_generation,
834            progress: self.inbound_progress.snapshot(),
835            diagnostic_sink: self.diagnostic_sink.clone(),
836        };
837        claim_termination_and_fail_all_pending(
838            &self.termination_lifecycle,
839            &self.connected,
840            &self.connected_tx,
841            &self.shutdown_tx,
842            &self.pending,
843            &termination,
844        );
845    }
846}
847
848#[cfg(test)]
849mod tests;