前一篇: websocket基础知识介绍
关于websocket,网上搜索到的实现代码很多,但是用PHP做socket服务器的资料比较少,根据搜索到的资料以及自己的研究,记下此文。
websocket客户端的具体实现代码以及服务器(PHP)端代码介绍,本文的代码只是一个小的demo,实现简单的通信,客户端发送一个字符串,服务器端接收到字符串并响应给客户端浏览器(firefox),支持中文字符串通信,客户端响应正常
首先分析客户端代码,根据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 οnlοad="init()">
<h3>WebSocket v2.00</h3>
<div id="log"></div>
<input id="msg" type="textbox" οnkeypress="onkey(event)"/>
<button οnclick="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开源项目,有兴趣的同学下载来看看,地址:https://github.com/nicokaiser/php-websocket
参考资料:
PHP and websocket :http://code.google.com/p/phpwebsocket/