PHP实现wss请求,成功访问wss://

五级会员 站长
发表于PHP分类
php默认是不支持访问wss://地址的,需要通过ssl://来访问,需要用到以下文件:


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();


登录 分类