软件工程总结——Websocket学习

引言

最近和团队一起完成了软件工程的项目任务,实现一个在线聊天网页。我参与了后端的部分工作,但是具体到关键的websocket部分,我并没有了解得很详细、具体。所以我会花一些时间从我们自己的项目中总结一下websocket,作为今后的学习和复习资料。

websocket协议

可以把websocket看作是HTTP协议为了支持长连接所做的一个大补丁。

http对于长连接的存在的问题

  1. HTTP协议中所谓的keep-alive connection是指在一次TCP连接中完成多个HTTP请求,但是对每个请求仍然要单独发header
  2. 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
			}
		}
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lingwu_hb

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值