【问题标题】:Sending a Close Control Frame to HTML5 Client from PHP-CLI Socket Server从 PHP-CLI 套接字服务器向 HTML5 客户端发送关闭控制帧
【发布时间】:2020-12-02 12:52:49
【问题描述】:

我正在开发一个免费的 PHP 套接字测试平台。我正在尝试发送和接收关闭控制帧。我想在套接字异常关闭时触发自动重新连接,这不是1000

我的前端代码如下:

$socket.close(1000, "Disconnect");

我编写的后端响应如下所示:

socket_close($client);

在客户端上,侦听套接字关闭事件 ($event.code),我得到1006 的状态。应该是1000。代码1006 表示 socket_close($client) 没有发出关闭帧。

客户的文档来源:

对于后端,我使用的是 PHP 套接字 API: https://www.php.net/manual/en/book.sockets.php

这是我的代码的更简单版本。有 HTML 结构、JS 客户端程序和 PHP CLI 后端:

1- HTML

<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name='viewport' content='width=device-width, initial-scale=1'>
        <meta name='description' content='How to build a heavy-duty web socket server using PHP socket API'>
        <meta name='keywords' content='PHP Web Socket Server, Web Socket API, Object Oriented PHP'>
        <title>Building a Heavy-Duty Web Socket Server in PHP</title>
        <script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
        <script async defer src="client.js"></script>
        <style>
            html,
            body{
                font-family: 'Courier New', Courier, monospace;
                font-size: 14px;
            }
            #wrapper,
            #socketLoader{
                margin: 0 auto;
            }
            #wrapper{
                max-width: 50em;
            }
            #socketLoader{
                width: 80%;
                margin-top: 1em;
            }
            h1,
            #commandCenter{ 
                text-align: center;
            }
            .open{
                color: hsl(218, 90%, 61%);
            }
            .message{
                color: hsl(141, 65%, 41%);
            }
            .close{
                color: hsl(35, 96%, 35%);
            }
            .error{
                color: hsl(0, 91%, 55%);
            }
        </style>
    </head>
    <body>
        <div id="wrapper">
            <h1>HTML5 Client and PHP Socket Server</h1>
            <div id="webSocketContainer">
                <div id="commandCenter">
                    <button id="socketMsg">Send Socket Message</button>
                    <button id="closeBtn">Disconnect</button>
                    <button id="startBtn">Connect</button>
                </div>
                <ol id="socketLoader"></ol>
            </div>
        </div>
        <div id="loader" class="loader"></div>
    </body>
</html>

2- JS 客户端

class SocketClient {

    // Properties

    newResults = [];

    // Methods

    constructor() {
        this.sConnect();
    }

    sConnect () { // Connect using sockets $protocol is the equivalent of $type in connect()


        // Variables
    
        const $rTl = 300; // Response time limit
        const $reconnect = 10; // The number of reconnections
        let $i = 0;
        let $state = 'closing';

        // Methods 

        function connectToServer(){
            const $address = "ws://localhost:9000";
            let $socket = new WebSocket($address);
            $socket.binaryType = "blob";
            return $socket;
        }

        function updateStatus ($status) { // Update the status of a socket connection
            switch($status){
                case 0:
                    $state = 'connecting';
                    break;
                case 1:
                    $state = 'open';
                    break;
                case 2:
                    $state = 'closing';
                    break;
                case 3:
                    $state = 'closed';
                    break;
            }
        }

        function render ($data, $type) {
            const $commandPattern = /^\~\w+\~$/g;
            if($data.match($commandPattern) == null){
                $type = (typeof $type !== 'undefined') ? "class = '"+ $type + "'" : '';
                $("#socketLoader").append("<li " + $type + ">" + $data + "</li>");
            }
            else{
                $("#socketLoader").append("<li " + $type + "><em>Performing application command</em></li>");
            }
        }

        function sendData ($data) { // Send data in pieces if necessary
            $socket.send($data);
            let $count = 0;
            const $checkMsg = setInterval(() => { // Keep checking if the message was sent
                if ($socket.bufferedAmount == 0 && $state !== 'open'){ // The data was sent
                    render("Data sent &#10003;", "open");
                    clearInterval($checkMsg);
                }
                else if($count >= $reconnect){ // Stop trying to reconnect after several attempts
                    clearInterval($checkMsg);
                }
                else { // Unsubmitted data queue in the buffer
                    if($state !== 'open'){ // Check if the connection was not closed (is open)
                        $socket.send($data);
                        render("<em>resending</em> The client did not reflect the data being sent", "error");
                    }
                }
                $count++;
            }, $rTl);
        }

        function closeControlFrame ($message) { // Determine if the server requested to expire the handshake
            if($message === '~closehandshake~'){
                $socket.close(1000, "Work complete");
            }
        }

        function socketOpen (){
            updateStatus($socket.readyState);
            render("Connected", "open");
            render("Sending data on load", "open");
            sendData(">>> This message was sent by the client on load via web socket");
        }

        function socketMessage ($event) {
            if($state !== 'close'){
                updateStatus($socket.readyState);
                render("Incoming", "message");
                render($event.data, "server message");
            }
            else{
                render('The server is offline x_x', 'error');
            }
            closeControlFrame($event.data);
        }

        function socketClose ($event) {
            let $errorMsg = "";
            const $closeMsg = "The socket connection is closed. ";
            render($closeMsg, "close");
            updateStatus($socket.readyState);
            switch ($event.code) {
                case 1000:
                    $errorMsg += "Internal application request";
                    break;
                case 1001:
                    $errorMsg += "User left or server is down";
                    break;
                case 1002:
                    $errorMsg += "Protocol error";
                    break;
                case 1003:
                    $errorMsg += "Wrong data type";
                    break;
                case 1005:
                    $errorMsg += "Unknown status code";
                    break;
                case 1006:
                    $errorMsg += "Without sending or receiving a close control frame";
                    break;
                case 1007:
                    $errorMsg += "Inconsistent data type";
                    break;
                case 1008:
                    $errorMsg += "Policy breach";
                    break;
                case 1009:
                    $errorMsg += "Data length exceeds threshold";
                    break;
                case 1010:
                    $errorMsg += "Handshake error";
                    break;
                case 1011:
                    $errorMsg += "Internal server error";
                    break;
                case 1012:
                    $errorMsg += "Server is restarting";
                    break;
                case 1013:
                    $errorMsg += "Server overload";
                    break;
                case 1014:
                    $errorMsg += "Bad gateway";
                    break;
                case 1015:
                    $errorMsg += "TLS handshake error";
                    break;
            }
            if($event.wasClean == false){
                render("Error " +  $event.code + ". Reason: <em>" + $errorMsg + "</em>", "error");
                console.warn(">>> Full Reason: ", $event); 
            }                   
        }   

        function socketError ($event) {
            /**
             * Handle errors in almost the same way as in on socket close
             */
            updateStatus($socket.readyState);
            const $errMsg = (typeof $event.message != 'undefined') ? ': [' + $event.message + ']' : '.';
            render("There was an error" + $errMsg, "error");
            console.error($event);
        }

        function socketMsg () {
            if($state != 'close'){
                let $message = ">>> Message No. " + ($i + 1);
                render("The user requested data");
                render("(client) Sending : " + $message);
                sendData($message);
                $i++;
            }
            else{
                render("Your socket server instance is offline.", "close");
            }
        }  

        function closeBtn (){
            sendData("~shutdown~");
            render("Shutting down...");
        }

        function startBtn (){
            $socket = connectToServer();
            $socket.onopen = socketOpen;
            $socket.onmessage = socketMessage;
            $socket.onclose = socketClose;
            $socket.onerror = socketError;
        }                                                                                        

        // Runtime operations

        render("Initialization");

        // Socket event based operations

        let $socket = connectToServer();
        $socket.onopen = socketOpen;
        $socket.onmessage = socketMessage;
        $socket.onclose = socketClose;
        $socket.onerror = socketError;

        // Application event based operations

        $("#socketMsg").on("click", socketMsg);
        $("#closeBtn").on("click", closeBtn);
        $("#startBtn").on("click", startBtn);

    }

};

$(function(){ // Init
    new SocketClient();
});

3- PHP CLI

