精简版ssh(去掉认证和加密流程,更清晰的展示远程登录的底层实现)

天天用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
}
相关推荐
0+1111 小时前
Linux --应用层自定义协议与序列化
linux·运维·服务器·网络·tcp
dyxal1 小时前
Linux Crontab 防重复执行利器:flock 文件锁实战详解(脱敏版)
linux·运维·服务器
niuTaylor1 小时前
RK3568 Linux SDK 详解:SDK 是什么、板级差异与完整构建流程
linux·运维·服务器
拾贰_C1 小时前
【Ubuntu | port】Linux进程端口号冲突问题解决
linux·运维·ubuntu
wdfk_prog2 小时前
Wi-Fi Direct 教程 07:第一次 P2P Scan 如何真正发出——WPS/P2P IE、radio work 与 driver request
运维·服务器·ubuntu·wifi-direct
机核研创社2 小时前
服装自动化观察:接橡筋机的超声波熔切与冷刀,差别到底在哪
运维·自动化
欣欣之王来了2 小时前
AI合规专项:AI自动化决策的合规管控要点
运维·人工智能·自动化
guo_wen_qiang2 小时前
docker安装常见软件集合
运维·docker·容器
谷哥的小弟2 小时前
CentOS寿终正寝
linux·运维·centos