Joyber 发布的文章

问题
工作中经常发现一些第三方写的docker容器运行有问题,这时我们会通过docker logs命令观察容器的运行日志。很可惜,有时容器中运行的程序仅从日志很难查明问题。这时我们会通过docker exec在目标容器中执行某些命令以探查问题,有时却发现一些镜像很精简,连基本的sh、bash、netstat等命令都没包含。这时就很尴尬了,诊断问题很困难。

docker-debug工具,这个工具的使用方法也很简单,参考以下命令:

# Suppose the container below is a container which should be checked
docker run -d --name dev -p 8000:80 nginx:latest
# Enter a shell where we can access the above container's namespaces (ipc, pid, network, etc, filesystem)
docker-debug dev bash -l

docker-debug的实现原理
简单说执行docker-debug命令也会使用一个包含了常用诊断命令的镜像启动一个诊断容器,该诊断容器将在目标容器相关的命名空间中运行,这样在这个容器中就可以访问目标容器的ipc, pid, network, etc, filesystem,然后使用docker exec命令在诊断容器运行命令,并将docker exec运行命令的输入输出pipe到docker-debug命令的输入输出上。

另外,还发现类似的工具kube-debug,以后诊断pod中的问题方便多了。

Electronhttps://www.electronjs.org/docs
electron-builder:打包工具

项目github地址:
https://github.com/electron/electron
https://github.com/electron/electron-quick-start

Electron是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。 嵌入 Chromium 和 Node.js 到 二进制的 Electron 允许您保持一个 JavaScript 代码代码库并创建 在Windows上运行的跨平台应用 macOS和Linux——不需要本地开发 经验。

electron 桌面开发工具 与用户操作系统和node的集成方法文档
https://www.electronjs.org/zh/docs/latest/tutorial/examples

一些开发经验:

创建 BrowserWindow 窗口

const {app, BrowserWindow} = require('electron')

let remoteURL, debug

debug = !app.isPackaged
if (debug) {
  remoteURL = 'https://www.xxx.com/notifyDeskApp/login.html';
  //忽略证书的检测
  app.commandLine.appendSwitch('ignore-certificate-errors')
} else {
  remoteURL = 'https://www.xxx.com/notifyDeskApp/login.html';
}

let trayIcon = path.join(__dirname, "icon", "notify-logo.ico");

let mainWindow, logined, quit

function createWindow () {
  // Create the browser window.
  mainWindow = new BrowserWindow({
    width: 400,
    height: 300,
    fullscreenable: false,
    maximizable: false,
    icon: trayIcon,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  })

  mainWindow.setAppDetails({
    appIconPath: trayIcon,
  })

  // and load the index.html of the app.
  mainWindow.loadURL(remoteURL)
  // mainWindow.loadFile('index.html')

  mainWindow.on('minimize',(e)=>{
    if (logined) {
      mainWindow.hide();
    }
  })

  mainWindow.on('close',(e)=>{
    if (logined && !quit) {
      e.preventDefault()
      mainWindow.hide();
    } else {
      app.quit()
    }
  })

}

app.whenReady().then(() => {


  createWindow()

  app.on('activate', function () {
    // On macOS it's common to re-create a window in the app when the
    // dock icon is clicked and there are no other windows open.
    if (BrowserWindow.getAllWindows().length === 0) createWindow()
  })

})

显示隐藏窗口

  mainWindow.hide();
  mainWindow.restore()

渲染进程与主进程通信
创建窗口的时候配置 preload.js, main.js

  mainWindow = new BrowserWindow({
    width: 400,
    height: 300,
    fullscreenable: false,
    maximizable: false,
    icon: trayIcon,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  })

窗口渲染页面与preload通过window事件,来传递事件消息
窗口加载的网页中的js事件

            //客户端登录事件:字段名称说明是否可选类型默认值detail表示该事件中需要被传递的数据,在 EventListener 获取。可选Anynullbubbles表示该事件是否冒泡。可选Booleanfalsecancelable表示该事件能否被取消。可选Booleanfalse
            let eventInit = {bubbles: false, cancelable: false, detail: {token: token, trueName: trueName}}
            let loginedEvent = new CustomEvent('logined', eventInit)
            window.dispatchEvent(loginedEvent)

