webSocket原理探索

本文概述

Web Sockets的目标是在一个单独的持久链接上提供全双工、双向通讯。在Javascript建立了Web Socket以后,会有一个HTTP请求发送到浏览器以发起链接。在取得服务器响应后,创建的链接会将HTTP升级从HTTP协议交换为WebSocket协议。
因为WebSocket使用自定义的协议,因此URL模式也略有不一样。未加密的链接再也不是http://,而是ws://;加密的链接也不是https://,而是wss://。在使用WebSocket URL时,必须带着这个模式,由于未来还有可能支持其余的模式。
使用自定义协议而非HTTP协议的好处是,可以在客户端和服务器之间发送很是少许的数据,而没必要担忧HTTP那样字节级的开销。因为传递的数据包很小,因此WebSocket很是适合移动应用。
上文中只是对Web Sockets进行了笼统的描述,接下来的篇幅会对Web Sockets的细节实现进行深刻的探索,本文接下来的四个小节不会涉及到大量的代码片断,可是会对相关的API和技术原理进行分析,相信你们读完下文以后再来看这段描述,会有一种豁然开朗的感受。git

1、WebSocket复用了HTTP的握手通道

“握手通道”是HTTP协议中客户端和服务端经过"TCP三次握手"创建的链接通道。客户端和服务端使用HTTP协议进行的每次交互都须要先创建这样一条“通道”,而后经过这条通道进行通讯。咱们熟悉的ajax交互就是在这样一个通道上完成数据传输的,下面是HTTP协议中创建“握手通道”的过程示意图:
图片描述github

上文中咱们提到:在Javascript建立了WebSocket以后,会有一个HTTP请求发送到浏览器以发起链接,而后服务端响应,这就是“握手“的过程,在这个握手的过程中,客户端和服务端主要作了两件事情:web

  1. 创建了一条链接“握手通道”用于通讯(这点和HTTP协议相同,不一样的是HTTP协议完成数据交互后就释放了这条握手通道,这就是所谓的“短链接”,它的生命周期是一次数据交互的时间,一般是毫秒级别的。)
  2. 将HTTP协议升级到WebSocket协议,并复用HTTP协议的握手通道,从而创建一条持久链接。

    说到这里可能有人会问:HTTP协议为何不复用本身的“握手通道”,而非要在每次进行数据交互的时候都经过TCP三次握手从新创建“握手通道”呢?答案是这样的:虽然“长链接”在客户端和服务端交互的过程当中省去了每次都创建“握手通道”的麻烦步骤,可是维持这样一条“长链接”是须要消耗服务器资源的,而在大多数状况下,这种资源的消耗又是没必要要的,能够说HTTP标准的制定通过了深思熟虑的考量。到咱们后边说到WebSocket协议数据帧时,你们可能就会明白,维持一条“持久链接”服务端和客户端须要作的事情太多了。ajax

    说完了握手通道,咱们再来看HTTP协议如何升级到WebSocket协议的。算法

2、HTTP协议升级为WebSocket协议

升级协议须要客户端和服务端交流,服务端怎么知道要将HTTP协议升级到WebSocket协议呢?它必定是接收到了客户端发送过来的某种信号。下面是我从谷歌浏览器中截取的“客户端发起协议升级请求的报文”,经过分析这段报文,咱们可以获得有关WebSocket中协议升级的更多细节。

图片描述

首先,客户端发起协议升级请求。采用的是标准的HTTP报文格式,且只支持GET方法。下面是重点请求的首部的意义:
  1. Connection:Upgrade:表示要升级的协议
  2. Upgrade: websocket:表示要升级到websocket协议
  3. Sec-WebSocket-Version: 13:表示websocket的版本
  4. Sec-WebSocket-Key:UdTUf90CC561cQXn4n5XRg== :与Response Header中的响应首部Sec-WebSocket-Accept: GZk41FJZSYY0CmsrZPGpUGRQzkY=是配套的,提供基本的防御,好比恶意的链接或者无心的链接。

    其中Connection就是咱们前边提到的,客户端发送给服务端的信号,服务端接受到信号以后,才会对HTTP协议进行升级。那么服务端怎样确认客户端发送过来的请求是不是合法的呢?在客户端每次发起协议升级请求的时候都会产生一个惟一码:Sec-WebSocket-Key。服务端拿到这个码后,经过一个算法进行校验,而后经过Sec-WebSocket-Accept响应给客户端,客户端再对Sec-WebSocket-Accept进行校验来完成验证。这个算法很简单:跨域

