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