3

Gotty源码分析

 2 years ago
source link: https://qiankunli.github.io/2019/11/01/gotty_source.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Gotty源码分析 | 李乾坤的博客

容器Web Console技术实现

  1. 本机启动gotty -w bash
  2. 本机启动浏览器,localhost:8080 浏览器便是一个全黑的console 界面

和kubernetes 结合使用

  1. 本机启动gotty -w --permit-arguments kubectl exec -it
  2. 本机启动浏览器 http://localhost:8080/?arg=fm-barge-backend-stable-69f77f6767-jw69l&arg=%2fbin%2fbash

传入两个arg,一个是pod 的名字,一个是/bin/bash ,其实就是把 两个arg 拼在kubectl exec -it 之后

/main.go        // 入口文件,命令行工具库用的"github.com/codegangsta/cli"
/server
    /server.go  // 定义server struct
    /ws_wrapper.go  // 定义wsWrapper struct
/backend
    /localcommand
        /local_command.go
/webtty
    /webtty.go  // 定义WebTTY struct

main.go 的核心逻辑app.Run ==> app.Action,去掉参数校验、日志等逻辑,核心流程为创建Server 并run 起来

app.Action = func(c *cli.Context) {
    configFile := c.String("config")
    utils.ApplyFlags(cliFlags, flagMappings, c, appOptions, backendOptions)
    args := c.Args()
    factory, err := localcommand.NewFactory(args[0], args[1:], backendOptions)
    srv, err := server.New(factory, appOptions)
    errs := make(chan error, 1)
    go func() {
        errs <- srv.Run(ctx, server.WithGracefullContext(gCtx))
    }()
    err = waitSignals(errs, cancel, gCancel)
    if err != nil && err != context.Canceled {
        fmt.Printf("Error: %s\n", err)
        exit(err, 8)
    }
}

交互命令是如何执行的

http 升级为web socket

可见,gotty 要处理两种请求 http 和 websocket 请求, 它们的处理逻辑在Server.setupHandlers 中指定。基于 siteMux 创建siteHandler,然后siteHandler wrap wsMux 等,使其对http、websocket、staticFile 都具备处理能力。 可以看到 server.generateHandleWS 提供了处理 websocket 请求的handler

func (server *Server) setupHandlers(ctx context.Context, cancel context.CancelFunc, pathPrefix string, counter *counter) http.Handler {
    staticFileHandler := http.FileServer(
        &assetfs.AssetFS{Asset: Asset, AssetDir: AssetDir, Prefix: "static"},
    )

    var siteMux = http.NewServeMux()
    siteMux.HandleFunc(pathPrefix, server.handleIndex)
    siteMux.Handle(pathPrefix+"js/", http.StripPrefix(pathPrefix, staticFileHandler))
    siteMux.Handle(pathPrefix+"favicon.png", http.StripPrefix(pathPrefix, staticFileHandler))
    siteMux.Handle(pathPrefix+"css/", http.StripPrefix(pathPrefix, staticFileHandler))

    siteMux.HandleFunc(pathPrefix+"auth_token.js", server.handleAuthToken)
    siteMux.HandleFunc(pathPrefix+"config.js", server.handleConfig)

    siteHandler := http.Handler(siteMux)

    if server.options.EnableBasicAuth {
        log.Printf("Using Basic Authentication")
        siteHandler = server.wrapBasicAuth(siteHandler, server.options.Credential)
    }

    withGz := gziphandler.GzipHandler(server.wrapHeaders(siteHandler))
    siteHandler = server.wrapLogger(withGz)
    // 处理websocket 请求
    wsMux := http.NewServeMux()
    wsMux.Handle("/", siteHandler)
    wsMux.HandleFunc(pathPrefix+"ws", server.generateHandleWS(ctx, cancel, counter))
    siteHandler = http.Handler(wsMux)

    return siteHandler
}

本机启动 gotty -w bash,然后浏览器访问 localhost:8080,浏览器发出请求下载一系列js文件, 其中的关键是 发出了ws://localhost:8080/ws,然后服务端返回http status=101(Switching Protocols 服务器将遵从客户的请求转换到另外一种协议)进行了协议升级。

处理websocket 请求的逻辑

webSocketConn 代表浏览器websocket 连接,localcommand 代表 用户命令的执行。

func (server *Server) generateHandleWS(ctx context.Context, cancel context.CancelFunc, counter *counter) http.HandlerFunc{
    ...
    return func(w http.ResponseWriter, r *http.Request) {
        ...
        conn, err := server.upgrader.Upgrade(w, r, nil)
        defer conn.Close()
        ...
        err = server.processWSConn(ctx, conn)
        ...
    }
}
func (server *Server) processWSConn(ctx context.Context, conn *websocket.Conn) error {
    typ, initLine, err := conn.ReadMessage()
    err = json.Unmarshal(initLine, &init)
    queryPath := "?"
    if server.options.PermitArguments && init.Arguments != "" {
        queryPath = init.Arguments
    }
    query, err := url.Parse(queryPath)
    ...
    params := query.Query()
    var slave Slave
    slave, err = server.factory.New(params)
    ...
    tty, err := webtty.New(&wsWrapper{conn}, slave, opts...)
    if err != nil {
        return errors.Wrapf(err, "failed to create webtty")
    }
    err = tty.Run(ctx)
    return err
}

