天天用ssh远程登录linux服务器,能自己写一个ssh客户端 和 服务端吗?
ssh涉及认证、加密(非对称加密算法的公钥、私钥,对称加密,加密算法选择,还可以增加国密算法)
可以跳过这些安全考虑,不要认证和加密,不要公私钥,只看远程登录核心流程。
代码实现如下:
- 零认证零加密:连接即直接登录,无需用户名密码
- 终端窗口同步:自动同步本地窗口大小变化
- 完整PTY支持:和SSH一样的终端体验,支持
- 方向键、退格键、Tab补全
- Ctrl+C/Z中断、Ctrl+D退出
- vim、nano等全屏编辑器
- top、htop等交互式程序
功能特性
✅ 纯字节透传:程序本身不做任何字符转换,所有终端交互完全交给两端终端处理,和你想要的SSH透明模式完全一致
✅ 完整PTY伪终端:完美支持方向键、Tab补全、Ctrl+C/Z/D快捷键、vim/nano/top等全屏程序
✅ 自动窗口同步:本地终端调整大小时,远端程序自动适配布局
✅ 自动资源清理:断开连接后自动恢复本地终端设置,不会出现输入不显示的终端异常
✅ 高并发支持:服务端支持同时多个客户端连接(每个连接独立Shell会话)
✅ 自动Shell识别:自动使用用户默认Shell(bash/zsh/sh等)
go
// remote_shell.go ------ 极简远程登录程序(无认证 / 无加密)
// go build -o remote_shell main.go
// 设计目标:
// - 不做任何认证,不做任何加密,连上即获得一个完整的交互式 Shell
// - 程序本身只做「字节透传」+「伪终端(PTY)分配」,字符层面的所有处理
// 全部交给两端的终端自己完成(等价于你想要的 Telnet 透明模式)
//
// 用法:
// 服务端: ./remote_shell server [--host 0.0.0.0] [--port 4444]
// 客户端: ./remote_shell client <远端IP> [--port 4444]
//
// 平台: Linux(依赖 /dev/ptmx 与 /dev/pts)
// 依赖: 无,纯标准库,编译后为单个二进制文件
//
// 警告: 无任何认证与加密,任何能访问该端口的人都能直接控制本机,
// 仅限完全信任的内网 / 本机调试使用,切勿暴露到公网!
package main
import (
"encoding/binary"
"fmt"
"io"
"log"
"net"
"os"
"os/exec"
"os/signal"
"strconv"
"sync"
"syscall"
"unsafe"
)
// ---------------------------------------------------------------------------
// ioctl 相关(Linux)
// ---------------------------------------------------------------------------
const (
// 以下常量在 syscall 包中不一定导出,这里直接写死(均为 Linux 通用值)
ioctlTIOCGPTN = 0x80045430 // 获取从端 PTY 编号
ioctlTIOCSPTLCK = 0x40045431 // 解锁 PTY 从端
)
type winsize struct {
Row uint16
Col uint16
Xpixel uint16
Ypixel uint16
}
func ioctl(fd uintptr, req uintptr, arg unsafe.Pointer) error {
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, fd, req, uintptr(arg))
if errno != 0 {
return errno
}
return nil
}
// openPTY 打开一个伪终端,返回 master 与 slave
func openPTY() (*os.File, *os.File, error) {
master, err := os.OpenFile("/dev/ptmx", os.O_RDWR|syscall.O_NOCTTY, 0)
if err != nil {
return nil, nil, fmt.Errorf("打开 /dev/ptmx 失败: %w", err)
}
// 解锁从端
var unlock int32 = 0
if err := ioctl(master.Fd(), ioctlTIOCSPTLCK, unsafe.Pointer(&unlock)); err != nil {
master.Close()
return nil, nil, fmt.Errorf("解锁 PTY 失败: %w", err)
}
// 取从端编号
var ptn uint32
if err := ioctl(master.Fd(), ioctlTIOCGPTN, unsafe.Pointer(&ptn)); err != nil {
master.Close()
return nil, nil, fmt.Errorf("获取 PTY 编号失败: %w", err)
}
slavePath := fmt.Sprintf("/dev/pts/%d", ptn)
slave, err := os.OpenFile(slavePath, os.O_RDWR|syscall.O_NOCTTY, 0)
if err != nil {
master.Close()
return nil, nil, fmt.Errorf("打开 %s 失败: %w", slavePath, err)
}
return master, slave, nil
}
func getWinsize(fd int) (*winsize, error) {
ws := &winsize{}
if err := ioctl(uintptr(fd), syscall.TIOCGWINSZ, unsafe.Pointer(ws)); err != nil {
return nil, err
}
return ws, nil
}
func setWinsize(fd int, ws *winsize) error {
return ioctl(uintptr(fd), syscall.TIOCSWINSZ, unsafe.Pointer(ws))
}
// isTerminal 判断 fd 是否为终端
func isTerminal(fd int) bool {
var t syscall.Termios
return ioctl(uintptr(fd), syscall.TCGETS, unsafe.Pointer(&t)) == nil
}
// makeRaw 把终端切到原始模式,返回原状态用于恢复
func makeRaw(fd int) (*syscall.Termios, error) {
var old syscall.Termios
if err := ioctl(uintptr(fd), syscall.TCGETS, unsafe.Pointer(&old)); err != nil {
return nil, err
}
raw := old
raw.Iflag &^= syscall.IGNBRK | syscall.BRKINT | syscall.PARMRK | syscall.ISTRIP |
syscall.INLCR | syscall.IGNCR | syscall.ICRNL | syscall.IXON
raw.Oflag &^= syscall.OPOST
raw.Lflag &^= syscall.ECHO | syscall.ECHONL | syscall.ICANON | syscall.ISIG | syscall.IEXTEN
raw.Cflag &^= syscall.CSIZE | syscall.PARENB
raw.Cflag |= syscall.CS8
raw.Cc[syscall.VMIN] = 1
raw.Cc[syscall.VTIME] = 0
if err := ioctl(uintptr(fd), syscall.TCSETS, unsafe.Pointer(&raw)); err != nil {
return nil, err
}
return &old, nil
}
func restoreTerm(fd int, t *syscall.Termios) {
if t != nil {
ioctl(uintptr(fd), syscall.TCSETS, unsafe.Pointer(t))
}
}
// ---------------------------------------------------------------------------
// 客户端 -> 服务端 的最小控制帧
// 帧格式: [1 字节 类型][4 字节 大端长度][payload]
// 类型 0 = 终端数据;类型 1 = 窗口大小(8 字节: rows, cols)
// 服务端 -> 客户端 方向为纯原始字节,不加任何封装
// ---------------------------------------------------------------------------
const (
msgData byte = 0
msgResize byte = 1
)
type framedConn struct {
conn net.Conn
mu sync.Mutex
}
func (f *framedConn) send(typ byte, payload []byte) error {
f.mu.Lock()
defer f.mu.Unlock()
hdr := make([]byte, 5)
hdr[0] = typ
binary.BigEndian.PutUint32(hdr[1:], uint32(len(payload)))
if _, err := f.conn.Write(hdr); err != nil {
return err
}
if len(payload) > 0 {
if _, err := f.conn.Write(payload); err != nil {
return err
}
}
return nil
}
func readFrame(r io.Reader) (byte, []byte, error) {
hdr := make([]byte, 5)
if _, err := io.ReadFull(r, hdr); err != nil {
return 0, nil, err
}
n := binary.BigEndian.Uint32(hdr[1:])
if n > 1<<20 {
return 0, nil, fmt.Errorf("控制帧过大: %d", n)
}
if n == 0 {
return hdr[0], nil, nil
}
payload := make([]byte, n)
if _, err := io.ReadFull(r, payload); err != nil {
return 0, nil, err
}
return hdr[0], payload, nil
}
func applyWinsize(fd int, payload []byte) {
if len(payload) != 8 {
return
}
rows := binary.BigEndian.Uint32(payload[0:4])
cols := binary.BigEndian.Uint32(payload[4:8])
if rows > 0 && cols > 0 {
setWinsize(fd, &winsize{Row: uint16(rows), Col: uint16(cols)})
}
}
// ---------------------------------------------------------------------------
// 服务端
// ---------------------------------------------------------------------------
func runServer(host string, port int) {
addr := net.JoinHostPort(host, strconv.Itoa(port))
ln, err := net.Listen("tcp", addr)
if err != nil {
log.Fatalf("监听 %s 失败: %v", addr, err)
}
defer ln.Close()
log.Printf("[*] 极简远程 Shell 服务端已启动")
log.Printf("[*] 监听: %s", addr)
log.Printf("[*] 客户端连接: %s client <本机IP> --port %d", os.Args[0], port)
log.Printf("[!] 警告: 无认证无加密,请勿暴露到公网!\n")
for {
conn, err := ln.Accept()
if err != nil {
log.Printf("[-] 接受连接失败: %v", err)
continue
}
go handleConn(conn)
}
}
func handleConn(conn net.Conn) {
defer conn.Close()
remote := conn.RemoteAddr().String()
master, slave, err := openPTY()
if err != nil {
log.Printf("[-] %s 分配 PTY 失败: %v", remote, err)
return
}
// 首个帧应是客户端发来的初始窗口大小
var leftover []byte
typ, payload, err := readFrame(conn)
if err != nil {
log.Printf("[-] %s 握手失败: %v", remote, err)
master.Close()
slave.Close()
return
}
if typ == msgResize {
applyWinsize(int(master.Fd()), payload)
} else {
leftover = payload
}
shell := os.Getenv("SHELL")
if shell == "" {
shell = "/bin/sh"
}
cmd := exec.Command(shell)
cmd.Stdin = slave
cmd.Stdout = slave
cmd.Stderr = slave
cmd.Env = append(os.Environ(), "TERM=xterm-256color")
// Setsid + Setctty: 让 Shell 成为会话首进程并把 slave 设为控制终端
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true, Setctty: true, Ctty: 0}
if err := cmd.Start(); err != nil {
log.Printf("[-] %s 启动 Shell 失败: %v", remote, err)
master.Close()
slave.Close()
return
}
slave.Close()
log.Printf("[+] %s 已连接 (Shell: %s, PID=%d)", remote, shell, cmd.Process.Pid)
//关注下面三段处理的次序,先启动服务端到客户端的拷贝任务,再把之前客户端发过来的剩余字节leftover发给伪终端pty,最后启动一个持续读取客户端消息并发给伪终端pty的任务。
// PTY -> 网络(原始字节透传)
go func() {
io.Copy(conn, master)
conn.Close()
}()
if len(leftover) > 0 {
master.Write(leftover)
}
// 网络 -> PTY(解析控制帧)
pump(conn, master)
master.Close()
cmd.Process.Signal(syscall.SIGHUP)
cmd.Wait()
log.Printf("[-] %s 连接断开", remote)
}
func pump(conn net.Conn, master *os.File) {
for {
typ, payload, err := readFrame(conn)
if err != nil {
return
}
switch typ {
case msgData:
if _, err := master.Write(payload); err != nil {
return
}
case msgResize:
applyWinsize(int(master.Fd()), payload)
}
}
}
// ---------------------------------------------------------------------------
// 客户端
// ---------------------------------------------------------------------------
func sendLocalSize(fc *framedConn, fd int) {
var payload [8]byte
if ws, err := getWinsize(fd); err == nil && ws.Row > 0 && ws.Col > 0 {
binary.BigEndian.PutUint32(payload[0:4], uint32(ws.Row))
binary.BigEndian.PutUint32(payload[4:8], uint32(ws.Col))
} else {
binary.BigEndian.PutUint32(payload[0:4], 24)
binary.BigEndian.PutUint32(payload[4:8], 80)
}
fc.send(msgResize, payload[:])
}
func runClient(host string, port int) {
addr := net.JoinHostPort(host, strconv.Itoa(port))
conn, err := net.Dial("tcp", addr)
if err != nil {
log.Fatalf("连接 %s 失败: %v", addr, err)
}
defer conn.Close()
fd := int(os.Stdin.Fd())
var oldState *syscall.Termios
if isTerminal(fd) {
oldState, err = makeRaw(fd)
if err != nil {
log.Printf("[!] 设置终端原始模式失败: %v", err)
} else {
defer restoreTerm(fd, oldState)
// 收到终止类信号时先把终端恢复回去再退出
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
go func() {
<-sigCh
restoreTerm(fd, oldState)
os.Exit(0)
}()
}
}
fc := &framedConn{conn: conn}
sendLocalSize(fc, fd) // 同步初始窗口大小
// 本地窗口大小变化 -> 通知服务端
if isTerminal(fd) {
winCh := make(chan os.Signal, 1)
signal.Notify(winCh, syscall.SIGWINCH)
go func() {
for range winCh {
sendLocalSize(fc, fd)
}
}()
}
// 本地键盘输入 -> 远端
go func() {
buf := make([]byte, 4096)
for {
n, err := os.Stdin.Read(buf)
if n > 0 {
if fc.send(msgData, buf[:n]) != nil {
return
}
}
if err != nil {
return
}
}
}()
// 远端输出 -> 本地终端
// 阻塞式复制:从 conn 读取,写入 os.Stdout
// 直至 conn 关闭或发生 I/O 错误(如连接重置、超时)
io.Copy(os.Stdout, conn)
fmt.Fprint(os.Stdout, "\r\n[+] 连接已关闭\r\n")
}
// ---------------------------------------------------------------------------
// 入口
// ---------------------------------------------------------------------------
func usage() {
fmt.Fprintf(os.Stderr, `极简远程登录程序 ------ 无认证 / 无加密
用法:
%s server [--host 0.0.0.0] [--port 4444] 启动服务端
%s client <远端IP> [--port 4444] 连接服务端
示例:
%s server --port 5555
%s client 192.168.1.100 --port 5555
警告: 无任何认证与加密,仅限受信任的内网 / 本机调试使用。
`, os.Args[0], os.Args[0], os.Args[0], os.Args[0])
}
// parseArgs 手工解析,允许 --port/--host 出现在位置参数前后
func parseArgs(args []string) (positional []string, host string, port int) {
host = "0.0.0.0"
port = 4444
for i := 0; i < len(args); i++ {
switch args[i] {
case "--port", "-port":
if i+1 < len(args) {
i++
if p, err := strconv.Atoi(args[i]); err == nil {
port = p
}
}
case "--host", "-host":
if i+1 < len(args) {
i++
host = args[i]
}
default:
positional = append(positional, args[i])
}
}
return positional, host, port
}
func main() {
if len(os.Args) < 2 {
usage()
os.Exit(1)
}
rest, host, port := parseArgs(os.Args[2:])
switch os.Args[1] {
case "server":
runServer(host, port)
case "client":
if len(rest) < 1 {
fmt.Fprintln(os.Stderr, "错误: 客户端模式需要指定远端 IP")
usage()
os.Exit(1)
}
runClient(rest[0], port)
default:
usage()
os.Exit(1)
}
}
在两个Linux上运行良好,top指令这种刷新当前的屏幕的指令都没有问题。
但在windows上是编译不过的,需要单独适配成windows客户端。
代码如下:
go
// remote_shell_client.go ------ Windows平台极简远程Shell客户端
// 协议完全兼容Linux服务端(remote_shell.go),无认证无加密,纯字节透传
// 编译: go build -o remote_shell_client.exe remote_shell_client.go
// 运行: .\remote_shell_client.exe 192.168.218.136
package main
import (
"encoding/binary"
"flag"
"fmt"
"io"
"net"
"os"
"os/signal"
"sync"
"syscall"
"time"
"unsafe"
)
// ---------------------------------------------------------------------------
// Windows Kernel32 API 调用(纯标准库,零第三方依赖)
// ---------------------------------------------------------------------------
var (
kernel32 = syscall.NewLazyDLL("kernel32.dll")
procGetStdHandle = kernel32.NewProc("GetStdHandle")
procGetConsoleMode = kernel32.NewProc("GetConsoleMode")
procSetConsoleMode = kernel32.NewProc("SetConsoleMode")
procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo")
procSetConsoleCP = kernel32.NewProc("SetConsoleCP")
procSetConsoleOutputCP = kernel32.NewProc("SetConsoleOutputCP")
)
const (
// 标准句柄ID
STD_INPUT_HANDLE = ^uint32(0) - 10 + 1 // (DWORD)-10
STD_OUTPUT_HANDLE = ^uint32(0) - 11 + 1 // (DWORD)-11
// 控制台输入模式标志
ENABLE_ECHO_INPUT = 0x0004
ENABLE_LINE_INPUT = 0x0002
ENABLE_PROCESSED_INPUT = 0x0001
ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200
// 控制台输出模式标志
ENABLE_PROCESSED_OUTPUT = 0x0001
ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
CP_UTF8 = 65001
// **************** 协议常量,和Linux服务端完全一致 ****************
msgData byte = 0 // 普通终端数据帧
msgResize byte = 1 // 窗口大小调整帧
)
// ---------------------------------------------------------------------------
// Windows 控制台结构
// ---------------------------------------------------------------------------
type COORD struct {
X int16
Y int16
}
type SMALL_RECT struct {
Left int16
Top int16
Right int16
Bottom int16
}
type CONSOLE_SCREEN_BUFFER_INFO struct {
Size COORD
CursorPosition COORD
Attributes uint16
Window SMALL_RECT
MaximumWindowSize COORD
}
// ---------------------------------------------------------------------------
// Windows 控制台辅助函数
// ---------------------------------------------------------------------------
func getStdHandle(which uint32) syscall.Handle {
h, _, _ := procGetStdHandle.Call(uintptr(which))
return syscall.Handle(h)
}
func getConsoleMode(h syscall.Handle) (uint32, error) {
var mode uint32
r, _, err := procGetConsoleMode.Call(uintptr(h), uintptr(unsafe.Pointer(&mode)))
if r == 0 {
return 0, err
}
return mode, nil
}
func setConsoleMode(h syscall.Handle, mode uint32) error {
r, _, err := procSetConsoleMode.Call(uintptr(h), uintptr(mode))
if r == 0 {
return err
}
return nil
}
func getConsoleSize(h syscall.Handle) (int, int, error) {
var csbi CONSOLE_SCREEN_BUFFER_INFO
r, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(h), uintptr(unsafe.Pointer(&csbi)))
if r == 0 {
return 80, 24, err
}
cols := int(csbi.Window.Right - csbi.Window.Left + 1)
rows := int(csbi.Window.Bottom - csbi.Window.Top + 1)
return cols, rows, nil
}
// ---------------------------------------------------------------------------
// 帧发送逻辑(和服务端协议完全对齐)
// 帧格式: [1字节类型][4字节大端长度][payload]
//
// 类型0 = 终端数据,payload为用户输入原始字节
// 类型1 = 窗口调整,payload固定8字节:4字节大端rows + 4字节大端cols
//
// ---------------------------------------------------------------------------
var sendMu sync.Mutex
func sendFrame(conn net.Conn, typ byte, payload []byte) error {
sendMu.Lock()
defer sendMu.Unlock()
hdr := make([]byte, 5)
hdr[0] = typ
binary.BigEndian.PutUint32(hdr[1:], uint32(len(payload)))
if _, err := conn.Write(hdr); err != nil {
return err
}
if len(payload) > 0 {
if _, err := conn.Write(payload); err != nil {
return err
}
}
return nil
}
func sendResize(conn net.Conn, cols, rows int) error {
payload := make([]byte, 8)
binary.BigEndian.PutUint32(payload[0:4], uint32(rows))
binary.BigEndian.PutUint32(payload[4:8], uint32(cols))
return sendFrame(conn, msgResize, payload)
}
// ---------------------------------------------------------------------------
// 客户端主逻辑
// ---------------------------------------------------------------------------
func main() {
// 命令行参数和原Linux客户端完全一致
port := flag.Int("port", 4444, "远程服务端端口")
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Windows远程Shell客户端(兼容Linux服务端)\n用法: %s <服务端IP地址> [--port 端口]\n默认端口: 4444\n", os.Args[0])
flag.PrintDefaults()
}
flag.Parse()
if flag.NArg() != 1 {
flag.Usage()
os.Exit(1)
}
serverHost := flag.Arg(0)
serverAddr := fmt.Sprintf("%s:%d", serverHost, *port)
// 建立TCP连接
fmt.Printf("正在连接 %s ...\n", serverAddr)
conn, err := net.DialTimeout("tcp", serverAddr, 5*time.Second)
if err != nil {
fmt.Fprintf(os.Stderr, "连接失败: %v\n", err)
os.Exit(1)
}
defer conn.Close()
fmt.Println("✅ 连接成功,已进入远程Shell")
// 获取控制台句柄
stdinHandle := getStdHandle(STD_INPUT_HANDLE)
stdoutHandle := getStdHandle(STD_OUTPUT_HANDLE)
// 备份原始控制台模式,退出时必须恢复,避免终端乱码/不回显
origInMode, err := getConsoleMode(stdinHandle)
if err != nil {
fmt.Fprintf(os.Stderr, "获取控制台状态失败: %v\n", err)
os.Exit(1)
}
origOutMode, err := getConsoleMode(stdoutHandle)
if err != nil {
fmt.Fprintf(os.Stderr, "获取控制台状态失败: %v\n", err)
os.Exit(1)
}
// 无论怎么退出都恢复终端
defer func() {
setConsoleMode(stdinHandle, origInMode)
setConsoleMode(stdoutHandle, origOutMode)
fmt.Println("\n🔌 已断开连接,终端已恢复正常")
}()
// 设置UTF-8代码页,避免中文乱码
procSetConsoleCP.Call(uintptr(CP_UTF8))
procSetConsoleOutputCP.Call(uintptr(CP_UTF8))
// 设置输入为原始透传模式:关闭回显、行编辑、系统快捷键处理,开启VT输入兼容
rawInMode := (origInMode &^ (ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT)) | ENABLE_VIRTUAL_TERMINAL_INPUT
if err := setConsoleMode(stdinHandle, rawInMode); err != nil {
fmt.Fprintf(os.Stderr, "设置终端模式失败: %v\n", err)
os.Exit(1)
}
// 输出开启VT序列处理,支持ANSI颜色、光标控制(兼容xterm,vim/htop有颜色)
rawOutMode := origOutMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING | ENABLE_PROCESSED_OUTPUT | ENABLE_WRAP_AT_EOL_OUTPUT
_ = setConsoleMode(stdoutHandle, rawOutMode) // 旧版Windows开不了也不影响基础功能
// ---------------- 握手:首帧必须是初始窗口大小,和服务端协议对齐 ----------------
initCols, initRows, _ := getConsoleSize(stdoutHandle)
if err := sendResize(conn, initCols, initRows); err != nil {
fmt.Fprintf(os.Stderr, "发送初始窗口大小失败: %v\n", err)
os.Exit(1)
}
// 监听中断信号,确保异常退出时也能恢复终端
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
go func() {
<-sigChan
conn.Close()
}()
// netDone 在"网络方向结束"(远端断开或出错)时关闭,主流程据此立刻收尾。
// 关键:绝不能等待 stdin 协程结束。Windows 控制台的 stdin 读取是阻塞的,
// 远端断开后它仍会卡在 os.Stdin.Read 上,直到用户再敲一次回车才返回------
// 这正是"exit 回车后仍要再按一次回车才提示断开"的原因。
netDone := make(chan struct{})
// 转发方向1: 网络 -> 本地控制台(远端输出纯原始字节透传,无需解帧)
// 注意:服务端->客户端方向是无封装的纯字节流,和Linux客户端一致
go func() {
defer close(netDone)
defer conn.Close()
buf := make([]byte, 32*1024)
for {
n, err := conn.Read(buf)
if n > 0 {
os.Stdout.Write(buf[:n])
}
if err != nil {
if err != io.EOF {
fmt.Fprintf(os.Stderr, "\n连接中断: %v\n", err)
}
return
}
}
}()
// 转发方向2: 本地控制台 -> 网络(键盘输入封装为msgData帧)
// 该协程在远端断开后仍可能阻塞在 os.Stdin.Read 上,属预期行为,
// 进程退出时自动回收,不再拖住断开提示。
go func() {
defer conn.Close()
buf := make([]byte, 32*1024)
for {
n, err := os.Stdin.Read(buf)
if n > 0 {
if err := sendFrame(conn, msgData, buf[:n]); err != nil {
return
}
}
if err != nil {
return
}
}
}()
// 后台同步窗口大小:每500ms检查一次,窗口调整时自动通知远端
go func() {
lastCols, lastRows := initCols, initRows
for {
time.Sleep(500 * time.Millisecond)
curCols, curRows, err := getConsoleSize(stdoutHandle)
if err != nil {
continue
}
if curCols != lastCols || curRows != lastRows {
if err := sendResize(conn, curCols, curRows); err == nil {
lastCols, lastRows = curCols, curRows
}
}
}
}()
// 一旦远端断开,立即返回,触发 defer 恢复终端并打印断开提示
<-netDone
}