Swoole WebSocket初探

一、什么是WebSocket

  • WebSocketHTML5 开始提供的一种在单个TCP连接上进行全双工通讯的协议。
  • WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。
  • WebSocket Api中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

二、WebSocket的作用

主要用于客户端和服务端的双向通讯,一般的使用场景有社交聊天、多人游戏、体育实况更新、弹幕等高实时性应用。

三、WebSocket出现的背景是什么

  • WebSocket出现以前,最常用的技术就是轮询获取服务器资源,轮询分普通轮询和长轮询
    • 普通轮询就是通过客户端不断的请求服务端来获取数据,这种方式效率低下,每次都需要建立连接,释放连接,且只有在服务端有需要的数据的时候,请求才是真正有效的请求,其他大部分的请求都是试探性的不必要的请求。这样就造成了资源的浪费,并且对实时性要求较高的应用很不友好。
      客户端:有没有消息
      服务端:没有
      客户端:有没有消息
      服务端:没有
      客户端:有没有消息
      服务端:没有
      客户端:有没有消息
      服务端:有,消息是...
      客户端:有没有消息
      服务端:没有
      
    • 长轮询是通过客户端请求,服务端阻塞住请求,当有数据的时候就返回给客户端,这种方式虽然实时性较好,但是却占用了服务端的资源,因为是在服务端进行阻塞,所以对并发并不友好,只能用于小企业内部少量使用。
      客户端:有消息你通知我啊
      服务端:好的,有消息我通知你
      服务端:有消息了,消息是...
      
  • 轮询最主的问题还是服务端无法推送消息给客户端,只能由客户端请求服务端来获取数据,为了解决这些问题,就出现了WebSocket协议。
  • 伴随着HTML5推出的WebSocket,真正实现了Web的实时通信,使B/S模式具备了C/S模式的实时通信能力。

在这里插入图片描述

上图可以看出轮询是Http协议是一次请求对应一次响应,每次请求都需要由客户端发起,而WebSocket的通信只需要客户端与服务器通过Http协议建立握手,双方便可以平等地、无差别地相互传送信息了,直至任意一方主动断开连接结束。

