首頁 > 軟體

雲vscode搭建之使用容器化部署的方法

2022-09-06 18:06:43

Vscode作為一個輕量級的IDE,其支援豐富的外掛,而通過這些外掛我們就可以實現在Vscode中寫任何語言的程式碼。Code-Server是Vscode的網頁版,啟動Code-Server之後我們就可以在瀏覽器中開啟vscode來愉快的編寫程式碼了。這種方式非常適合我們做linux程式設計。使用C/C++的時候,在windows上編寫的程式碼在linux上可能跑不了,而且安裝linux圖形介面,然後在影象介面中使用vscode又很麻煩。當然也可以使用vscode的遠端開發。但是我認為啟動code-server來在瀏覽器上使用vscode也是非常方便的。

隨著容器化的發展,現在湧現出了很多雲IDE,比如騰訊的Cloud Studio,但是其也是基於Code-Server進行開發部署的,用了它的雲IDE後,我便產生出了自己部署一個這樣的雲IDE的想法。

1、Code-Server下載部署

1.1 Code-Server下載

下載地址:https://github.com/coder/code-server/releases/

在上面的網址中下載code-server,並將其傳輸到linux伺服器上。

也可以在linux伺服器中直接使用命令來下載:

wget https://github.com/coder/code-server/releases/download/v4.6.1/code-server-4.6.1-linux-amd64.tar.gz

1.2 Code-Server部署

1.解壓tar.gz檔案:

 tar -zxvf code-server-4.6.1-linux-amd64.tar.gz

2.進入code-server目錄:

cd code-server-4.6.1-linux-amd64

3.設定密碼到環境變數中

export PASSWORD="xxxx"

4.啟動code-server

./code-server --port 8888 --host 0.0.0.0 --auth password 

在瀏覽器中輸入ip:8888來存取如下:

後臺執行的方式:

nohup ./code-server --port 8888 --host 0.0.0.0 --auth password &

1.3 Docker部署Code-Server

接下來將介紹使用Docker的方式來部署Code-Server:

下面的Dockerfile建立了一個帶有Golang開發環境的容器,然後在容器中執行Code-Server,將Dockerfile放在跟code-server-4.4.0-linux-amd64.tar.gz同目錄。

1.建立名為Dockerfile的檔案(Dockerfile要跟code-server的tar.gz檔案在同一目錄中),將下面內容複製進去

FROM golang

WORKDIR /workspace

