iT邦幫忙

0

websocket 1006 異常關閉問題 (on GCE)

各位大大好
之前小弟懵懵懂懂時 po了一篇文
https://ithelp.ithome.com.tw/questions/10205154#answer-376127
經過幾天的測試大概釐清了問題,但真正的問題還沒解決
先提供環境

  • 後端程式:server.php(使用原生 php socket function) 運行在 .3:8080 port
  • 前端程式:client.html(簡單 javascript)
  • GCE: Ubuntu 18.04, PHP 7.3, apache 2.4.29
  • GCE: 內部IP: .3, 外部IP: .62
  • on premise: windows 10
  • 沒有使用任何 google cloud sdk

以下一些問題:

  1. 我已經成功能把 websocket 運行在 GCE 的 8080 port 上
    排除了之前所說的防火牆問題,其實最根本的原因就是前端的 websocket 開不起來
    後端運行是很正常的
    以下提供 GCE 測試流程資訊
    a. 在 GCE run server.php
    能夠看到 8080 port 有 process 運行
    https://ithelp.ithome.com.tw/upload/images/20210910/20123565BAq9eygfHZ.png
    b. 在 GCE 用 telnet 去 try 8080 回應
    是可以成功連接
    https://ithelp.ithome.com.tw/upload/images/20210910/20123565gacwmJlLl9.png
    c. on premise 用 curl 去 try GCE 上的 8080 回應
    成功回應,也有回握手訊息
    https://ithelp.ithome.com.tw/upload/images/20210910/20123565HIXKTMRTmw.png
    https://ithelp.ithome.com.tw/upload/images/20210910/20123565dCGNk6Qaxc.png

目前問題:
在 GCE 上開 client.html 仍然無法開啟 websocket 會回傳 1006 錯誤
https://ithelp.ithome.com.tw/upload/images/20210910/20123565bNtcBhOPDq.png
2. 還是很好奇上一篇發生的原因

當我 local 啟動 server 時
GCE 上的 client 竟然就能成功連線
很好奇這是為何 我完全沒使用 SDK 之類的
GCE 的 VM 竟然可以抓到我 on premise 的 websocket

我不理解為什麼我 on premise 的裝置的 localhost websocket 竟然能讓 GCE 的程式通
(原本我 GCE 上和 on premise 的 domain 都是寫 localhost)

code:
server.php

<?php
error_reporting(E_ALL);
set_time_limit(0);

class WebSocket {
    const LOG_PATH = '/tmp/';
    const LISTEN_SOCKET_NUM = 9;

    private $sockets = [];
    private $master;

    public function __construct($host, $port) {
        try {
            $this->master = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
            
            socket_set_option($this->master, SOL_SOCKET, SO_REUSEADDR, 1);
            
            socket_bind($this->master, $host, $port);
            
            socket_listen($this->master, self::LISTEN_SOCKET_NUM);
        } catch (\Exception $e) {
            $err_code = socket_last_error();
            $err_msg = socket_strerror($err_code);

        }

        $this->sockets[0] = ['resource' => $this->master];
        $pid = posix_getpid();

        while (true) {
            try {
                $this->doServer();
            } catch (\Exception $e) {
               
            }
        }
    }

    private function doServer() {
        $write = $except = NULL;
        $sockets = array_column($this->sockets, 'resource');
        $read_num = socket_select($sockets, $write, $except, NULL);
      
        if (false === $read_num) {
                        return;
        }

        foreach ($sockets as $socket) {

            if ($socket == $this->master) {
                $client = socket_accept($this->master);
                
                if (false === $client) {
                    
                    continue;
                } else {
                    self::connect($client);
                    continue;
                }
            } else {
                $bytes = @socket_recv($socket, $buffer, 2048, 0);
                if ($bytes < 9) {
                    $recv_msg = $this->disconnect($socket);
                } else {
                    if (!$this->sockets[(int)$socket]['handshake']) {
                        self::handShake($socket, $buffer);
                        continue;
                    } else {
                        $recv_msg = self::parse($buffer);
                    }
                }
                array_unshift($recv_msg, 'receive_msg');
                $msg = self::dealMsg($socket, $recv_msg);

                $this->broadcast($msg);
            }
        }
    }

    public function connect($socket) {
        socket_getpeername($socket, $ip, $port);
        $socket_info = [
            'resource' => $socket,
            'uname' => '',
            'handshake' => false,
            'ip' => $ip,
            'port' => $port,
        ];
        $this->sockets[(int)$socket] = $socket_info;
    }


    private function disconnect($socket) {
        $recv_msg = [
            'type' => 'logout',
            'content' => $this->sockets[(int)$socket]['uname'],
        ];
        unset($this->sockets[(int)$socket]);

        return $recv_msg;
    }

    public function handShake($socket, $buffer) {
        $line_with_key = substr($buffer, strpos($buffer, 'Sec-WebSocket-Key:') + 18);
        $key = trim(substr($line_with_key, 0, strpos($line_with_key, "\r\n")));

        $upgrade_key = base64_encode(sha1($key . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true));
        $upgrade_message = "HTTP/1.1 101 Switching Protocols\r\n";
        $upgrade_message .= "Upgrade: websocket\r\n";
        $upgrade_message .= "Sec-WebSocket-Version: 13\r\n";
        $upgrade_message .= "Connection: Upgrade\r\n";
        $upgrade_message .= "Sec-WebSocket-Accept:" . $upgrade_key . "\r\n\r\n";

        socket_write($socket, $upgrade_message, strlen($upgrade_message));
        $this->sockets[(int)$socket]['handshake'] = true;

        socket_getpeername($socket, $ip, $port);

        $msg = [
            'type' => 'handshake',
            'content' => 'done',
        ];
        $msg = $this->build(json_encode($msg));
        socket_write($socket, $msg, strlen($msg));
        return true;
    }

    private function parse($buffer) {
        $decoded = '';
        $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);
        }
        for ($index = 0; $index < strlen($data); $index++) {
            $decoded .= $data[$index] ^ $masks[$index % 4];
        }

        return json_decode($decoded, true);
    }

    private function build($msg) {
        $frame = [];
        $frame[0] = '81';
        $len = strlen($msg);
        if ($len < 126) {
            $frame[1] = $len < 16 ? '0' . dechex($len) : dechex($len);
        } else if ($len < 65025) {
            $s = dechex($len);
            $frame[1] = '7e' . str_repeat('0', 4 - strlen($s)) . $s;
        } else {
            $s = dechex($len);
            $frame[1] = '7f' . str_repeat('0', 16 - strlen($s)) . $s;
        }

        $data = '';
        $l = strlen($msg);
        for ($i = 0; $i < $l; $i++) {
            $data .= dechex(ord($msg{$i}));
        }
        $frame[2] = $data;

        $data = implode('', $frame);

        return pack("H*", $data);
    }

    private function dealMsg($socket, $recv_msg) {
        $msg_type = $recv_msg['type'];
        $msg_content = $recv_msg['content'];
        $response = [];

        switch ($msg_type) {
            case 'login':
                $this->sockets[(int)$socket]['uname'] = $msg_content;
                $user_list = array_column($this->sockets, 'uname');
                $response['type'] = 'login';
                $response['content'] = $msg_content;
                $response['user_list'] = $user_list;
                break;
            case 'logout':
                $user_list = array_column($this->sockets, 'uname');
                $response['type'] = 'logout';
                $response['content'] = $msg_content;
                $response['user_list'] = $user_list;
                break;
            case 'user':
                $uname = $this->sockets[(int)$socket]['uname'];
                $response['type'] = 'user';
                $response['from'] = $uname;
                $response['content'] = $msg_content;
                break;
        }

        return $this->build(json_encode($response));
    }

    private function broadcast($data) {
        foreach ($this->sockets as $socket) {
            if ($socket['resource'] == $this->master) {
                continue;
            }
            socket_write($socket['resource'], $data, strlen($data));
        }
    }


  
}
$ws = new WebSocket("GCE_VM_INTERNAL_IP", "8080");

client.html

<html>
    <head>
        <title></title>
        <meta http-equiv="content-type" content="text/html;charset=utf-8">
        <style>
            p {
                text-align: left;
                padding-left: 20px;
            }
        </style>
    </head>
    <body>
        <div style="width: 800px;height: 600px;margin: 30px auto;text-align: center">
            <h1>chat room</h1>
            <div style="width: 800px;border: 1px solid gray;height: 300px;">
                <div style="width: 200px;height: 300px;float: left;text-align: left;">
                    <p><span>online:</span><span id="user_num">0</span></p>
                    <div id="user_list" style="overflow: auto;">

                    </div>
                </div>
                <div id="msg_list" style="width: 598px;border:  1px solid gray; height: 300px;overflow: scroll;float: left;">
                </div>
            </div>
            <br>
            <textarea id="msg_box" rows="6" cols="50" onkeydown="confirm(event)"></textarea><br>
            <input type="button" value="send" onclick="send()">
        </div>
    </body>
</html>

<script type="text/javascript">
    var uname = prompt('type your name', 'user');
    var ws = new WebSocket("ws://GCE_VM_INTERNAL_IP:8080");
    ws.onopen = function () {
        var data = "system:connect successfully";
        listMsg(data);
    };

    ws.onmessage = function (e) {
        var msg = JSON.parse(e.data);
        var sender, user_name, name_list, change_type;

        switch (msg.type) {
            case 'system':
                sender = 'system: ';
                break;
            case 'user':
                sender = msg.from + ': ';
                break;
            case 'handshake':
                var user_info = {'type': 'login', 'content': uname};
                sendMsg(user_info);
                return;
            case 'login':
            case 'logout':
                user_name = msg.content;
                name_list = msg.user_list;
                change_type = msg.type;
                dealUser(user_name, change_type, name_list);
                return;
        }

        var data = sender + msg.content;
        listMsg(data);
    };

    ws.onerror = function () {
        var data = "system : something wrong";
        listMsg(data);
    };

    function confirm(event) {
        var key_num = event.keyCode;
        if (13 == key_num) {
            send();
        } else {
            return false;
        }
    }

    function send() {
        var msg_box = document.getElementById("msg_box");
        var content = msg_box.value;
        var reg = new RegExp("\r\n", "g");
        content = content.replace(reg, "");
        var msg = {'content': content.trim(), 'type': 'user'};
        sendMsg(msg);
        msg_box.value = '';
    }

    function listMsg(data) {
        var msg_list = document.getElementById("msg_list");
        var msg = document.createElement("p");

        msg.innerHTML = data;
        msg_list.appendChild(msg);
        msg_list.scrollTop = msg_list.scrollHeight;
    }


    function dealUser(user_name, type, name_list) {
        var user_list = document.getElementById("user_list");
        var user_num = document.getElementById("user_num");
        while(user_list.hasChildNodes()) {
            user_list.removeChild(user_list.firstChild);
        }

        for (var index in name_list) {
            var user = document.createElement("p");
            user.innerHTML = name_list[index];
            user_list.appendChild(user);
        }
        user_num.innerHTML = name_list.length;
        user_list.scrollTop = user_list.scrollHeight;

        var change = type == 'login' ? 'online' : 'offline';

        var data = 'system:' + user_name + ' is' + change;
        listMsg(data);
    }

    function sendMsg(msg) {
        var data = JSON.stringify(msg);
        ws.send(data);
    }

    
</script>

尚未有邦友回答

立即登入回答