四、Swoole WebSocket

  • Swoole1.7.9增加了内置的WebSocket服务器支持,通过几行PHP代码就可以写出一个异步非阻塞多进程的WebSocket服务器。
  • WebSocket除了接收Swoole\Server和Swoole\Http\Server基类的回调函数外,额外增加了3个回调函数设置。其中:onMessage回调函数为必选,onOpenonHandShake回调函数为可选
    • function onMessage(swoole_websocket_server $server, swoole_websocket_frame $frame)
      • 当服务器收到来自客户端的数据帧时会回调此函数。
      • $frameswoole_websocket_frame对象,包含了客户端发来的数据帧信息
        • $frame->fd,客户端的socket id,使用$server->push推送数据时需要用到
        • $frame->data,数据内容,可以是文本内容也可以是二进制数据,可以通过opcode的值来判断
        • $frame->opcodeWebSocketOpCode类型,可以参考WebSocket协议标准文档
        • $frame->finish, 表示数据帧是否完整,一个WebSocket请求可能会分成多个数据帧进行发送(底层已经实现了自动合并数据帧,现在不用担心接收到的数据帧不完整)

        $data 如果是文本类型,编码格式必然是UTF-8,这是WebSocket协议规定的

      • onMessage回调必须被设置,未设置服务器将无法启动
      • 客户端发送的ping帧不会触发onMessage,底层会自动回复pong
    • function onOpen(swoole_websocket_server $svr, swoole_http_request $req)
      • WebSocket客户端与服务器建立连接并完成握手后会回调此函数。
      • onOpen事件回调是可选的
      • $req是一个Http请求对象,包含了客户端发来的握手请求信息
      • onOpen事件函数中可以调用push向客户端发送数据或者调用close关闭连接
    • function onHandShake(swoole_http_request $request, swoole_http_response $response)
      • WebSocket建立连接后进行握手。WebSocket服务器已经内置了handshake,如果用户希望自己进行握手处理,可以设置onHandShake事件回调函数。
      • onHandShake事件回调是可选的
      • 设置onHandShake回调函数后不会再触发onOpen事件,需要应用代码自行处理
      • onHandShake中必须调用response->status设置状态码为101并调用end响应, 否则会握手失败.
      • 内置的握手协议为Sec-WebSocket-Version: 13,低版本浏览器需要自行实现握手
      • 示例代码
        $server->on('handshake', function (\swoole_http_request $request, \swoole_http_response $response) {
            // print_r( $request->header );
            // if (如果不满足我某些自定义的需求条件,那么返回end输出,返回false,握手失败) {
            //    $response->end();
            //     return false;
            // }
        
            // websocket握手连接算法验证
            $secWebSocketKey = $request->header['sec-websocket-key'];
            $patten = '#^[+/0-9A-Za-z]{21}[AQgw]==$#';
            if (0 === preg_match($patten, $secWebSocketKey) || 16 !== strlen(base64_decode($secWebSocketKey))) {
                $response->end();
                return false;
            }
            echo $request->header['sec-websocket-key'];
            $key = base64_encode(sha1(
                $request->header['sec-websocket-key'] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11',
                true
            ));
        
            $headers = [
                'Upgrade' => 'websocket',
                'Connection' => 'Upgrade',
                'Sec-WebSocket-Accept' => $key,
                'Sec-WebSocket-Version' => '13',
            ];
        
            if (isset($request->header['sec-websocket-protocol'])) {
                $headers['Sec-WebSocket-Protocol'] = $request->header['sec-websocket-protocol'];
            }
        
            foreach ($headers as $key => $val) {
                $response->header($key, $val);
            }
        
            $response->status(101);
            $response->end();
        });
        
  • WebSocket常用方法
    • function WebSocket\Server->push(int $fd, $data, int $opcode = 1, bool $finish = true)

      WebSocket\Server->push1.7.11以上版本可用

      • websocket客户端连接推送数据,长度最大不得超过2M。
      • 传参分两种模式
      • 模式一
        • $fd 客户端连接的ID,如果指定的$fd对应的TCP连接并非websocket客户端,将会发送失败
        • $data 要发送的数据内容
        • $opcode,指定发送数据内容的格式,默认为文本。发送二进制内容$opcode参数需要设置为WEBSOCKET_OPCODE_BINARY
        • 发送成功返回true,发送失败返回false
      • 模式二

        需要4.2.0及以上版本

        • $data也就是第一个参数, 可以传入一个swoole_websocket_frame对象, 支持发送各种帧类型
    • function WebSocket\Server->exist(int $fd)

      4.3.0以后, 此API仅用于判断连接是否存在, 请使用isEstablished判断是否为WebSocket连接

      • 判断WebSocket客户端是否存在,并且状态为Active状态。
      • 连接存在,并且已完成WebSocket握手,返回true
      • 连接不存在或尚未完成握手,返回false
    • function WebSocket\Server::pack(string $data, int $opcode = 1, bool $finish = true, bool $mask = false)
      • 打包WebSocket消息。
      • $data:消息内容
      • $opcodeWebSocketopcode指令类型,1表示文本,2表示二进制数据,9表示心跳ping
      • $finish:帧是否完成
      • $mask:是否设置掩码
      • 返回打包好的WebSocket数据包,可通过Socket发送给对端
    • function WebSocket\Server::unpack(string $data)
      • 解析WebSocket数据帧。
      • 解析失败返回false,解析成功返回Swoole\WebSocket\Frame对象
    • function WebSocket\Server->disconnect(int $fd, int $code = 1000, string $reason = "")

      4.0.3以上版本可用

      • 主动向WebSocket客户端发送关闭帧并关闭该连接
      • $fd 客户端连接的ID,如果指定的$fd对应的TCP连接并非WebSocket客户端,将会发送失败
      • $code 关闭连接的状态码,根据RFC6455,对于应用程序关闭连接状态码,取值范围为1000或4000-4999之间
      • $reason 关闭连接的原因,utf-8格式字符串,字节长度不超过125
      • 发送成功返回true,发送失败或状态码非法时返回false
    • function WebSocket\Server->isEstablished(int $fd);
      • 检查连接是否为有效的WebSocket客户端连接。此函数与exist方法不同,exist方法仅判断是否为TCP连接,无法判断是否为已完成握手的WebSocket客户端。
  • WebSocket\CloseFrame
    • 之前介绍了一个Frame对象,除此之外还有一个CloseFrame对象,对比Frame对象多了两个属性codereason
    • 如果服务端需要接收close frame, 需要通过$server->set开启open_websocket_close_frame参数
      • 启用WebSocket协议中关闭帧(opcode0x08的帧)在onMessage回调中接收,默认为false
      • 开启后,可在WebSocketServer中的onMessage回调中接收到客户端或服务端发送的关闭帧,开发者可自行对其进行处理。