preload.js

const {ipcRenderer} = require('electron');

window.addEventListener('logined', (e) => {
    ipcRenderer.send('logined', e.detail);
})

window.addEventListener('logout', (e) => {
    ipcRenderer.send('logout', e.detail);
})

window.addEventListener('open-url', (e) => {
    ipcRenderer.send('open-url', e.detail);
})

window.addEventListener('exit', (e) => {
    ipcRenderer.send('exit', e.detail);
})

main.js

const {app,shell} = require('electron')
ipcMain.on('logined', (e, data) => {
  logined = true
  mainWindow.hide();
})

ipcMain.on('open-url', (e, data) => {
  console.log(data)
  shell.openExternal(data.url);
})

ipcMain.on('logout', (e, data) => {
  logined = false
  mainWindow.restore()
})

ipcMain.on('exit', (e, data) => {
  logined = false
  quit = true
  app.quit()
})

托盘

const {app, BrowserWindow, Menu, Tray , ipcMain, shell} = require('electron')

let tray = null

const showMainWindow = (menuItem, browserWindow, event) => {
  if (mainWindow) {
    mainWindow.restore()
    mainWindow.show()
    mainWindow.focus()
  }
}

const appExit = (menuItem, browserWindow, event) => {
  quit = true
  app.quit()
}

const openDevTools = (menuItem, browserWindow, event) => {
  // Open the DevTools.
  if (mainWindow)  mainWindow.webContents.openDevTools({ mode: 'detach' })
}

let firstrun = process.argv[1] == '--squirrel-firstrun'

const setAutoOpen = function () {
  let autoOpenSet = app.getLoginItemSettings()

  let option = {
    openAtLogin: !autoOpenSet.openAtLogin, // Boolean 在登录时启动应用
    openAsHidden: !autoOpenSet.openAsHidden, // Boolean (可选) mac 表示以隐藏的方式启动应用。~~~~
  }
  if (!debug) app.setLoginItemSettings(option)
  tray.displayBalloon({iconType: 'info', title: '操作提示', content: '开机自启动: ' + (option.openAtLogin ? '已开启' : '已关闭')})
}


app.whenReady().then(() => {

  //托盘
  tray = new Tray(trayIcon)
  const contextMenu = Menu.buildFromTemplate([
    { label: '显示主窗口', type: 'normal', click: showMainWindow },
    { label: '开机自启动', type: 'normal', click: setAutoOpen, visible: !debug },
    { label: '调试窗口', type: 'normal', click: openDevTools, visible: debug },
    { label: '退出客户端', type: 'normal', click: appExit },
  ])
  tray.setToolTip(app.name)
  tray.setContextMenu(contextMenu)

})

开机自启动

app.setLoginItemSettings({
    openAtLogin: true, // Boolean 在登录时启动应用
    openAsHidden: true, // Boolean (可选) mac 表示以隐藏的方式启动应用。~~~~
  })

开发与正式环境判断: 打包后 app.isPackaged 为 true

debug = !app.isPackaged

使用系统默认的浏览器打开一个网页,而不是客户端新开一个窗口

const {shell} = require('electron')
  shell.openExternal(data.url);

只允许开一个程序

const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
  return app.quit()
}

修改桌面通知时的程序标题

app.setAppUserModelId('myapp name')

electron-builder:配置参考 (electron-vue, package.json)

