您的位置:首页 > 其它

WebSocket即时聊天

2015-08-09 07:48 281 查看
WebSocket即时聊天

这是我个人看了一些博客和资料对websocket的了解,如果有错误的地方希望大家指出,互相学习。我建议大家可以读读这基本书,可能对websocket有更深如的理解。图解http和websocket权威指南。图解websocket网上有英文版的,用有道翻译不是很难。

用php写即时聊天系统,最简单的方法就是用ajax写,通过设置轮询每隔3秒左右请求一次服务器,但是这个有一点不好,就是不管有没有新消息,都会发送一次请求有没有消息,服务器“没有”,客户端“哦,等会儿我再来”(3秒一次)。这种请求就是延迟服务器的响应,客户端也潜在的产生很多tcp连接,基于请求-响应模式,服务端不能主动发送数据。那么websocket的好处就是客户端只有一个tcp连接,服务端可以主动发送数据到客户端,支持多个客户端,web客户。

写websocket代码之前,我们要看看websocket的网络协议。每一个websocket连接都是始于一个http请求,该请求和其他请求类似,但是包含一个特殊的首标Upgrade。Upgrade首标表示客户端把连接升级到不同的协议。这个不同的协议就是WebSocket。看看客户端发送的http请求代码。

客户端请求代码

服务端响应代码

上面图展示要求和可选的首标。有些首标是必须存在并且精确才能保证WebSocket连接成功。在协议升级成功之后,连接的语法切换为用于表示WebSocket消息的数据帧格式。除非服务器响应101代码,Upgrade首标和Sec-WebSocket-Accept首标,否则WebSocket连接不成功。Sec-WebSocket-Accept响应的首标的值从Sec-WebSocket-Key请求首标继承而来,包含一个特殊的响应键值,必须与客户端的预期精确匹配。

为了完成握手,WebSocket服务器必须响应一个计算出来的键值。这个响应说明服务器理解了WebSocket协议。没有精确的响应,就可能哄骗一些轻信的http服务器意外的升级一个连接。响应函数从客户端发的Sec-WebSocket-Key首标取得键值,并在Sec-WebSocket-Accept中返回客户端预期计算的键值。

正则获取客户端key

function getKey($req) {
$key = null;
if (preg_match("/Sec-WebSocket-Key: (.*)\r\n/", $req, $match)) {
$key = $match[1];
}
return $key;
}



加密Sec-WebSocket-Key值
function encry($req){
$key = $this->getKey($req);
$mask = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";

return base64_encode(sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
}
将 SHA-1
加密后的字符串再进行一次 base64 加密。如果加密算法错误,客户端在进行校检的时候会直接报错。

下面我们再说说,数据帧。

数据帧格式

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 ...                |
+---------------------------------------------------------------+


看了这个我瞬间整个人就不好了,好后悔大学没有好好学习有关这个们课程。不说了直接上解释。

FIN :1bit ,表示是消息的最后一帧,如果消息只有一帧那么第一帧也就是最后一帧。
RSV1,RSV2,RSV3:每个1bit,必须是0,除非扩展定义为非零。如果接受到的是非零值但是扩展没有定义,则需要关闭连接。
Opcode:4bit,解释Payload数据,规定有以下不同的状态,如果是未知的,接收方必须马上关闭连接。状态如下:0x0(附加数据帧) 0x1(文本数据帧) 0x2(二进制数据帧) 0x3-7(保留为之后非控制帧使用) 0xB-F(保留为后面的控制帧使用) 0x8(关闭连接帧) 0x9(ping) 0xA(pong)
Mask:1bit,掩码,定义payload数据是否进行了掩码处理,如果是1表示进行了掩码处理。
Masking-key域的数据即是掩码密钥,用于解码PayloadData。客户端发出的数据帧需要进行掩码处理,所以此位是1。


Payload length:7位,7 + 16位,7+64位,payload数据的长度,如果是0-125,就是真实的payload长度,如果是126,那么接着后面的2个字节对应的16位无符号整数就是payload数据长度;如果是127,那么接着后面的8个字节对应的64位无符号整数就是payload数据的长度。
Masking-key:0到4字节,如果MASK位设为1则有4个字节的掩码解密密钥,否则就没有。
Payload data:任意长度数据。包含有扩展定义数据和应用数据,如果没有定义扩展则没有此项,仅含有应用数据。

数据帧的解析和编码

解析



function decodeDataFrame(e){
var i=0,j,s,frame={
//解析前两个字节的基本数据
FIN:e[i]>>7,Opcode:e[i++]&15,Mask:e[i]>>7,
PayloadLength:e[i++]&0x7F
};
//处理特殊长度126和127
if(frame.PayloadLength==126)
frame.length=(e[i++]<<8)+e[i++];
if(frame.PayloadLength==127)
i+=4, //长度一般用四字节的整型,前四个字节通常为长整形留空的
frame.length=(e[i++]<<24)+(e[i++]<<16)+(e[i++]<<8)+e[i++];
//判断是否使用掩码
if(frame.Mask){
//获取掩码实体
frame.MaskingKey=[e[i++],e[i++],e[i++],e[i++]];
//对数据和掩码做异或运算
for(j=0,s=[];j<frame.PayloadLength;j++)
s.push(e[i+j]^frame.MaskingKey[j%4]);
}else s=e.slice(i,frame.PayloadLength); //否则直接使用数据
//数组转换成缓冲区来使用
s=new Buffer(s);
//如果有必要则把缓冲区转换成字符串来使用
if(frame.Opcode==1)s=s.toString();
//设置上数据部分
frame.PayloadData=s;
//返回数据帧
return frame;
}
编码

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;
}