五、浅析建立连接

  • 目前主流浏览器都支持WebSocket协议,此处使用的是谷歌浏览器,首先创建一个WebSocket对象,传入服务端的ip和监听的WebSocket端口,
  • WebSocket的统一资源标志符为ws,如果是加密版本则为wssWebSocket 使用和 HTTP 相同的 TCP 端口,可以绕过大多数防火墙的限制。默认情况下,WebSocket 协议使用 80 端口;运行在 TLS 之上时,默认使用 443 端口。
  • js中WebSocket有如下的事件:onopen、onmessage、onerror、onclose,分别在连接、收到客户端消息、通信发生错误和关闭连接时候触发
  • js中WebSocket有如下的方法:send、close,分别是发送消息和主动关闭连接

Websocket首先通过http协议进行握手的

在这里插入图片描述

在请求头中除了Http带有的请求信息还会带有如下的信息

request headersvalueexplain
ConnectionUpgrade表示客户端希望连接升级。
Upgradewebsocket表示希望升级到WebSocket协议。
Sec-WebSocket-KeyVI8UvTbArkqKYJ5xSEsoyA==浏览器生成的Base64编码的16位随机字符
Sec-WebSocket-Version13表示支持的WebSocket版本

服务端响应(状态代码101表示协议切换,到此完成协议升级,后续的数据交互都按照新的协议来)

response headersvalueexplain
ConnectionUpgrade表示连接升级。
Upgradewebsocket表示使用的是WebSocket协议。
Sec-Websocket-AcceptLwwMZdLAE7yn4D1HpimICz99NlU=Sec-WebSocket-Key258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接,通过sha1加密并转换成base64生成
Sec-WebSocket-Version13表示支持的WebSocket版本

六、代码演示

1.客户端代码
<!DOCTYPE HTML>
<html>
<head>
    <meta charset="utf-8">
    <title>websocket</title>
    <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
    <script type="text/javascript">
        if ("WebSocket" in window)
        {
            //创建webSocket对象
            var ws = new WebSocket("ws://192.168.2.163:3190");

            //连接建立时触发
            ws.onopen = function()
            {
                console.log('建立连接');
            }

            //客户端接收服务端数据时触发
            ws.onmessage = function (evt)
            {
                var received_msg = evt.data;
                console.log('收到消:'+received_msg);
            }

            //连接关闭时触发
            ws.onclose = function(evt)
            {
                console.log("连接已关闭...");
            }

            //连接关闭时触发
            ws.onerror = function()
            {
                console.log("连接发生错误...");
            }
        }

        else
        {
            // 浏览器不支持 WebSocket
            alert("您的浏览器不支持 WebSocket!");
        }
        
        function sendData() {
            var msg = $("#msg").val();
            ws.send(msg);
        }
    </script>

</head>
<body>

<input type="text" placeholder="请输入要发送的文字" id="msg">

<button onclick="sendData()">点击发送</button>

</body>
</html>
2.服务端代码
<?php
//创建WebSocket服务
$server = new Swoole\WebSocket\Server("0.0.0.0", 3190);

//启用websocket协议中关闭帧
$server->set(array("open_websocket_close_frame" => true));

//连接建立时触发
$server->on('open', function (Swoole\WebSocket\Server $server, $request) {
    foreach ($server->connections as $connection) {
        $server->push($connection, "用户{$request->fd}进入聊天");
    }
});

//收到客户端数据时触发
$server->on('message', function (Swoole\WebSocket\Server $server, $frame) {
    if ($frame->opcode == 0x08) {
        foreach ($server->connections as $connection) {
            $server->push($connection, "用户{$frame->fd}退出聊天");
        }
        return;
    }

    foreach ($server->connections as $connection) {
        if ($connection == $frame->fd) {
            $server->push($frame->fd, "您说:{$frame->data}");
        }else{
            $server->push($connection, "用户{$frame->fd}说:{$frame->data}");
        }
    }
});

//关闭连接是触发
$server->on('close', function ($ser, $fd) {
    echo "客户端{$fd} 关闭\n";
});

$server->start();
3.运行结果
步骤1:执行php server.php运行服务端
步骤2:登录用户1

在这里插入图片描述

步骤3:登录用户2

在这里插入图片描述

步骤3:用户1发送消息

在这里插入图片描述

步骤3:用户2发送消息

在这里插入图片描述

步骤4:关闭用户1浏览器

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值