{
  ...
  "build": {  // electron-builder配置
    "productName":"xxxx",//项目名 这也是生成的exe文件的前缀名
    "appId": "xxxxx",//包名  
    "copyright":"xxxx",//版权  信息
    "compression": "store", // "store" | "normal"| "maximum" 打包压缩情况(store 相对较快),store 39749kb, maximum 39186kb
    "directories": {
        "output": "build" // 输出文件夹
    }, 
    "asar": false, // asar打包
    "extraResources":  { // 拷贝dll等静态文件到指定位置
        "from": "./app-update.yml",
        "to": "./b.txt"
    },
    "win": {  
        "icon": "xxx/icon.ico"//图标路径,
        "extraResources":  { // 拷贝dll等静态文件到指定位置(用于某个系统配置)
            "from": "./app-update.yml",
            "to": "./b.txt"
        }
    },
    "nsis": {
        "oneClick": false, // 一键安装
        "guid": "xxxx", //注册表名字,不推荐修改
        "perMachine": true, // 是否开启安装时权限限制(此电脑或当前用户)
        "allowElevation": true, // 允许请求提升。 如果为false,则用户必须使用提升的权限重新启动安装程序。
        "allowToChangeInstallationDirectory": true, // 允许修改安装目录
        "installerIcon": "./build/icons/aaa.ico", // 安装图标
        "uninstallerIcon": "./build/icons/bbb.ico", //卸载图标
        "installerHeaderIcon": "./build/icons/aaa.ico", // 安装时头部图标
        "createDesktopShortcut": true, // 创建桌面图标
        "createStartMenuShortcut": true, // 创建开始菜单图标
        "shortcutName": "xxxx" // 图标名称
    }
  }
  ...
}

https://github.com/joewalnes/websocketd

这个工具可以把普通命令行命令转成ws协议,web客户端可以通过websocket连接,互换数据;
当有连接过来的时候,会启动进程执行指定的命令,如果命令结束或者连接断开,则连接关闭,进程结束;
连接后,将客户端send过来的数据,作为STDIN标准输入,传入进程,进程的标准输出STDOUT会作为message消息发送给客户端(\n结束作为一条消息);
启动服务:

cat startNotify.sh

#!/usr/bin/env bash
websocketd --port=1234 ./demp.sh

./startNotify.sh
#或者后台运行
nohup ./startNotify.sh > /dev/null 2>&1 &

一个发什么回什么的服务端脚本代码大概这样子:demp.sh

#! /pathto/php
<?hp

while (!feof(STDIN)) {
    $line = fgets(STDIN);
    echo $line;
}

?>

建立websocket连接,web网页

<script>
        var ws = new WebSocket('ws://127.0.0.1:1234')
        ws.onopen = function () {
            console.log('ws connected')
            ws.onclose = function () {
                console.log('ws closed');
            }
            ws.onmessage = function (res) { console.log(res.data) }

            var count = 0;
            
            setInterval(function () {
                ws.send(count++)
            }, 2000)
        }
        ws.onerror = function (e) {console.log(e)}
</script>

ssl网页,要用wss协议连接,通过nginx反代吧

map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}

upstream notify_ws {
    server 127.0.0.1:1234; # appserver_ip:ws_port
}

server
{

location / {

         proxy_pass http://notify_ws;
         proxy_read_timeout 300s;
         proxy_send_timeout 300s;
         
         proxy_set_header Host $host;
         proxy_set_header X-Real-IP $remote_addr;
         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
         
         proxy_http_version 1.1;
         proxy_set_header Upgrade $http_upgrade;
         proxy_set_header Connection $connection_upgrade;
         
}

}

关键在于这两行:
最重要的就是在反向代理的配置中增加了如下两行,其它的部分和普通的HTTP反向代理没有任何差别。

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;

这里面的关键部分在于HTTP的请求中多了如下头部:

Upgrade: websocket
Connection: Upgrade

这两个字段表示请求服务器升级协议为WebSocket。服务器处理完请求后,响应如下报文:

# 状态码为101
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: upgrade

告诉客户端已成功切换协议,升级为Websocket协议。握手成功之后,服务器端和客户端便角色对等,就像普通的Socket一样,能够双向通信。 不再进行HTTP的交互,而是开始WebSocket的数据帧协议实现数据交换。

这里使用map指令可以将变量组合成为新的变量,会根据客户端传来的连接中是否带有Upgrade头来决定是否给源站传递Connection头, 这样做的方法比直接全部传递upgrade更加优雅。

