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

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

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值