Golang使用WebSocket+ChromeDP实现实时页面监控

WebSocket+Chromedp实现页面实时监控

出发点

  • 理解与应用 Golang 的 Gorouting

  • Channel 通道 chan

  • WebSocket

  • chromedp

    查看 socket 使用时,想到能否结合应用下 chan, 正好又看到chromedp 的文章.就思考能否通过 websocket + chan 实现 headless chrome 监控页面信息

    Socket

    网络中的 Socket 并不是什么协议,而是为了使用 TCP,UDP 而抽象出来的一层 API,它是位于应用层和传输层之间的一个抽象层。Socket 是对 TCP/IP 的封装;
    Golang 本身net 包 即可实现 Socket 服务 .

WebSocket

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议. 因为不了解WebSocket导致闹了个笑话所以列出来

  • WebSocket 与 HTTP都是基于TCP/IP协议上的,可靠性传输协议,都是应用层协议.
  • WebSocket是双向通信协议,模拟Socket协议,可以双向发送或接受信息。HTTP是单向的。
  • WebSocket是需要握手进行建立连接的。
    • 1.浏览器、服务器建立TCP连接,三次握手。这是通信的基础,传输控制层,若失败后续都不执行。
    • 2.TCP连接成功后,浏览器通过HTTP协议向服务器传送WebSocket支持的版本号等信息。(开始前的HTTP握手)
    • 3.服务器收到客户端的握手请求后,同样采用HTTP协议回馈数据。
    • 4.当收到了连接成功的消息后,通过TCP通道进行传输通信。

chromedp

用于驱动 支持 Chrome DevTools Protocol 的浏览器

Package chromedp is a faster, simpler way to drive browsers supporting the Chrome DevTools Protocolin Go without external dependencies (like Selenium or PhantomJS).

Examples

Example Description
click2 use a selector to click on an element
click use a selector to click on an element
screenshot take a screenshot of a specific element and of the entire browser viewport

功能逻辑

  • main
  • go func GR1 开启浏览器
    • GR1 常驻截屏,写入 全局 chan
  • go func GR2 开启消息读取
    • GR2 读取到消息后,压缩图片信息,
    • GR2 遍历当前所有WebSocket连接对象 发送图片数据(blob|base64)
  • 启动 WebSocket 服务,接受 客户端连接,插入到 全局 连接 Map 中
    • 接受客户端的点击事件,等比计算,映射到 chromedp 监控的页面中
      1
      2
      graph LR
      A-->B

实现代码

Golang Server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
 package main

import (
"context"
"encoding/base64"
"flag"
"fmt"
"io/ioutil"
"log"
"net/http"
"time"

"github.com/chromedp/chromedp"
"github.com/gorilla/websocket"
)

var addr = flag.String("addr", "localhost:9999", "http service address")

var upgrader = websocket.Upgrader{} // use default options

// 用来记录所有的客户端连接
var ConnMap map[string]*websocket.Conn

var MAX_WATCH_TIME = 500

const DATE_TIME_FORMAT = "2006-01-02 15:04:05"
const DATE_FORMAT = "2006-01-02"
const DATE_FORMAT_NUM = "20060102"

var CH_SC = make(chan []byte)

func echo(w http.ResponseWriter, r *http.Request) {
// 允许跨域
upgrader.CheckOrigin = func(r *http.Request) bool { return true }
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Print("upgrade:", err)
return
}
defer c.Close()

// 新连接加入map
ConnMap[c.RemoteAddr().String()] = c

for {
mt, message, err := c.ReadMessage()
if err != nil {
log.Println("read:", err)
break
}
log.Printf("recv: %s", message)
err = c.WriteMessage(mt, message)
if err != nil {
log.Println("write:", err)
break
}
}
}

func main() {
flag.Parse()
log.SetFlags(0)
begin := time.Now()
ConnMap = make(map[string]*websocket.Conn)

// 启动 chrome 并开启截屏进程
go func() {
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.Flag("headless", true),
chromedp.Flag("disable-gpu", false),
chromedp.Flag("enable-automation", false),
chromedp.Flag("disable-extensions", true),
)

allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
defer cancel()

// create context
ctx, cancel := chromedp.NewContext(allocCtx, chromedp.WithLogf(log.Printf))
defer cancel()

urlstr := `https://youku.com/`
// 需要截图的元素,支持CSS selector以及XPath query
selector := `#m_15319` //`#m_2556` //

if err := chromedp.Run(ctx,
// emulate iPhone 7 landscape
// chromedp.Emulate(device.IPhone7landscape),
chromedp.Navigate(urlstr),
chromedp.EmulateViewport(1200, 0, chromedp.EmulateScale(1)),
); err != nil {
log.Fatal(err)
}

var buf []byte
for {
buf = []byte{}
if err := chromedp.Run(ctx, elementScreenshot(urlstr, selector, &buf)); err != nil {
log.Fatal(err)
}
// 发送到全局channel
CH_SC <- buf
fmt.Println(int64(time.Since(begin).Seconds()), MAX_WATCH_TIME)
// if int64(time.Since(begin).Seconds()) > int64(MAX_WATCH_TIME) {
// return
// }
}
}()

// 启动发送消息通道
go func() {
for {
select {
case task := <-CH_SC:
//task.Process()
fmt.Println(time.Now().Format(DATE_TIME_FORMAT))
sendPictureData(task)
// 写入文件
if err := ioutil.WriteFile("youku_b.png", task, 0644); err != nil {
log.Fatal(err)
}
}
}
}()

http.HandleFunc("/echo", echo)
log.Fatal(http.ListenAndServe(*addr, nil))

}

// 发送二进制图片数据
func sendPictureData(picture []byte) {
// img, _ := png.Decode(bytes.NewReader(picture))
// resizeImg := resize.Resize(hight, 0, img, resize.Lanczos3)
//base64压缩
sourcestring := base64.StdEncoding.EncodeToString(picture)
// err := c.WriteMessage(websocket.BinaryMessage, []byte(sourcestring))
for _, c := range ConnMap {
err := c.WriteMessage(websocket.TextMessage, []byte(sourcestring))
if err != nil {
log.Println("write:", err)
}
}
}

// 截图方法
func elementScreenshot(urlstr, sel string, res *[]byte) chromedp.Tasks {
// ssOpts := append()
return chromedp.Tasks{
// chromedp.Navigate(urlstr),
chromedp.WaitVisible(sel, chromedp.ByID),
//chromedp.Sleep(time.Duration(3) * time.Second),
// 执行截图
chromedp.Screenshot(sel, res, chromedp.NodeVisible, chromedp.ByID),
}
}

JS 处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
 function arrayBufferToBase64(buffer) {
var binary = '';
var bytes = new Uint8Array(buffer);
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
}

//**blob to dataURL**
function blobToDataURL(blob, callback) {
var a = new FileReader();
a.onload = function(e) {
callback(e.target.result);
}
a.readAsDataURL(blob);
return a;
}

var canvas = document.getElementById("animation_canvas");
canvas.width = 640;
canvas.height = 1030;

var ctx = canvas.getContext("2d");
let ws = new WebSocket("ws://localhost:9999/echo");
var player = document.getElementById('player');
ctx.drawImage(player, 0, 0);

ws.onopen = function () {
ws.send(JSON.stringify({ message: "hello server!" }))
}
ws.onmessage = function(evt) {
// console.log(evt.data,typeof(evt.data))
// base64数据直接展示
if(typeof(evt.data)=="string"){
player.src='data:image/png;base64,'+evt.data;
player.onload=function(){
canvas.width = 1200;
canvas.height = this.height;
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.drawImage(this, 0, 0);
}
}
// 二进制数据转图片
// result = blobToDataURL(evt.data, function(dataurl) {
// // 截取 base64,
// var arr = dataurl.split(',');
// var mime = arr[0].match(/:(.*?);/)[1];
// var player = document.getElementById('player');
// ctx.drawImage(player, 0, 0);
// var url= arrayBufferToBase64(evt.data);
// player.src='data:image/png;base64,'+arr[1];
// player.onload=function(){
// canvas.width = 1200;
// canvas.height = this.height;
// ctx.clearRect(0,0,canvas.width,canvas.height);
// ctx.drawImage(this, 0, 0);
// }

// });
}
ws.onerror = function (event) {
console.debug(event)
}