PHP实现wss请求,成功访问wss://
Base.php
<?php
/**
 * Copyright (C) 2014-2020 Textalk/Abicart and contributors.
 *
 * This file is part of Websocket PHP and is free software under the ISC License.
 * License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
 */
namespace WebSocket;
class Base
{
    protected $socket;
    protected $options      = [];
    protected $is_closing   = false;
    protected $last_opcode  = null;
    protected $close_status = null;
    protected static $opcodes = array(
        'continuation' => 0,
        'text'         => 1,
        'binary'       => 2,
        'close'        => 8,
        'ping'         => 9,
        'pong'         => 10,
    );
    public function getLastOpcode()
    {
        return $this->last_opcode;
    }
    public function getCloseStatus()
    {
        return $this->close_status;
    }
    public function isConnected()
    {
        return $this->socket && get_resource_type($this->socket) == 'stream';
    }
    public function setTimeout($timeout)
    {
        $this->options['timeout'] = $timeout;
        if ($this->isConnected()) {
            stream_set_timeout($this->socket, $timeout);
        }
    }
    public function setFragmentSize($fragment_size)
    {
        $this->options['fragment_size'] = $fragment_size;
        return $this;
    }
    public function getFragmentSize()
    {
        return $this->options['fragment_size'];
    }
    public function send($payload, $opcode = 'text', $masked = true)
    {
        if (!$this->isConnected()) {
            $this->connect();
        }
        if (!in_array($opcode, array_keys(self::$opcodes))) {
            exit('数据流非法');
        }
        // record the length of the payload
        $payload_length = strlen($payload);
        $fragment_cursor = 0;
        // while we have data to send
        while ($payload_length > $fragment_cursor) {
            // get a fragment of the payload
            $sub_payload = substr($payload, $fragment_cursor, $this->options['fragment_size']);
            // advance the cursor
            $fragment_cursor += $this->options['fragment_size'];
            // is this the final fragment to send?
            $final = $payload_length <= $fragment_cursor;
            // send the fragment
            $this->sendFragment($final, $sub_payload, $opcode, $masked);
            // all fragments after the first will be marked a continuation
            $opcode = 'continuation';
        }
    }
    protected function sendFragment($final, $payload, $opcode, $masked)
    {
        // Binary string for header.
        $frame_head_binstr = '';
        // Write FIN, final fragment bit.
        $frame_head_binstr .= (bool) $final ? '1' : '0';
        // RSV 1, 2, & 3 false and unused.
        $frame_head_binstr .= '000';
        // Opcode rest of the byte.
        $frame_head_binstr .= sprintf('%04b', self::$opcodes[$opcode]);
        // Use masking?
        $frame_head_binstr .= $masked ? '1' : '0';
        // 7 bits of payload length...
        $payload_length = strlen($payload);
        if ($payload_length > 65535) {
            $frame_head_binstr .= decbin(127);
            $frame_head_binstr .= sprintf('%064b', $payload_length);
        } elseif ($payload_length > 125) {
            $frame_head_binstr .= decbin(126);
            $frame_head_binstr .= sprintf('%016b', $payload_length);
        } else {
            $frame_head_binstr .= sprintf('%07b', $payload_length);
        }
        $frame = '';
        // Write frame head to frame.
        foreach (str_split($frame_head_binstr, 8) as $binstr) {
            $frame .= chr(bindec($binstr));
        }
        // Handle masking
        if ($masked) {
            // generate a random mask:
            $mask = '';
            for ($i = 0; $i < 4; $i++) {
                $mask .= chr(rand(0, 255));
            }
            $frame .= $mask;
        }
        // Append payload to frame:
        for ($i = 0; $i < $payload_length; $i++) {
            $frame .= (true === $masked) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i];
        }
        $this->write($frame);
    }
    public function receive()
    {
        if (!$this->isConnected()) {
            $this->connect();
        }
        $payload = '';
        do {
            $response = $this->receiveFragment();
            $payload .= $response[0];
        } while (!$response[1]);
        return $payload;
    }
    protected function receiveFragment()
    {
        // Just read the main fragment information first.
        $data = $this->read(2);
        // Is this the final fragment?  // Bit 0 in byte 0
        $final = (bool) (ord($data[0])&1 << 7);
        // Should be unused, and must be false…  // Bits 1, 2, & 3
        $rsv1 = (bool) (ord($data[0])&1 << 6);
        $rsv2 = (bool) (ord($data[0])&1 << 5);
        $rsv3 = (bool) (ord($data[0])&1 << 4);
        // Parse opcode
        $opcode_int  = ord($data[0])&31; // Bits 4-7
        $opcode_ints = array_flip(self::$opcodes);
        if (!array_key_exists($opcode_int, $opcode_ints)) {
            exit('数据传输失败');
        }
        $opcode = $opcode_ints[$opcode_int];
        // Record the opcode if we are not receiving a continutation fragment
        if ('continuation' !== $opcode) {
            $this->last_opcode = $opcode;
        }
        // Masking?
        $mask = (bool) (ord($data[1]) >> 7); // Bit 0 in byte 1
        $payload = '';
        // Payload length
        $payload_length = (int) ord($data[1])&127; // Bits 1-7 in byte 1
        if ($payload_length > 125) {
            if (126 === $payload_length) {
                $data = $this->read(2); // 126: Payload is a 16-bit unsigned int
            } else {
                $data = $this->read(8); // 127: Payload is a 64-bit unsigned int
            }
            $payload_length = bindec(self::sprintB($data));
        }
        // Get masking key.
        if ($mask) {
            $masking_key = $this->read(4);
        }
        // Get the actual payload, if any (might not be for e.g. close frames.
        if ($payload_length > 0) {
            $data = $this->read($payload_length);
            if ($mask) {
                // Unmask payload.
                for ($i = 0; $i < $payload_length; $i++) {
                    $payload .= ($data[$i] ^ $masking_key[$i % 4]);
                }
            } else {
                $payload = $data;
            }
        }
        // if we received a ping, send a pong
        if ('ping' === $opcode) {
            $this->send($payload, 'pong', true);
        }
        if ('close' === $opcode) {
            // Get the close status.
            if ($payload_length > 0) {
                $status_bin         = $payload[0] . $payload[1];
                $status             = bindec(sprintf("%08b%08b", ord($payload[0]), ord($payload[1])));
                $this->close_status = $status;
            }
            // Get additional close message-
            if ($payload_length >= 2) {
                $payload = substr($payload, 2);
            }
            if ($this->is_closing) {
                $this->is_closing = false; // A close response, all done.
            } else {
                $this->send($status_bin . 'Close acknowledged: ' . $status, 'close', true); // Respond.
            }
            // Close the socket.
            fclose($this->socket);
            // Closing should not return message.
            return [null, true];
        }
        return [$payload, $final];
    }
    /**
     * Tell the socket to close.
     *
     * @param integer $status  http://tools.ietf.org/html/rfc6455#section-7.4
     * @param string  $message A closing message, max 125 bytes.
     */
    public function close($status = 1000, $message = 'ttfn')
    {
        if (!$this->isConnected()) {
            return null;
        }
        $status_binstr = sprintf('%016b', $status);
        $status_str    = '';
        foreach (str_split($status_binstr, 8) as $binstr) {
            $status_str .= chr(bindec($binstr));
        }
        $this->send($status_str . $message, 'close', true);
        $this->is_closing = true;
        $this->receive(); // Receiving a close frame will close the socket now.
    }
    protected function write($data)
    {
        $written = fwrite($this->socket, $data);
        if ($written < strlen($data)) {
            exit("最多只能输入{$written}个字");
        }
    }
    protected function read($length)
    {
        $data = '';
        while (strlen($data) < $length) {
            $buffer = fread($this->socket, $length - strlen($data));
            if (false === $buffer) {
                $metadata = stream_get_meta_data($this->socket);
                exit('数据接收失败');
            }
            if ('' === $buffer) {
                exit('连接超时');
                $metadata = stream_get_meta_data($this->socket);
            }
            $data .= $buffer;
        }
        return $data;
    }
    /**
     * Helper to convert a binary to a string of '0' and '1'.
     */
    protected static function sprintB($string)
    {
        $return = '';
        for ($i = 0; $i < strlen($string); $i++) {
            $return .= sprintf("%08b", ord($string[$i]));
        }
        return $return;
    }
}
Client.php
<?php
/**
 * Copyright (C) 2014-2020 Textalk/Abicart and contributors.
 *
 * This file is part of Websocket PHP and is free software under the ISC License.
 * License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING
 */
