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案例