如何为实时应用程序创建WebSocket服务器

2023-12-01 12:43:11 浏览数 (2)

Ratchet

Ratchet 是一个用于异步服务WebSockets的PHP库。通过简单的接口构建应用程序,并通过组合不同的组件重用应用程序,而无需更改其任何代码。

什么是 WebSocket

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

通信流程

典型握手通信

客户端请求
代码语言:javascript复制
GET /chat HTTP/1.1
Host: wss.tinywan.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
服务器响应
代码语言:javascript复制
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK xOo=
Sec-WebSocket-Protocol: chat
字段说明
  • Connection必须设置Upgrade,表示客户端希望连接升级。
  • Upgrade字段必须设置Websocket,表示希望升级到Websocket协议。
  • Sec-WebSocket-Key是随机的字符串,服务器端会用这些数据来构造出一个SHA-1的信息摘要。把Sec-WebSocket-Key加上一个特殊字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11,然后计算SHA-1摘要,之后进行Base64编码,将结果做为Sec-WebSocket-Accept头的值,返回给客户端。如此操作,可以尽量避免普通HTTP请求被误认为Websocket协议。
  • Sec-WebSocket-Version表示支持的Websocket版本。RFC6455要求使用的版本是13,之前草案的版本均应当弃用。
  • Origin字段是可选的,通常用来表示在浏览器中发起此Websocket连接所在的页面,类似于Referer。但是,与Referer不同的是,Origin只包含了协议和主机名称。其他一些定义在HTTP协议中的字段,如Cookie等,也可以在Websocket中使用。

案例

服务端

新建目录
代码语言:javascript复制
mkdir ratchet.websocket.tinywan.cn
安装依赖包
代码语言:javascript复制
E:dnmpwwwratchet.websocket.tinywan.cn>composer require cboden/ratchet
./composer.json has been created
Running composer update cboden/ratchet
Loading composer repositories with package information
Updating dependencies
Lock file operations: 18 installs, 0 updates, 0 removals
  - Locking cboden/ratchet (v0.4.4)
  - Locking evenement/evenement (v3.0.2)
  - Locking guzzlehttp/psr7 (2.6.1)
  - Locking psr/http-factory (1.0.2)
  - Locking psr/http-message (2.0)
  - Locking ralouphie/getallheaders (3.0.3)
  - Locking ratchet/rfc6455 (v0.3.1)
  - Locking react/cache (v1.2.0)
  - Locking react/dns (v1.12.0)
  - Locking react/event-loop (v1.5.0)
  - Locking react/promise (v3.0.0)
  - Locking react/socket (v1.14.0)
  - Locking react/stream (v1.3.0)
  - Locking symfony/deprecation-contracts (v2.5.2)
  - Locking symfony/http-foundation (v5.4.32)
  - Locking symfony/polyfill-mbstring (v1.28.0)
  - Locking symfony/polyfill-php80 (v1.28.0)
  - Locking symfony/routing (v5.4.26)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 18 installs, 0 updates, 0 removals
  - Downloading symfony/routing (v5.4.26)
  - Downloading symfony/http-foundation (v5.4.32)
  - Downloading react/event-loop (v1.5.0)
  - Downloading evenement/evenement (v3.0.2)
  - Downloading react/stream (v1.3.0)
  - Downloading react/promise (v3.0.0)
  - Downloading react/cache (v1.2.0)
  - Downloading react/dns (v1.12.0)
  - Downloading react/socket (v1.14.0)
  - Downloading ratchet/rfc6455 (v0.3.1)
  - Downloading cboden/ratchet (v0.4.4)
  - Installing symfony/polyfill-php80 (v1.28.0): Extracting archive
  - Installing symfony/deprecation-contracts (v2.5.2): Extracting archive
  - Installing symfony/routing (v5.4.26): Extracting archive
  - Installing symfony/polyfill-mbstring (v1.28.0): Extracting archive
  - Installing symfony/http-foundation (v5.4.32): Extracting archive
  - Installing react/event-loop (v1.5.0): Extracting archive
  - Installing evenement/evenement (v3.0.2): Extracting archive
  - Installing react/stream (v1.3.0): Extracting archive
  - Installing react/promise (v3.0.0): Extracting archive
  - Installing react/cache (v1.2.0): Extracting archive
  - Installing react/dns (v1.12.0): Extracting archive
  - Installing react/socket (v1.14.0): Extracting archive
  - Installing ralouphie/getallheaders (3.0.3): Extracting archive
  - Installing psr/http-message (2.0): Extracting archive
  - Installing psr/http-factory (1.0.2): Extracting archive
  - Installing guzzlehttp/psr7 (2.6.1): Extracting archive
  - Installing ratchet/rfc6455 (v0.3.1): Extracting archive
  - Installing cboden/ratchet (v0.4.4): Extracting archive
6 package suggestions were added by new dependencies, use `composer suggest` to see details.
Generating autoload files
12 packages you are using are looking for funding.
Use the `composer fund` command to find out more!
No security vulnerability advisories found
Using version ^0.4.4 for cboden/ratchet
WebsocketServer.php 代码
代码语言:javascript复制
<?php
/**
 * @desc Websocket 服务端代码
 * @author Tinywan(ShaoBo Wan)
 * @date 2023/11/30 9:20
 */

require __DIR__ . '/vendor/autoload.php';

use RatchetConnectionInterface;
use RatchetRFC6455MessagingMessageInterface;


class WebsocketServer implements RatchetWebSocketMessageComponentInterface
{
    /**
     * @var SplObjectStorage
     */
    protected $clients;

    /**
     * WebsocketServer constructor.
     */
    public function __construct()
    {
        $this->clients = new SplObjectStorage;
    }

    /**
     * @desc: onOpen 描述
     * @param ConnectionInterface $conn
     * @author Tinywan(ShaoBo Wan)
     */
    public function onOpen(ConnectionInterface $conn)
    {
        // Store the new connection to send messages to later
        $this->clients->attach($conn);

        echo "New connection! ({$conn->resourceId})n";
    }

    /**
     * @desc: onMessage 描述
     * @param ConnectionInterface $from
     * @param MessageInterface $msg
     * @author Tinywan(ShaoBo Wan)
     */
    public function onMessage(ConnectionInterface $from, MessageInterface $msg)
    {
        $numRecv = count($this->clients) - 1;
        echo sprintf('Connection %d sending message "%s" to %d other connection%s' . "n"
            , $from->resourceId, $msg, $numRecv, $numRecv == 1 ? '' : 's');

        foreach ($this->clients as $client) {
            $client->send($msg);
        }
    }

    /**
     * @desc: onClose 描述
     * @param ConnectionInterface $conn
     * @author Tinywan(ShaoBo Wan)
     */
    public function onClose(ConnectionInterface $conn)
    {
        // The connection is closed, remove it, as we can no longer send it messages
        $this->clients->detach($conn);

        echo "Connection {$conn->resourceId} has disconnectedn";
    }

    /**
     * @desc: onError 描述
     * @param ConnectionInterface $conn
     * @param Exception $e
     * @author Tinywan(ShaoBo Wan)
     */
    public function onError(ConnectionInterface $conn, Exception $e)
    {
        echo "An error has occurred: {$e->getMessage()}n";

        $conn->close();
    }
}

$server = RatchetServerIoServer::factory(
    new RatchetHttpHttpServer(
        new RatchetWebSocketWsServer(
            new WebsocketServer()
        )
    ),
    8585
);
$server->run();
运行 WebSocket 服务器
代码语言:javascript复制
php WebsocketServer.php

客户端

chat.html

代码语言:javascript复制
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Real-time WebSocket</title>
  <script src="http://cdn.bootcss.com/jquery/2.1.3/jquery.min.js" type="text/javascript"></script>
</head>
<body>
<h2>开源技术小栈 WebSocket 演示</h2>
<h3>发送一个websocket数据:</h3>
<input type="text" style="height: 50px; width: 100%;" name="data" id="data"><p></p>
<button id="submit" onclick="sub()" style="height: 50px; width: 100%;" >提交</button> <p></p>
<div id="output"></div>
<div id="message"></div>
<script type="text/javascript">
  var wsUri = "ws://127.0.0.1:8585";
  var output;
  var websocket;

  function init() {
    output = document.getElementById("output");
    testWebSocket();
       websocket.send("hello n");
  }

  function testWebSocket() {
    websocket = new WebSocket(wsUri);
    websocket.onopen = function (evt) {
      onOpen(evt)
    };
    websocket.onclose = function (evt) {
      onClose(evt)
    };
    websocket.onmessage = function (evt) {
      onMessage(evt)
    };
    websocket.onerror = function (evt) {
      onError(evt)
    };
  }
  function onOpen(evt) {
    writeToScreen("CONNECTED");
  }

  function onClose(evt) {
    writeToScreen("DISCONNECTED");
  }

  function onMessage(evt) {
    writeToScreen('<span style="color: blue;">RESPONSE: '   evt.data   '</span>');
    $("#message").append(evt.data)
  }

  function onError(evt) {
    writeToScreen('<span style="color: red;">ERROR:</span> '   evt.data);
  }

  function writeToScreen(message) {
    var pre = document.createElement("p");
    pre.style.wordWrap = "break-word";
    pre.innerHTML = message;
    output.appendChild(pre);
  }

  function sub()
  {
    websocket.send($("#data").val());
  }
  window.addEventListener("load", init, false);
</script>
</body>
</html>

演示效果

服务端
客户端

0 人点赞