[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接口,这样就能够实现上述功能。