WebSocket protocol 是HTML5一种新的协议。它实现了浏览器与服务器全双工通信(full-duplex)。

原理

以前网站为了实现即时通讯,所用的技术都是轮询(polling)。轮询是在特定的的时间间隔(如每隔1秒),由浏览器对服务器发出HTTP request,然后由服务器返回最新的数据给客服端的浏览器。这种传统的HTTP request 的模式带来很明显的缺点 – 浏览器需要不断的向服务器发出请求,然而HTTP request 的header是非常长的,里面包含的有用数据可能只是一个很小的值,这样会占用很多的带宽。
在 WebSocket API,浏览器和服务器只需要要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

握手协议

在实现websocket连线过程中,需要通过浏览器发出websocket连线请求,然后服务器发出回应,这个过程通常称为“握手” (handshaking)。 
该方案处在草案阶段,目前在使用的有两个版本,一个是以chrome为首的使用的version 13(目前最新),该版本出现在RFC6455中。另一个是以safari(包括桌面和移动版本)为首的使用的draft-ietf-hybi版。 

以下分别介绍两个版本的握手方法

Chrome版

客户端请求web socket连接时,会向服务器端发送握手请求

客户端请求:
var ws = new WebSocket('ws://192.168.0.10:8080');
  • 1
  • 2
  • 1
  • 2

请求内容大致如下:

GET / HTTP/1.1
Host: 192.168.0.10:8080
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Origin: null
Sec-WebSocket-Version: 13
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-CN,zh;q=0.8
Sec-WebSocket-Key: VR+OReqwhymoQ21dBtoIMQ==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

请求包说明: 
* 必须是有效的http request 格式; 
* HTTP request method 必须是GET,协议应不小于1.1 如: Get / HTTP/1.1; 
* 必须包括Upgrade头域,并且其值为”websocket”; 
* 必须包括”Connection” 头域,并且其值为”Upgrade”; 
* 必须包括”Sec-WebSocket-Key”头域,其值采用base64编码的随机16字节长的字符序列; 
* 如果请求来自浏览器客户端,还必须包括Origin头域 。 该头域用于防止未授权的跨域脚本攻击,服务器可以从Origin决定是否接受该WebSocket连接; 
* 必须包括”Sec-webSocket-Version” 头域,当前值必须是13; 
* 可能包括”Sec-WebSocket-Protocol”,表示client(应用程序)支持的协议列表,server选择一个或者没有可接受的协议响应之; 
* 可能包括”Sec-WebSocket-Extensions”, 协议扩展, 某类协议可能支持多个扩展,通过它可以实现协议增强; 
* 可能包括任意其他域,如cookie.

服务器端响应如下:

HTTP/1.1 101 Web Socket Protocol Handshake
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: Y+Te7S7wQJC0FwXumEdGbv9/Mek=
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

应答包说明: 
*必须包括Upgrade头域,并且其值为”websocket”; 
*必须包括Connection头域,并且其值为”Upgrade”; 
*必须包括Sec-WebSocket-Accept头域,其值是将请求包“Sec-WebSocket-Key”的值,与”258EAFA5-E914-47DA-95CA-C5AB0DC85B11″这个字符串进行拼接,然后对拼接后的字符串进行sha-1运算,再进行base64编码,就是“Sec-WebSocket-Accept”的值; 
*应答包中冒号后面有一个空格; 
*最后需要两个空行作为应答包结束。

<?php
//获取请求包中Sec-WebSocket-Key的值
//$req为请求包内容
if(preg_match("/Sec-WebSocket-Key: (.*)\r\n/",$req,$match)){ $key=$match[1]; }
?>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5
<?php
    //计算Sec-WebSocket-Accept值算法
    private function websocket_accept_key($strkey){
        $strkey .= "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
        $hash_data = base64_encode(sha1($strkey,true));
        return $hash_data;
    }
?>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
<?php
//组装应答包代码
$hash_data = $this->websocket_accept_key($strkey);
$upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" .
    "Upgrade: websocket\r\n" .
    "Connection: Upgrade\r\n" .
    "Sec-WebSocket-Accept: " . $hash_data . "\r\n" .
    "\r\n";
?>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

握手成功!

Safari版

请求内容大致如下:

GET / HTTP/1.1
Upgrade: WebSocket
Connection: Upgrade
Host: 192.168.0.10:8080
Origin: file://
Sec-WebSocket-Key1: 4 @1  46546xW%0l 1 5
Sec-WebSocket-Key2: 12998 5 Y3 1  .P00

^n:ds[4U
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

请求包说明: 
*必须包括”Sec-WebSocket-Key1”头域,由可见字符组成; 
*必须包括”Sec-WebSocket-Key2”头域,由可见字符组成; 
*在请求包的最后,会有一行请求正文,它不属于任何头域,我们可以理解为”Sec-WebSocket-Key3”,因为它需要参与到计算应答体; 
*请求正文的上一行是一行空行; 
*请求正文大部分情况下是非可见字符,俗称乱码,但这不影响后面的计算。

服务器端响应如下:

HTTP/1.1 101 WebSocket Protocol Handshake
Upgrade: WebSocket
Connection: Upgrade
Sec-WebSocket-Origin: file://
Sec-WebSocket-Location: ws://192.168.0.10:8080/

8jKS'y:G*Co,Wxa-
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

应答包说明: 
*必须包括”Upgrade”头域,其值为WebSocket; 
*必须包括”Connection”头域,其值为Upgrade; 
*必须包括”Sec-WebSocket-Origin”头域; 
*必须包括”Sec-WebSocket-Location”头域; 
*应答正文的上一行是一行空行; 
*应答正文后面没有任何结束符或者换行符。

<?php
//获取请求包中Sec-WebSocket-Key1和Sec-WebSocket-Key2值
//$req为请求包内容
if(preg_match("/Sec-WebSocket-Key1: (.*)\r\n/",$req,$match)){ $key1=$match[1]; }
if(preg_match("/Sec-WebSocket-Key2: (.*)\r\n/",$req,$match)){ $key2=$match[1]; }
?>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
<?php
//获取请求正文内容
$arr = explode("\r\n", $req);
$body = end($arr);
?>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5
<?php
 /*
    计算应答正文内容
    1.把Sec-WebSocket-Key1中的数字从左到右提取出来,上面的例子是:4146546015;
    2.把Sec-WebSocket-Key2中的数字从左到右提取出来,上面的例子是:1299853100;
    3.计算Sec-WebSocket-Key1中的空格数,上面的例子是:5(冒号后面的空格不算);
    4.计算Sec-WebSocket-Key2中的空格数,上面的例子是:5(冒号后面的空格不算);
    5.将提取出来的数字除以空格数,去整,分别得到829309203 和 259970620;
    6.将上一步计算得到的数字分别以Big-Endian的方式打包,拼接,然后再与请求正文的8个字节拼接,计算其MD5值。
 */
 public function websocket_accept_key_76($key1, $key2, $body){

        $tmp = array();
        $kv1 = 0;
        $kv2 = 0;
        $c1 = 0;
        $c2 = 0;

        $key1Len = strlen($key1);
        $key2Len = strlen($key2);

        for($i = 0; $i < $key1Len; $i++){
            if($key1[$i]>='0' && $key1[$i]<='9') $kv1 = $kv1*10+($key1[$i]-'0');
            else if($key1[$i]==' ') $c1++;
        }

        for($i = 0; $i < $key2Len; $i++){
            if($key2[$i]>='0' && $key2[$i]<='9') $kv2 = $kv2*10+($key2[$i]-'0');
            else if($key2[$i]==' ') $c2++;
        }

        $kv1 = $kv1/$c1;
        $kv2 = $kv2/$c2;

        $key1_sec = pack("N",$kv1);
        $key2_sec = pack("N",$kv2);

        $hash_data = md5($key1_sec.$key2_sec.$body,true);
        return $hash_data;
    }

?>
  • 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
  • 41
  • 42
  • 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
  • 41
  • 42

获得的这个就是应答体正文,通常是非可见字符–“乱码”。 
然后就是返回应答包给浏览器了

<?php
//组装应答包代码
$hash_data = $this->websocket_accept_key_76($strkey);
$upgrade = ""HTTP/1.1 101 WebSocket Protocol Handshake\r\n" .
                "Upgrade: WebSocket\r\n" .
                "Connection: Upgrade\r\n" .
                "Sec-WebSocket-Origin: $origin\r\n".
                "Sec-WebSocket-Location: ws://$host/\r\n".
                "\r\n" . $hash_data;
?>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

握手成功!