RUN cp /usr/local/go/bin/* /usr/local/bin

COPY code-server-4.4.0-linux-amd64.tar.gz .
RUN tar zxvf code-server-4.4.0-linux-amd64.tar.gz

ENV GO111MODULE on 
ENV GOPROXY https://goproxy.cn

ENV PASSWORD abc123

WORKDIR /workspace/code-server-4.4.0-linux-amd64
EXPOSE 9999
CMD ["./code-server", "--port", "9999", "--host", "0.0.0.0", "--auth", "password"]

2.然後執行命令構建Docker映象:

docker build -t code-server .

3.執行容器

docker run -d --name code-server -p 9999:9999 code-server

2. 一個小問題

下面的內容針對Docker部署的Code-Server。

想象這樣一個場景,我們開發了一個類似Cloud Studio的雲IDE,每啟動一個工作空間我們就通過Docker或者Kubernetes來建立一個容器,然後在容器中部署一個Code-Server,最後通過將埠暴露出去給使用者使用。

雲IDE使用起來很方便,開啟和銷燬的很迅速,即開即用。使用者使用Golang在雲IDE中寫了一個http伺服器,想要在他電腦的瀏覽器上存取,卻發現存取不了。那麼為什麼Code-Server的埠就可以存取,其它埠無法存取呢。因為我們在啟動容器的時候就已經預想到要存取這個埠,然後將埠暴露出去了,也就是建立了埠對映。在容器中新啟動的埠並沒有建立對映,因此只能在伺服器內部存取,而不能在使用者電腦上存取。

那麼如何讓使用者也可以存取到呢,我們可以在主機上部署一個代理伺服器,使用者存取這個代理伺服器,然後轉發請求到容器中,再將響應轉發給使用者。

那麼如何發現使用者啟動伺服器監聽了哪個埠呢,首先我能想到的就是啟動一個程式每隔一段時間查詢一下是否有新的埠被監聽。獲取埠資訊可以使用netstat命令或者lsof命令,在此我選擇了netstat,就有了下面的一個程式:

2.1 埠監聽

這個程式會每隔一秒獲取一下有哪些埠處於LISTEN狀態,然後對比上一次的狀態,看是否有新的埠被監聽。當我們監聽了新的80埠後,就會輸出:Find new port: 80

package main

import (
	"bytes"
	"fmt"
	"os/exec"
	"strconv"
	"time"
)

func main() {
	listener := NewPortListener()
	pc := listener.GetPortChan()
	go listener.FindNewPortLoop()
	for {
		port := <-pc
		fmt.Println("Find new port:", port)
	}
}

type PortListener struct {
	portChan chan uint16
}

func NewPortListener() *PortListener {
	return &PortListener{
		portChan: make(chan uint16, 1),
	}
}

func (p *PortListener) GetPortChan() <-chan uint16 {
	return p.portChan
}

func (p *PortListener) FindNewPortLoop() {
	ports := p.getListeningPorts()          // 程式啟動後先獲取一次處於Listen狀態的埠
	set := map[uint16]struct{}{}
	for _, port := range ports {
		set[port] = struct{}{}
	}

	for {                                   // 然後每隔一秒獲取一次,並與前一次的資訊進行對比,查詢是否啟動了新的埠
		tmpSet := map[uint16]struct{}{}     
		ports = p.getListeningPorts()
		for _, port := range ports {
			if _, ok := set[port]; !ok {
				p.portChan <- port
			}
			tmpSet[port] = struct{}{}
		}
		set = tmpSet
		time.Sleep(time.Second * 3)
	}
}

func (p *PortListener) getListeningPorts() []uint16 {
	cmd := exec.Command("netstat", "-ntlp")          // 執行netstat命令獲取處於Listen狀態的埠資訊
	res, err := cmd.CombinedOutput()                 // 獲取結果 
	fmt.Println(string(res))
	if err != nil {
		fmt.Println("Execute netstat failed")
		return nil
	}

	return p.parsePort(res)                         // 對結果進行解析
}

func (p *PortListener) parsePort(msg []byte) []uint16 {       // 解析出處於LISTEN狀態的埠,只要埠號
	idx := bytes.Index(msg, []byte("tcp"))
	colums := bytes.Split(msg[idx:], []byte("n"))
	res := make([]uint16, 0, len(colums)-1)
	for i := 0; i < len(colums)-1; i++ {
		item := p.findThirdItem(colums[i])
		if item != nil {
			m := bytes.IndexByte(item, ':') + 1
			for item[m] == ':' {
				m++
			}
			p, err := strconv.Atoi(string(item[m:]))
			if err == nil {
				res = append(res, uint16(p))
			} else {
				fmt.Println(err)
			}
		}
	}

	return res
}

func (p *PortListener) findThirdItem(colum []byte) []byte {
	count := 0
	for i := 0; i < len(colum); {
		if colum[i] == ' ' {
			for colum[i] == ' ' {
				i++
			}
			count++
			continue
		}
		if count == 3 {
			start := i
			for colum[i] != ' ' {
				i++
			}
			return colum[start:i]
		}
		i++
	}

	return nil
}

2.2 使用VS-Code外掛

但是上面的程式也無法通知到使用者,在使用Cloud Studio的時候,啟動了新的埠,這個雲IDE就會提醒發現了新的埠,是否要在瀏覽器中存取。因此我就想到了實現這樣一個外掛,因此下面部分就是實現一個vscode的外掛來發現是否有新的埠被監聽了,然後提醒使用者是否在瀏覽器中存取。

下面只是簡單介紹,想要了解vscode外掛的詳細開發過程的自行搜尋。

1.首先安裝yeoman腳手架工具,以及官方提供的腳手架工具:

npm install -g yo generator-code

2.建立專案,選擇要建立的專案以及其它資訊

yo code

3.建立完成後,就可以編寫外掛了

// extension.js
// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
const vscode = require('vscode');

// this method is called when your extension is activated
// your extension is activated the very first time the command is executed

/**
 * @param {vscode.ExtensionContext} context
 */
function activate(context) {

	// Use the console to output diagnostic information (console.log) and errors (console.error)
	// This line of code will only be executed once when your extension is activated
	

	// The command has been defined in the package.json file
	// Now provide the implementation of the command with  registerCommand
	// The commandId parameter must match the command field in package.json
	let disposable = vscode.commands.registerCommand('port-finder.helloWorld', function () {
		// The code you place here will be executed every time your command is executed

		// Display a message box to the user
		vscode.window.showInformationMessage('Hello World from port_finder!');
	});

	context.subscriptions.push(disposable);

	initGetPorts()
}

var s = new Set() 

function initGetPorts() {
    getListeningPorts(function(ports) {
        ports.forEach(p => {
            s.add(p)
        })

        setInterval(function() {        // 設定定時器,每隔一秒獲取一次
            listenPortChange()
        }, 1000)
    })
}

function listenPortChange() {
    // 獲取處於LISTEN狀態的埠
    getListeningPorts(function(ports) {
        var tmpSet = new Set()     
        ports.forEach(p => {
            if (!s.has(p)) {
                // 發現新的埠被監聽就提醒使用者是否在瀏覽器中開啟
                vscode.window.showInformationMessage("發現新開啟的埠:" + p + ",是否在瀏覽器中存取?", "是", "否", "不再提示")
                .then(result=> {
                    if (result === "是") {
                        // 在瀏覽器中開啟來存取代理伺服器,後面帶上埠資訊,以便代理伺服器知道存取容器的哪個埠
                        vscode.env.openExternal(vscode.Uri.parse(`http://192.168.44.100/proxy/` + p))
                    } 
                })
            }
            tmpSet.add(p)
        })
        s = tmpSet
    })
}

