Heim >php教程 >php手册 >Web-Echtzeit-Chat mit PHP zur Implementierung von WebSocket

Web-Echtzeit-Chat mit PHP zur Implementierung von WebSocket

WBOY
WBOYOriginal
2016-11-30 23:59:331599Durchsuche

Vorwort

Als neue Funktion in HTML5 hat Websocket schon immer viel Aufmerksamkeit auf sich gezogen, weil es das herkömmliche HTTP-„Anfrage-Antwort“-Konzept durchbricht und es dem Server ermöglicht, Nachrichten aktiv an den Client zu senden um es zu verwenden. PHP und JS verwenden Websocket, um einen Web-Echtzeit-Chatroom zu implementieren

Ich habe zuvor einen Artikel darüber geschrieben, wie man Ajax-Long-Polling verwendet, um Echtzeit-Webchat zu implementieren, siehe den Link: Echtzeit-Webchat mit js und jQuery, um Ajax-Long-Polling zu implementieren, aber Polling und Server-Pending sind alle unnötig Konsum. WebSocket ist der neue Trend.

Vor kurzem habe ich einige Zeit mit Mühe „herausgequetscht“, den Websocket-Server „Request-Return as is“ perfektioniert, den ich vor langer Zeit erstellt habe, und die Client-Funktion mit js verbessert. Ich werde den Prozess und die Ideen teilen Lassen Sie uns einen Blick auf das Wissen rund um WebSocket werfen. Natürlich werde ich jetzt einige theoretische Dinge überspringen und Referenzartikel zur Auswahl und zum Lesen bereitstellen.

Bevor ich mit dem Text beginne, möchte ich eine Darstellung des Chatrooms posten (bitte achten Sie nicht auf die CSS-Drecksseite):

Dann natürlich der Quellcode:

Ich bin der Quellcode-Link - Github - Pillow Book


Websocket

Einführung

WebSocket ist keine Technologie, sondern ein brandneues Protokoll. Es verwendet TCPs Socket (Socket) und definiert eine neue wichtige Funktion für Netzwerkanwendungen: Vollduplexübertragung und bidirektionale Kommunikation zwischen Client und Server. Es ist ein neuer Trend für Server, Client-Nachrichten nach Java-Applets, XMLHttpRequest, Adobe Flash, ActiveXObject und verschiedenen Comet-Technologien zu pushen.

Beziehung mit http

In Bezug auf die Netzwerkschicht sind Websocket- und http-Protokolle beide Protokolle der Anwendungsschicht. Sie basieren beide auf der TCP-Transportschicht. Websocket leiht sich jedoch das 101-Switch-Protokoll von http aus, um beim Verbindungsaufbau eine Protokollkonvertierung (Upgrade) zu erreichen . Ja, wechseln Sie vom HTTP-Protokoll zum WebSocket-Kommunikationsprotokoll

Nachdem der Handshake erfolgreich war, verwendet Websocket die in seinem eigenen Protokoll angegebene Methode zur Kommunikation und hat nichts mit http zu tun.

Handschlag

Hier ist ein typischer Handshake-HTTP-Header, der von meinem eigenen Browser gesendet wird:

Nach dem Empfang der Handshake-Anfrage extrahiert der Server das Feld „Sec-WebSocket-Key“ im Anforderungsheader, stellt eine feste Zeichenfolge „258EAFA5-E914-47DA-95CA-C5AB0DC85B11“ wieder her und führt dann die SHA1-Verschlüsselung durch Konvertieren Sie in die Base64-Codierung und verwenden Sie das Feld „Sec-WebSocket-Accept“ als Schlüssel für die Rückkehr zum Client. Nachdem der Client mit diesem Schlüssel übereinstimmt, wird die Verbindung hergestellt und der Handshake abgeschlossen

Datenübertragung

Websocket verfügt über ein eigenes spezifiziertes Datenübertragungsformat namens Frame. Die folgende Abbildung zeigt die Struktur eines Datenrahmens, wobei die Einheit Bit ist:

<code class="language-none">  0                   1                   2                   3
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-------+-+-------------+-------------------------------+
 |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
 |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
 |N|V|V|V|       |S|             |   (if payload len==126/127)   |
 | |1|2|3|       |K|             |                               |
 +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
 |     Extended payload length continued, if payload len == 127  |
 + - - - - - - - - - - - - - - - +-------------------------------+
 |                               |Masking-key, if MASK set to 1  |
 +-------------------------------+-------------------------------+
 | Masking-key (continued)       |          Payload Data         |
 +-------------------------------- - - - - - - - - - - - - - - - +
 :                     Payload Data continued ...                :
 + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
 |                     Payload Data continued ...                |
 +---------------------------------------------------------------+</code>
Was ist die spezifische Bedeutung der einzelnen Felder? Wenn Sie interessiert sind, können Sie diesen Artikel lesen: Das WebSocket-Protokoll 5. Der Datenrahmen fühlt sich bei binären Operationen nicht sehr flexibel an, sodass es keine Herausforderung darstellt, einen Algorithmus zu schreiben Um die Daten zu analysieren, werden für die Datenrahmenanalyse und -kapselung Online-Algorithmen verwendet.

Bei meiner Arbeit beim Schreiben von Zahlungsgateways werden jedoch häufig hexadezimale Datenoperationen verwendet. Dies muss sorgfältig untersucht und zusammengefasst werden.

PHP implementiert einen WebSocket-Server

Wenn PHP Websocket implementiert, verwendet es hauptsächlich die Socket-Funktionsbibliothek von PHP:

Die Socket-Funktionsbibliothek von PHP ist der Socket-Funktion der C-Sprache sehr ähnlich. Ich habe APUE schon einmal gelesen, daher denke ich, dass sie ziemlich einfach zu verstehen ist. Nachdem ich die Socket-Funktion im PHP-Handbuch gelesen habe, denke ich, dass jeder auch ein gewisses Verständnis für die Socket-Programmierung in PHP haben kann.

Die verwendeten Funktionen werden im folgenden Code kurz kommentiert.

Dateideskriptor

Sie werden vielleicht ein wenig überrascht sein, wenn plötzlich „Dateideskriptor“ erwähnt wird.

Aber als Server ist es notwendig, den angeschlossenen Socket zu speichern und zu identifizieren. Jeder Socket stellt einen Benutzer dar. Die Zuordnung und Abfrage der Korrespondenz zwischen Benutzerinformationen und Socket ist hier ein kleiner Trick über Dateideskriptoren.

Wir wissen, dass Linux „alles eine Datei“ ist und die Socket-Implementierung in der C-Sprache ein „Dateideskriptor“ ist. Dieser Dateideskriptor ist im Allgemeinen ein int-Wert, der in der Reihenfolge, in der die Datei geöffnet wird, zunimmt von 0. (Natürlich hat das System Einschränkungen). Jeder Socket entspricht einer Datei, und Lese- und Schreibsockets arbeiten mit der entsprechenden Datei, sodass die Lese- und Schreibfunktionen auch wie ein Dateisystem angewendet werden können.

Tipps: Unter Linux entspricht die Standardeingabe dem Dateideskriptor 1; die Standardausgabe entspricht dem Dateideskriptor 2, sodass wir 0 1 2 verwenden können, um Eingabe und Ausgabe umzuleiten.

Dann erben PHP-Sockets, die C-Sockets ähneln, dies natürlich, und die von ihnen erstellten Sockets sind ebenfalls vom Typ Ressourcentyp wie int mit einem Wert von 4 5. Wir können die Funktion (int) oder intval() verwenden, um den Socket in eine eindeutige ID umzuwandeln, sodass ein „Klassenindex-Array“ zum Speichern von Socket-Ressourcen und entsprechenden Benutzerinformationen verwendet werden kann;

Die Ergebnisse sind ähnlich:

Server-Socket erstellen
<code class="language-none">$connected_sockets = array(
    (int)$socket => array(
        'resource' => $socket,
        'name' => $name,
        'ip' => $ip,
        'port' => $port,
        ...
    )
)</code>
Das Folgende ist ein Code zum Erstellen eines Server-Sockets:

这样,我们就得到一个服务器 socket,当有客户端连接到此 socket 上时,它将改变状态为可读,那就看接下来服务器的处理逻辑了。

服务器逻辑

这里着重讲一下 socket_select($read, $write, $except, $tv_sec [, $tv_usec]):

select 函数使用传统的 select 模型,可读、写、异常的 socket 会被分别放入 $socket, $write, $except 数组中,然后返回 状态改变的 socket 的数目,如果发生了错误,函数将会返回 false.

需要注意的是最后两个时间参数,它们只有单位不同,可以搭配使用,用来表示 socket_select 阻塞的时长,为0时此函数立即返回,可以用于轮询机制。 为 NULL 时,函数会一直阻塞下去, 这里我们置 $tv_sec 参数为null,让它一直阻塞,直到有可操作的 socket 返回。

下面是服务器的主要逻辑:

<code class="language-none">$write = $except = NULL;
$sockets = array_column($this->sockets, 'resource'); // 获取到全部的 socket 资源
$read_num = socket_select($sockets, $write, $except, NULL);

foreach ($sockets as $socket) {
        // 如果可读的是服务器 socket, 则处理连接逻辑;            
        if ($socket == $this->master) {
            socket_accept($this->master);
            // socket_accept() 接受 请求 “正在 listen 的 socket(像我们的服务器 socket )” 的连接, 并一个客户端 socket, 错误时返回 false;
             self::connect($client);
             continue;
            }
        // 如果可读的是其他已连接 socket ,则读取其数据,并处理应答逻辑
        } else {
            // 函数 socket_recv() 从 socket 中接受长度为 len 字节的数据,并保存在 $buffer 中。
            $bytes = @socket_recv($socket, $buffer, 2048, 0);

            if ($bytes < 9) {
                // 当客户端忽然中断时,服务器会接收到一个 8 字节长度的消息(由于其数据帧机制,8字节的消息我们认为它是客户端异常中断消息),服务器处理下线逻辑,并将其封装为消息广播出去
                $recv_msg = $this->disconnect($socket);
            } else {
                // 如果此客户端还未握手,执行握手逻辑
                if (!$this->sockets[(int)$socket]['handshake']) {
                    self::handShake($socket, $buffer);
                    continue;
                } else {
                    $recv_msg = self::parse($buffer);
                }
            }

            // 广播消息
            $this->broadcast($msg);
        }
    }
}</code>

这里只是服务器处理消息的基础代码,日志记录和异常处理都略过了,而且还有些数据帧解析和封装的方法,各位也不一定看爱,有兴趣的可以去 github 上支持一下我的源码~~

此外,为了便于服务器与客户端的交互,我自己定义了 json 类型的消息格式,形似:

<code class="language-none">$msg = [
    'type' => $msg_type, // 有普通消息,上下线消息,服务器消息
    'from' => $msg_resource, // 消息来源
    'content' => $msg_content, // 消息内容
    'user_list' => $uname_list, // 便于同步当前在线人数与姓名
    ];</code>

客户端

创建客户端

前端我们使用 js 调用 Websocket 方法很简单就能创建一个 websocket 连接,服务器会为帮我们完成连接、握手的操作,js 使用事件机制来处理浏览器与服务器的交互:

<code class="language-none">// 创建一个 websocket 连接
var ws = new WebSocket("ws://127.0.0.1:8080");

// websocket 创建成功事件
ws.onopen = function () {
};

// websocket 接收到消息事件
ws.onmessage = function (e) {
    var msg = JSON.parse(e.data);
}

// websocket 错误事件
ws.onerror = function () {
};</code>

发送消息也很简单,直接调用 ws.send(msg) 方法就行了。

页面功能

页面部分主要是让用户使用起来方便,这里给消息框 textarea 添加了一个键盘监控事件,当用户按下回车键时直接发送消息;

<code class="language-none">function confirm(event) {
    var key_num = event.keyCode;
    if (13 == key_num) {
        send();
    } else {
        return false;
    }
}</code>

还有用户打开客户端时生成一个默认唯一用户名;

然后是一些对数据的解析构造,对客户端页面的更新,这里就不再啰嗦了,感兴趣的可以看源码。

用户名异步处理

这里不得不提一下用户登陆时确定用户名时的一个小问题,我原来是想在客户端创建一个连接后直接发送用户名到服务器,可是控制台里报出了 “websocket 仍在连接中或已关闭” 的错误信息。

Uncaught DOMException: Failed to execute 'send' on 'WebSocket': Still in CONNECTING state.

考虑到连接可能还没处理好,我就实现了 sleep 方法等了一秒再发送用户名,可是错误仍然存在。

后来忽然想到 js 的单线程阻塞机制,才明白使用 sleep 一直阻塞也是没有用的,利用好 js 的事件机制才是正道:于是在服务器端添加逻辑,在握手成功后,向客户端发送握手已成功的消息;客户端先将用户名存入一个全局变量,接收到服务器的握手成功的提醒消息后再发送用户名,于是成功在第一时间更新用户名。


小结

聊天室扩展方向

简易聊天室已经完成,当然还要给它带有希望的美好未来,希望有人去实现:

  • 页面美化(信息添加颜色等)
  • 服务器识别 '@' 字符而只向某一个 socket 写数据实现聊天室的私聊;
  • 多进程(使用 redis 等缓存数据库来实现资源的共享),可参考我以前的一篇文章: 初探PHP多进程
  • 消息记录数据库持久化(log 日志还是不方便分析)
  • ...

总结

多读些经典书籍还是很有用的,有些东西真的是触类旁通,APUE/UNP 还是要再多翻几遍。此外互联网技术日新月异,挑一些自己喜欢的学习一下,跟大家分享一下也是挺舒服的(虽然程序和博客加一块用了至少10个小时...)。

参考:

websocket协议翻译

刨根问底 HTTP 和 WebSocket 协议(下)

学习WebSocket协议—从顶层到底层的实现原理(修订版)

嗯,持续更新。喜欢的可以点个推荐或关注,有错漏之处,请指正,谢谢。

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn