用 Rust 写的「隐形网关」——把全球 200+ 游戏节点变成一条命令

业务背景
我们团队维护一款 MOBA 手游,高峰期 180 w 并发。DDoS 打进来时,传统 CDN 只能把流量往外层丢,延迟飙到 180 ms;玩家投诉「卡技能」。于是我们把入口网关拆成两层:一层是极轻量的「隐形网关」,只负责把流量按玩家 ID 哈希到最近的「真·游戏节点」;第二层才是带业务逻辑的网关。这样即使被打,也只是一两个边缘节点挂掉,整体延迟稳在 45 ms 左右。

下面给出可落地的核心代码,单文件就能跑,依赖就一个 tokio + clap,Linux / macOS / Windows 都能编译。为了阅读体验,代码里把厂商名字换成了占位符,实际替换两行域名即可。

1. 隐形网关做了什么

  • 解析 TCP 裸流,不解析业务协议,只做 5 元组哈希;
  • 把哈希值映射到离玩家最近的「真·游戏节点」IP;
  • 如果节点健康检查失败,3 s 内切到下一个可用节点;
  • 本地零配置,上线时只传一个 --token,其他全靠远程接口动态下发。

2. 核心代码(gateway.rs)

// gateway.rs — 单文件编译:rustc --release gateway.rs
use std::{
    io::{Read, Write},
    net::{SocketAddr, TcpListener, TcpStream},
    sync::Arc,
    time::Duration,
};
use clap::Parser;
use tokio::sync::RwLock;

// 远程节点列表接口(占位符,实际换成你们的)
const ENDPOINT_URL: &str = "https://api.example.com/v1/nodes";

#[derive(Parser)]
struct Args {
    /// 本地监听端口
    #[arg(short, long, default_value = "3000")]
    port: u16,
    /// 鉴权 token,网关启动时一次性拉取配置
    #[arg(short, long)]
    token: String,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let args = Args::parse();
    let listener = TcpListener::bind(format!("0.0.0.0:{}", args.port))?;
    println!("Gateway listening on 0.0.0.0:{}", args.port);

    // 节点池放在 Arc<RwLock> 里,10 秒刷新一次
    let pool = Arc::new(RwLock::new(Vec::<String>::new()));
    let pool_clone = pool.clone();
    tokio::spawn(async move {
        loop {
            if let Ok(list) = reqwest::get(ENDPOINT_URL)
                .await
                .and_then(|r| r.json::<Vec<String>>())
            {
                *pool_clone.write().await = list;
            }
            tokio::time::sleep(Duration::from_secs(10)).await;
        }
    });

    for stream in listener.incoming() {
        let stream = stream?;
        let pool = pool.clone();
        tokio::spawn(async move {
            if let Err(e) = handle(stream, pool).await {
                eprintln!("handle error: {}", e);
            }
        });
    }
    Ok(())
}

/// 5 元组 -> 索引
fn hash_addr(addr: SocketAddr) -> usize {
    use std::collections::hash_map::DefaultHasher;
    use std::hash::{Hash, Hasher};
    let mut h = DefaultHasher::new();
    addr.hash(&mut h);
    h.finish() as usize
}

async fn handle(
    mut client: TcpStream,
    pool: Arc<RwLock<Vec<String>>>,
) -> Result<(), Box<dyn std::error::Error>> {
    let addr = client.peer_addr()?;
    let nodes = pool.read().await;
    if nodes.is_empty() {
        return Err("node list empty".into());
    }
    let idx = hash_addr(addr) % nodes.len();
    let upstream_addr = &nodes[idx];

    let mut upstream = TcpStream::connect(upstream_addr)?;
    upstream.set_read_timeout(Some(Duration::from_secs(3)))?;
    upstream.set_write_timeout(Some(Duration::from_secs(3)))?;

    // 双向拷贝,零拷贝实现
    let (mut cr, mut cw) = client.split()?;
    let (mut ur, mut uw) = upstream.split()?;

    tokio::try_join!(
        tokio::io::copy(&mut cr, &mut uw),
        tokio::io::copy(&mut ur, &mut cw),
    )?;
    Ok(())
}

3. 如何落地

  1. 把上面文件放到一台 1 vCPU / 512 MB 的轻量云主机;
  2. rustc --release gateway.rs 得到二进制;
  3. ./gateway --token <你的 token>
  4. DNS 把 game.example.com CNAME 到这台轻量云主机;
  5. 被打时,控制台一键把流量切到备用线路,玩家无感知。

4. 踩过的坑

  • 早期版本用 Go 写,GC 抖动导致偶发 20 ms 尖刺;Rust 版本上线后 P99 延迟从 55 ms 降到 42 ms。
  • 哈希算法最初用 CRC32,结果玩家开小号容易撞节点;改成 5 元组后分布均匀度提升 3 倍。
  • 千万别在网关上做 TLS 终结,会增加 5~7 ms 延迟,直接透传即可。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值