默认情况下,连接将会在无数据传输60秒后关闭,proxy_read_timeout参数可以延长这个时间。源站通过定期发送ping帧以保持连接并确认连接是否还在使用。

两个超时参数

proxy_read_timeout

语法 proxy_read_timeout time 默认值 60s 上下文 http server location 说明 该指令设置与代理服务器的读超时时间。它决定了nginx会等待多长时间来获得请求的响应。 这个时间不是获得整个response的时间,而是两次reading操作的时间。

proxy_send_timeout

语法 proxy_send_timeout time 默认值 60s 上下文 http server location 说明 这个指定设置了发送请求给upstream服务器的超时时间。超时设置不是为了整个发送期间,而是在两次write操作期间。 如果超时后,upstream没有收到新的数据,nginx会关闭连接

多次代理转发
工作中遇见过一种情况,就是某个域名在移动网络下面访问不了,这样的话我需要通过一个前段代理服务器做转发,这样就涉及到两次代理。

比如访问的websocket服务URL为:

wss://test.enzhico.net

这个在腾讯云公网IP上面,所有网络都能访问。另外一个域名board.xncoding.com解析到电信网络,部署在网关中心,这个域名腾讯云可以访问到。

在腾讯云主机上面:

map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}

server {
     server_name test.enzhico.net;
     location / {
         proxy_pass http://board.xncoding.com;
         proxy_read_timeout 300s;
         proxy_send_timeout 300s;
         #proxy_set_header Host $host;
         proxy_set_header X-Real-IP $remote_addr;
         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
         proxy_http_version 1.1;
         proxy_set_header Upgrade $http_upgrade;
         proxy_set_header Connection $connection_upgrade;
    }
    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/test.enzhico.net/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/test.enzhico.net/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}

上面唯一要注意的是忙,把proxy_set_header Host $host;这一行注释掉了。

而在网关中心主机上面:

map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}

upstream websocket {
    server localhost:8282; # appserver_ip:ws_port
}

server {
    listen 80;
    server_name board.xncoding.com;
    location / {
        proxy_pass http://websocket;
        proxy_read_timeout 300s;
        proxy_send_timeout 300s;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }
}

只需要最外层使用wss协议,里面的交互都使用ws协议,所以监听80端口即可。

参考文章:
https://www.xncoding.com/2018/03/12/fullstack/nginx-websocket.html
https://www.hi-linux.com/posts/42176.html

centos8安装docker 和 docker-compose

cd /etc/yum.repos.d/
wget     https://download.docker.com/linux/centos/docker-ce.repo
yum install docker-ce docker-ce-cli containerd.io
docker --version

systemctl status docker
systemctl enable docker
systemctl start docker


cd ~/
wget -O /usr/bin/docker-compose https://github.com/docker/compose/releases/download/1.29.2/docker-compose-Linux-x86_64
chmod a+x /usr/bin/docker-compose

登录自己喜欢的clash服务平台,下载clash配置文件

局域网使用别忘记把config.yaml中的这个开关打开

allow-lan: true

docker-compose.yaml

version: '3'
services:
  clash:
    image: dreamacro/clash
    volumes:
      - ./config.yaml:/root/.config/clash/config.yaml
    ports:
      - "9090:9090"
      - "7890:7890"
      - "7891:7891"
      # If you need external controller, you can export this port.
      # - "8080:8080"
    restart: always
    # When your system is Linux, you can use `network_mode: "host"` directly.
    # network_mode: "bridge"
    container_name: clash
  clashui:
    image: haishanh/yacd
    ports:
      - "1234:80"
    restart: always
    # network_mode: "bridge"
    container_name: classui

config.yaml和docker-compose.yml文件放在一个目录中,执行如入命令

sudo docker-compose up -d

修改了配置文件,重启容器

sudo docker-compose restart

访问UI面板(局域网中,12是虚拟机IP)
http://192.168.10.12:1234
添加接口地址 192.168.10.12:9090
选择好节点后,浏览器用192.168.10.12:7890 即可上网