Go语言使用 WebSocket 总结

WebSocket是什么鬼?

1.「WebSocket」是一种在TCP连接上进行全双工通信的协议。WebSocket 通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准;

2.WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接(长连接),并进行双向数据传输(全双工);

3.WebSocket能更好的节省服务器资源和带宽,并且能够更实时的进行通信。一旦建立WebSocket连接,无论是服务器还是客户端,任意一方都可以直接向对方发送数据;

4.客户端为了和服务端建立一个WebSocket连接,首先要向服务器发起一个HTTP请求,这个请求和普通的请求不一样;会包含一些附加头信息,比如:Upgrade:WebSocket,服务器会解析这些附加头信息;

5.WebSocket协议为了兼容现有浏览器,所以在握手阶段使用了HTTP协议;WebSocket是类似TCP长连接的通信模式;WebSocket协议建立在TCP协议之上;

为什么需要 WebSocket?

因为HTTP协议是短连接,客户端发起请求,服务端响应请求整个通信就结束了;HTTP协议有一个缺陷:通信只能由客户端发起;客户端要获取服务端的实时状态只能使用类似服务轮询请求的方式去实现;轮询的方式效率非常低、消耗服务器资源还要不停地连接和断开连接的过程;

WebSocket的有点有:

  • 1.建立在TCP协议之上,服务器端的实现比较容易;
  • 2.与HTTP协议有着良好的兼容性;默认端口也是80443,并且握手阶段采用HTTP协议
  • 3.数据格式比较轻量,性能开销小,通信高效;
  • 4.可以发送文本,也可以发送二进制数据;
  • 5.没有同源限制,客户端可以与任意服务器通信(双工通信);
  • 6.协议标识符是ws(如果加密,则为wss),服务器网址就是 URL;

Go语言使用 WebSocket

专门做了一下调研;目前Golang的生态中,使用WebSocket的开源包比较多的是https://github.com/gorilla/websocket

1
2
3
go get -v  github.com/gorilla/websocket
或者
go install github.com/gorilla/websocket@latest

下面使用Go语言分别实现一个「客户端」和一个websocket的「服务端」程序;服务端程序监听服务器端口;客户端通过HTTP协议连接到服务端;

服务端实现:websocket/server.go

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
package main

import (
"github.com/gorilla/websocket"
"log"
"net/http"
)

var (
addr = "127.0.0.1:8888"
upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
)

func main() {
http.HandleFunc("/hello", serveWsHandler)
log.Fatal(http.ListenAndServe(addr, nil))
}

func serveWsHandler(w http.ResponseWriter, r *http.Request) {
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("upgrade err:", err)
return
}
defer ws.Close()

for {
mt, message, err := ws.ReadMessage()
if err != nil {
log.Println("read message err:", err)
break
}
log.Println("received:", string(message))
if err := ws.WriteMessage(mt, message); err != nil {
log.Println("write message err:", err)
break
}
}
}

客户端实现:websocket/client.go

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
package main

import (
"github.com/gorilla/websocket"
"log"
"net/url"
"os"
"os/signal"
"time"
)

var host = "127.0.0.1:8888"

func main() {
done := make(chan struct{})
interrupt := make(chan os.Signal)

signal.Notify(interrupt, os.Interrupt)

socketURL := url.URL{Scheme: "ws", Host: host, Path: "/hello"}
log.Println("connecting to ", socketURL.String())
conn, _, err := websocket.DefaultDialer.Dial(socketURL.String(), nil)
if err != nil {
log.Println("conn to server err:", err)
return
}
defer conn.Close()
go receiveHandler(conn, done)

for {
select {
case <-time.After(time.Duration(1) * time.Millisecond * 1000):
err := conn.WriteMessage(websocket.TextMessage, []byte("Hello WebSocket!"))
if err != nil {
return
}
case <-interrupt: // Ctrl + C SIGINT
log.Println("received SIGINT interrupt signal")
err := conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
if err != nil {
log.Println("closing websocket err:", err)
return
}
select {
case <-done:
log.Println("received channel closed! exiting....")
case <-time.After(time.Duration(1) * time.Second):
log.Println("timeout in closing receiving channel. exiting....")
}
return
}
}
}

func receiveHandler(conn *websocket.Conn, done chan struct{}) {
defer close(done)
for {
_, msg, err := conn.ReadMessage()
if err != nil {
log.Println("read message err:", err)
return
}
log.Println("received:", string(msg))
}
}