GO WebSocket 编程

164 阅读3分钟

WebSocket 协议解读

image.png

WebSocket和http协议的关联:

  • 都是应用层协议,都基于TCP 传输协议。
  • 跟HTTP 有良好的兼容性,WebSocket 和HTTP 的默认端口都是80,wss 和https 的默认端口都是443。
  • WebSocket在握手阶段采用http发送数据。

Websocket 和HTTP 协议的差异:

  • HTTP 是半双工,而WebSocket 通过多路复用实现了全双工。
  • HTTP 只能由Client 主动发起数据请求,而WebSocket 还可以由Server 主动向Client 推送数据。在需要及时刷新的场景中,HTTP 只能靠Client 高频地轮询,浪费严重。
  • HTTP 是短连接(也可以实现长连接, HTTP1.1 的连接默认使用长连接),每次数据请求都得经过三次握手重新建立连接,而WebSocket 是长连接。
  • HTTP 长连接中每次请求都要带上header ,而WebSocket 在传输数据阶段不需要带header 。

WebSocket 握手协议:

Request Header

Sec-Websocket-Version:13
Upgrade:websocket
Connection:Upgrade
Sec-Websocket-Key:duR0pUQxNgBJsRQKj2Jxsw==

Response Header

Upgrade:websocket
Connection:Upgrade
Sec-Websocket-Accept:a1y2oy1zvgHsVyHMx+hZ1AYrEHI=
  • Upgrade:websocket 和Connection :Upgrade 指明使用WebSocket协议。
  • Sec-WebSocket-Version:指定Websocket协议版本。
  • Sec-WebSocket-Key:一个Base64 encode 的值,是浏览器随机生成的。
  • 服务端收到Sec-WebSocket-Key 后拼接上一个固定的GUID ,进行一次SHA-1 摘要,再转成Base64编码,得到Sec-WebSocket-Accept 返回给客户端。
  • 客户端对本地的Sec-WebSocket-Key 执行同样的操作跟服务端返回的结果进行对比,如果不一致会返回错误关闭连接。如此操作是为了把WebSocket headerHTTP header 区分开。

WebSocket 消息类型

  • TextMessage:文本消息
  • BinaryMessage:二进制消息
  • CloseMessage:关闭帧,接收方收到该消息就关闭连接
  • PingMessagePongMessage 是保持心跳的帧,发送方接收方是PingMessage ,接收方发送方是PongMessage,目前浏览器没有相关api 发送ping 给服务器,只能由服务器发ping 给浏览器,浏览器返回pong 消息。

WebSocket CS架构实现

  • 首先需要安装gorilla的websocket包。
go get github.com/gorilla/websocket
  • Upgrader
    • Upgrader指定了用于将HTTP连接升级为WebSocket连接的参数。

    • 同时调用Upgrader的方法是安全的。

    type Upgrader struct {
	HandshakeTimeout time.Duration //握手超时时间

       // ReadBufferSize和WriteBufferSize指定I/O缓冲区的大小,单位为字节。
       // 如果缓冲区大小为零,那么就使用HTTP服务器分配的缓冲区。
       // I/O缓冲区的大小并不限制可以发送或接收的信息的大小。或接收的消息的大小。
	ReadBufferSize, WriteBufferSize int
       ...
}
    upgrade := &websocket.Upgrader{
		HandshakeTimeout: 5 * time.Second, 
		ReadBufferSize:   2048,           
		WriteBufferSize:  1024,
	}
  1. 将http升级到WebSocket协议。
func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*websocket.Conn, error)
  1. Client 通过调用带有background context 的DialContext ,创建一个新的Client 端连接。
func (*websocket.Dialer) Dial(urlStr string, requestHeader http.Header) (*websocket.Conn, *http.Response, error)
  1. Server 端基于connection 进行read 和write 。
type (
  Request struct {
  	A int
  	B int
  }
  Response struct {
  	Sum int
  }
)

func home(w http.ResponseWriter, r *http.Request) {
  upgrade := &websocket.Upgrader{
  	HandshakeTimeout: 5 * time.Second, //握手超时时间
  	ReadBufferSize:   2048,            //读缓冲大小
  	WriteBufferSize:  1024,
  }
  conn, err := upgrade.Upgrade(w, r, nil) //将http协议升级到websocket协议
  if err != nil {
  	fmt.Printf("upgrade http to websocket error: %v\n", err)
  	return
  }
  defer conn.Close()

  for { //长连接
  	conn.SetReadDeadline(time.Now().Add(20 * time.Second))
  	var request socket.Request
  	if err := conn.ReadJSON(&request); err != nil {
  		//判断是不是超时
  		if netError, ok := err.(net.Error); ok { //如果ok==true,说明类型断言成功
  			if netError.Timeout() {
  				fmt.Printf("read message timeout, remote %s\n", conn.RemoteAddr().String())
  				return
  			}
  		}
  		//忽略websocket.CloseGoingAway/websocket.CloseNormalClosure这2种closeErr,如果是其他closeErr就打一条错误日志
  		if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) {
  			fmt.Printf("read message from %s error %v\n", conn.RemoteAddr().String(), err)
  		}
  		return //只要ReadMessage发生错误,就关闭这条连接
  	} else {
  		response := socket.Response{Sum: request.A + request.B}
  		if err = conn.WriteJSON(&response); err != nil {
  			fmt.Printf("write response failed: %v", err)
  		} else {
  			fmt.Printf("write response %d\n", response.Sum)
  		}
  	}
  }
}

func main() {
  http.HandleFunc("/", home)
  if err := http.ListenAndServe("127.0.0.1:5657", nil); err != nil {
  	fmt.Println(err)
  }
}