1.将Sec-WebSocket-Key跟全局惟一的(GUID,[RFC4122])标识:258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接浏览器

2.经过SHA1计算出摘要,并转成base64字符串服务器

258EAFA5-E914-47DA-95CA-C5AB0DC85B11这个字符串又叫“魔串",至于为何要使用它做为Websocket握手计算中使用的字符串,这点咱们无需关心,只须要知道它是RFC标准规定就能够了,官方的解析也只是简单的说此值不大可能被不明白WebSocket协议的网络终端使用。咱们仍是用世界上最好的语言来描述一下这个算法吧。websocket

public function dohandshake($sock, $data, $key) {
        if (preg_match("/Sec-WebSocket-Key: (.*)\r\n/", $data, $match)) {
            $response = base64_encode(sha1($match[1] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
            $upgrade  = "HTTP/1.1 101 Switching Protocol\r\n" .
                "Upgrade: websocket\r\n" .
                "Connection: Upgrade\r\n" .
                "Sec-WebSocket-Accept: " . $response . "\r\n\r\n";
            socket_write($sock, $upgrade, strlen($upgrade));
            $this->isHand[$key] = true;
        }
    }

服务端响应客户端的头部信息和HTTP协议的格式是相同的,因此这里Sec-WebSocket-Accept字段后边的两个换行符是少不了的,这和咱们使用curl工具模拟get请求是一个道理。这样展现结果彷佛不太直观,咱们使用命令行CLI来根据上图中的Sec-WebSocket-Key和握手算法来计算一下服务端返回的Sec-WebSocket-Accept是否正确:网络

图片描述

从图中能够看到,经过算法算出来的base64字符串和Sec-WebSocket-Accept是同样的。那么假如服务端在握手的过程当中返回一个错误的Sec-WebSocket-Accept字符串会怎么样呢?固然是客户端会报错,链接会创建失败,你们最好尝试一下,例如将全局惟一标识符258EAFA5-E914-47DA-95CA-C5AB0DC85B11改成258EAFA5-E914-47DA-95CA-C5AB0DC85B12。

3、WebSocket的帧和数据分片传输

下图是我作的一个测试:将小说《飘》的第一章内容复制成文本数据,经过客户端发送到服务端,而后服务端响应相同的信息完成了一次通讯。

图片描述

能够看到一篇足足有将近15000字节的数据在客户端和服务端完成通讯只用了150ms的时间。咱们还能够清晰的看到浏览器控制台中frame栏中显示的客户端发送和服务端响应的文本数据,你必定惊讶WebSocket通讯强大的数据传输能力。数据是否真的像frame中展现的那样客户端直接将一大篇文本数据发送到服务端,服务端接收到数据以后,再将一大篇文本数据返回给客户端呢?这固然是不可能的,咱们都知道HTTP协议是基于TCP实现的,HTTP发送数据也是分包转发的,就是将大数据根据报文形式分割成一小块一小块发送到服务端,服务端接收到客户端发送的报文后,再将小块的数据拼接组装。关于HTTP的分包策略,你们能够查看相关资料进行研究,websocket协议也是经过分片打包数据进行转发的,不过策略上和HTTP的分包不同。frame(帧)是websocket发送数据的基本单位,下边是它的报文格式:
图片描述
报文内容中规定了数据标示,操做代码、掩码、数据、数据长度等格式。不太理解不要紧,下面我经过讲解你们只要理解报文中重要标志的做用就能够了。首先咱们明白了客户端和服务端进行Websocket消息传递是这样的:

  1. 客户端:将消息切割成多个帧,并发送给服务端。
  2. 服务端:接收消息帧,并将关联的帧从新组装成完整的消息。

服务端在接收到客户端发送的帧消息的时候,将这些帧进行组装,它怎么知道什么时候数据组装完成的呢?这就是报文中左上角FIN(占一个比特)存储的信息,1表示这是消息的最后一个分片(fragment)若是是0,表示不是消息的最后一个分片。websocket通讯中,客户端发送数据分片是有序的,这一点和HTTP不同,HTTP将消息分包以后,是并发无序的发送给服务端的,包信息在数据中的位置则在HTTP报文中存储,而websocket仅仅须要一个FIN比特位就能保证将数据完整的发送到服务端。
接下来的RSV1,RSV2,RSV3三个比特位的做用又是什么呢?这三个标志位是留给客户端开发者和服务端开发者开发过程当中协商进行拓展的,默认是0。拓展如何使用必须在握手的阶段就协商好,其实握手自己也是客户端和服务端的协商。

4、Websocket链接保持和心跳检测

Websocket是长链接,为了保持客户端和服务端的实时双向通讯,须要确保客户端和服务端之间的TCP通道保持链接没有断开。可是对于长时间没有数据往来的链接,若是依旧保持着,可能会浪费服务端资源。可是不排除有些场景,客户端和服务端虽然长时间没有数据往来,仍然须要保持链接,就好比说你几个月没有和一个QQ好友聊天了,忽然有一天他发QQ消息告诉你他要结婚了,你仍是能在第一时间收到。那是由于,客户端和服务端一直再采用心跳来检查链接。客户端和服务端的心跳链接检测就像打乒乓球同样:

  • 发送方->接收方:ping
  • 接收方->发送方:pong

等何时没有ping、pong了,那么链接必定是存在问题了。
说了这么多,接下来我使用Go语言来实现一个心跳检测,Websocket通讯实现细节是一件繁琐的事情,直接使用开源的类库是比较不错的选择,我使用的是:gorilla/websocket。这个类库已经将websocket的实现细节(握手,数据解码)封装的很好啦。下面我就直接贴代码了:

package main

import (
    "net/http"
    "time"

    "github.com/gorilla/websocket"
)

var (
    //完成握手操做
    upgrade = websocket.Upgrader{
       //容许跨域(通常来说,websocket都是独立部署的)
       CheckOrigin:func(r *http.Request) bool {
            return true
       },
    }
)

func wsHandler(w http.ResponseWriter, r *http.Request) {
   var (
         conn *websocket.Conn
         err error
         data []byte
   )
   //服务端对客户端的http请求(升级为websocket协议)进行应答,应答以后,协议升级为websocket,http创建链接时的tcp三次握手将保持。
   if conn, err = upgrade.Upgrade(w, r, nil); err != nil {
        return
   }

    //启动一个协程,每隔1s向客户端发送一次心跳消息
    go func() {
        var (
            err error
        )
        for {
            if err = conn.WriteMessage(websocket.TextMessage, []byte("heartbeat")); err != nil {
                return
            }
            time.Sleep(1 * time.Second)
        }
    }()

   //获得websocket的长连接以后,就能够对客户端传递的数据进行操做了
   for {
         //经过websocket长连接读到的数据能够是text文本数据,也能够是二进制Binary
        if _, data, err = conn.ReadMessage(); err != nil {
            goto ERR
     }
     if err = conn.WriteMessage(websocket.TextMessage, data); err != nil {
         goto ERR
     }
   }
ERR:
    //出错以后,关闭socket链接
    conn.Close()
}

func main() {
    http.HandleFunc("/ws", wsHandler)
    http.ListenAndServe("0.0.0.0:7777", nil)
}

借助go语言很容易搭建协程的特色,我专门开启了一个协程每秒向客户端发送一条消息。打开客户端浏览器能够看到,frame中每秒的心跳数据一直在跳动,当长连接断开以后,心跳就没有了,就像人没有了心跳同样:
图片描述