首先认识HTML5的websocket:
什么是WebSocket API?
WebSocket API是下一代客户端-服务器的异步通信方法。该通信取代了单个的TCP套接字,使用ws或wss协议,可用于任意的客户端和服务器程序。 WebSocket目前由W3C进行标准化。WebSocket已经受到Firefox 4、Chrome 4、Opera 10.70以及Safari 5等浏览器的支持。
WebSocket API最伟大之处在于服务器和客户端可以在给定的时间范围内的任意时刻,相互推送信息。WebSocket并不限于以Ajax(或XHR)方式通信,因为 Ajax技术需要客户端发起请求,而WebSocket服务器和客户端可以彼此相互推送信息;XHR受到域的限制,而WebSocket允许跨域通信。
Ajax技术很聪明的一点是没有设计要使用的方式。WebSocket为指定目标创建,用于双向推送消息。
关于web实时通信技术的发展(poll,ajax,comet等)以及websocket的介绍具体请参见:
使用 HTML5 WebSocket 构建实时 Web 应用 http://www.linuxidc.com/Linux/2012-02/54014.htm
链接的文章介绍了websocket的旧版协议草案,并用.net实现了该草案。
在2011年7月份,websocket发布了最新版的协议草案,草案的最新版本是草案10,草案的链接地址为:http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10,新的草案增加了安全性和可扩展性。
新草案客户端与服务器端的握手协议:
客户端发起websocket请求
- socket = new MozWebSocket(“ws://localhost:12345/websocket/server.php”)
这里用的是firefox浏览器,所以用MozWebSocket(),其他浏览器像chrome,需要用WebSocket()
请求头信息格式:
- GET /chat HTTP/1.1
- Host: localhost:12345
- Upgrade: websocket
- Connection: Upgrade
- Sec-WebSocket-Key: kHuChwCCkr9PZDPWo+nMXg==
- Sec-WebSocket-Origin: http://localhost
- Sec-WebSocket-Protocol: websocket
- Sec-WebSocket-Version: 8
服务器端取得请求信息,主要是Sec-WebSocket-Key的值,取得该值之 后,连接上字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11,然后计算其sha1散列值,生成一个20位的字符串,再对 该字符串进行base64编码,最后得到的值,按照下列响应信息格式返回给客户端
- HTTP/1.1 101 Switching Protocols
- Upgrade: websocket
- Connection: Upgrade
- Sec-WebSocket-Accept: 0VXXdxNQxqV7u7vtIhrxqdYUgRA=
- Sec-WebSocket-Protocol: websocket
客户端接收到服务器的响应信息,连接建立。
新草案的数据传输格式请参考下文:
2、数据传输的格式:
以下是一个格式标准图:
FIN:1位,用来表明这是一个消息的最后的消息片断,当然第一个消息片断也可能是最后的一个消息片断;
RSV1, RSV2, RSV3: 分别都是1位,如果双方之间没有约定自定义协议,那么这几位的值都必须为0,否则必须断掉WebSocket连接;
Opcode:4位操作码,定义有效负载数据,如果收到了一个未知的操作码,连接也必须断掉,以下是定义的操作码:
* %x0 表示连续消息片断
* %x1 表示文本消息片断
* %x2 表未二进制消息片断
* %x3-7 为将来的非控制消息片断保留的操作码
* %x8 表示连接关闭
* %x9 表示心跳检查的ping
* %xA 表示心跳检查的pong
* %xB-F 为将来的控制消息片断的保留操作码Mask:1位,定义传输的数据是否有加掩码,如果设置为1,掩码键必须放在masking-key区域,客户端发送给服务端的所有消息,此位的值都是1;
Payload length: 传输数据的长度,以字节的形式表示:7位、7+16位、或者7+64位。如果 这个值以字节表示是0-125这个范围,那这个值就表示传输数据的长度;如果这个值是126,则随后的两个字节表示的是一个16进制无符号数,用来表示传 输数据的长度;如果这个值是127,则随后的是8个字节表示的一个64位无符合数,这个数用来表示传输数据的长度。多字节长度的数量是以网络字节的顺序表 示。负载数据的长度为扩展数据及应用数据之和,扩展数据的长度可能为0,因而此时负载数据的长度就为应用数据的长度。
Masking-key:0或4个字节,客户端发送给服务端的数据,都是通过内嵌的一个32位值作为掩码的;掩码键只有在掩码位设置为1的时候存在。
Payload data: (x+y)位,负载数据为扩展数据及应用数据长度之和。
Extension data:x位,如果客户端与服务端之间没有特殊约定,那么扩展数据的长度始终为0,任何的扩展都必须指定扩展数据的长度,或者长度的计算方式,以及在握手时如何确定正确的握手方式。如果存在扩展数据,则扩展数据就会包括在负载数据的长度之内。
Application data:y位,任意的应用数据,放在扩展数据之后,应用数据的长度=负载数据的长度-扩展数据的长度。
数据帧协议是按照扩展的巴科斯范式(ANBF:Augmented Backus-Naur Form RFC5234)组成的:
- ws-frame = frame-fin
- frame-rsv1
- frame-rsv2
- frame-rsv3
- frame-opcode
- frame-masked
- frame-payload-length
- [ frame-masking-key ]
- frame-payload-data
- frame-fin = %x0 ; 表示这不是当前消息的最后一帧,后面还有消息
- / %x1 ; 表示这是当前消息的最后一帧
- frame-rsv1 = %x0
- ; 1 bit, 如果没有扩展约定,该值必须为0
- frame-rsv2 = %x0
- ; 1 bit, 如果没有扩展约定,该值必须为0
- frame-rsv3 = %x0
- ; 1 bit, 如果没有扩展约定,该值必须为0
- frame-opcode = %x0 ; 表示这是一个连续帧消息
- / %x1 ; 表示文本消息
- / %x2 ; 表示二进制消息
- / %x3-7 ; 保留
- / %x8 ; 表示客户端发起的关闭
- / %x9 ; ping(用于心跳)
- / %xA ; pong(用于心跳)
- / %xB-F ; 保留
- frame-masked = %x0 ; 数据帧没有加掩码,后面没有掩码key
- / %x1 ; 数据帧加了掩码,后面有掩码key
- frame-payload-length = %x00-7D
- / %x7E frame-payload-length-16
- / %x7F frame-payload-length-63
- ; 表示数据帧的长度
- frame-payload-length-16 = %x0000-FFFF
- ; 表示数据帧的长度
- frame-payload-length-63 = %x0000000000000000-7FFFFFFFFFFFFFFF
- ; 表示数据帧的长度
- frame-masking-key = 4( %0x00-FF ) ; 掩码key,只有当掩码位为1时出现
- frame-payload-data = (frame-masked-extension-data
- frame-masked-application-data) ; 当掩码位为1时,这里的数据为带掩码的数据,扩展数据及应用数据都带掩码
- / (frame-unmasked-extension-data
- frame-unmasked-application-data) ; 当掩码位为0时,这里的数据为不带掩码的数据,扩展数据及应用数据都不带掩码
- frame-masked-extension-data = *( %x00-FF ) ; 目前保留,以后定义
- frame-masked-application-data = *( %x00-FF )
- frame-unmasked-extension-data = *( %x00-FF ) ; 目前保留,以后定义
- frame-unmasked-application-data = *( %x00-FF )
上面是websocket的简单介绍,以作备忘。
websocket的知识学习了几天,网上搜索到的实现代码很多,但是用PHP做socket服务器的资料比较少,根据搜索到的资料以及自己的研究,记下此文。
websocket客户端的具体实现代码以及服务器(PHP)端代码介绍,本文的代码只是一个小的demo,实现简单的通信,客户端发送一个字符 串,服务器端接收到字符串并响应给客户端浏览器(firefox),支持中文字符串通信,客户端响应正常,但是在服务器端显示的乱码o(╯□╰)o
首先分析客户端代码,根据websocket的介绍,我们需要用MozWebSocket(host)来向服务器发送连接请求,然后为建立的socket绑定事件,socket的方法:
onopen:socket连接建立成功时触发的事件
onmessage:客户端接收到服务器端返回的信息时触发的事件
onerror:建立连接出错时触发的事件
onclose:断开连接时触发的事件
client.html代码:
- <script>
- var socket;
- /**
- * 初始化请求服务器端以建立websocket连接,host为本地localhost,请求的端口号为12345
- * 之后为socket的各事件绑定事件内容
- */
- function init(){
- var host = “ws://localhost:12345/websocket/server.php”;
- try{
- socket = new MozWebSocket(host);
- log(‘WebSocket – status ‘+socket.readyState);
- socket.onopen = function(msg){ log(“Welcome – status “+this.readyState); };
- socket.onmessage = function(msg){ log(“Received: “+msg.data); };
- socket.onclose = function(msg){ log(“Disconnected – status “+this.readyState); };
- } catch(ex) {
- log(ex);
- }
- }
- /**
- * send方法发送消息到服务器端
- */
- function send(){
- var msg = $(“msg”).value;
- if (!msg) return false;
- $(“msg”).value=“”;
- try{
- socket.send(msg);
- log(‘Sent: ‘+msg);
- } catch(ex) {
- log(ex);
- }
- }
- //初始化的其他方法
- function $(id) {
- return document.getElementById(id);
- }
- function log(msg) {
- $(“log”).innerHTML+=“<br>”+msg;
- }
- function onkey(event){
- if (event.keyCode == 13) send();
- }
- </script>
- <body onload=“init()”>
- <h3>WebSocket v2.00</h3>
- <div id=“log”></div>
- <input id=“msg” type=“textbox” onkeypress=“onkey(event)”/>
- <button onclick=“send()”>Send</button>
- </body>
其次是服务器端分析,这里用PHP建立服务器端的socket服务器,虽然PHP不支持多线程,但这里只是个人的学习demo,重要的是通信成功,上篇文章介绍了要握手成功需要响应给客户端的信息格式,这里不做介绍。
首先用socket_create等方法建立一个socket服务器,端口号即前面客户端请求的端口12345,代码 如下:
- $master = WebSocket(“localhost”,12345);
- $sockets[] = $master;
- function WebSocket($address,$port) {
- $master=socket_create(AF_INET, SOCK_STREAM, SOL_TCP) or die(“socket_create() failed”);
- socket_set_option($master, SOL_SOCKET, SO_REUSEADDR, 1) or die(“socket_option() failed”);
- socket_bind($master, $address, $port) or die(“socket_bind() failed”);
- socket_listen($master,20) or die(“socket_listen() failed”);
- echo “Server Started : “.date(‘Y-m-d H:i:s’).“\n”;
- echo “Master socket : “.$master.“\n”;
- echo “Listening on : “.$address.” port “.$port.“\n\n”;
- return $master;
- }
服务器建立成功之后,如果有客户端请求连接本服务器,需要用socket_accept等方法建立一个新的socket连接,并接收客户端的请求信息,处理之后,返回响应信息,然后握手成功。
接下来是字符串通信,客户端send过来一段字符串信息,服务器端接收到并返回给客户端这个字符串。
首先我们处理接收到的信息,根据上篇文章介绍的数据传输格式, 并firefox的FIN一直为1,RSV1,2,3为0,如果是文本消息,那么opcode为1,所以数据包的第一个数据是0x81,然后是一位 mask值,firefox发来的数据是加了掩码的,所以mask值为1,后面跟7位是数据信息长度,我们以客户端发送hi为例,那么长度就是2个字节, 则第二个数据就是0x82,这里没有约定扩展数据,所以不存在扩展数据长度字节,接下来是4个数据的掩码(因为我们这里是发送hi,2个字节的信息,小于125个字节,所以掩码是第3-第6个数据,根据数据长度的不同,掩码的位置也不同,如果取到那7位表示的值是126,则掩码为第5-第8个数据,如果取到那7位表示的值是127,则掩码为第11-第14个数据),后面跟客户端发送的内容数据,处理接收到的数据我们需要用取到的掩码依次轮流跟内容数据做异或(^)运算,第一个内容数据与第一个掩码异或,第二个内容数据与第二个掩码异或……第五个内容数据与第一个掩码异或……以此类推,一直到结束,然后对内容进行编码。
其次是服务器端发送给客户端的响应信息,数据格式跟接收的信息一样,只是我们不需要生成掩码,掩码位为0,后面也不存在4个掩码数据。
具体的PHP代码如下:
server.php
- <?php
- /**
- * php – websocket
- */
- error_reporting(E_ALL);
- set_time_limit(0);
- ob_implicit_flush(true);
- date_default_timezone_set(“Asia/shanghai”);
- $sockets = array();
- $users = array();
- $master = WebSocket(“localhost”,12345);
- $sockets[] = $master;
- while(true){
- $changed = $sockets;
- socket_select($changed,$write=NULL,$except=NULL,NULL);
- foreach ($changed as $socket) {
- if ($socket == $master) {
- $client=socket_accept($master);
- if ($client !== false) {
- skConnect($client);
- }
- } else {
- $data = @socket_recv($socket,$buffer,2048,0);
- if ($data != 0) {
- $user = getuserbysocket($socket);
- if (!$user->handshake) {
- dohandshake($user,$buffer);
- } else {
- process($socket,$buffer);
- }
- }
- }
- }
- sleep(1);
- }
- //—————————————————————
- function WebSocket($address,$port) {
- $master=socket_create(AF_INET, SOCK_STREAM, SOL_TCP) or die(“socket_create() failed”);
- socket_set_option($master, SOL_SOCKET, SO_REUSEADDR, 1) or die(“socket_option() failed”);
- socket_bind($master, $address, $port) or die(“socket_bind() failed”);
- socket_listen($master,20) or die(“socket_listen() failed”);
- echo “Server Started : “.date(‘Y-m-d H:i:s’).“\n”;
- echo “Master socket : “.$master.“\n”;
- echo “Listening on : “.$address.” port “.$port.“\n\n”;
- return $master;
- }
- function getuserbysocket($socket){
- global $users;
- $found=null;
- foreach($users as $user){
- if($user->socket==$socket){ $found=$user; break; }
- }
- return $found;
- }
- function skConnect($socket){
- global $sockets,$users;
- $user = new User();
- $user->id = uniqid();
- $user->socket = $socket;
- $users[] = $user;
- $sockets[] = $socket;
- }
- function disconnect($socket){
- global $sockets,$users;
- $found=null;
- $n=count($users);
- for($i=0;$i<$n;$i++){
- if($users[$i]->socket==$socket){ $found=$i; break; }
- }
- if(!is_null($found)){ array_splice($users,$found,1); }
- $index = array_search($socket,$sockets);
- socket_close($socket);
- if($index>=0){ array_splice($sockets,$index,1); }
- }
- function getheaders($req){
- $r=$h=$o=null;
- if(preg_match(“/GET (.*) HTTP\/1\.1\r\n/” ,$req,$match)){ $r=$match[1]; }
- if(preg_match(“/Host: (.*)\r\n/” ,$req,$match)){ $h=$match[1]; }
- if(preg_match(“/Sec-WebSocket-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 dohandshake($user,$buffer){
- list($resource,$host,$origin,$strkey) = getheaders($buffer);
- $strkey .= “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”;
- $hash_data = base64_encode(sha1($strkey,true));
- $upgrade = “HTTP/1.1 101 Switching Protocols\r\n” .
- “Upgrade: websocket\r\n” .
- “Connection: Upgrade\r\n” .
- “Sec-WebSocket-Accept: “ . $hash_data . “\r\n” .
- “Sec-WebSocket-Protocol: websocket\r\n” .
- “\r\n”;
- socket_write($user->socket,$upgrade,strlen($upgrade));
- $user->handshake=true;
- return true;
- }
- function process($socket,$msg){
- $action = unwrap($msg);
- say(“< “.$action);
- send($socket, $action);
- }
- function send($client,$msg){
- say(“> “.$msg);
- $msg = wrap($msg);
- socket_write($client,$msg,strlen($msg));
- return true;
- }
- function ord_hex($data)
- {
- $msg = “”;
- $l = strlen($data);
- for ($i= 0; $i< $l; $i++) {
- $msg .= dechex(ord($data{$i}));
- }
- return $msg;
- }
- function wrap($msg=“”) {
- $frame = array();
- $frame[0] = “81”;
- $msg .= ” is ok!”;
- $len = strlen($msg);
- $frame[1] = $len<16?“0”.dechex($len):dechex($len);
- $frame[2] = ord_hex($msg);
- $data = implode(“”,$frame);
- return pack(“H*”, $data);
- }
- function unwrap($msg=“”) {
- $mask = array();
- $data = “”;
- $msg = unpack(“H*”,$msg);
- $head = substr($msg[1],0,2);
- if (hexdec($head{1}) === 8) {
- $data = false;
- } else if (hexdec($head{1}) === 1) {
- $mask[] = hexdec(substr($msg[1],4,2));
- $mask[] = hexdec(substr($msg[1],6,2));
- $mask[] = hexdec(substr($msg[1],8,2));
- $mask[] = hexdec(substr($msg[1],10,2));
- $s = 12;
- $e = strlen($msg[1])-2;
- $n = 0;
- for ($i= $s; $i<= $e; $i+= 2) {
- $data .= chr($mask[$n%4]^hexdec(substr($msg[1],$i,2)));
- $n++;
- }
- }
- return $data;
- }
- function say($msg=“”){ print_r($msg.“\n”); }
- class User{
- var $id;
- var $socket;
- var $handshake;
- }
这里打包跟解包是用的pack跟unpack,当然还有很多其他的方式。
建立服务器需要在dos窗口下用PHP运行该server.php文件。
貌似一个成熟的phpwebsocket开源项目,有兴趣的同学下载来看看,地址:
免费下载地址在 http://linux.linuxidc.com/
用户名与密码都是www.linuxidc.com
具体下载目录在 /2012年资料/2月/12日/HTML5新功能WebSocket的学习与备忘-PHP版/