function getListeningPorts(callback) {
    var exec = require('child_process').exec;
    
    exec('netstat -nlt', function(error, stdout, stderr){
        if(error) {
            console.error('error: ' + error);
            return;
        }
        
        var ports = parsePort(stdout)
        callback(ports)
    })
}

function parsePort(msg) {
    var idx = msg.indexOf("tcp")
    msg = msg.slice(idx, msg.length)
    var colums = msg.split("n")
    var ret = new Array()
    colums = colums.slice(0, colums.length - 1)
    colums.forEach(element => {
        
        var port = findPort(element)
        if (port != -1) {
            ret.push(port)
        }
    });

    return ret;
}

function findPort(colum) {
    var idx = colum.indexOf(':')
    var first = colum.slice(0, idx)
    while (colum[idx] == ':') {
        idx++
    }
    var second = colum.slice(idx, colum.length)
    var fidx = first.lastIndexOf(' ')
    var sidx = second.indexOf(' ')
    var ip = first.slice(fidx + 1, first.length)
    var port = second.slice(0, sidx)

    if (ip == "127.0.0.1") {
        return -1
    } else {
        return Number(port)
    }
}

// this method is called when your extension is deactivated
function deactivate() {}

module.exports = {
	activate,
	deactivate
}

4.然後構建專案,首先安裝vsce庫,再打包

npm i -g vsce
vsce package

5.打包後生成了vsix檔案,將vsix檔案上傳到伺服器,然後再拷貝到docker容器中

# docker拷貝命令
docker cp 主機檔名 容器ID或容器名:/容器內路徑

然後在瀏覽器中的vscode中選擇vsix檔案來安裝外掛

安裝完之後,我們的外掛在vscode開啟後就會啟動,然後每隔一秒查詢一個埠情況。

測試

接下來,測試一下外掛:

在vscode中寫了一個http伺服器,然後啟動這個伺服器,看外掛是否能發現這個埠被監聽了

package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

type KK struct {
	Name          string `json:"name"`
	Prictice_time string `json:"prictice time"`
	Hobby         string `json:"hobby"`
}

func main() {
	engine := gin.Default()
	engine.GET("/", func(ctx *gin.Context) {
		ctx.JSON(http.StatusOK, &KK{
			Name:          "kunkun",
			Prictice_time: "two and a half years",
			Hobby:         "sing jump and rap",
		})
	})

	engine.Run(":8080")
}

執行http伺服器:

go run main.go

可以看到,它彈出了提示,提示我們是否在瀏覽器中開啟

但是現在在瀏覽器中開啟是存取不了容器中的http伺服器的,因為埠沒有被對映到主機埠上。

2.3 代理伺服器實現

在此,為了驗證我的想法是否能成功,只是實現了一個簡單的代理伺服器,它將請求轉發的容器中,然後再轉發容器中伺服器的響應。(因為代理伺服器是直接執行在主機上的,因此可以通過容器IP+埠來存取)

程式碼如下:

package main

import (
	"fmt"
	"io"
	"net/http"
	"strings"

	"github.com/gin-gonic/gin"
)

func main() {
	engine := gin.Default()
	engine.GET("/proxy/*all", func(ctx *gin.Context) {
		all := ctx.Param("all")                    // 獲取/proxy後面的引數 
		if len(all) <= 0 {
			ctx.Status(http.StatusBadRequest)
			return
		}
		all = all[1:]                             // 丟棄第一個'/'
		idx := strings.Index(all, "/")
		var url string
		if idx < 0 {                              // 只有埠
			url = fmt.Sprintf("http://172.17.0.3:%s", all)
		} else {                                 // 有埠和其它存取路徑
			port := all[:idx]
			url = fmt.Sprintf("http://172.17.0.3:%s%s", port, all[idx:])
		}
		resp, err := http.Get(url)               // 存取容器中的伺服器
		if err != nil {
			ctx.Status(http.StatusBadRequest)
			return
		}
		io.Copy(ctx.Writer, resp.Body)            // 轉發響應
	})

	engine.Run(":80")
}

在主機伺服器上執行代理伺服器,不要使用容器來啟動:

go build 
nohup ./porxy_server &           # 後臺執行

然後我們再啟動瀏覽器vscode中的伺服器看是否可以存取到:

選擇"是",然後在新彈出的視窗中就可以存取到容器中的服務了:

這裡實現的只是一個非常簡易的版本,只是提供了一個這樣的思路。如何要實現一個類似Cloud Studio的雲IDE要考慮的還要更多。
最終效果如下:

到此這篇關於雲vscode搭建使用容器化部署的文章就介紹到這了,更多相關雲vscode搭建內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com