目录

概述

远程执行pod命令的基本方法

后端websocket的实现

前端

总结


概述

上一章通过http chunked 长连接实现了pod的日志读取和展现;

本章将通过前端的xterm.js库以及websocket来实现pod终端在浏览器页面上的实现。

远程执行pod命令的基本方法

首先构建一个请求

	option := &v1.PodExecOptions{
		Container: container,
		Command:   command,
		//如果是一次性执行命令"sh -c ls"这种就关闭stdin(打开也不影响)
		Stdin:  true,
		Stdout: true,
		Stderr: true,
		//终端
		TTY: true,
	}
	//构建一个地址
	req := client.CoreV1().RESTClient().Post().Resource("pods").
		Namespace(ns).
		Name(pod).
		SubResource("exec").
		Param("color", "false").
		VersionedParams(
			option,
			scheme.ParameterCodec,
		)

如果是一次性执行命令,我们会将command以

[]string{"你要执行的命令"}

的值传入;

以这个请求获取到url地址来创建远程命令执行对象

exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())

值得一提的是SPDY是谷歌开发的通信协议,在TCP层之上,HTTP/2的关键功能主要来自SPDY技术,换言之,SPDY的成果被采纳而最终演变为HTTP/2。

通过前步获得到的远程命令执行对象,我们可以开启流式传输,并制定标准输入/输出的方向

	exec.Stream(remotecommand.StreamOptions{ 
			Stdin:  os.Stdin,
			Stdout: os.Stdout,
			Stderr: os.Stderr,
			Tty:    true,
		})

 执行,即可在Terminal控制台观察到相应的结果返回。

后端websocket的实现

我们知道,当我们在前端键入指令,传送到后端时。这个指令不可能是写死的,这就要求前后有一定的交互,并且后端执行后的结果需要通知到前端,来对页面进行渲染。所以我们需要使用到websocket。在上一节中我们将远程执行命令对象的返回结果定向到了os标准输出,也就是控制台,这一节我们来尝试将它通过 websocket 客户端返回给前端。

本文以gin框架来演示;

从*gin.Context中获取我们必要的参数,即交互的容器

    ns := c.Query("ns")
	pod := c.Query("name")
	container := c.Query("cname")

 老套路,把http连接升级成websocket连接

    wsClient, err := wscore.Upgrader.Upgrade(c.Writer, c.Request, nil)

接下来很关键,我们需要把这个ws客户端取代os标准输出,作为远程命令对象的输出。

查看StreamOptions的成员变量

type StreamOptions struct {
   Stdin             io.Reader
   Stdout            io.Writer
   Stderr            io.Writer
   Tty               bool
   TerminalSizeQueue TerminalSizeQueue
}

因此,我们需要通过ws客户端构建对象,并且实现Reader/Writer接口,其实就是往ws客户端里面灌数据/取数据

type WsShellClient struct {
	client *websocket.Conn
}

func NewWsShellClient(client *websocket.Conn) *WsShellClient {
	return &WsShellClient{client: client}
}

func (this *WsShellClient) Write(p []byte) (n int, err error) {
	err = this.client.WriteMessage(websocket.TextMessage,
		p)
	if err != nil {
		return 0, err
	}
	return len(p), nil
}
func (this *WsShellClient) Read(p []byte) (n int, err error) {

	_, b, err := this.client.ReadMessage()

	if err != nil {
		return 0, err
	}
	return copy(p, string(b)), nil
}

至此,后端部分就完成了;通过对应接口访问进来,远程命令执行对象会将通过ws收到的消息作为命令去执行,并通过ws连接把命令执行结果返回回去。

完整代码:

func PodConnect(c *gin.Context) {
	//获取容器相关对应参数
	ns := c.Query("ns")
	pod := c.Query("name")
	container := c.Query("cname")
	//升级http客户端到ws客户端
	wsClient, err := wscore.Upgrader.Upgrade(c.Writer, c.Request, nil)
	if err != nil {
		log.Println(err)
		return
	}
	shellClient := wscore.NewWsShellClient(wsClient) //以ws客户端构建实现reader/writer接口的对象
    //构建远程执行命令对象
	err = helpers.HandleCommand(ns, pod, container, this.Client, this.Config, []string{"sh"}).
		Stream(remotecommand.StreamOptions{ //以流的方式来读取结果
			Stdin:  shellClient,
			Stdout: shellClient,
			Stderr: shellClient,
			Tty:    true,
		})
	return
}

前端

附前端部分完整代码

<template>

  <div
    style="min-height: 500px;
    padding: 10px"
  >
    <div style="padding-left: 20px;padding-top:30px">
    容器: 
      <el-select  @change="containerChange"  placeholder="选择容器"
                        v-model="selectedContainer">
        <el-option v-for="c in containers "
                  :label="c.Name"
                  :value="c.Name"/>
      </el-select>
    </div>
    <div id="terminal" ref="terminal"></div>
  </div>

</template>
<script>
  import { Terminal } from "xterm";
  import "xterm/css/xterm.css";
  import { getPodContainers } from "@/api/pod";

  export default {

    data(){
      return {
        Name: "",
        NameSpace: "",
        containers: [],
        selectedContainer: "",
        rows: 40,
        cols: 100,
        term:null,//终端对象
        ws:null, //ws 客户端
        wsInited:false  //是否初始化完毕
      }
    },
    created() {
      this.Name = this.$route.params.name
      this.NameSpace = this.$route.params.ns
      if(this.Name===undefined||this.NameSpace===undefined){
        alert("错误的参数!")
      } else {
        getPodContainers(this.NameSpace,this.Name).then(rsp=>{
          this.containers=rsp.data
        })
      }
    },
    methods:{
      containerChange(){
        this.initWS()// 初始化 websocket
        this.initTerm()
      },
      initTerm(){
        let term = new Terminal({
          rendererType: "canvas", //渲染类型
          rows: parseInt(this.rows), //行数
          cols: parseInt(this.cols), // 不指定行数,自动回车后光标从下一行开始
          convertEol: true, //启用时,光标将设置为下一行的开头
          disableStdin: false, //是否应禁用输入。
          cursorStyle: "underline", //光标样式
          cursorBlink: true, //光标闪烁
          theme: {
            foreground: "#7e9192", //字体
            background: "#002833", //背景色
            cursor: "help", //设置光标
            lineHeight: 16
          }
        });
        // 创建terminal实例
        term.open(this.$refs["terminal"]);
        term.prompt = () => {
          term.writeln("\n\n Welcome. ");
          term.writeln("\n 正在初始化终端");
        };
        term.prompt();

        //回车触发
        term.onData((key)=> {
            if(this.wsInited){
              this.ws.send(key)
            }
        });


        this.term=term
      },
      //初始化 websocket 客户端
      initWS(){
        var ws = new WebSocket("ws://localhost:8080/podws?ns="+
        this.NameSpace+"&name="+this.Name+"&cname="+this.selectedContainer);
        ws.onopen = function(){
          console.log("open");
        }
        ws.onmessage = (e)=>{
          this.wsInited=true //初始化完毕
          this.term.write(e.data) //调用term的打印方法打印后端返回的消息结果
        }
        ws.onclose = function(e){
          console.log("close");
        }
        ws.onerror = function(e){
          console.log(e);
        }
        this.ws=ws
      }
    }

  }

</script>

当我们在终端输入框键入命令并回车结束时,会触发终端组件的onData事件,onData事件会通过ws客户端将这部分数据发送给后端。后端将处理完成的结果通过ws连接返回给前端,触发了ws客户端的onmessage事件,然后调用终端组建的write方法,将结果再打印出来。

总结

至此,我们就完成了pod shell的实现。

在下一章中,我们将在此基础上进一步实现node shell。其实原理是类似的,基本思路就是通过ssh开启一个会话,并同样以其构造实现io.Reader和io.Writer接口的结构体。

Logo

K8S/Kubernetes社区为您提供最前沿的新闻资讯和知识内容

更多推荐