这是我暂时理解的,下面指直接上demo。
运行ws.html文件前,要运行demo.php服务。
demo.php

<?php

class WS
{
public $master;
public $sockets = array();
public $debug = false;
public $handshake = false;
public $users;

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);
$key=uniqid();
$this->sockets[]=$client;
//存入数组
$this->users[$key]=array(
'socket'=>$client,
'shou'=>false
);
} else {
$this->say("^^^^".$client);
$bytes = @socket_recv($socket, $buffer, 2048, 0);
$this->say("^^^^".$client);
//返回客户端
$k=$this->search($socket);

if ($bytes == 0) {
$this->disConnect($k);
} else {
if (!$this->users[$k]['shou']) {
$this->doHandShake($k, $buffer);
} else {
$buffer = $this->decode($buffer);
$this->say("^^wew^^".$buffer);
$this->send($k, $buffer);
}
}
}
}
}
}

//客户端
function search($sock){
foreach ($this->users as $k=>$v){
if($sock==$v['socket'])
return $k;
}
return false;
}

function send($client, $msg)
{
$this->log("> " . $msg);
$msg = $this->frame($msg);
socket_write($this->users[$client]['socket'], $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($k)
{
socket_close($this->users[$k]['socket']);
unset($this->users[$k]);
$this->sockets=array($this->master);
foreach($this->users as $v){
$this->sockets[]=$v['socket'];
}
}

function doHandShake($k, $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($this->users[$k]['socket'], $upgrade, strlen($upgrade));
$this->users[$k]['shou']=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;
$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;
}

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; }

function say($msg = "")
{
echo $msg . "\n";
}

function log($msg = "")
{
if ($this->debug) {
echo $msg . "\n";
}
}
}

new WS('localhost', 4000);


连接成功的界面
ws.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="http://apps.bdimg.com/libs/jquery/2.1.1/jquery.min.js" type="text/javascript"></script>
</head>
<body>
<input id="text" type="text" name="test"><input id="send" type="button" value="ok">
</body>
</html>

<script type="text/javascript">
var ws = new WebSocket("ws://localhost:4000");
ws.onopen = function(){
console.log("握手成功");
}
ws.onmessage = function(e){
alert("message:" + e.data);
}
ws.onerror = function(){
console.log("error");
}
$('#send').click(function(){
ws.send("{name:"+$('#text').val()+"}");
});
</script>


这是我看的博客,这个博客写的比我全。http://www.cnblogs.com/hustskyking/p/websocket-with-php.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: