ホームページ > バックエンド開発 > PHPチュートリアル > WebSocketチャットルームを実現するPHP+Socketシリーズ

WebSocketチャットルームを実現するPHP+Socketシリーズ

藏色散人
リリース: 2023-04-11 10:22:02
転載
4201 人が閲覧しました

この記事では、php ソケットに関する関連知識を紹介します。主に、php ネイティブ ソケットを使用して簡単な Web チャット ルームを実装する方法を紹介します。興味のある方は以下をご覧ください。皆様のお役に立てれば幸いです。

php ネイティブ ソケットは WebSocket チャット ルームを実装します

序文

この記事では、php ネイティブ ソケットを使用して単純な Web チャット ルームを実装します。最終コードは次のとおりです。記事の一番下。

なんなら、これがこのシリーズの最後の記事になるはずです。このシリーズの記事を書いたときは、とても簡単なことだと思っていましたが、ここ数回の記事を書いているうちに、ほとんど読んでしまいました。 Workerman のコードを使用します。ですから、あまり野心的になることはありませんが、自分で試してみてください。何かを本当に理解していることを証明するには、それを書き留めるのが最善です。

WebSocket の概要

webSocket プロトコル 2008 年に誕生し、2011 年に国際標準となったネットワーク通信プロトコルで、通信規格は RFC6455 で定められており、現在はすべてのブラウザでサポートされています。 webSocket は、HTML5 が提供し始めた 1 つの TCP 接続上で全二重通信を行うためのプロトコルで、サーバーからクライアントにメッセージをアクティブにプッシュしたり、クライアントもサーバーにメッセージをアクティブに送信したりできます。
WebSocket は通信プロトコルの仕様を規定しており、ハンドシェイク機構によりクライアント (ブラウザ) とサーバー (Web サーバー) 間で tcp のような接続を確立し、CS 通信を容易にします。

WebSocket が必要な理由

HTTP プロトコルは、ステートレス、コネクションレス、一方向のアプリケーション層プロトコルです。 request=>response モデルを採用しています。通信リクエストはクライアントによってのみ開始でき、サーバーはリクエストに応答します。この通信モデルには欠点があります。サーバーが積極的に送信することは不可能です。クライアントへのメッセージ。メッセージを開始します。従来の HTTP リクエストの同時実行機能は、複数の TCP 接続を開始してサーバーに同時にアクセスすることによって実現されます。Websocket を使用すると、ws 接続で複数のリクエストを同時に発行できます。つまり、A リクエストが送信された後、A レスポンスはまだ到着していません。引き続き B リクエストを発行できます。 TCP のスロー スタート機能と接続自体のハンドシェイク損失により、WebSocket プロトコルのこの機能により効率が大幅に向上しました。

WebSocketチャットルームを実現するPHP+Socketシリーズ

#特徴

  • TCP プロトコルに基づいて構築されているため、サーバーの実装は比較的簡単です

  • HTTP プロトコルとの互換性が高く、デフォルトのポートも 80 と 443 で、ハンドシェイクフェーズで HTTP プロトコルが使用されるため、ハンドシェイク中にブロックされにくく、さまざまな HTTP プロキシ サーバーを通過できます。

  • データ形式は比較的軽量で、パフォーマンスのオーバーヘッドが低く、通信が効率的です。

  • テキストまたはバイナリ データを送信できます。

  • 送信元の制限はなく、クライアントは任意のサーバーと通信できます。

  • プロトコル識別子は ws (暗号化されている場合は wss)、サービス アドレスは URL です。

PHP は websocket を実装します

クライアントとサーバーのハンドシェイク

websocket プロトコルでは、接続前にハンドシェイクが必要です[^2]。通常、次のハンドシェイク メソッドがあります。

  • フラッシュ ベースのハンドシェイク プロトコル (非推奨)

  • MD5 暗号化ベースのハンドシェイク プロトコル

    MD5 暗号化を使用した 2 つのキーを使用した以前のハンドシェイク方法

  • sha1 暗号化に基づくハンドシェイク プロトコル

    現在の主要なハンドシェイク プロトコル。この記事ではこのプロトコルに焦点を当てます。

      #クライアントによって報告された
    • Sec-WebSocket-key を取得します

    • スプライシング
    • key

      258EAFA5-E914-47DA-95CA-C5AB0DC85B11

    • 文字列に対して
    • SHA1

      計算を実行し、その結果を使用します。base64 で暗号化されています。 、そして最終的にクライアントに返されます

  • クライアントのリクエスト情報は次のとおりです:
GET /chat HTTP/1.1Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
ログイン後にコピー

クライアントは次のデータを返す必要があります。 :

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Sec-WebSocket-Version: 13Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
ログイン後にコピー

このプロトコルに従って PHP を通じて実装します:

<?php

$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, true);
socket_bind($socket, 0, 8888);
socket_listen($socket);

while (true) {
    $conn_sock = socket_accept($socket);
    $request = socket_read($conn_sock, 102400);

    $new_key = getShaKey($request);

    $response = "HTTP/1.1 101 Switching Protocols\r\n";
    $response .= "Upgrade: websocket\r\n";
    $response .= "Sec-WebSocket-Version: 13\r\n";
    $response .= "Connection: Upgrade\r\n";
    $response .= "Sec-WebSocket-Accept: {$new_key}\r\n\r\n";

    socket_write($conn_sock, $response);
}

function getShaKey($request)
{
    // 获取 Sec-WebSocket-key
    preg_match("/Sec-WebSocket-Key: (.*)\r\n/", $request, $match);

    // 拼接 key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
    $new_key = trim($match[1]) . &#39;258EAFA5-E914-47DA-95CA-C5AB0DC85B11&#39;;

    // 对字符串做 `SHA1` 计算,再把得到的结果通过 `base64` 加密
    return base64_encode(sha1($new_key, true));
}
ログイン後にコピー

関連する構文の説明は

以前の記事

を参照してください。この記事では詳しくは紹介しません。 フロントエンド テストを使用し、ブラウザ コンソール (コンソール) のいずれかを開き、次のコンテンツを入力します。返された WebSocket オブジェクトのreadyState は 1 で、ハンドシェイクが成功したことを意味します。これはフロントエンドです。この記事では詳しくは紹介しませんが、

新人チュートリアル

:

データの送信と受信を参照してください。

使用 websocket 协议传输协议需要遵循特定的格式规范,详情请参考 datatracker.ietf.org/doc/html/rfc6...

WebSocketチャットルームを実現するPHP+Socketシリーズ

为了方便,这里直接贴出加解密代码,以下代码借鉴与 workermansrc/Protocols/Websocket.php 文件:

// 解码客户端发送的消息
function decode($buffer)
{
    $len = \ord($buffer[1]) & 127;
    if ($len === 126) {
        $masks = \substr($buffer, 4, 4);
        $data = \substr($buffer, 8);
    } else {
        if ($len === 127) {
            $masks = \substr($buffer, 10, 4);
            $data = \substr($buffer, 14);
        } else {
            $masks = \substr($buffer, 2, 4);
            $data = \substr($buffer, 6);
        }
    }
    $dataLength = \strlen($data);
    $masks = \str_repeat($masks, \floor($dataLength / 4)) . \substr($masks, 0, $dataLength % 4);
    return $data ^ $masks;
}

// 编码发送给客户端的消息
function encode($buffer)
{
    if (!is_scalar($buffer)) {
        throw new \Exception("You can&#39;t send(" . \gettype($buffer) . ") to client, you need to convert it to a string. ");
    }
    $len = \strlen($buffer);

    $first_byte = "\x81";

    if ($len <= 125) {
        $encode_buffer = $first_byte . \chr($len) . $buffer;
    } else {
        if ($len <= 65535) {
            $encode_buffer = $first_byte . \chr(126) . \pack("n", $len) . $buffer;
        } else {
            $encode_buffer = $first_byte . \chr(127) . \pack("xxxxN", $len) . $buffer;
        }
    }

    return $encode_buffer;
}
ログイン後にコピー

