[golang] android应用中websocket的处理

最近在学习golang,完成了一个聊天app,效果如下:

整个项目后端基于golang,主要通过websocket来实现。根据websocket的协议,在socket连接之后需要进行ping-pong发包,ping-pong应该由服务器自己实现,无需客户端的参与,用来保证websocket连接的正常,下面我们用golang来实现一下。
首先构建如下测试项目结构:

1
2
3
4
5
6
7
8
.
├── go.mod
├── go.sum
├── main
├── main.go
├── model.go
├── session.go
└── websock_hdl.go

在main.go中构建一个http服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

package main

import (
"net/http"
)
var global struct {
Store *SessionStore
}
func main() {
server := http.NewServeMux()
global.Store = &SessionStore{
sessCache: make(map[string]*Session),
}
server.HandleFunc("/ws", WebsocketHandler)
http.ListenAndServe(":6060", server)
}

在session.go中需要实现两个goroutine,一个readLoop用来监听客户端发给server的信息,一个writeLoop用来监听服务端发给客户端的信息。

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

// WriteLoop is loop
func (sess *Session) writeLoop() {
ticker := time.NewTicker((time.Second * 10 * 9) / 10)
defer func() {
ticker.Stop()
sess.closeWS()
}()
for {
select {
case msg, ok := <-sess.send:

fmt.Println("发送消息", msg)
case <-ticker.C:
sess.ws.SetWriteDeadline(time.Now().Add(time.Second * 1))
if err := sess.ws.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}

func (sess *Session) readLoop() {
defer func() {
sess.closeWS()
}()
sess.ws.SetReadLimit(1024)
sess.ws.SetReadDeadline(time.Now().Add(time.Second * 10))
sess.ws.SetPongHandler(func(appData string) error {
fmt.Println("客户端返回pong包", appData)
sess.ws.SetReadDeadline(time.Now().Add(time.Second * 10))
return nil
})
for {
_, raw, err := sess.ws.ReadMessage()
fmt.Println("收到客户端消息内容", string(raw))
}
}

在writeLoop中需要定义一个NewTicker,它的作用是每间隔一段固定的时间,将当前的时间发送到channel ticker.C中,这样我们可以通过监听该通道,用来在固定时间内向客户端发送一个ping消息。当websocket收到ping消息之后在readLoop中实现setPongHandler函数,用来向server返回一个事件,用来表示当前客户端连接并未断开。这个事件就是SetReadDeadline这个事件,它用来表示该websocket在几秒之后会失效(当超过该时间后会使websocket断开)。所以pong处理时需要更新websocket的ReadDeadline的时间,保证socket的长期连接。

在webcok_hdl.go中将http请求升级成websocket:

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

type Student struct {
Name string `json:"name"`
Age int16 `json:"age"`
Class *Class `json:"class"`
}
type Class struct {
StudyNumber string `json:"studyNumber"`
Number int16 `json:"number"`
}

// WebsocketHandler 处理socket链接
func WebsocketHandler(w http.ResponseWriter, r *http.Request) {
//升级http请求
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
websock, err := upgrader.Upgrade(w, r, nil)
if err != nil {
fmt.Println("Error", err)
return
}
var sess *Session = global.Store.NewSession(websock)
fmt.Println("ws: 开始websocket监听")
go sess.writeLoop()
go sess.readLoop()
js, _ := json.Marshal(&Student{
Name: "abc",
Age: 18,
Class: &Class{
StudyNumber: "2016221097121",
Number: 189,
},
})
sess.send <- js
}

注意:在golang中函数与方法是有区别的,一个函数可以拥有自己的方法,一个方法会有自己的接受者,这与传统的面向对象的语言不同。所以我们可能会看见一些奇怪的使用方法,比如定义一个函数类型:

1
type HandlerFunc func(ResponseWriter, *Request)

定义一个Hnadler接口:

1
2
3
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}

给HandlerFunc函数类型实现Handler接口中的方法:

1
2
3
4

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}

在golang的net/http内置包中的HandlerFunc函数(用来将路由映射到视图函数)就运用了上述的技巧,使你能够将任意命名的函数只要参数是(ResponseWriter,*Resquest),就均能作为视图函数,来处理路由。如下:

1
server.HandleFunc("/ws", WebsocketHandler)

在源码中跟踪上述函数:

1
2
3
4

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
mux.Handle(pattern, HandlerFunc(handler))
}

HandleFunc中调用了Handle函数:

1
func (mux *ServeMux) Handle(pattern string, handler Handler)

Handle函数的第二个参数应该是一个Handle接口:

1
2
3
4

type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}

注意在Handle函数中,第二个参数使用了一个HandlerFunc函数(多一个r),这个函数的定义如下:

1
2
3
4
5
6
7

type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}

可见,这里的HandlerFunc(handler))可以理解为强制类型转化,将handler函数强制转化为Handler接口,这样就能够实现上述功能。