一、背景及定义
工作中需要经常使用SSH远程连接设备,每次都需要win+R,cmd,输入一长串ssh user@192.168.1.XX命令。想到可以做一个脚本工具,像应用那样双击即用,能快速连接到设备,无需过多输入,然后操作。
定义:SSH(Secure Shell)是一种网络协议和安全工具,用于在不安全的网络上安全地进行远程登录和数据传输。它提供了加密的通信通道,以保护敏感数据的机密性和完整性。SSH协议支持多种应用,其中最常用的是
SSH(远程登录)、SCP(Secure Copy Protocol)和SFTP(SSH File Transfer Protocol)。
二、主流SSH工具情况
市面上常见的ssh工具有Xshell、PUTTY、Finalshell、Mobaxterm、VSCode等,这些工具都可用于连接,各有自己的优势和特长。
1.Xshell
Xshell易用性好,功能强大,可填写邮箱申请家庭和教育版。
2. PUTTY
PUTTY优点是小而轻巧,也很容易使用,只有几兆大小,虽然界面可能看起来一般。
3.Finalshell
Finalshell是国产的免费开源ssh工具,必需要支援一波,可以命令也可传输文件,第一次上手需要一丢丢时间,熟悉之后很好用。
4. Mobaxterm
Mobaxterm优点是可以用于设备的群体控制,简单意思讲就是可以多开,同时在多个终端输入相同内容Multipaste。
5.VScode
VScode优点是拓展性强,使用ssh连接需要安装插件,同时提一嘴,设备端也需要安装openssh-server。具体使用方法就不详细介绍了,可以参考以下链接中的内容使用。
VScode使用ssh远程连接Ubuntu【超简单且详细步骤】_vscode ssh-CSDN博客
每种ssh工具都有自己的优势,而我想要的工具是:一方面能够快速连接远程设备,同时不需要过多的输入,前面网段在同一WIFI下一般不会变,也就是只输入最后一个数字就能连接,在断联后也能够快速重连。
三、SSH连接工具基本情况
1.基于python开发,部署在本地windows终端,使用pyinstaller封装,双击即用,补齐IP地址最后一段即可连接。
2.支持修改用户名和固定网段
3.断联后,支持按照原来的ip地址快速重连,或重新开始输入新的IP连接,或只打开普通的cmd终端,或关闭终端
4.第一次连接新设备也无需输入yes
四、使用步骤
1.双击SSH连接工具v1.0.2.exe
2.修改示例用户名和固定网段
(第一次使用需要修改示例用户名和固定网段)(后期使用不用每次修改)
示例用户名为abc,改为自己想连接的设备用户名,比如我想要远程的用户名为hhh
按1,输入新用户名,可以看到连接命令里的abc已经变为了hhh
按2修改固定网段,例如我的固定网段为192.168.77.XX
3.回车开始连接
补齐IP地址最后一段,例如我这里IP为102(可以设置设备开机后,自动通过企业微信群机器人播报自己的IP地址,参考doubao等AI助手)
输入密码
连接成功
##.连接断开
按1可以快速重连刚才的主机
按2,可以开始连接其他主机
按3,开启cmd终端
按q关闭终端
五、源代码
exe可执行文件地址https://wwbav.lanzouu.com/i7q6B3s4lktg 密码:h9mz
import json import subprocess import sys import time from pathlib import Path from typing import Any, Dict, Optional # --------------------------------------------------------------------------- # 终端样式(ANSI) # --------------------------------------------------------------------------- class _T: RST = "\033[0m" BOLD = "\033[1m" CYAN = "\033[36m" GREEN = "\033[32m" YELLOW = "\033[33m" RED = "\033[31m" BLUE = "\033[34m" WHITE = "\033[97m" def _enable_ansi_if_windows() -> None: if sys.platform != "win32": return try: import ctypes handle = ctypes.windll.kernel32.GetStdHandle(-11) mode = ctypes.c_uint32() if ctypes.windll.kernel32.GetConsoleMode(handle, ctypes.byref(mode)) == 0: return new_mode = mode.value | 0x0004 ctypes.windll.kernel32.SetConsoleMode(handle, new_mode) except Exception: pass def _clear_screen() -> None: print("\033[3J\033[2J\033[H", end="", flush=True) def _hr(char: str = "─", width: int = 65) -> None: print(f"{_T.WHITE}{char * width}{_T.RST}") def _blank(n: int = 1) -> None: for _ in range(n): print() def _title(text: str) -> None: _blank(1) print(f"{_T.BOLD}{_T.CYAN}{'═' * 65}{_T.RST}") print(f"{_T.BOLD}{_T.CYAN} {text}{_T.RST}") print(f"{_T.BOLD}{_T.CYAN}{'═' * 65}{_T.RST}") _blank(1) def _kv(label: str, value: str, indent: str = " ") -> None: print(f"{indent}{_T.WHITE}{label}{_T.RST} {_T.GREEN}{value}{_T.RST}") def _menu_line(key: str, desc: str, indent: str = " ") -> None: print(f"{indent}{_T.YELLOW}{key}{_T.RST} {_T.WHITE}{desc}{_T.RST}") def _hint(text: str, indent: str = " ") -> None: print(f"{indent}{_T.WHITE}{text}{_T.RST}") def _ok(text: str) -> None: print(f" {_T.GREEN}{text}{_T.RST}") def _warn(text: str) -> None: print(f" {_T.YELLOW}{text}{_T.RST}") def _err(text: str) -> None: print(f" {_T.RED}{text}{_T.RST}") def _prompt(text: str) -> str: print(f"{_T.BOLD}{_T.BLUE}{text}{_T.RST}", end="", flush=True) return input() # --------------------------------------------------------------------------- # 配置:EXE 兼容 # --------------------------------------------------------------------------- DEFAULT_CONFIG = { "remote_user": "lab", "ip_prefix": "192.168.77.", } def get_config_path() -> Path: if getattr(sys, 'frozen', False): base = Path(sys.executable).parent else: base = Path(__file__).parent return base / "ssh_config.json" def load_config() -> Dict[str, str]: cfg = DEFAULT_CONFIG.copy() path = get_config_path() if path.exists(): try: data = json.loads(path.read_text(encoding="utf-8")) for k in cfg: if isinstance(data.get(k), str) and data[k].strip(): cfg[k] = data[k].strip() except: pass return cfg def save_config(cfg: dict) -> None: path = get_config_path() payload = {k: str(cfg.get(k, DEFAULT_CONFIG[k])).strip() for k in DEFAULT_CONFIG} path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8") # --------------------------------------------------------------------------- # 主逻辑 # --------------------------------------------------------------------------- class RobotSSHConnector: def __init__(self): self.config = load_config() self.last_host = None self.last_user = None def _normalize_prefix(self): prefix = self.config["ip_prefix"] if prefix and not prefix.endswith("."): prefix = prefix + "." self.config["ip_prefix"] = prefix return prefix def _preview_command(self) -> str: user = self.config["remote_user"] prefix = self._normalize_prefix() return f"ssh -o StrictHostKeyChecking=accept-new {user}@{prefix}<主机号>" def settings_menu(self) -> None: while True: self._normalize_prefix() _title("SSH 连接工具 · 设置") _kv("远程用户名", self.config["remote_user"]) _kv("IP 网段前缀", self.config["ip_prefix"]) _blank(1) _kv("连接命令", self._preview_command()) _blank(1) _hr() _menu_line("回车", "开始连接") _menu_line("1", "修改用户名") _menu_line("2", "修改网段") _hr() _blank(1) choice = _prompt("选择(1/2/回车):").strip() if choice == "": save_config(self.config) break if choice == "1": u = _prompt("新用户名:").strip() if u: self.config["remote_user"] = u save_config(self.config) _ok("已保存") else: _warn("未修改") time.sleep(0.4) elif choice == "2": p = _prompt("网段前缀(如 192.168.77.):").strip() if p: if not p.endswith("."): p += "." self.config["ip_prefix"] = p save_config(self.config) _ok("已保存") else: _warn("未修改") time.sleep(0.4) else: _warn("无效选项") time.sleep(0.4) def connect(self, user: str, host: str): self.last_user = user self.last_host = host target = f"{user}@{host}" cmd = ["ssh", "-o", "StrictHostKeyChecking=accept-new", target] print(_T.RST, end="", flush=True) _clear_screen() try: code = subprocess.call(cmd) _blank(2) _hint(f"✅ SSH 会话已断开,退出码:{code}") except FileNotFoundError: _err("❌ 未找到 ssh 命令,请安装 OpenSSH 客户端") except Exception as e: _err(f"❌ 连接异常:{e}") def _new_cmd_tab_in_current_terminal(self): """在当前 Windows Terminal 新开 CMD 标签页;普通 CMD 则 fallback 新窗口""" if sys.platform != "win32": subprocess.Popen("bash", shell=True) return # 优先用 wt.exe 在当前窗口新开 CMD 标签页 try: # wt.exe 是 Windows Terminal;-p 后面是你的 CMD profile 名 subprocess.Popen( ["wt.exe", "new-tab", "-p", "命令提示符"], creationflags=subprocess.CREATE_NO_WINDOW ) _ok("✅ 已在当前终端打开新 CMD 标签页") return except Exception: pass # 没有 Windows Terminal 时,fallback 到独立 CMD 窗口 try: subprocess.Popen("cmd.exe", creationflags=subprocess.CREATE_NEW_CONSOLE) _ok("✅ 已打开独立 CMD 窗口(未检测到 Windows Terminal)") except Exception as e: _err(f"❌ 打开终端失败:{e}") def show_reconnect_menu(self): while True: _blank(1) _hr("=", 65) _hint("连接已断开,请选择操作:") _menu_line("1", "重新快速连接(直接连刚才的主机)") _menu_line("2", "开始新的连接(回到设置页面)") _menu_line("3", "打开新的CMD标签页") _menu_line("q", "关闭窗口退出") _hr("=", 65) _blank(1) choice = _prompt("请输入选择 [1/2/3/q]:").strip().lower() if choice == "1": if self.last_host and self.last_user: _ok(f"✅ 正在重新连接:{self.last_user}@{self.last_host}") self.connect(self.last_user, self.last_host) else: _warn("❌ 无最近连接记录,无法重连") time.sleep(1) elif choice == "2": _ok("✅ 返回设置页面,开始新连接") time.sleep(0.8) self.settings_menu() self.connect_in_this_terminal() break elif choice == "3": self._new_cmd_tab_in_current_terminal() time.sleep(1) elif choice == "q": _ok("👋 正在退出...") break else: _warn("⚠️ 无效输入,请输入 1/2/3/q") time.sleep(0.5) def connect_in_this_terminal(self) -> None: prefix = self._normalize_prefix() user = self.config["remote_user"] _title("连接主机") _kv("网段", prefix) _hint("输入主机号,或完整IP") _hr() _blank(1) hint = _prompt("主机号/IP:").strip() if not hint: _warn("已取消") return host = hint if "." in hint else f"{prefix}{hint}" self.connect(user, host) self.show_reconnect_menu() def run() -> None: _enable_ansi_if_windows() try: app = RobotSSHConnector() app.settings_menu() app.connect_in_this_terminal() except KeyboardInterrupt: _warn("🛑 已退出") time.sleep(0.8) except Exception as e: _err(f"错误:{e}") input("按回车关闭...") if __name__ == "__main__": run()