GitHub 主页
我写了四十多年的代码。我刚开始编程的时候,打孔卡还是主流,互联网还只是大学实验室里一个遥不可及的梦想。我见证了无数语言和框架的兴衰起落,如同王朝更迭。我曾驾驭过技术的浪潮,也曾目睹它们在现实的海岸上撞得粉碎。如果说我从中学到了什么,那就是复杂性是真正的敌人。我指的不是那种解决棘手问题所必需的良性复杂性,而是那种有害的复杂性。那种框架为了无休止地堆砌功能,让你编写的样板代码比实际业务逻辑还要多的复杂性。
在过去的十年里,我感觉自己就快要被那种复杂性淹没了。每一个新项目,每一个新团队,故事都如出一辙。我们会选择一个流行的框架——Node.js 配 Express,Java 世界的 Spring Boot,或是 Python 领域的 Django。它们都承诺能实现快速开发。一开始,它们确实做到了。你可以在几分钟内启动一个“hello world”服务器。但随后,真正的工作才刚刚开始。
需要自定义中间件?你必须记住一个特定的函数签名,如果参数顺序搞错了,那真是叫天天不应。想要 WebSocket?那得再加一个库,一个新的依赖,以及又一层需要你去攻克的抽象。性能调优?准备好一头扎进配置选项、垃圾回收器调优和各种深奥命令行标志组成的迷宫里吧。我发现自己花在阅读框架文档和对抗其幕后“魔法”上的时间,比解决用户实际问题的时间还要多。我的代码变得沉重、臃肿、脆弱。它就像一个建立在无数 NPM 包基础上的纸牌屋。
我们来看一个简单的例子。一个基础的 Node.js Web 服务器,使用 Express 框架,包含几个路由,一个记录请求的中间件,以及一个 WebSocket 端点。这是一个相当普遍的需求。代码大概是这个样子。
const express = require('express');
const http = require('http');
const { WebSocketServer } = require('ws');const app = express();
const server = http.createServer(app);
const wss = new WebSocketServer({ server });// 记录每个请求的中间件
app.use((req, res, next) => {console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);next();
});// 一个简单的 REST 端点
app.get('/', (req, res) => {res.send('来自旧世界的问候!');
});// 另一个端点
app.get('/api/data', (req, res) => {res.json({ message: '这是一些数据' });
});// WebSocket 连接处理器
wss.on('connection', (ws) => {console.log('客户端已连接');ws.on('message', (message) => {console.log(`收到消息 => ${message}`);// 将消息回显ws.send(`你说了: ${message}`);});ws.on('close', () => {console.log('客户端已断开');});
});const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {console.log(`服务器正在监听端口 ${PORT}`);
});
看看这段代码。它不算太糟糕,但也绝谈不上优雅。我们一开始就引入了三个独立的模块。我们必须手动将 http
服务器与 express
应用拼接在一起,然后再与 WebSocketServer
拼接。中间件是一个带有 next
回调的函数,这种模式因为开发者忘记调用它而引发了无数的 bug。它能工作,但感觉……像是东拼西凑起来的。这就是我所说的臃肿。这是一种温水煮青蛙般的痛苦。
我曾一度以为,这就是现代 Web 开发的代价。我错了。几个月前,一位年轻的同事看到我备受挫折,悄悄建议我研究一下他个人项目里正在使用的一个基于 Rust 的框架。我当时很怀疑。所谓的“下一个伟大的技术”,我见得多了。但我很尊重这位同事,所以我决定试一试。这个框架叫做 hyperlane
。
我花了一个周末的时间研究它。然后,我感受到了多年未有的编程的火花。那感觉就像回家一样。它的设计简洁,API 直观,性能惊人。它没有试图成为一个无所不包的万能框架。它专注于做一件事——成为一个极其出色的 Web 服务器,并且它以一种我久未见到的优雅方式做到了这一点。
我决定用它来重现之前那个简单的 Express 服务器。下面是 hyperlane
的等效代码。
use hyperlane::*;async fn connected_hook(ctx: Context) {if !ctx.get_request().await.is_ws() {return;}let socket_addr: String = ctx.get_socket_addr_string().await;let _ = ctx.set_response_body(socket_addr).await.send_body().await;
}async fn request_middleware(ctx: Context) {let socket_addr: String = ctx.get_socket_addr_string().await;ctx.set_response_version(HttpVersion::HTTP1_1).await.set_response_status_code(200).await.set_response_header(SERVER, HYPERLANE).await.set_response_header(CONNECTION, KEEP_ALIVE).await.set_response_header(CONTENT_TYPE, TEXT_PLAIN).await.set_response_header("SocketAddr", socket_addr).await;
}async fn response_middleware(ctx: Context) {if ctx.get_request().await.is_ws() {return;}let _ = ctx.send().await;
}async fn root_route(ctx: Context) {let path: RequestPath = ctx.get_request_path().await;let response_body: String = format!("Hello hyperlane => {}", path);let cookie1: String = CookieBuilder::new("key1", "value1").http_only().build();let cookie2: String = CookieBuilder::new("key2", "value2").http_only().build();ctx.set_response_status_code(200).await.set_response_header(SET_COOKIE, cookie1).await.set_response_header(SET_COOKIE, cookie2).await.set_response_body(response_body).await;
}async fn ws_route(ctx: Context) {let key: RequestHeadersValueItem = ctx.try_get_request_header_back(SEC_WEBSOCKET_KEY).await.unwrap_or_default();let request_body: Vec<u8> = ctx.get_request_body().await;let _ = ctx.set_response_body(key).await.send_body().await;let _ = ctx.set_response_body(request_body).await.send_body().await;
}async fn sse_route(ctx: Context) {let _ = ctx.set_response_header(CONTENT_TYPE, TEXT_EVENT_STREAM).await.send().await;for i in 0..10 {let _ = ctx.set_response_body(format!("data:{}{}", i, HTTP_DOUBLE_BR)).await.send_body().await;}let _ = ctx.closed().await;
}async fn dynamic_route(ctx: Context) {let param: RouteParams = ctx.get_route_params().await;panic!("Test panic {:?}", param);
}async fn panic_hook(ctx: Context) {let error: Panic = ctx.try_get_panic().await.unwrap_or_default();let response_body: String = error.to_string();eprintln!("{}", response_body);let _ = std::io::Write::flush(&mut std::io::stderr());let content_type: String = ContentType::format_content_type_with_charset(TEXT_PLAIN, UTF8);let _ = ctx.set_response_version(HttpVersion::HTTP1_1).await.set_response_status_code(500).await.clear_response_headers().await.set_response_header(SERVER, HYPERLANE).await.set_response_header(CONTENT_TYPE, content_type).await.set_response_body(response_body).await.send().await;
}#[tokio::main]
async fn main() {let config: ServerConfig = ServerConfig::new().await;config.host("0.0.0.0").await;config.port(60000).await;config.enable_nodelay().await;config.http_buffer(4096).await;config.ws_buffer(4096).await;let server: Server = Server::from(config).await;server.panic_hook(panic_hook).await;server.connected_hook(connected_hook).await;server.prologue_upgrade_hook(request_middleware).await;server.request_middleware(request_middleware).await;server.response_middleware(response_middleware).await;server.route("/", root_route).await;server.route("/ws", ws_route).await;server.route("/sse", sse_route).await;server.route("/dynamic/{routing}", dynamic_route).await;server.route("/regex/{file:^.*$}", dynamic_route).await;let server_hook: ServerHook = server.run().await.unwrap_or_default();server_hook.wait().await;
}
这简直是天壤之别。所有功能都是内置的。WebSocket 和服务器发送事件(SSE)不是事后添加的补丁,而是一等公民。整个服务器通过一个统一、连贯的 Server
对象进行配置和运行。中间件和钩子(hook)只是接收一个 Context
对象的异步函数。再也没有需要记住去调用的 next
回调了。你只管……写你的代码。用于构建服务器和响应的流式 API 使用起来非常愉悦。它会引导你用正确的方式去构建应用。
至于性能呢?这根本不是一个公平的比较。一个在 Tokio 上运行的已编译的 Rust 二进制文件,其性能可以轻松秒杀像 JavaScript 这样需要即时编译和垃圾回收的语言。它只使用一小部分内存,就能在相同的硬件上处理多得多的并发连接。这不仅仅是理论上的基准测试,而是你能实际感受到的。响应更迅速,延迟更低,整个系统也更稳定。你再也不会在凌晨三点接到电话,说某个第三方库的内存泄漏导致服务器崩溃了。
对我来说,hyperlane
真正出彩的地方在于其可扩展性。它的钩子系统非常出色。你有用于客户端连接(connected_hook
)的钩子,有用于处理 panic(panic_hook
)的钩子,还有一个清晰、定义明确的中间件管道。它为你提供了这些精确的、外科手术般的切入点来添加功能。你不再需要为了增加一个简单的日志记录器而将整个应用程序包裹在层层中间件之中。你可以将你的逻辑精确地注入到它需要去的地方。这使得代码更清晰,更容易理解,也极大地提高了可维护性。
我感觉自己像是花了十年时间用乐高积木来建造摩天大楼,而现在,有人递给了我一套精密加工的工业级工具。hyperlane
不仅仅是又一个框架。它是一种哲学宣言。它坚信,你可以同时拥有性能、安全和世界级的开发者体验。这是对简约的回归,而我,作为其中的一员,绝不回头。
GitHub 主页