Rust开发SSH客户端方案推荐
学习笔记作者:admin日期:2025-10-07点击:19
摘要:本文总结了使用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"
### 2. `main.rs`
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())),
)
}
### 3. `ssh_session.rs`(简化版)
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)
}
}