<?php

    error_reporting(E_ALL);

    set_time_limit(0);

    ob_implicit_flush();

    class Socket_Server {

        /**
         * An initial socket connection may be longer than expected.
         * Every other requests should be fast.
         */

        // Properties
        
        private $telnet = false;
        private $open = false; // Server status (find something using a method for this)
        private $time_limit = 0;
        private $address = '127.0.0.1';
        private $broadcast = 0; // Allow UDP broadcasts
        private $port = 9000;
        private $socket;
        private $header;
        private $level = SOL_SOCKET;
        private $option_name = SO_REUSEADDR; 
        private $receiving = SO_RCVTIMEO;
        private $sending = SO_SNDTIMEO;
        private $seconds = 0;
        private $micro_seconds = 800;
        private $duration = array('sec' => 0, 'usec' => 800);
        private $option_value = 1;
        private $length = 634;
        private $read_binary = PHP_BINARY_READ; // Safe binary data
        private $read_php = PHP_NORMAL_READ;
        private $accept; // accept incoming connections on socket instance
        private $request;
        private $request_check;
        private $receive_mode = MSG_PEEK; // Receive data from the beginning of the receive queue without removing it from the queue. 
        private $domain = AF_INET; // IPv4
        private $type = SOCK_STREAM; // Full-duplex
        private $protocol = SOL_TCP; // TCP/UDP
        private $backlog = 25; // Incoming connection queue
        private $clients = array(); // Accept multiple clients
        private $sockets = array();
        private $blacklist = NULL; // To see if a write will not block
        private $whitelist = NULL; // Exceptions
        private $timeout = 0; // Watch timeout
        
        // Methods       
        
        private function create () {
            $socket = socket_create($this->domain, $this->type, $this->protocol);
            return $socket;
        }

        private function bind () { // Bind a name to a socket
            return socket_bind($this->socket, $this->broadcast, $this->port);
        }

        private function listen () {
            return socket_listen($this->socket, $this->backlog);
        }

        private function deliver ($message, $client = false, $format = false) { // Push data to the client
            if(isset($message) && !empty($message)){
                if (!$this->telnet && $format !== 'text'){
                    $message = $this->seal($message);
                }
                if($client && socket_write($client, $message, strlen($message))){
                    return $this->open = true;
                }
                else if(socket_write($this->accept, $message, strlen($message))){
                    return $this->open = true; // If the socket can write, it is open
                }
                else{
                    return $this->open = false;
                }
            }
        }

        /**
         * Dynamically scale the length of the read() and socket_recv()
         */

        private function read ($socket) { // Maximum bytes from socket 
            if(isset($socket)){
                return trim(socket_read($socket, $this->length, $this->read_binary));
            }
            else{
                return false;
            }
        }

        private function read_client_response ($client, $debug = false) { // Return the full client response string/object
            if(isset($client)){
                $html_client_response;
                $results_length = socket_recv($client, $results, $this->length, $this->receive_mode);
                if($debug == true){
                    $this->response("> results (length) : " . $results_length);
                }
                if($results_length == 0){
                    return false;
                }
                return $this->unseal($results); // Binary data is expected by default
      
            }
            else{
                return false;
            }
        }

        private function format_header () { // Format an HTTP response header
            $header = array();
            $data = preg_split("/\r\n/", $this->header);
            foreach($data as $datum) {
                $datum = chop($datum);
                if(preg_match('/\A(\S+): (.*)\z/', $datum, $matches)) {
                    $header[$matches[1]] = $matches[2];
                }
            }
            return $header;
        }

        private function set_header(){ // Use the Sever class to build this dynamically
            $nL = "\r\n"; // New line delimiter
            $header = $this->format_header();
            $secKey = isset($header['Sec-WebSocket-Key']) ? $header['Sec-WebSocket-Key'] : '';
            $secAccept = base64_encode(pack('H*', sha1($secKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
            $data  = 'HTTP/1.1 101 Web Socket Protocol Handshake' . $nL;
            $data .= 'Upgrade: websocket' . $nL;
            $data .= 'Connection: Upgrade' . $nL;
            $data .= 'WebSocket-Origin: ' . $this->address . $nL;
            $data .= 'WebSocket-Location: ws://'. $this->address. ':' . $this->port. $nL;
            $data .= 'Sec-WebSocket-Accept: ' . $secAccept . $nL . $nL;
            $this->deliver($data, $this->accept, 'text');
            return $data;
        }

        private function set_close_control_frame(){ // Use the Sever class to build this dynamically
            $nL = "\r\n"; // New line delimiter
            $header = $this->format_header();
            $secKey = isset($header['Sec-WebSocket-Key']) ? $header['Sec-WebSocket-Key'] : '';
            $secAccept = base64_encode(pack('H*', sha1($secKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
            $data  = 'HTTP/1.1 101 Web Socket Protocol Handshake' . $nL;
            $data .= 'Upgrade: websocket' . $nL;
            $data .= 'Connection: Upgrade' . $nL;
            $data .= 'WebSocket-Origin: ' . $this->address . $nL;
            $data .= 'WebSocket-Location: ws://'. $this->address. ':' . $this->port. $nL;
            $data .= 'Expires: Fri, 20 Oct 2000 10:00:00 GMT' . $nL;
            $data .= 'Sec-WebSocket-Accept: ' . $secAccept . $nL . $nL;
            $this->deliver($data, $this->accept, 'text');
            return $data;
        }

        private function response ($object) { // Output to server
            echo $object . "\r\n";
        }

        private function close ($socket) {
            socket_close($socket);
            return $this->open = false;
        }

        private function switch_to () { // Register multiple sockets to allow a multi-client connection
            $this->sockets[] = $this->socket;
            $this->sockets = array_merge($this->sockets, $this->clients);
        }

        private function options () { // Reuse previously created socket connection
            $options = array ();
            $options['receiving'] = socket_set_option($this->socket, $this->level, $this->receiving, $this->duration);
            $options['sending'] = socket_set_option($this->socket, $this->level, $this->sending, $this->duration);
            $options['reusable'] = socket_set_option($this->socket, $this->level, $this->option_name, $this->option_value);
            return $options['reusable'];
        }

        private function watch () {
            return (socket_select(
                $this->sockets, 
                $this->blacklist, 
                $this->whitelist, 
                $this->timeout
            ) < 1);
        }

        private function clients_no () { // Count the total number of clients
            if(isset($this->clients) && isset($this->accept)){
                return array_keys($this->clients, $this->accept)[0];
            }
            return 0;
        }

        private function unseal ($socket_data) { // Nagle's algorithm for decoding
            if(isset($socket_data) && isset($socket_data[1])){
                $results = ""; // The string to hold the decoded packets
                $length = ord($socket_data[1]) & 127; // Return the ASCII value
                if($length == 126) {
                    $masks = substr($socket_data, 4, 4);
                    $data = substr($socket_data, 8);
                }
                else if($length == 127) {
                    $masks = substr($socket_data, 10, 4);
                    $data = substr($socket_data, 14);
                }
                else {
                    $masks = substr($socket_data, 2, 4);
                    $data = substr($socket_data, 6);
                }
                for ($i = 0; $i < strlen($data); ++$i) {
                    $results .= $data[$i] ^ $masks[$i%4];
                }
                return $results;
            }
            return false;
        }

        private function seal($socket_data) { // Nagle's algorithm for encoding
            if($socket_data){
                $b1 = 0x80 | (0x1 & 0x0f); // binary string
                $length = strlen($socket_data);
                if($length <= 125) {
                    $header = pack('CC', $b1, $length);
                }
                else if($length > 125 && $length < 65536) {
                    $header = pack('CCn', $b1, 126, $length);
                }
                else if($length >= 65536) {
                    $header = pack('CCNN', $b1, 127, $length);
                }
                return $header.$socket_data;
            }
            return false;
        }

        private function unregister_sockets ($key, $client) {
            unset($this->clients[$key]); // Un-register the client
            $skey = array_search($this->socket, $this->sockets); // Find the socket the watch array
            unset($this->sockets[$skey]); // Un-register the socket watch
            $this->close($client);  
        }

        // Wrapper methods

        private function run ($property, $message = false) { // Run a specific function
            if($property === false){
                $this->display_error_message($message);
                return true;
            }
            return false;
        }

        private function display_error_message ($message) {
            $this->response($message . ' ' . socket_strerror(socket_last_error()));
        }

        // Constructor
        
        function __construct ( ) {
            $this->socket = $this->create();   
            $this->run($this->socket, 'socket_create() failed: reason: ');
            $this->run($this->options(), 'socket_set_option() failed: reason ');
            $this->run($this->bind(), 'socket_bind() failed: reason: ');
            $this->run($this->listen(), 'socket_listen() failed: reason: ');
            $this->response(">>> Web socket server initiated"); //////
            do {
                $this->switch_to();
                if($this->watch()){
                    continue;
                }
                if(in_array($this->socket, $this->sockets)){ // Handle new connections
                    $this->response(">>> Handling new connections"); //////
                    $this->accept = socket_accept($this->socket);
                    $this->run($this->accept, 'socket_accept() failed: reason: ');
                    $this->clients[] = $this->accept; // Register the client
                    $this->header = $this->read($this->accept);
                    $this->run($this->header, 'The socket header failed: reason: ');
                    $this->run($this->set_header(), 'The socket header could not be set: reason: ');
                    /**
                     * For Telnet tests
                     */
                    if($this->telnet){
                        $key = array_keys($this->clients, $this->accept);
                        $this->deliver('Client No. '. ($key[0] + 1));
                    }
                }
                foreach($this->clients as $key => $client){
                    $this->response(">>> For each client"); //////
                    if(in_array($client, $this->sockets)){
                        $this->request_check = $this->read_client_response($client, true); // Read the corresponding client
                        $this->request = $this->read($client); // Read the corresponding client in binary
                        $this->response(">>> in a watch list"); //////
                        switch ($this->request_check){
                            // Get the appropriate message on failure
                            case $this->run($this->accept, 'socket_read() failed: reason: '):
                                $this->response("The socket connection accept failed");
                            case !$this->request_check:
                                $this->response("The socket connection read failed");
                            case '':
                                $this->response("The socket disconnected");
                            // Perform those actions on failure
                            case $this->run($this->accept, 'socket_read() failed: reason: '):
                            case !$this->request_check:
                            case '':
                                $this->unregister_sockets($key, $client);  
                                break; 
                            case '~shutdown~':
                                $this->deliver('~closehandshake~');
                                $this->unregister_sockets ($key, $client);
                                break;      
                            // On success                         
                            default:
                                $client_no = $key + 1;
                                /**
                                 * For Telnet tests
                                 */
                                if($this->telnet){
                                    $this->deliver("PHP > Client ID {$client_no} said '$this->request_check'.\n", $client); // Write to specific client
                                }
                                else{
                                    $message  = '(server) Client ID: ' . $client_no . ' <blockquote>' . $this->request_check . '.' . "\r\n"; // Message to deliver to client
                                    $message .= 'Total number of connections: ' . count($this->clients) . '</blockquote>' . "\r\n";
                                    $this->deliver($message, $client); // This method should be deliver()
                                }
                                break;
                        }
                    }
                }
            } while (true);
            $this->close($this->socket);
        }
    }

    new Socket_Server();

?>

我尝试通过添加socket_shutdown($client,2 ) 和带有SO_LINGERSO_KEEPALIVE 的选项行来改变套接字服务器的行为(以防关闭连接需要时间),还尝试了像stream_socket_shutdown 这样的PHP 流函数.

PHP 流函数:https://www.php.net/manual/en/ref.stream.php

【问题讨论】:

  • 我使用 PHP 服务器发出返回命令作为控制框架。因此,我不得不听 $this->deliver('~closehandshake~');在前端并包装关闭 socketClose() 以检查 $event.code 之后,这意味着我仍然有能力判断连接是否断开,然后相应地重新连接客户端。

标签: javascript php html websocket


【解决方案1】:

当客户端向服务器发出'~shutdown~' 命令时,我必须将'~closehandshake~' 发送回前端,然后从那里,我使用开关($event.code) 来检查if($event.data === '~closehandshake~'),它绕过了1006 情况下,除非服务器从未发送过它。

检查$event.data 是否收到自定义“关闭控制框架”(即'~closehandshake~')的更简单方法是更新updateStatus 方法,并允许它存储自定义状态,如下所示:

        function updateStatus ($status) { // Update the status of a socket connection
            switch($status){
                case 0:
                    $state = 'connecting';
                    break;
                case 1:
                    $state = 'open';
                    break;
                case 2:
                    $state = 'closing';
                    break;
                case 3:
                    $state = 'closed';
                    break;
                default: // Typically a user defined status
                    $state = $status;
                    break;
            }
        }

您可以在 $state 变量中设置状态,如下所示:

        function closeControlFrame ($event) { // Determine if the server requested to expire the handshake
            if($event.data === '~closehandshake~'){
                updateStatus("cleanclosure"); // Confirm a clean close control frame using a custom code
                $socket.close(1000, "Work complete");
            }
        }

然后使用 $state 变量检查状态:

    if($state !== "cleanclosure"){
        switch ($event.code) {
            // The rest of the cases should be here as well
        }
    }

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-10-26
    • 2013-04-01
    • 2014-08-11
    • 2011-08-12
    • 2017-07-10
    • 2016-03-03
    • 2013-01-16
    相关资源
    最近更新 更多