其实开发一个程序思路一直都是一致的,并不会因为是前端就会差/简单多少。再加上之前因为有「多端」这个「产品概念」之后,所以程序上一直在解决一个问题:如何将 Web 之外的逻辑固定下来?于是有了 Banshee,下次细说

既然有一个框架了,并且也选择使用跟 Angular 类似的 root 级 DI 这种路线之后,开始有一个想法:

流向很清晰,这个时候有一个测试监控一体机是不是实现方便作用不小?

于是有了 Banshee Cockpit,但它也跟 Banshee 一样,下次细说

又到了技术选型环节:早在三年前就尝试的 electron 方案 electron-react-koa-template(已归档),当时是为了直接在树莓派运行智能闹钟程序时顺便提供 API 以远程操控,所以让 electron 附带的 node 跑起来一个 koa

でも、选型依然是我目前的经典搭配:Tauri + Solid。那首先要解决的有两个问题:

  1. 还是一样,需要跑一个 server
  2. 因为需要收发 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.sendsocket这个参数开始有「所有权问题」,你只能在socket.recv的作用域内用socket.send。这就相当抽象:只能回复是吧

好在这不是一个非常见问题,毕竟在不同作用域里处理信息和发送信息是一个非常常见的需求。搜了一圈,发现可以用 stream::StreamExtSinkExt 拆分一个流

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)
            }
            // 其他情况丢弃,函数是 () 返回不处理
            _ => {}
        }
    }
})

现在基本上做到了收发自由

References