Rust开发SSH客户端方案推荐
学习笔记作者:admin日期:2025-10-07点击:104
摘要:本文总结了使用Rust语言结合russh库开发跨平台SSH客户端的推荐方案,包括UI框架选择、技术栈建议和示例代码。
项目目标总结
| 需求 | 要求 | |------|------| | 开发语言 | Rust | | SSH 库 | `russh`(纯 Rust,异步,安全) | | 跨平台 | macOS / Windows / Linux | | UI 界面 | 美观、现代、响应式 | | 开发效率 | 尽量“低代码”、使用流行框架 | | 功能 | 连接管理、终端显示、SFTP(可选) |推荐整体技术栈
| 模块 | 推荐方案 | 理由 | |------|---------|------| | **SSH 核心** | [`russh`](https://github.com/warp-rs/russh) | 纯 Rust,异步,安全,支持密钥、密码、跳板机等 | | **UI 框架** | [`tao` + `wry` + `slint`] 或 [`eframe` (egui)] | 跨平台桌面 UI,Rust 原生,低代码友好 | | **终端渲染** | 自研或集成 `xterm.js`(Web) / `copypasta`(本地) | 显示 SSH 终端输出 | | **配置管理** | `serde` + `serde_json` + `directories` | 存储连接配置 | | **异步运行时** | `tokio` | `russh` 依赖异步运行时 |推荐架构方案(推荐使用 eframe + egui)
### ✅ 方案一:`eframe` + `egui`(推荐!低代码、跨平台、快速开发) > **最适合“低代码 + 快速原型 + 美观 UI”需求** #### ? 技术栈 - `eframe`: egui 的桌面应用框架(支持 Web 和 Native) - `egui`: 声明式 UI 框架,类似 React,Rust 原生 - `russh`: SSH 客户端 - `tokio`: 异步运行时 - `tui`: 可选,用于本地终端模拟(或直接用 `egui` 绘制终端) - `serde` / `config_dir`: 配置持久化 #### ✅ 优点 - UI 开发极快,**接近“低代码”体验** - 跨平台完美支持(Win/macOS/Linux) - 主题可定制,支持深色模式 - 社区活跃,有大量示例 - 可打包为独立二进制(无浏览器依赖) #### ? 示例项目结构 ```bash ssh-client/ ├── src/ │ ├── main.rs # eframe 入口 │ ├── app.rs # 主应用逻辑 │ ├── ssh_session.rs # russh 连接管理 │ └── config.rs # 连接配置存储 ├── assets/ # 图标、配置文件 ├── Cargo.toml ``` #### ?️ UI 设计建议 - 左侧:连接列表(树状或列表) - 中部:终端显示区域(用 `egui::TextEdit::multiline` 模拟终端) - 顶部:连接按钮、保存配置 - 支持多标签页(可选 `egui_extras::Strip` 或自定义 tab)推荐选择:`eframe + egui + russh`
快速开始示例(eframe + russh)
### 1. `Cargo.toml`[package]
name = "rusty-ssh-client"
version = "0.1.0"
edition = "2021"
[dependencies]
eframe = "0.24"
egui = "0.24"
russh = "0.34"
russh-clipboard = "0.3"
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
directories = "5.0"
futures-util = "0.3"
use eframe::egui;
use std::sync::{Arc, Mutex};
use tokio::runtime::Runtime;
mod ssh_session;
#[derive(Default)]
struct SshClientApp {
    address: String,
    username: String,
    password: String,
    output: String,
    is_connected: bool,
    session: Option>>>,
    rt: Option,
}
impl eframe::App for SshClientApp {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        egui::CentralPanel::default().show(ctx, |ui| {
            ui.heading("Rusty SSH Client");
            ui.horizontal(|ui| {
                ui.label("Address:");
                ui.text_edit_singleline(&mut self.address);
            });
            ui.horizontal(|ui| {
                ui.label("Username:");
                ui.text_edit_singleline(&mut self.username);
            });
            ui.horizontal(|ui| {
                ui.label("Password:");
                ui.text_edit_singleline(&mut self.password);
            });
            if ui.button("Connect").clicked() && !self.is_connected {
                if let Ok(rt) = tokio::runtime::Runtime::new() {
                    let session = Arc::new(Mutex::new(None));
                    let session_clone = session.clone();
                    rt.spawn(async move {
                        if let Ok(mut ssh) = ssh_session::connect(&self.address, &self.username, &self.password).await {
                            if let Ok(output) = ssh.execute_command("uname -a").await {
                                let mut guard = session_clone.lock().unwrap();
                                *guard = Some(ssh);
                                // 这里可以发消息回 UI(建议用 channel)
                            }
                        }
                    });
                    self.rt = Some(rt);
                    self.session = Some(session);
                    self.is_connected = true;
                }
            }
            ui.add(egui::TextEdit::multiline(&mut self.output).desired_rows(10));
        });
    }
}
fn main() -> Result<(), eframe::Error> {
    env_logger::init();
    let options = eframe::NativeOptions::default();
    eframe::run_native(
        "SSH Client",
        options,
        Box::new(|_cc| Box::new(SshClientApp::default())),
    )
}
  use russh::{self, Client, Config};
use russh::client::{Msg, Session};
use std::net::TcpStream;
use std::sync::Arc;
use tokio::net::TcpStream as TokioTcp;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
pub struct SshSession {
    pub session: Session,
}
pub async fn connect(
    addr: &str,
    user: &str,
    password: &str,
) -> Result> {
    let config = Arc::new(Config::default());
    let mut session = Session::connect(config, addr, tokio::io::stdout()).await?;
    session.authenticate_password(user, password).await?;
    Ok(SshSession { session })
}
impl SshSession {
    pub async fn execute_command(&mut self, cmd: &str) -> Result> {
        let mut channel = self.session.channel_open_session().await?;
        channel.exec(true, cmd).await?;
        let mut output = String::new();
        channel.read_to_string(&mut output).await?;
        Ok(output)
    }
}