namespace WebSocket;
class Client extends Base
{
    // Default options
    protected static $default_options = [
        'timeout'       => 10,
        'fragment_size' => 4096,
        'context'       => null,
        'headers'       => null,
        'origin'        => null, // @deprecated
    ];
    protected $socket_uri;
    /**
     * @param string $uri     A ws/wss-URI
     * @param array  $options
     *   Associative array containing:
     *   - context:       Set the stream context. Default: empty context
     *   - timeout:       Set the socket timeout in seconds.  Default: 5
     *   - fragment_size: Set framgemnt size.  Default: 4096
     *   - headers:       Associative array of headers to set/override.
     */
    public function __construct($uri, $options = array())
    {
        $this->options    = array_merge(self::$default_options, $options);
        $this->socket_uri = $uri;
    }
    public function __destruct()
    {
        if ($this->isConnected()) {
            fclose($this->socket);
        }
        $this->socket = null;
    }
    /**
     * Perform WebSocket handshake
     */
    protected function connect()
    {
        $url_parts = parse_url($this->socket_uri);
        if (empty($url_parts) || empty($url_parts['scheme']) || empty($url_parts['host'])) {
            exit('请求地址非法');
        }
        $scheme   = $url_parts['scheme'];
        $host     = $url_parts['host'];
        $user     = isset($url_parts['user']) ? $url_parts['user'] : '';
        $pass     = isset($url_parts['pass']) ? $url_parts['pass'] : '';
        $port     = isset($url_parts['port']) ? $url_parts['port'] : ('wss' === $scheme ? 443 : 80);
        $path     = isset($url_parts['path']) ? $url_parts['path'] : '/';
        $query    = isset($url_parts['query']) ? $url_parts['query'] : '';
        $fragment = isset($url_parts['fragment']) ? $url_parts['fragment'] : '';
        $path_with_query = $path;
        if (!empty($query)) {
            $path_with_query .= '?' . $query;
        }
        if (!empty($fragment)) {
            $path_with_query .= '#' . $fragment;
        }
        if (!in_array($scheme, array('ws', 'wss'))) {
            exit('仅允许ws或wss方式');
        }
        $host_uri = ('wss' === $scheme ? 'ssl' : 'tcp') . '://' . $host;
        // Set the stream context options if they're already set in the config
        if (isset($this->options['context'])) {
            // Suppress the error since we'll catch it below
            if (@get_resource_type($this->options['context']) === 'stream-context') {
                $context = $this->options['context'];
            } else {
                exit('context参数错误');
            }
        } else {
            $context = stream_context_create();
        }
        // Open the socket.  @ is there to supress warning that we will catch in check below instead.
        $this->socket = @stream_socket_client(
            $host_uri . ':' . $port,
            $errno,
            $errstr,
            $this->options['timeout'],
            STREAM_CLIENT_CONNECT,
            $context
        );
        if (!$this->isConnected()) {
            exit('无法连接讯飞星火服务器');
        }
        // Set timeout on the stream as well.
        stream_set_timeout($this->socket, $this->options['timeout']);
        // Generate the WebSocket key.
        $key = self::generateKey();
        // Default headers
        $headers = array(
            'Host'                  => $host . ":" . $port,
            'User-Agent'            => 'www.hadsky.com',
            'Connection'            => 'Upgrade',
            'Upgrade'               => 'websocket',
            'Sec-WebSocket-Key'     => $key,
            'Sec-WebSocket-Version' => '13',
        );
        // Handle basic authentication.
        if ($user || $pass) {
            $headers['authorization'] = 'Basic ' . base64_encode($user . ':' . $pass) . "\r\n";
        }
        // Deprecated way of adding origin (use headers instead).
        if (isset($this->options['origin'])) {
            $headers['origin'] = $this->options['origin'];
        }
        // Add and override with headers from options.
        if (isset($this->options['headers'])) {
            $headers = array_merge($headers, $this->options['headers']);
        }
        $header = "GET " . $path_with_query . " HTTP/1.1\r\n" . implode(
            "\r\n",
            array_map(
                function ($key, $value) {
                    return "$key: $value";
                },
                array_keys($headers),
                $headers
            )
        ) . "\r\n\r\n";
        // Send headers.
        $this->write($header);
        // Get server response header (terminated with double CR+LF).
        $response = stream_get_line($this->socket, 1024, "\r\n\r\n");
        /// @todo Handle version switching
        // Validate response.
        if (!preg_match('#Sec-WebSocket-Accept:\s(.*)$#mUi', $response, $matches)) {
            $address = $scheme . '://' . $host . $path_with_query;
            exit('连接被远程服务器拒绝');
        }
        $keyAccept = trim($matches[1]);
        $expectedResonse
        = base64_encode(pack('H*', sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
        if ($keyAccept !== $expectedResonse) {
            exit('连接失败');
        }
    }
    /**
     * Generate a random string for WebSocket key.
     *
     * @return string Random string
     */
    protected static function generateKey()
    {
        $chars        = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$&/()=[]{}0123456789';
        $key          = '';
        $chars_length = strlen($chars);
        for ($i = 0; $i < 16; $i++) {
            $key .= $chars[mt_rand(0, $chars_length - 1)];
        }
        return base64_encode($key);
    }
}
具体用法:
<?php
use WebSocket\Client;
require 'Base.php';
require 'Client.php';
$wss = 'wss://www.hadsky.com/index.php?from=puyuetian';
$client = new Client($wss);
$client->send('发送的数据');
echo $client->receive();
$client->close();