feat(proxy-tunnel): 实现隧道连接池与 TCP 底层优化

Rust 端:
- 支持每个 server 多条并行 WebSocket 连接 (tunnel_connections 配置)
- 手动控制 TCP 连接: connect/handshake 超时、keepalive、NODELAY (socket2)
- 预构建共享 TLS ClientConfig 避免每次重连重新解析根证书
- 增加 stale timeout 检测无数据连接,智能 backoff 按连接存活时长重置
- 每条连接独立 reconnect 计数器,仅主连接 (conn_idx=0) 发送心跳
- dispatcher 的错误帧和 PONG 改用 try_send 避免阻塞读循环
- stream_handler 增加 frame 发送超时保护防止写阻塞

Python 端:
- TunnelManager 改为连接池,按 least-loaded 策略分配请求
- handle_incoming_frame 按连接实例路由,unregister 精确移除单条连接
- 心跳和 PONG 回复改为 fire-and-forget 避免阻塞主读循环
- send_frame 增加超时保护防止 TCP 写阻塞级联
- WebSocket 先 accept 再认证,auth 加超时
- 调整 idle timeout (90s) 和 ping 间隔 (15s)
This commit is contained in:
fawney19
2026-02-27 21:25:42 +08:00
parent a174cf1b02
commit ee356c5e6e
13 changed files with 600 additions and 135 deletions

View File

@@ -21,6 +21,11 @@ use super::writer::FrameSender;
/// Maximum response body chunk size per frame (32 KB).
const MAX_CHUNK_SIZE: usize = 32 * 1024;
/// Timeout for sending a single frame to the writer channel.
/// If the writer is congested (TCP backpressure), we abandon the stream
/// rather than blocking indefinitely and exhausting the stream pool.
const FRAME_SEND_TIMEOUT: Duration = Duration::from_secs(30);
/// Handle a single stream: receive body, execute upstream, send response.
pub async fn handle_stream(
state: Arc<AppState>,
@@ -39,6 +44,22 @@ pub async fn handle_stream(
server.metrics.record_request(start.elapsed());
}
/// Send a frame to the writer with a timeout. Returns false if send failed.
async fn send_frame(tx: &FrameSender, frame: Frame) -> bool {
match tokio::time::timeout(FRAME_SEND_TIMEOUT, tx.send(frame)).await {
Ok(Ok(())) => true,
Ok(Err(_)) => {
// Channel closed (writer exited)
false
}
Err(_) => {
// Timeout — writer is congested
warn!("frame send timeout (writer congested), abandoning stream");
false
}
}
}
async fn handle_stream_inner(
state: &AppState,
server: &ServerContext,
@@ -190,14 +211,14 @@ async fn handle_stream_inner(
headers: resp_headers,
};
let meta_json = serde_json::to_vec(&resp_meta).unwrap_or_default();
let _ = frame_tx
.send(Frame::new(
stream_id,
MsgType::ResponseHeaders,
0,
meta_json,
))
.await;
if !send_frame(
frame_tx,
Frame::new(stream_id, MsgType::ResponseHeaders, 0, meta_json),
)
.await
{
return;
}
// Stream response body
let mut stream = response.bytes_stream();
@@ -205,19 +226,28 @@ async fn handle_stream_inner(
match chunk_result {
Ok(chunk) => {
if chunk.len() <= MAX_CHUNK_SIZE {
// 大多数 chunk 无需分割,直接零拷贝发送
let _ = frame_tx
.send(Frame::new(stream_id, MsgType::ResponseBody, 0, chunk))
.await;
if !send_frame(
frame_tx,
Frame::new(stream_id, MsgType::ResponseBody, 0, chunk),
)
.await
{
return;
}
} else {
// 超大 chunk 按 MAX_CHUNK_SIZE 分割(使用 Bytes::slice 避免拷贝)
// Split oversized chunks
let mut offset = 0;
while offset < chunk.len() {
let end = (offset + MAX_CHUNK_SIZE).min(chunk.len());
let slice = chunk.slice(offset..end);
let _ = frame_tx
.send(Frame::new(stream_id, MsgType::ResponseBody, 0, slice))
.await;
if !send_frame(
frame_tx,
Frame::new(stream_id, MsgType::ResponseBody, 0, slice),
)
.await
{
return;
}
offset = end;
}
}
@@ -231,27 +261,32 @@ async fn handle_stream_inner(
}
// Send STREAM_END
let _ = frame_tx
.send(Frame::new(
let _ = send_frame(
frame_tx,
Frame::new(
stream_id,
MsgType::StreamEnd,
flags::END_STREAM,
Bytes::new(),
))
.await;
),
)
.await;
debug!(stream_id, status, "stream completed");
}
async fn send_error(tx: &FrameSender, stream_id: u32, msg: &str) {
let _ = tx
.send(Frame::new(
// Error frames use best-effort delivery — don't block if writer is congested
let _ = send_frame(
tx,
Frame::new(
stream_id,
MsgType::StreamError,
0,
Bytes::from(msg.to_string()),
))
.await;
),
)
.await;
}
fn decompress_gzip(data: &[u8]) -> Result<Bytes, std::io::Error> {