websocket 协议的官方文档 :
https://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-13#section-5
Linux 下c语言 实现 websocket 包含客户端 和服务器测试代码 :
http://blog.youkuaiyun.com/sguniver_22/article/details/74273839
c语言实现websocket服务器:
http://blog.youkuaiyun.com/lell3538/article/details/60470558
细说webosokcet-php篇
https://www.cnblogs.com/hustskyking/p/websocket-with-php.html
需要学习哪些东西?
1. 如何建立连接
2. 如何交换数据
3. 数据帧格式
4. 如何维持连接
websocket连接建立过程:
websocket 复用了HTTP的握手通道。具体指的是,客户端HTTP请求与websocket 服务端协商升级协议。
1. client -> server 发送Sec-WebSocket-Key
2. server-> client 加密返回Sec-WebScoket-Accept
3 client -> server 本地校验
1. 客户端发起协议升级请求。 采用标准的HTTP报文格式,只支持 GET
GET / HTTP/1.1
Host: localhost:8080
Origin: http://127.0.0.1:3000
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw==
2. 服务端相应协议升级
HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=
注意每个header都以\r\n结尾,并且最后一行加上一个额外的空行\r\n
Sec-WebSocket-Accept的计算
伪代码如下:
>toBase64( sha1( Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 ) )
php 代码如下:
// Calculation websocket key.
$new_key = \base64_encode(\sha1($Sec_WebSocket_Key . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true));
三 数据帧格式
WebSocket客户端,服务端通信的最小单位是帧(frame),由一个或多个帧组成一条完整的消息(message)
1. 发送端: 将消息切割成多个帧,并发送给服务端;
2. 接收端: 接受消息帧,并将关联的帧重新组装成完整的消息;
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/63) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
数据帧格式详解:
FIN: 如果是1,表示这是消息(message)的最后一个分片(fragment),如果是0,表示不是消息(message)的最后一个分片(fragment)
Opcode: 4个比特。
操作码,Opcode的值决定了应该如何解析后续的数据载荷(data payload)。如果操作码是不认识的,那么接收端应该断开连接(fail the connection)
%x0 继续的 frame %x1 文本 frame %x2 二进制 frame %x3-7 被保留 for further non-control frames %x8 连接关闭 connection close %x9 ping %xA pong %xB-F 被保留 for further control frames
Mask: 1个比特。
从客户端向服务端发送数据时,需要对数据进行掩码操作;从服务端向客户端发送数据时,不需要对数据进行掩码操作。 如果服务端接收到的数据没有进行过掩码操作,服务端需要断开连接。
Payload length: 数据载荷的长度。 7位 或 7 + 16 位 或 7 + 64位。
假设 Payload length == x
x 为 0-126: 数据长度为x字节.
x 为 126 : 后续2个字节代表一个16位无符号整数
x 为 127 : 后续8个字节代表一个64位无符号整数
掩码算法:
首先,假设:
- original-octet-i:为原始数据的第i字节。
- transformed-octet-i:为转换后的数据的第i字节。
- j:为
i mod 4
的结果。 - masking-key-octet-j:为mask key第j字节。
算法描述为: original-octet-i 与 masking-key-octet-j 异或后,得到 transformed-octet-i。
j = i MOD 4
transformed-octet-i = original-octet-i XOR masking-key-octet-j
六、数据传递
一旦WebSocket 客户端、服务端连接后,后续的操作都是基于数据帧的传递。
1、数据分片
第一条消息
FIN=1, 表示是当前消息的最后一个数据帧。服务端收到当前数据帧后,可以处理消息。opcode=0x1,表示客户端发送的是文本类型。
第二条消息
- FIN=0,opcode=0x1,表示发送的是文本类型,且消息还没发送完成,还有后续的数据帧。
- FIN=0,opcode=0x0,表示消息还没发送完成,还有后续的数据帧,当前的数据帧需要接在上一条数据帧之后。
- FIN=1,opcode=0x0,表示消息已经发送完成,没有后续的数据帧,当前的数据帧需要接在上一条数据帧之后。服务端可以将关联的数据帧组装成完整的消息。
Client: FIN=1, opcode=0x1, msg="hello" Server: (process complete message immediately) Hi. Client: FIN=0, opcode=0x1, msg="and a" Server: (listening, new message containing text started) Client: FIN=0, opcode=0x0, msg="happy new" Server: (listening, payload concatenated to previous message) Client: FIN=1, opcode=0x0, msg="year!" Server: (process complete message) Happy new year to you too!
七、 连接保持+心跳
WebSocket为了保持客户端,服务端的实时双向通信,需要确保客户端和服务端之间的TCP通道长期连接不断开。然而,对于长时间没有数据往来的连接,如果依旧长时间保持着,可能会浪费连接资源。 但是不排除有些场景,客户端和服务端虽然长时间没有数据往来,但仍需保持连接。 这个时候可以采用心跳 实现。
发送方-> 接收方 :ping
接收方-> 发送方 : pong
八、Sec-WebSocket-Key/Accept的作用?
2. 确保服务端理解websocket连接,因为握手阶段采用的是http协议,因此ws连接可能是被一个http服务器处理并返回的,此时客户端可以通过Sec-WebSocket-Key来确保服务端认识ws协议。(并非百分百保险,比如总是存在那么些无聊的http服务器,光处理Sec-WebSocket-Key,但并没有实现ws协议。。。)
5. Sec-WebSocket-Key主要目的并不是保证数据的安全,因为Sec-WebSocket-Key, Sec-WebSocket-Accept的转换计算公式是公开的,而且很简单,主要作用是防止一些常见的以外情况(非故意的)
九,数据掩码的作用
安全。但并不是为了防止数据泄密,而是为了防止早期版本协议存在的代理缓存污染攻击(proxy cache poisoning attacks) 等问题。 (不甚理解)
下面给出一段 php实现 websocket 服务端 ,作为研究包解析,数据解码,链接建立过程等,不能作为生产环境使用!感谢原作者,
<?php class WS { var $master; var $sockets = array(); var $debug = false; var $handshake = false; function __construct($address, $port){ $this->master=socket_create(AF_INET, SOCK_STREAM, SOL_TCP) or die("socket_create() failed"); socket_set_option($this->master, SOL_SOCKET, SO_REUSEADDR, 1) or die("socket_option() failed"); socket_bind($this->master, $address, $port) or die("socket_bind() failed"); socket_listen($this->master,20) or die("socket_listen() failed"); $this->sockets[] = $this->master; $this->say("Server Started : ".date('Y-m-d H:i:s')); $this->say("Listening on : ".$address." port ".$port); $this->say("Master socket : ".$this->master."\n"); while(true){ $socketArr = $this->sockets; $write = NULL; $except = NULL; socket_select($socketArr, $write, $except, NULL); //自动选择来消息的socket 如果是握手 自动选择主机 foreach ($socketArr as $socket){ if ($socket == $this->master){ //主机 $client = socket_accept($this->master); if ($client < 0){ $this->log("socket_accept() failed"); continue; } else{ $this->connect($client); } } else { $this->log("^^^^"); $bytes = @socket_recv($socket,$buffer,2048,0); echo 'buffer:'.$buffer; $this->log("^^^^"); if ($bytes == 0){ $this->disConnect($socket); } else{ if (!$this->handshake){ $this->doHandShake($socket, $buffer); //这个应该是 websocket 协议的握手 } else{ $buffer = $this->decode($buffer); //客户端 ws.send 的数据 会执行她 $this->send($socket, $buffer); } } } } } } function send($client, $msg){ $this->log("> " . $msg); $msg = $this->frame($msg); socket_write($client, $msg, strlen($msg)); $this->log("! " . strlen($msg)); } function connect($socket){ array_push($this->sockets, $socket); $this->say("\n" . $socket . " CONNECTED!"); $this->say(date("Y-n-d H:i:s")); } function disConnect($socket){ $index = array_search($socket, $this->sockets); socket_close($socket); $this->say($socket . " DISCONNECTED!"); if ($index >= 0){ array_splice($this->sockets, $index, 1); } } function doHandShake($socket, $buffer){ $this->log("\nRequesting handshake..."); $this->log($buffer); list($resource, $host, $origin, $key) = $this->getHeaders($buffer); $this->log("Handshaking..."); $upgrade = "HTTP/1.1 101 Switching Protocol\r\n" . "Upgrade: websocket\r\n" . "Connection: Upgrade\r\n" . "Sec-WebSocket-Accept: " . $this->calcKey($key) . "\r\n\r\n"; //必须以两个回车结尾 $this->log($upgrade); $sent = socket_write($socket, $upgrade, strlen($upgrade)); $this->handshake=true; $this->log("Done handshaking..."); return true; } function getHeaders($req){ $r = $h = $o = $key = null; if (preg_match("/GET (.*) HTTP/" ,$req,$match)) { $r = $match[1]; } if (preg_match("/Host: (.*)\r\n/" ,$req,$match)) { $h = $match[1]; } if (preg_match("/Origin: (.*)\r\n/" ,$req,$match)) { $o = $match[1]; } if (preg_match("/Sec-WebSocket-Key: (.*)\r\n/",$req,$match)) { $key = $match[1]; } return array($r, $h, $o, $key); } function calcKey($key){ //基于websocket version 13 $accept = base64_encode(sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true)); return $accept; } function decode($buffer) { $len = $masks = $data = $decoded = null; //含义: 数据的长度(不包含websocket的头) $len = ord($buffer[1]) & 127; //取出tcp 包内容 的 8- 15 位 (或说 第二个字节); 为什么要与& 127因为 索引8 的位置 代码 MASK位(占一位) echo 'len:',$len; echo 'size[2]:',ord($buffer[2]); echo 'size[3]:',ord($buffer[3]); if ($len === 126) { // 126<=数据长度<= 65535 Extended payload length 有 占用2个字节 $masks = substr($buffer, 4, 4); //帧格式 Masking-key 占用4字节 $data = substr($buffer, 8); //第九个字节(索引 8 )开始 是数据 } else if ($len === 127) { // 65535 <= 数据长度 ;Extended payload length 有 占用8个字节 $masks = substr($buffer, 10, 4); // 说明 Extended payload length 占用 8个字节 2 + 8 所以从 10开始 $data = substr($buffer, 14); } else { //长度小于126 $masks = substr($buffer, 2, 4); $data = substr($buffer, 6); } for ($index = 0; $index < strlen($data); $index++) { $decoded .= $data[$index] ^ $masks[$index % 4]; // } echo 'decode:'.$decoded; return $decoded; } function frame($s){ $a = str_split($s, 125); if (count($a) == 1){ return "\x81" . chr(strlen($a[0])) . $a[0]; // \x81 转为二进制 1000 0001 ;将长度转换为:ascii 所指定的单个字符 } $ns = ""; foreach ($a as $o){ //怎么感觉不对呢? $ns .= "\x81" . chr(strlen($o)) . $o; } return $ns; } function say($msg = ""){ echo $msg . "\n"; } function log($msg = ""){ if ($this->debug){ echo $msg . "\n"; } } } new WS('localhost', 4000);