您的位置:首页 > 其它

Socket学习 - 撕开websocket神秘的外衣

2017-01-09 16:29 211 查看
websocket,我们可以理解嵌入在浏览器中的socket客户端

那么问题来了

1、它有专门的协议?

2、是否和HTTP协议一样,和服务端交互

3、服务端代码怎么写?



1.客户端websocket_client.html:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script>
//创建一个socket实例
var socket = new WebSocket('ws://127.0.0.1:9090');

//打开socket
socket.onopen = function (e) {
//发送一个初始化消息
socket.send('init msg');
};

socket.onmessage = function(e){
console.log('收到消息',e);
};

//监听socket的关闭
socket.onclose = function(e){
console.log('关闭',e);
};
</script>
</body>
</html>


2.服务端websocket_server.php

<?php

$server = socket_create(AF_INET,SOCK_STREAM,SOL_TCP);  // 购买电话机
socket_bind($server,'127.0.0.1',9090);  // 绑定电话机
socket_listen($server,5);   // 开机

while(true){
$client = socket_accept($server);   //接收客户端连接
$buf = socket_read($client,8024);   //一次读取数据的长度
echo $buf;

//回复
socket_write($client,'404');

//关掉客户端
socket_close($client);
}

//关机
socket_close($server);


3.把服务端运行起来,然后浏览器 客户端访问:

http://localhost:63343/socket/websocket_client.html



发现报错了,报错并不说明websocket就没有连接,我们查看服务端控制台会打印如下内容:



说明,只要客户端
var socket = new WebSocket('ws://127.0.0.1:9090');
这一步,服务端就会收到。

4.报错的原因是:服务端没有按照websocket的协议回复内容

握手方法

客户端指定服务端发送一个握手请求,如果服务端返回合法的HTTP头,则握手成功。

1、客户端会发送一个字段:
Sec-WebSocket-Key: xxxxxooooooo


2、服务端需要截取此值,把该值累加字符串
258EAFA5-E914-47DA-95CA-C5AB0DC85B11
,然后进行sha1,最后再base64_encode

3、拼凑对应的响应协议内容

<?php

$server = socket_create(AF_INET,SOCK_STREAM,SOL_TCP);  // 购买电话机
socket_bind($server,'127.0.0.1',9090);  // 绑定电话机
socket_listen($server,5);   // 开机

while(true){
$client = socket_accept($server);   //接收客户端连接
$buf = socket_read($client,8024);   //一次读取数据的长度
//echo $buf;

if(preg_match("/Sec-WebSocket-Key: (.*)\r\n/i",$buf,$matches)){
$key = base64_encode(sha1($matches[1].'258EAFA5-E914-47DA-95CA-C5AB0DC85B11',true));
$res = 'HTTP/1.1 101 Switching Protocol'.PHP_EOL
.'Upgrade: Websocket'.PHP_EOL
.'Connection: Upgrade'.PHP_EOL
.'WebSoket-Location: ws://127.0.0.1:9090'.PHP_EOL
.'Sec-WebSocket-Accept:'.$key.PHP_EOL.PHP_EOL;

//socket回复
socket_write($client,$res,strlen($res));
socket_write($client,'hello websocket');
}

}

//关机
socket_close($server);


握手成功后就比较复杂了

相关的数据发送是需要有一定的格式的,这里边涉及到一些二进制的计算,服务端要对数据进行处理,我们这里就不深入了。

拿来主义,直接使用现成的代码

/**
* @param $msg要处理的数据内容
*/
function buildMsg($msg){
$frame = [];
$frame[0] = '81';
$len = strlen($msg);
if($len < 126){
$frame[1] = $len < 16 ? '0'.dechex($len) : dechex($len);
}elseif($len < 65025){
$s = dechex($len);
$frame[1] = '7e'.str_repeat('0',4-strlen($s)).$s;
}else{
$s = dechex($len);
$frame[1] = '7f'.str_repeat('0',16-strlen($s)).$s;
}

$data = '';
$l = strlen($msg);
for($i=0;$i<$l;$i++){
$data .= dechex(ord($msg{$i}));
}
$frame[2] = $data;
$data = implode('',$frame);
return pack('H*',$data);
}


在服务端回复数据的时候,用此函数处理