我们修改刚才 客户端与服务端握手 阶段的代码,修改后全代码全文如下,该段代码实现了将客户端发送的消息转为大写后返回给客户端(当然只是为了演示):

<?php

$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, true);
socket_bind($socket, 0, 8888);
socket_listen($socket);

while (true) {
    $conn_sock = socket_accept($socket);
    $request = socket_read($conn_sock, 102400);

    $new_key = getShaKey($request);

    $response = "HTTP/1.1 101 Switching Protocols\r\n";
    $response .= "Upgrade: websocket\r\n";
    $response .= "Sec-WebSocket-Version: 13\r\n";
    $response .= "Connection: Upgrade\r\n";
    $response .= "Sec-WebSocket-Accept: {$new_key}\r\n\r\n";

    // 发送握手数据
    socket_write($conn_sock, $response);

    // 新增内容,获取客户端发送的消息并转为大写还给客户端
    $msg = socket_read($conn_sock, 102400);
    socket_write($conn_sock, encode(strtoupper(decode($msg))));
}

function getShaKey($request)
{
    // 获取 Sec-WebSocket-key
    preg_match("/Sec-WebSocket-Key: (.*)\r\n/", $request, $match);

    // 拼接 key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
    $new_key = trim($match[1]) . &#39;258EAFA5-E914-47DA-95CA-C5AB0DC85B11&#39;;

    // 对字符串做 `SHA1` 计算,再把得到的结果通过 `base64` 加密
    return base64_encode(sha1($new_key, true));
}

function decode($buffer)
{
    $len = \ord($buffer[1]) & 127;
    if ($len === 126) {
        $masks = \substr($buffer, 4, 4);
        $data = \substr($buffer, 8);
    } else {
        if ($len === 127) {
            $masks = \substr($buffer, 10, 4);
            $data = \substr($buffer, 14);
        } else {
            $masks = \substr($buffer, 2, 4);
            $data = \substr($buffer, 6);
        }
    }
    $dataLength = \strlen($data);
    $masks = \str_repeat($masks, \floor($dataLength / 4)) . \substr($masks, 0, $dataLength % 4);
    return $data ^ $masks;
}

function encode($buffer)
{
    if (!is_scalar($buffer)) {
        throw new \Exception("You can&#39;t send(" . \gettype($buffer) . ") to client, you need to convert it to a string. ");
    }
    $len = \strlen($buffer);

    $first_byte = "\x81";

    if ($len <= 125) {
        $encode_buffer = $first_byte . \chr($len) . $buffer;
    } else {
        if ($len <= 65535) {
            $encode_buffer = $first_byte . \chr(126) . \pack("n", $len) . $buffer;
        } else {
            $encode_buffer = $first_byte . \chr(127) . \pack("xxxxN", $len) . $buffer;
        }
    }

    return $encode_buffer;
}
ログイン後にコピー

使用 在线测试工具 进行测试,可以看到消息已经可以正常发送接收,接下来的文章将继续优化代码,实现简易聊天室,敬请关注:

WebSocketチャットルームを実現するPHP+Socketシリーズ

实现web聊天室

我们紧接着上文的代码继续优化,以实现简易的web聊天室

多路复用

其实就是加一下 socket_select() 函数 WebSocketチャットルームを実現するPHP+Socketシリーズ ,本文就不写原理与语法了,详情可参考 之前的文章,以下代码修改自前文 WebSocketチャットルームを実現するPHP+Socketシリーズ

...

socket_listen($socket);

+$sockets[] = $socket;
+$user = [];
while (true) {
+   $tmp_sockets = $sockets;
+   socket_select($tmp_sockets, $write, $except, null);

+   foreach ($tmp_sockets as $sock) {
+       if ($sock == $socket) {
+           $sockets[] = socket_accept($socket);
+           $user[] = [&#39;socket&#39; => $socket, &#39;handshake&#39; => false];
+       } else {
+           $curr_user = $user[array_search($sock, $user)];
+           if ($curr_user[&#39;handshake&#39;]) { // 已握手
+               $msg = socket_read($sock, 102400);
+               echo &#39;客户端发来消息&#39; . decode($msg);
+               socket_write($sock, encode(&#39;这是来自服务端的消息&#39;));
+           } else {
+               // 握手
+           }
+       }
+   }

-   $conn_sock = socket_accept($socket);
-   $request = socket_read($conn_sock, 102400);

...
ログイン後にコピー

实现聊天室

最终成果演示

WebSocketチャットルームを実現するPHP+Socketシリーズ

我们将上述代码改造成类,并在类变量储存用户信息,添加消息处理等逻辑,最后贴出代码,建议保存下来自己尝试一下,也许会有全新的认知,后端代码:

<?php

new WebSocket();

class Websocket
{
    /**
     * @var resource
     */
    protected $socket;

    /**
     * @var array 用户列表
     */
    protected $user = [];

    /**
     * @var array 存放所有 socket 资源
     */
    protected $socket_list = [];

    public function __construct()
    {
        $this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
        socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, true);
        socket_bind($this->socket, 0, 8888);
        socket_listen($this->socket);

        // 将 socket 资源放入 socket_list
        $this->socket_list[] = $this->socket;

        while (true) {
            $tmp_sockets = $this->socket_list;
            socket_select($tmp_sockets, $write, $except, null);

            foreach ($tmp_sockets as $sock) {
                if ($sock == $this->socket) {
                    $conn_sock = socket_accept($sock);
                    $this->socket_list[] = $conn_sock;
                    $this->user[] = [&#39;socket&#39; => $conn_sock, &#39;handshake&#39; => false, &#39;name&#39; => &#39;无名氏&#39;];
                } else {
                    $request = socket_read($sock, 102400);
                    $k = $this->getUserIndex($sock);

                    if (!$request) {
                        continue;
                    }

                    // 用户端断开连接
                    if ((\ord($request[0]) & 0xf) == 0x8) {
                        $this->close($k);
                        continue;
                    }

                    if (!$this->user[$k][&#39;handshake&#39;]) {
                        // 握手
                        $this->handshake($k, $request);
                    } else {
                        // 已握手
                        $this->send($k, $request);
                    }
                }
            }
        }
    }

    /**
     * 关闭连接
     *
     * @param $k
     */
    protected function close($k)
    {
        $u_name = $this->user[$k][&#39;name&#39;] ?? &#39;无名氏&#39;;
        socket_close($this->user[$k][&#39;socket&#39;]);
        $socket_key = array_search($this->user[$k][&#39;socket&#39;], $this->socket_list);
        unset($this->socket_list[$socket_key]);
        unset($this->user[$k]);

        $user = [];
        foreach ($this->user as $v) {
            $user[] = $v[&#39;name&#39;];
        }
        $res = [
            &#39;type&#39; => &#39;close&#39;,
            &#39;users&#39; => $user,
            &#39;msg&#39; => $u_name . &#39;已退出&#39;,
            &#39;time&#39; => date(&#39;Y-m-d H:i:s&#39;)
        ];
        $this->sendAllUser($res);
    }

    /**
     * 获取用户索引
     *
     * @param $socket
     * @return int|string
     */
    protected function getUserIndex($socket)
    {
        foreach ($this->user as $k => $v) {
            if ($v[&#39;socket&#39;] == $socket) {
                return $k;
            }
        }
    }

    /**
     * 握手
     * @param $k
     * @param $request
     */
    protected function handshake($k, $request)
    {
        preg_match("/Sec-WebSocket-Key: (.*)\r\n/", $request, $match);
        $key = base64_encode(sha1($match[1] . &#39;258EAFA5-E914-47DA-95CA-C5AB0DC85B11&#39;, true));

        $response = "HTTP/1.1 101 Switching Protocols\r\n";
        $response .= "Upgrade: websocket\r\n";
        $response .= "Connection: Upgrade\r\n";
        $response .= "Sec-WebSocket-Accept: {$key}\r\n\r\n";
        socket_write($this->user[$k][&#39;socket&#39;], $response);
        $this->user[$k][&#39;handshake&#39;] = true;
    }

    /**
     * 接收并处理消息
     *
     * @param $k
     * @param $msg
     */
    public function send($k, $msg)
    {
        $msg = $this->decode($msg);
        $msg = json_decode($msg, true);

        if (!isset($msg[&#39;type&#39;])) {
            return;
        }

        switch ($msg[&#39;type&#39;]) {
            case &#39;login&#39;: // 登录
                $this->user[$k][&#39;name&#39;] = $msg[&#39;name&#39;] ?? &#39;无名氏&#39;;
                $users = [];
                foreach ($this->user as $v) {
                    $users[] = $v[&#39;name&#39;];
                }
                $res = [
                    &#39;type&#39; => &#39;login&#39;,
                    &#39;name&#39; => $this->user[$k][&#39;name&#39;],
                    &#39;msg&#39; => $this->user[$k][&#39;name&#39;] . &#39;: login success&#39;,
                    &#39;users&#39; => $users,
                ];
                $this->sendAllUser($res);
                break;
            case &#39;message&#39;: // 接收并发送消息
                $res = [
                    &#39;type&#39; => &#39;message&#39;,
                    &#39;name&#39; => $this->user[$k][&#39;name&#39;] ?? &#39;无名氏&#39;,
                    &#39;msg&#39; => $msg[&#39;msg&#39;],
                    &#39;time&#39; => date(&#39;H:i:s&#39;),
                ];
                $this->sendAllUser($res);
                break;
        }
    }

    /**
     * 发送给所有人
     *
     */
    protected function sendAllUser($msg)
    {
        if (is_array($msg)) {
            $msg = json_encode($msg);
        }

        $msg = $this->encode($msg);

        foreach ($this->user as $k => $v) {
            socket_write($v[&#39;socket&#39;], $msg, strlen($msg));
        }
    }

    /**
     * 解码
     *
     * @param $buffer
     * @return string
     */
    protected function decode($buffer)
    {
        $len = \ord($buffer[1]) & 127;
        if ($len === 126) {
            $masks = \substr($buffer, 4, 4);
            $data = \substr($buffer, 8);
        } else {
            if ($len === 127) {
                $masks = \substr($buffer, 10, 4);
                $data = \substr($buffer, 14);
            } else {
                $masks = \substr($buffer, 2, 4);
                $data = \substr($buffer, 6);
            }
        }
        $dataLength = \strlen($data);
        $masks = \str_repeat($masks, \floor($dataLength / 4)) . \substr($masks, 0, $dataLength % 4);
        return $data ^ $masks;
    }

    protected function encode($buffer)
    {
        if (!is_scalar($buffer)) {
            throw new \Exception("You can&#39;t send(" . \gettype($buffer) . ") to client, you need to convert it to a string. ");
        }
        $len = \strlen($buffer);

        $first_byte = "\x81";

        if ($len <= 125) {
            $encode_buffer = $first_byte . \chr($len) . $buffer;
        } else {
            if ($len <= 65535) {
                $encode_buffer = $first_byte . \chr(126) . \pack("n", $len) . $buffer;
            } else {
                $encode_buffer = $first_byte . \chr(127) . \pack("xxxxN", $len) . $buffer;
            }
        }

        return $encode_buffer;
    }
}
ログイン後にコピー

前端代码如下(前端内容不在本文讨论范围之内,具体可参考 菜鸟教程):

<!doctype html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<style>
    * {
        margin: 0;
        padding: 0;
    }
    h3 {
        display: flex;
        justify-content: center;
        margin: 30px auto;
    }
    .but-box {
        border-radius: 5px;
        display: flex;
        justify-content: center;
        align-items: center;
        margin-top: 10px;
    }
    #box {
        display: flex;
        margin: 5px auto;
        border-radius: 5px;
        border: 1px #ccc solid;
        height: 400px;
        width: 700px;
        overflow-y: auto;
        overflow-x: hidden;
        position: relative;
    }
    #msg-box {
        width: 480px;
        margin-right: 111px;
        height: 100%;
        overflow-y: auto;
        overflow-x: hidden;
    }
    #user-box {
        width: 110px;
        overflow-y: auto;
        overflow-x: hidden;
        float: left;
        border-left: 1px #ccc solid;
        height: 100%;
        background-color: #F1F1F1;
    }
    button {
        float: right;
        width: 80px;
        height: 35px;
        font-size: 18px;
    }
    input {
        width: 100%;
        height: 30px;
        padding: 2px;
        line-height: 20px;
        outline: none;
        border: solid 1px #CCC;
    }
    .but-box p {
        margin-right: 160px;
    }
</style>
<body>

<h3>这是一个php socket实现的web聊天室</h3>

<div id="box">
    <div id="msg-box"></div>
    <div id="user-box"></div>
</div>

<div>

    <p><textarea cols="60" rows="3" style="resize:none;pedding: 10px"    id="content"> </textarea></p>
    <button id="send">发送</button>
</div>
<script src="https://cdn.bootcss.com/jquery/2.2.1/jquery.min.js"></script>
<script>
    let ws = new WebSocket(&#39;ws://124.222.85.67:8888&#39;);

    ws.onopen = function (event) {
        console.log(&#39;连接成功&#39;);

        var name = prompt(&#39;请输入用户名:&#39;);

        ws.send(JSON.stringify({
            type: &#39;login&#39;,
            name: name
        }));

        if (!name) {
            alert(&#39;好你个坏蛋,竟然没有输入用户名&#39;);
        }
    };
    ws.onmessage = function (event) {
        let data = JSON.parse(event.data);
        console.log(data);

        switch (data.type) {
            case &#39;close&#39;:
            case &#39;login&#39;:
                $("#user-box").html(&#39;&#39;);
                data.users.forEach(function (item) {
                    $("#user-box").append(`<p style="color: grey;">${item}</p>`);
                });
                if (data.msg) {
                    $("#msg-box").append(`<p style="color: grey;">${data.msg}</p>`);
                }
                break;
            case &#39;message&#39;:
                $("#msg-box").append(`<p><span style="color: #0A89FF">${data.time}</span><span style="color: red">${data.name}</span>${data.msg}</p>`);
                break;
        }
    };

    ws.onclose = function (event) {
        alert(&#39;连接关闭&#39;);
    };

    document.onkeydown = function (event) {
        if (event.keyCode == 13) {
            send();
        }
    }

    $("#send").click(function () {
        send();
    });

    function send() {
        let content = $("#content").val();
        $("#content").val(&#39;&#39;);
        if (!content) {
            return;
        }
        ws.send(JSON.stringify({
            type: &#39;message&#39;,
            msg: content
        }));
    }
</script>
</body>
</html>
ログイン後にコピー

[^1]:是通讯传输的一个术语。 通信允许数据在两个方向上同时传输,它在能力上相当于两个单工通信方式的结合
[^2]:  为了建立 websocket 连接,需要通过浏览器发出请求,之后服务器进行回应,这个过程通常称为“握手”(Handshaking)

推荐学习:《PHP视频教程》                                            

以上がWebSocketチャットルームを実現するPHP+Socketシリーズの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

関連ラベル:
ソース:learnku.com
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
最新の問題
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート