其实开发一个程序思路一直都是一致的,并不会因为是前端就会差/简单多少。再加上之前因为有「多端」这个「产品概念」之后,所以程序上一直在解决一个问题:如何将 Web 之外的逻辑固定下来?于是有了 Banshee
,下次细说
既然有一个框架了,并且也选择使用跟 Angular
类似的 root 级 DI 这种路线之后,开始有一个想法:
流向很清晰,这个时候有一个测试监控一体机是不是实现方便作用不小?
于是有了 Banshee Cockpit
,但它也跟 Banshee
一样,下次细说
又到了技术选型环节:早在三年前就尝试的 electron 方案 electron-react-koa-template(已归档),当时是为了直接在树莓派运行智能闹钟程序时顺便提供 API 以远程操控,所以让 electron
附带的 node
跑起来一个 koa
でも、选型依然是我目前的经典搭配:Tauri + Solid。那首先要解决的有两个问题:
- 还是一样,需要跑一个 server
- 因为需要收发
Banshee
的信息,所以需要WebSocket
TL;DR
- 你可以直接用
Rust
提供的基础库,但我用axum
- 集成起来很方便,麻烦的是
Rust
- 摆烂了,
Rust
对于内存的执着有种让人想一拳打爆屏幕的冲动
选个框架
原因还是跟三年前一样,从 NIO 开始是没必要的,集成一个服务端框架更方便使用。
Rust
现在「主流」的服务端框架有三种:
- actix-web
- rocket
- axum
actix-web
作为我最喜欢的框架(在该文章之后已经易位),有活肯定是第一个想到,就是不知道怎么塞进去比较可惜
更大的原因是因为 actix 和 tauri 使用了两套异步逻辑,即使让 actix 运行起来了效果也不好。GitHub 也有一个类似的相关讨论
而且还有一个我之前没发现的,作为客户端问题也很大的问题:这 B 东西体积有点大
Rocket
Rocket 集成起来非常简单,只需要在 tauri 的 main
程序挂一个 tokio
入口或者是 launch
,并且在 setup
把相关的上下文带进去,很轻松就完成
当我拿出香槟 🍾 打开的一瞬间,我发现有一个更大的问题:Rocket 不支持 WebSocket
…
直接抬走
axum
在 AI 时代之前获取信息全靠「漫反射」,但 AI 之后就不一样了
孤陋寡闻的我只知道两个框架,两个框架之后就没活了。但现在我可以打开之前跟我一起写小说的 Bing AI
,从那得知还有一个体积不大性能也不错的新秀 axum
。当我看到开发它的组织是 tokio-rs
,直接上了
跑个 server
跑起来确实也没什么感觉,虽然可以直接跟着当前线程执行,但我还是为了方便还是多开一个线程跑 server,毕竟不是 JavaScript
不阻塞
// create server
tauri::async_runtime::spawn(async move {
server::create_server(&main_window).await;
});
剩下的跟直接用 example
没有区别,直接跑个 Hello World 也就这样
async fn create_server() {
let app = Router::new().route("/", get(handler));
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
println!("listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
async fn handler() -> Html<&'static str> {
Html("<h1>Hello, World!</h1>")
}
因为我们的需求是跑一个 WebSocket
,按照 example 改改可以得到这个
pub async fn create_server(win: &Window) {
let app_state = AppState {
communication: Communication::new(win.clone()),
};
let server_app = Router::new()
.route("/cockpit-connect", get(ws_handler))
.with_state(Arc::new(app_state));
let port =
find_available_port().expect("No available port found in default, please provide a port.");
let addr = SocketAddr::from(([127, 0, 0, 1], port));
println!("Server running on port {}", port);
axum::Server::bind(&addr)
.serve(server_app.into_make_service())
.await
.unwrap();
}
async fn ws_handler(ws: WebSocketUpgrade, State(state): State<Arc<AppState>>) -> impl IntoResponse {
ws.on_upgrade(move |socket| handle_socket(socket, state))
}
async fn handle_socket(socket: WebSocket, state: Arc<AppState>) {
while let Some(msg) = socket.recv().await {
match msg {
// 这个需求不使用 text 传输
Ok(Message::Binary(data)) => {
println!("{:?}", data)
}
// 想用可以这么用
// Ok(Message::Text(data)) => {
// println!("{:?}", data)
// }
Ok(Message::Close(_)) => {
println!("{:?}", data)
}
Err(e) => {
println!("{:?}", data)
}
// 其他情况丢弃,函数是 () 返回不处理
_ => {}
}
}
}
rust 经典臭问题
到这会发现,如果使用了 socket.recv
还想在 socket.send
,socket
这个参数开始有「所有权问题」,你只能在socket.recv
的作用域内用socket.send
。这就相当抽象:只能回复是吧
好在这不是一个非常见问题,毕竟在不同作用域里处理信息和发送信息是一个非常常见的需求。搜了一圈,发现可以用 stream::StreamExt
和 SinkExt
拆分一个流
let (sender, mut receiver) = socket.split();
while let Some(msg) = receiver.next().await {
match msg {
// 这个需求不使用 text 传输
Ok(Message::Binary(data)) => {
println!("{:?}", data)
}
// 想用可以这么用
// Ok(Message::Text(data)) => {
// println!("{:?}", data)
// }
Ok(Message::Close(_)) => {
println!("{:?}", data)
}
Err(e) => {
println!("{:?}", data)
}
// 其他情况丢弃,函数是 () 返回不处理
_ => {}
}
}
现在看起来收发自由了,但是当进入到 receiver
时,整个应用会像冻结了一样,下面的逻辑都无法执行到,也就变成了:只要没收到信息,所有要发送的信息都发不出去…
好在这也不是一个非常见问题,因为好像除了 JavaScript
这种垃圾语言都是这样的 —— 都是同步阻塞类型
所以我暂时先这么做:将处理信息的逻辑送到另一条线程
spawn(async move {
while let Some(msg) = receiver.next().await {
match msg {
// 这个需求不使用 text 传输
Ok(Message::Binary(data)) => {
println!("{:?}", data)
}
// 想用可以这么用
// Ok(Message::Text(data)) => {
// println!("{:?}", data)
// }
Ok(Message::Close(_)) => {
println!("{:?}", data)
}
Err(e) => {
println!("{:?}", data)
}
// 其他情况丢弃,函数是 () 返回不处理
_ => {}
}
}
})
现在基本上做到了收发自由