引言
最近和团队一起完成了软件工程的项目任务,实现一个在线聊天网页。我参与了后端的部分工作,但是具体到关键的websocket部分,我并没有了解得很详细、具体。所以我会花一些时间从我们自己的项目中总结一下websocket,作为今后的学习和复习资料。
websocket协议
可以把websocket看作是HTTP协议为了支持长连接所做的一个大补丁。
http对于长连接的存在的问题
- HTTP协议中所谓的keep-alive connection是指在一次TCP连接中完成多个HTTP请求,但是对每个请求仍然要单独发header
- polling是指客户端不断主动得向服务器发送请求查询操作
这两种模式都有一个共同的缺点,就是除了真正的数据部分之外,服务端和客户端还需要交换大量的HTTP header,信息交换效率很低
websocket的优点
可以实现实时信息传递,同时websocket还可以绕过大多数防火墙的限制。使http协议变成真正的长连接,全双工协议。
websocket协议报文的头部比较轻量化,可以减少数据传输量,因为一旦websocket建立连接,双方的身份已经确定,所以不再需要像http一样发送额外的身份确认字段。
websocket基本原理
websocket之所以和http不同,很大的一个原因是相当于它把socket通信在应用层进行了一遍封装操作
websocket通信协议实现的是基于浏览器的原生socket,这样原先只有在c/s模式下的大量开发模式都可以搬到web上来了,基本就是通过浏览器的支持在web上实现了与服务器端的socket通信。
WebSocket没有试图在HTTP之上模拟server推送,而是直接在TCP之上定义了帧协议,因此WebSocket能够支持双向的通信。
websocket协议本质上也是使用系统socket,它是把socket引入了http通信,也就是不使用80端口进行http通信。它的目的是建立全双工的连接,可以用来解决服务器客户端保持长连接的问题。
websocket与http的区别和联系
联系
他们都是基于TCP的协议,并且都是应用层协议。websocket的建立需要使用一次http请求,该请求中需要有一些特殊的字段要求服务器升级协议。可以理解为webscoket借用了http协议完成了一部份握手
对于要求升级为websocket的请求,如果服务器支持websocket协议,那么服务器会发送101的HTTP响应。如果不支持,服务器会优雅得忽略掉该报文
- 要求升级报文的格式
该报文也是websocket协议握手的关键,报文结构如下所示
重点就在于Upgrade和Connection这两个字段。
区别
主要区别在于两者的持久性和生命周期。
websocket是一个持久化的协议;
对于一个http请求来说,一次请求只有一个Request和Response(HTTP1.1中可以使用长连接)。而websocket协议中,对于一个tcp连接,服务端和客户端都可以发送任意数量的报文,知道该连接中断。
websocket协议Go语言实现(框架理解)
学习github.com/gorilla/websocket开源仓库记录。参考/examples/chat案例学习
websocket报文格式
websocket虽然属于应用层协议,但是由于其已经基于TCP建立了发送方和接收方的连接,所以在后续的报文传输中,不再需要利用报文传输双方的身份信息。
具体的报文格式可以参考博客
chat案例具体架构
该案例具体代码在github.com/gorilla/websocket/examples/chat中。
服务端维护两种对象,Hub和Client。Hub是单例对象,Client与每一个用户一一对应。Client需要在Hub中进行注册,同时需要将用户发送过来的消息转发给Hub,Hub发送给Client的消息,Client需要转发给对应的用户。
简而言之,就是所有的Client统一由Hub调控,每一个Client负责与一个用户进行通信,同时将通信情况反馈给Hub。
需要注意的一点是:Hub和Client通信通过Channel进行,而Client和用户通信通过Websocket connection进行
chat案例重要代码
协议升级
在main.go中首先会对默认路由发送默认的资源html文件,然后浏览器会申请升级协议。此时就需要调用下面的serveWs中的Upgrade函数进行升级协议,升级协议成功返回的状态码为101 Switching protocal。
// serveWs handles websocket requests from the peer.
func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
// 标准库提供的升级协议函数,直接使用即可
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return
}
client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)}
client.hub.register <- client
// Allow collection of memory referenced by the caller by doing all work in
// new goroutines.
go client.writePump()
go client.readPump()
}
hub.go
func (h *Hub) run() {
for {
select {
case client := <-h.register:
h.clients[client] = true
case client := <-h.unregister:
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
close(client.send)
}
case message := <-h.broadcast:
for client := range h.clients {
select {
case client.send <- message:
default:
close(client.send)
delete(h.clients, client)
}
}
}
}
}
该函数为单独开启的一个gorutine,用于维护Client的注册、注销、以及广播消息。
通过client.send <- message,将从某一个Client中收到的消息广播到其他的每一个Client中。
client.go
Client主要负责两件事情,一件事情就是从websocket连接中获取消息,并将其发送给Hub;另外一件事情就是从Hub中获取信息,然后通过websocket连接发送给对应的用户。
前一件事情由readPump函数进行;后一件事情由writePump函数进行。
// readPump pumps messages from the websocket connection to the hub.
//
// The application runs readPump in a per-connection goroutine. The application
// ensures that there is at most one reader on a connection by executing all
// reads from this goroutine.
func (c *Client) readPump() {
defer func() {
c.hub.unregister <- c
c.conn.Close()
}()
c.conn.SetReadLimit(maxMessageSize)
c.conn.SetReadDeadline(time.Now().Add(pongWait))
c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
for {
_, message, err := c.conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
log.Printf("error: %v", err)
}
break
}
message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1))
c.hub.broadcast <- message
}
}
// writePump pumps messages from the hub to the websocket connection.
//
// A goroutine running writePump is started for each connection. The
// application ensures that there is at most one writer to a connection by
// executing all writes from this goroutine.
func (c *Client) writePump() {
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
c.conn.Close()
}()
for {
select {
case message, ok := <-c.send:
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if !ok {
// The hub closed the channel.
c.conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
w, err := c.conn.NextWriter(websocket.TextMessage)
if err != nil {
return
}
w.Write(message)
// Add queued chat messages to the current websocket message.
n := len(c.send)
for i := 0; i < n; i++ {
w.Write(newline)
w.Write(<-c.send)
}
if err := w.Close(); err != nil {
return
}
case <-ticker.C:
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}