socket_write($client,buildMsg("hello websocket")); //注意此处要双引号


然后客户端浏览器请求:



我们客户端监听到了socket服务端的消息

socket.onmessage = function(e){
console.log('收到消息',e);
};


我们继续优化,完成消息发送

客户端:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<button onclick="sendMsg()">发送消息</button>
<script>
//创建一个socket实例
var socket = new WebSocket('ws://127.0.0.1:9090');

//打开socket
socket.onopen = function (e) {
//发送一个初始化消息
socket.send('init msg');
};

socket.onmessage = function(e){ console.log('收到消息',e); };

//监听socket的关闭
socket.onclose = function(e){
console.log('关闭',e);
};

//自定义按钮事件
function sendMsg(){
socket.send('hello server');
}
</script>
</body>
</html>


客户端点击按钮,发送一条消息给服务端。

服务端
websocket_server.php
需要做相应处理:

<?php

$server = socket_create(AF_INET,SOCK_STREAM,SOL_TCP);  // 购买电话机
socket_bind($server,'127.0.0.1',9090);  // 绑定电话机
socket_listen($server,5);   // 开机

//定义一个数组
$allSockets = [$server];
while(true){
$copySockets = $allSockets;
if(socket_select($copySockets,$write,$except,0) === false){
exit('error');
}

if(in_array($server,$copySockets)){
$client = socket_accept($server);   //接收客户端连接
$buf = socket_read($client,8024);   //一次读取数据的长度
//echo $buf;

if(preg_match("/Sec-WebSocket-Key: (.*)\r\n/i",$buf,$matches)){
$key = base64_encode(sha1($matches[1].'258EAFA5-E914-47DA-95CA-C5AB0DC85B11',true));
$res = 'HTTP/1.1 101 Switching Protocol'.PHP_EOL
.'Upgrade: Websocket'.PHP_EOL
.'Connection: Upgrade'.PHP_EOL
.'WebSoket-Location: ws://127.0.0.1:9090'.PHP_EOL
.'Sec-WebSocket-Accept:'.$key.PHP_EOL.PHP_EOL;

//socket回复
socket_write($client,$res,strlen($res)); //握手成功
socket_write($client,buildMsg("hello websocket")); //注意此处要双引号

//把webSocket客户端的socket保存起来
$allSockets[] = $client;
}

//把服务端的socket移除
$k = array_search($server,$copySockets);
unset($copySockets[$k]);
}

foreach($copySockets as $s){
$buf = socket_read($s,8024);
if(strlen($buf) < 9){ //意味着客户端主动关闭了链接
$k = array_search($s,$allSockets);
unset($allSockets[$k]); //数组中删除该socket

//服务端也要关掉
socket_close($s);
continue;
}

echo getMsg($buf); //获取客户端消息并转码
echo PHP_EOL;
}

}

//关机
socket_close($server);

///////////////功能函数//////////////////////////

/**
* 编码发送给客户端的数据
* @param $msg要处理的数据内容
*/
function buildMsg($msg){
$frame = [];
$frame[0] = '81';
$len = strlen($msg);
if($len < 126){
$frame[1] = $len < 16 ? '0'.dechex($len) : dechex($len);
}elseif($len < 65025){
$s = dechex($len);
$frame[1] = '7e'.str_repeat('0',4-strlen($s)).$s;
}else{
$s = dechex($len);
$frame[1] = '7f'.str_repeat('0',16-strlen($s)).$s;
}

$data = '';
$l = strlen($msg);
for($i=0;$i<$l;$i++){
$data .= dechex(ord($msg{$i}));
}
$frame[2] = $data;
$data = implode('',$frame);
return pack('H*',$data);
}

/**
* 解析客户端发送过来的数据
* @param $buffer
*/
function getMsg($buffer){
$res = '';
$len = ord($buffer)&127;
if($len === 126){
$masks = substr($buffer,4,4);
$data = substr($buffer,8);
}elseif($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++){
$res .= $data[$index]^$masks[$index % 4];
}

return $res;
}


重新运行服务端,任何观察服务端控制的打印的信息。

刷新浏览器,服务端控制台打印:

init msg


点击“发送消息”,服务端控制台打印:

hello server #这个是客户端发送的


这样就完成了一个简单的websocket案例
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: