websocket解析和 php 实现

Websocket 协议解析

​ WebSocket protocol 是HTML5一种新的协议。它是实现了浏览器与服务器全双工通信(full-duplex)。现很多网站为了实现即时通讯,所用的技术都是轮询(polling)。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP request,然后由服务器返回最新的数据给客服端的浏览器。这种传统的HTTP request 的模式带来很明显的缺点 – 浏览器需要不断的向服务器发出请求,然而HTTP request 的header是非常长的,里面包含的数据可能只是一个很小的值,这样会占用很多的带宽。

​ 在 WebSocket API,浏览器和服务器只需要要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送,改变了原有的B/S模式。

server

Websocket 数据:

websocket

浏览器端的websocket 请求一般是

1
2
3
4
5
6
7
8
9
10
11
// javacsript
var ws = new WebSocket("ws://127.0.0.1:4000");
ws.onopen = function(){
console.log("succeed");
};
ws.onerror = function(){
console.log(“error”);
};
ws.onmessage = function(e){
console.log(e);
}

当 new 一个 websocket 对象之后,就会向服务器发送一个 get 请求

request

这个请求是对摸个服务器的端口发送的,一般的话,会预先在服务器将一个socket 绑定到一个端口上,客户端和服务器端在这个预定的端口上通信(我这里绑定的就是 4000 端口,默认情况下,websocke 使用 80 端口)。

然后,在服务器端的socket监听到这个packet 之后就生成一个新的 socket,将发送过来的数据中的 Sec-WebSocket-Key 解析出来,然后按照把“Sec-WebSocket-Ke”加上一个魔幻字符串“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”。使用SHA-1加密,之后进行BASE-64编码,将结果做为“Sec-WebSocket-Accept”头的值,返回给客户端。

客户端收到这个之后,就会将 通信协议 upgrade 到 websocket 协议。

response

然后就会在这个持久的通道下进行通信,包括浏览器的询问,服务器的push,双方是在一个全双工的状态下相互通信。 切换后的websocket 协议中的 数据传输帧的格式(此时不再使用html协议) 官方文档给出的说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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/64) |
|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 ... |
+---------------------------------------------------------------+

直接看这个,谁都会有点头大: 我花了一幅图来简单的解释这个 frame 的结构。

frame

各字段的解释:

FIN 1bit 表示信息的最后一帧,flag,也就是标记符

RSV 1-3 1bit each 以后备用的 默认都为 0

Opcode 4bit 帧类型,

Mask 1bit 掩码,是否加密数据,默认必须置为1

Payload len 7bit 数据的长度,当这个7 bit的数据 == 126 时,后面的2 个字节也是表示数 据长度,当它 == 127 时,后面的 8 个字节表示数据长度Masking-key 1 or 4 bit 掩码Payload data playload len bytes 数据

所以我们这里的代码,通过判断 Playload len的值,来用 substr 截取 Masking-key 和 PlayloadData。

根据掩码解析数据的方法是:

1
2
3
4
5
6

for( i = 0; i < data.length ; i++){

orginalData += data[i] ^ maskingKey[i mod 4];

}

在PHP中,当我们收到数据之后,按照这里的格式截取数据,并将其按照这里的方法解析后就得到了浏览器发送过来的数据。 当我们想把数据发送给浏览器时,也要按照这个格式组装frame。 这里是我的方法:

1
2
3
4
5
6
7
8
9
10
11
function frame($s){
$a = str_split($s, 125);
if (count($a) == 1){
return "\x81" . chr(strlen($a[0])) . $a[0];
}
$ns = "";
foreach ($a as $o){
$ns .= "\x81" . chr(strlen($o)) . $o;
}
return $ns;
}

强行将要发送的数据分割成 125 Byte / frame,这样 playload len 只需要 7 bits。也就是直接将数据的长度的ascall码拼接上去,然后后面跟上要发送的数据。 每一个 frame 前面加的 ‘\x81’ 用二进制就是: 1000 0001 1000 :

1 是 FIN

000 是三个备用的bit

0001 : 指的是 opcode 官方的解释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  |Opcode  | Meaning                             | Reference |
-+--------+-------------------------------------+-----------|
| 0 | Continuation Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
| 1 | Text Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
| 2 | Binary Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
| 8 | Connection Close Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
| 9 | Ping Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
| 10 | Pong Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|

可以设置上图中 opcode 的值,来告诉浏览器这个frame的数据属性。

php 实现简单的 websocket 服务端

自十月底,html5 宣布定稿之后,新一轮的关于html的讨论便开始了,现在这里,我也为大家介绍一种html5标准中提到的新技术 websocket,以及他的 php 实现范例。

WebSocketHTML5开始提供的一种浏览器服务器间进行全双工通讯的网络技术。WebSocket通信协议于2011年被IETF定为标准RFC 6455,WebSocketAPIW3C定为标准。

在WebSocket API中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

传统的web程序,都遵循这样的执行方式,浏览器发送一个请求,然后服务器端接收这个请求,处理完成浏览器的请求之后,再将处理完成的结果返回给浏览器,然后浏览器处理返回的html,将其呈现给用户。如下图所示:

server

即使后来出现了ajax这样的新的技术,它实际也是使用javascript的api来向服务器发送请求,然后等待相应的数据。也就是说,在浏览器和服务器的交互中,我们每想得到一个得到新的数据(更新页面,获得服务器端的最新状态)就必须要发起一个http请求,建立一条TCP/IP链接,当这次请求的数据被浏览器接收到之后,就断开这条TCP/IP连接。

新的websocket技术,在浏览器端发起一条请求之后,服务器端与浏览器端的请求进行握手应答之后,就建立起一条长久的,双工的长连接,基于这条连接,我们不必做一些轮询就能随时获得服务器端的状态,服务器也不必等待浏览器端的请求才能向用户推送数据,可以在这条连接上随时向以建立websocket连接的用户 push 数据。

这里是 websocket 协议的 RFC 文档。

我这里的实现是基于 php sockets的实现,调用了来自 php.net 的 socket-api php socket .

代码实现

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
<?php
class WsServer{
public $socket;
public $socketArray;
public $activatedSocketArray;
public function __construct($address,$port){
$this->socket = $this->init($address,$port);
if($this->socket == null)
exit(1);

}
private function init($address,$port){
$wssocket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($wssocket, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind($wssocket,$address,$port);
if(socket_listen($wssocket)){
$this->p("Socket init success");
return $wssocket;
}else{
$this->p("Socket init failure");
exit();
}

}

public function run(){
$this->socketArray[] = $this->socket;
$write = NULL;
$except = NULL;
while (true){
$this->activatedSocketArray = $this->socketArray;
socket_select($this->activatedSocketArray, $write, $except, null);

foreach($this->activatedSocketArray as $s){
if($s == $this->socket){
$client = socket_accept($this->socket);
socket_recv($client, $buffer, 2048, 0);

// response a handshake response
if(socket_write($client, $this->handshakeResponse($buffer))!== false){
$this->p('add a socket into the queue');
array_push($this->socketArray, $client);
}else{
$this->p('error on handshake');
$this->errorLog(socket_last_error());
}
}else{
socket_recv($s, $buffer, 2048, 0);

$frame = $this->parseFrame($buffer);
var_dump($frame);
$str = $this->decode($buffer);
$str = $this->frame($str);
socket_write($s,$str,strlen($str));
}
}

}
}
/**
* parse the frame
* */
private function parseFrame($d){
$firstByte = ord(substr($d,0,1));
$result =array();
$result['FIN'] = $this->getBit($firstByte,1);
$result['opcode'] = $firstByte & 15;
$result['length'] = ord(substr($d,1,1)) & 127;
return $result;
}
private function getBit($m,$n){
return ($n >> ($m-1)) & 1;
}
/*
* build the data frame sent to client by server socket.
*
* **/
function frame($s){
$a = str_split($s, 125);
if (count($a) == 1){
return "\x81" . chr(strlen($a[0])) . $a[0];
}
$ns = "";
foreach ($a as $o){
$ns .= "\x81" . chr(strlen($o)) . $o;
}
return $ns;
}
/*
* decode the client socket input
*
* **/
function decode($buffer) {
$len = $masks = $data = $decoded = null;
$len = ord($buffer[1]) & 127;

if ($len === 126) {
$masks = substr($buffer, 4, 4);
$data = substr($buffer, 8);
}
else if ($len === 127) {
$masks = substr($buffer, 10, 4);
$data = substr($buffer, 14);
}
else {
$masks = substr($buffer, 2, 4);
$data = substr($buffer, 6);
}

for ($index = 0; $index < strlen($data); $index++) {
$decoded .= $data[$index] ^ $masks[$index % 4];
}
return $decoded;
}
/*
* params @requestHeaders : read from request socket
* return an array of request
*
* */
function parseHeaders($requsetHeaders){
$resule =array();
if (preg_match("/GET (.*) HTTP/" ,$requsetHeaders,$match)) { $resule['reuqest'] = $match[1]; }
if (preg_match("/Host: (.*)\r\n/" ,$requsetHeaders,$match)) { $result['host'] = $match[1]; }
if (preg_match("/Origin: (.*)\r\n/" ,$requsetHeaders,$match)) { $result['origin'] = $match[1]; }
if (preg_match("/Sec-WebSocket-Key: (.*)\r\n/",$requsetHeaders,$match)) { $result['key'] = $match[1]; }
return $result;

}
/*
* protocol version : 13
* generting the key of handshaking
* return encrypted key
* */
function getKey($requestKey){
return base64_encode(sha1($requestKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));

}
/**params @parseRequest : request written in socket
* return handshakResponse witten in response socket
* */
function handshakeResponse($request){
$parsedRequest = $this->parseHeaders($request);
$encryptedKey = $this->getKey($parsedRequest['key']);
$response = "HTTP/1.1 101 Switching Protocol\r\n" .
"Upgrade: websocket\r\n" .
"Connection: Upgrade\r\n" .
"Sec-WebSocket-Accept: " .$encryptedKey. "\r\n\r\n";
return $response;

}
/*
* report last_error in socket
*
* */
function errorLog($ms){
echo 'Error:\n'.$ms;
}
/*
* print the log
*
* */
private function p($e){
echo $e."\n";
}
function test(){
$read[] = $this->socket;
$write = null;
$except = null;
socket_select($read, $write, $except,null);
var_dump($read);
}
}
$w = new WsServer('localhost',4000);
$w->run();
?>

这个类主要实现了对websocket的握手回应,将每一个连接成功的websocket加入到一个数组中之后,就能够在服务器端对多个websocket 客户端进行处理。

对websocket的握手请求,在接收到的报文中 会得到一个 Sec-WebSocket-Key 字段,要把“Sec-WebSocket-Key”加上一个魔幻字符串“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”。使用SHA-1加密,之后进行BASE-64编码,将结果做为“Sec-WebSocket-Accept”头的值,返回给客户端。这样就完成了与客户端之间的握手。

之后,就能在服务器端监听客户端发来的请求了,同时也可以操作在服务端的socket句柄,来向浏览器推送消息。