从PHP-CLI套接字服务器向HTML5客户端发送关闭控制帧



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

我的前端代码如下:

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

我编码的后端响应如下:

socket_close($client);

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

客户文件来源:

  • https://javascript.info/websocket
  • https://www.rfc-editor.org/rfc/rfc6455#section-7.4.1

对于后端,我使用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("/rn/", $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 = "rn"; // 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 = "rn"; // 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 . "rn";
}
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 . '.' . "rn"; // Message to deliver to client
$message .= 'Total number of connections: ' . count($this->clients) . '</blockquote>' . "rn";
$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

当客户端向服务器发出'~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
}
}

最新更新