[项目学习]墨鱼探针

这次来研究一个比较有意思的项目,叫做墨鱼探针。从功能上讲是实现了一个简单的系统性能监控加上一个前段数据展示的项目。虽然不是什么高大上的项目,但是经典的前后端分离架构再加上不错的项目质量用来作为学习的话是相当不错的。

  • 项目名称:墨鱼探针

  • 项目难度:2/5

  • 项目标签:WebApp/前后端分离

  • 技术栈:go(fiber) + vue2 + element-ui + nes.css

正文

后台部分

Web 框架

先从main出发, 这里使用了Go Fiber 的lib,下面是官方的介绍。

An Express-inspired web framework written in Go.

从代码上可见其 风格的确和 express和Koa 是比较相似的。都是使用路由来绑定Handle的方式。

app.Use(cors.New())

app.Use("/ws/*", middleware.UpgradeOptions)

app.Get("/sys_info", controller.GetSysInfo)
app.Get("/sys_status", controller.GetSysStatus)
app.Get("/ws/sys_status", websocket.New(controller.PushSysStatus))

在下面一段是用来指定静态目录的,这里直接把根路径绑定到了http提供的browser上面。比较惊讶还能这么搞。

一般是使用框架本本身的Static来操作的。

stripped, err := fs.Sub(frontend, "dist")
if err != nil {
  log.Fatal(err)
}
app.Use("/", filesystem.New(filesystem.Config{
  Root:   http.FS(stripped),
  Browse: true,
}))

API

WS

这里用来建立Ws的连接,用于数据的实时更新,简单来建联WebSocket。

// main
app.Use("/ws/*", middleware.UpgradeOptions) 
app.Get("/ws/sys_status", websocket.New(controller.PushSysStatus))

在UpgradeOptions中,简单的判断是否支持Upgrade,不支持的话就抛异常出去。

// UpgradeOptions
if websocket.IsWebSocketUpgrade(c) {
        return c.Next()
}
return fiber.ErrUpgradeRequired

建立新连接,并且轮询推送 Sysinfo 到建立起的 Ws的连接。

// main
app.Get("/ws/sys_status", websocket.New(controller.PushSysStatus))

// PushSysStatus
func PushSysStatus(c *websocket.Conn) {
  for {
    c.WriteJSON(service.GetSystemStatus())
    time.Sleep(1 * time.Second)
    }
}

sysinfo

这里就是采集系统信息的原理。开发者吧指标都拆分成了多个函数,这里选取部分来讲。

func GetSystemInfo() *SystemInfo {
    return &SystemInfo{
        HostInfo:     GetHostInfo(),
        CPUInfo:      GetCPUInfo(),
        MemoryInfo:   GetMemoryInfo(),
        DiskInfo:     GetDiskInfo(),
        NetworkInfo:  GetNetworkInfo(),
        SystemStatus: GetSystemStatus(),
    }
}

这里信息的获取比较简单,直接使用底层的接口来获取本本机信息,比较简单。看下面的源码就很容易看到。就不多赘述,主要是要熟悉lib以及一些格式的处理。

func GetHostInfo() *HostInfo {
    hostInfo, _ := host.Info()
    return &HostInfo{
        Hostname:        hostInfo.Hostname,
        Distribution:    hostInfo.Platform + " " + hostInfo.PlatformVersion,
        Arch:            hostInfo.KernelArch,
        Kernel:          hostInfo.KernelVersion,
        VirtualPlatform: hostInfo.VirtualizationSystem,
        Uptime:          hostInfo.Uptime,
    }
}

func GetCPUInfo() *CPUInfo {
    cpuInfoStats, _ := cpu.Info()
    cpuInfo := new(CPUInfo)
    cpuInfo.Count, _ = cpu.Counts(true)
    for _, infoStat := range cpuInfoStats {
        core := &Core{
            CPU:       infoStat.CPU,
            CoreID:    infoStat.CoreID,
            ModelName: infoStat.ModelName,
            Mhz:       infoStat.Mhz,
            Flags:     infoStat.Flags,
        }
        cpuInfo.AppendCore(core)
    }
    return cpuInfo
}

func GetNetworkInfo() *NetworkInfo {
    networkInfo := new(NetworkInfo)
    ioStats, _ := net.IOCounters(true)
    r, _ := regexp.Compile("^(eth|enp).*")
    for _, ioStat := range ioStats {
        if !r.MatchString(ioStat.Name) {
            continue
        }
        ifce := &Ifce{
            Name:     ioStat.Name,
            ByteSend: ioStat.BytesSent,
            ByteRecv: ioStat.BytesRecv,
        }
        networkInfo.AppendIfce(ifce)
    }
    return networkInfo
}

这里比较有趣的是,go 中的结构体的应用。因为初步接触Go语言, 所以这里也简单的记录下。

type SystemInfo struct {
    HostInfo     *HostInfo     `json:"host_info"`
    CPUInfo      *CPUInfo      `json:"cpu_info"`
    MemoryInfo   *MemoryInfo   `json:"memory_info"`
    NetworkInfo  *NetworkInfo  `json:"network_info"`
    DiskInfo     *DiskInfo     `json:"disk_info"`
    SystemStatus *SystemStatus `json:"system_status"`
}

func GetSystemInfo() *SystemInfo {
    return &SystemInfo{
        HostInfo:     GetHostInfo(),
        CPUInfo:      GetCPUInfo(),
        MemoryInfo:   GetMemoryInfo(),
        DiskInfo:     GetDiskInfo(),
        NetworkInfo:  GetNetworkInfo(),
        SystemStatus: GetSystemStatus(),
    }
}

小结

到这里后台部分都已经分析完成了。其基本原理上还是十分简单的。一个webserver 加上 Ws 的推送。使用底层的Lib来获取OS上的各种信息。使用Json 的格式进行组装和返回。

前端部分

该项目使用VUE来进行开发,

全局作用域

模块导入和与Vue本身的绑定,这样样式就可以在全局域进行使用。

import "modern-normalize/modern-normalize.css"
import {
  Row,
  Col,
  Container,
  Header,
  Main,
  Footer,
} from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import axios from 'axios'
import "nes.css/css/nes.min.css"

Vue.component(Row.name, Row)
Vue.component(Col.name, Col)
Vue.component(Container.name, Container)
Vue.component(Header.name, Header)
Vue.component(Main.name, Main)
Vue.component(Footer.name, Footer)

路由部分

使用Vue的前段路由,由于是单页应用,所以这里的路由就Page组件。

export default new Router({
  routes: [
    {
      path: '/',
      name: 'SystemPage',
      component: SystemPage
    }
  ]
})

页面

这里是页面的主体部分,原理很简单使用nes 的CSS 来设计一个主容器来包裹其他的状态条。

        <el-main>
            <div class="nes-container with-title">
                <p class="title">Basic System Information</p>
                <host :info="host_data"/>
                <div class="nes-container with-title is-centered component-container">
                    <p class="title">CPU</p>
                    <cpu :cpu_percent="cpu_percent" />
                </div>
                <div class="nes-container with-title is-centered component-container">
                    <p class="title">Memory</p>
                    <mem :info="memory_info" :status="memory_status" />
                </div>
                <div class="nes-container with-title is-centered component-container">
                    <p class="title">Disk</p>
                    <disk :partitions="partitions" />
                </div>
            </div>
        </el-main>

双向绑定

重点在于下面的传说中的Vue 的双向绑定。这里使用hostdata 来举例。因为Hostdata 是不需要进行多次渲染的。所以在组件的 Compute 周期就完成了数据的刷新。这里的函数逻辑主要是对接口的的的数据进行的计算。如此就可将得到的数据返回给 host_data 组件。

        computed: {
            host_data: function() {
                let hostname = this.host_info.hostname
                let os = this.host_info.distribution + " (" + this.host_info.kernel + " " + this.host_info.arch + ")"
                let cpu = 'unknown'
                let aes_ni = false
                let vm_x_amd_v = false
                if (this.cpu_info.cores.length > 0) {
                    cpu = this.cpu_info.cores[0].model + " @ " + this.cpu_info.cores[0].mhz
                    aes_ni = this.cpu_info.cores[0].flags.includes("aes")
                    vm_x_amd_v = this.cpu_info.cores[0].flags.includes("vmx") || this.cpu_info.cores[0].flags.includes("svm")
                }
                let virt = this.host_info.virtual_platform ? this.host_info.virtual_platform : 'unknown'
                let uptime = this.host_info.uptime
                let load_avg = this.load_avg
                let ifces = this.ifces
                return {
                    hostname,
                    os,
                    cpu,
                    aes_ni,
                    vm_x_amd_v,
                    virt,
                    uptime,
                    load_avg,
                    ifces,
                }
            },
        },

而Hostdata 的组件中定义了表单的格式以及内容。通过 info来获取父组件的数据流。但是在Hostinfo 中也是存在动态字段的。

数据处理

下面是hostdata 的数据处理过的函数,主要是提供了当前流量和当前包接受这种实时数据。里面需要学习的点有

  1. Vue 的过滤器,也就是下面定义的filter 可以用来对 Vue中的插值数据进行处理
  2. 这里对网速的计算很巧妙的使用了 sessionStorage 来进行临时的保存。没有使用另一个临时状态。思路可以进行学习。
    import {humanSec, humanByte} from "@/common/filters"
    export default {
        name: "host",
        props: ['info'],
        methods: {
            curNetSend: function(curTotalSend) {
                var lastTotalSend = sessionStorage.getItem("lastTotalSend")
                if (!lastTotalSend) {
                    lastTotalSend = curTotalSend
                }
                sessionStorage.setItem("lastTotalSend", curTotalSend)
                return curTotalSend - lastTotalSend
            },
            curNetRecv: function(curTotalRecv) {
                var lastTotalRecv = sessionStorage.getItem("lastTotalRecv")
                if (!lastTotalRecv) {
                    lastTotalRecv = curTotalRecv
                }
                sessionStorage.setItem("lastTotalRecv", curTotalRecv)
                return curTotalRecv - lastTotalRecv
            }
        },
        filters: {
            humanSec: function(sec) {
                return humanSec(sec)
            },
            humanByte: function(size) {
                return humanByte(size)
            }
        },
    }

接口数据

在组件的生命周期函数的 Create来进行数据的获取,并且进行数据的解析,来保存到各自的status 去。下面先列生命周期函数:

created() {
    this.initData()
    this.initWs()
},
destroyed() {
    this.ws.close()
},

InitData 调用前面写的后端的sysinfo接口来获取数据。

initData() {
    this.axios.get("/sys_info").then(
        res => {
            this.host_info = res.data.host_info
                        // ...
            this.timestamp = res.data.timestamp
        }
    ).catch(res => {
        console.log(res)
    })
},

InitWs 用来初始化Ws 的连接,这里对于数据的刷新是动态的。

initWs() {
    let wsProtocol = window.location.protocol == "https:" ? "wss://" : "ws://";
    let wsPort = window.location.port == "" ? "" : ":" + window.location.port;
    this.ws = new WebSocket(wsProtocol + window.location.hostname + wsPort + "/ws/sys_status")
    this.ws.onopen = this.wsOnOpen
    this.ws.onerror = this.wsOnError
    this.ws.onmessage = this.wsOnMessage
    this.ws.onclose = this.wsOnClose
},

wsOnMessage(e) {
    var systemStatus = JSON.parse(e.data)
    this.cpu_percent = systemStatus.cpu_percent
    // ...
    this.uptime = systemStatus.uptime
},

小结

比较简单的web单页,虽然不难,但是体现了较好的思想。数据初始化,动态渲染。以及分组件。

整体来说项目是比较简单的,自己做一个类似的仿品用来练练手也是可以的。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注