根据gotty -w $GOTTY_PERMIT_WRITE 中指定的command 以及arg 创建cmd 并接上 /dev/ptmx(参见文末的终端和伪终端)。

func (wt *WebTTY) Run(ctx context.Context) error {
    err := wt.sendInitializeMessage()
    go func() {
        errs <- func() error {
            buffer := make([]byte, wt.bufferSize)
            for {
                n, err := wt.slave.Read(buffer)
                err = wt.handleSlaveReadEvent(buffer[:n])
            }
        }()
    }()
    go func() {
        errs <- func() error {
            buffer := make([]byte, wt.bufferSize)
            for {
                n, err := wt.masterConn.Read(buffer)
                err = wt.handleMasterReadEvent(buffer[:n])
            }
        }()
    }()
    ...
}

GoTTY在收到用户请求后,会执行启动时设置的参数,得到进程的stdin和stdout。随后会在单独的goroutine中,循环读取进程的输出写到websocket中,循环从websocket中读取写到进程的输入中

数据流如下

  1. 发送指令: websocket.Conn ==> /dev/ptmx ==> /dev/pts/xx ==> localCommand
  2. 接收响应: websocket.Conn <== /dev/ptmx <== /dev/pts/xx <== localCommand

终端和伪终端

PS: 笔者一开始mac上试验,一些细节和linux 有所不同。 这里的主从理解起来比较难受,可以不用太关注。

tty, tty原意是远程输入机(teletypewriter),现在在unix系统中是 text terminal 的意思。在 GNU/Linux 和 Mac OS X 上,都有terminal程序,打开一个 terminal 程序就对应一个 tty (text terminal) 设备文件。往 /dev/tty 写入内容会在当前terminal里回显。

$ echo 'haha' > /dev/tty
haha

每次打开terminal时会有个唯一的tty文件与其对应,比如/dev/ttys000/dev/ttys001等,/dev/tty 会根据当前活动的terminal去找到对应文件ttys000或者ttys001。

terminal(终端)可以等同于 tty。terminal 是 shell 的包裹器(wrapper),terminal 接收用户输入的命令,并将命令传给 shell。

伪终端(Pseudo Terminal)是终端的发展,它是成对出现的逻辑终端设备,对master的操作会反映到slave,pts和ptmx 配合使用实现 pty。

深入理解sshd创建pty的过程历史上,有两套伪终端软件接口:

  1. BSD接口:较简单,master为/dev/pty[p-za-e][0-9a-f] ;slave为 /dev/tty[p-za-e][0-9a-f]
  2. Unix 98接口:使用一个/dev/ptmx作为master设备,在每次打开操作时会得到一个master设备fd,并在/dev/pts/目录下得到一个slave设备如 /dev/pts/3

master /dev/ptmx Master 将命令传给 slave 或者将 slave 的数据显示出来。 slave /dev/pts/xx Slave 就是 pts(pseudo terminal slave),不同在于 terminal 直接连接在主机上,pts 通过一些软件连接到主机上。

// github.com/kr/pty/run.go
func Start(c *exec.Cmd) (pty *os.File, err error) {
    pty, tty, err := Open()
    if err != nil {
        return nil, err
    }
    defer tty.Close()
    c.Stdout = tty
    c.Stdin = tty
    c.Stderr = tty
    c.SysProcAttr = &syscall.SysProcAttr{Setctty: true, Setsid: true}
    err = c.Start()
    if err != nil {
        pty.Close()
        return nil, err
    }
    return pty, err
}

func open() (pty, tty *os.File, err error) {
    p, err := os.OpenFile("/dev/ptmx", os.O_RDWR, 0)
    sname, err := ptsname(p)
    err = grantpt(p)
    err = unlockpt(p)
    // 看样子是 根据/dev/ptmx 创建一个/dev/pts/xx
    t, err := os.OpenFile(sname, os.O_RDWR, 0)
    return p, t, nil
}

tty file 是根据 pty file 创建,assigns a pseudo-terminal tty os.File to cmd.Stdin, cmd.Stdout,and cmd.Stderr, calls c.Start, and returns the File of the tty’s corresponding pty.

  1. 发送指令:sshd ==> /dev/ptmx ==> /dev/pts/xx ==> bash
  2. 接收指令的数据: sshd <== /dev/ptmx <== /dev/pts/xx <== bash

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK