I am writing a WebSocket server. While handshaking is successful and server can send encoded data using RFC standard, socket_select() can only detect changes when a new client is connected, not when the client send data to the server. What is happening?
class Server{
private $address;
private $port;
private $master;
private $sockets;
private $stream_sockets;
private $clients;
private $verbose_mode;
function __construct($address = '127.0.0.1', $port = 5001, $verbose_mode = true){
$this->address = $address;
$this->port = $port;
$this->verbose_mode = $verbose_mode;
$this->console("Socket server is starting...");
//socket creation
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if(!socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1))
{
$this->console("Failed to set socket option: ".socket_strerror(socket_last_error()));
}
if(!socket_set_nonblock($socket))
{
$this->console("Failed to set socket nonblock: ".socket_strerror(socket_last_error()));
}
if(!is_resource($socket))
{
$this->console("Failed to create socket: ".socket_strerror(socket_last_error()));
}
if(!socket_bind($socket, $this->address, $this->port))
{
$this->console("Failed to bind socket: ".socket_strerror(socket_last_error()));
}
if(!socket_listen($socket, 20))
{
$this->console("Failed to listen: ".socket_strerror(socket_last_error()));
}
$this->master = $socket; //add current socket resource as master socket
$this->sockets = array($socket);//add current resource to all sockets
$this->console("Socket server started on {$this->address}:{$this->port}");
}
public function run(){
$this->console("Start running...");
$write = array();
$except = array();
while(isset($this->sockets[0])){
$changed_sockets = $this->sockets;
$result = socket_select($changed_sockets, $write, $except, 1 ,0);
//#stream_select($changed_sockets, $write = null, $except = null, 0, 5000);
if($result > 0)
{
$this->console("number of sockets: ".count($this->sockets));
$this->console("number of changed sockets: ".$result);
foreach($changed_sockets as $socket)
{
if($socket == $this->master)//self changed
{
if(($accepted_socket = socket_accept($this->master))!== false)//incoming connection
{
$this->connect($accepted_socket);//add as a client to the pool with info to be tracked without handshaking at first
}
else
{
$this->console("Socket error: ".socket_strerror(socket_last_error()));
}
}
else //must be others in sockets pool
{
$this->console("Finding socket associated with the client...");
$client = $this->get_client_by_socket($socket); //get client object to track them using socket that they are associated with
if($client){
$this->console("receiving data from the client.");
$bytes = #socket_recv($socket, $data, 2048, MSG_DONTWAIT);
$this->console("byte size received: $bytes");
if(!$client->get_handshake())//check if handshaking has done
{
$this->console("handshaking...");
$this->handshake($client, $data);//handshaking if it is not done previously
}
else if($bytes === 0)
{
$this->disconnect($client);
}
else
{
$this->console("incoming data from client {client->get_id()}");
$this->read($client, $data);//read from client if there are changes in sockets
}
}
}
}
}
}
}
private function slingshot($client, $read){
$send="00:00:00:00".",DM,SAY,0,".$read;
fwrite($client->get_stream_socket(), $send);//output to apollo
//workaround for apollo
if($client->get_initial())
{
$initial = 7;
$continue = 0;
}
else
{
$initial = 8;
$continue = 1;
}
while(TRUE)
{
//input from iris
$num = fgets($client->get_stream_socket(), $initial);//$number of words
if(ltrim($num) > 0)
{
$res = fgets($client->get_stream_socket(), ltrim($num)+1);
if($res!="")
{
fgets($fp,1);
$client->set_initial(false);
$res = $num.$res;
$res = substr($res,6+$continue);
//output to client
$message = rtrim($res);
send($client, $message);
break;
}
}
}
}
private function read($client, $received){
$read = $this->unmask($received);
$this->console("received from client: ".$read);
if($read == "##client exit##") {
$this->console("Killing a child process");
posix_kill($client->get_pid(), SIGTERM);
$this->console("Process {$client->get_pid()} is terminated.");
}
else
{
$this->console("start a child process");
$pid = pcntl_fork();
if($pid == -1)
{
die('could not fork.');
}
else if($pid)
{
$client->set_pid($pid);
}
else
{
//we are the child
$this->slingshot($client, $read);
}
}
}
private function disconnect($client){
$this->console("Disconnecting client #{$client->get_id()}");
$i = array_search($client, $this->clients);//search client in clients pool
$j = array_search($client->get_socket(), $this->sockets);//search client's socket in socket pool
if($j >= 0)
{
array_splice($this->sockets, $j, 1);
socket_close($client->get_socket());
$this->console("Socket closed.");
}
if($i >= 0)
{
array_splice($this->clients, $i, 1);
}
$this->console("Client #{$client->get_id()} disconnected.");
}
private function unmask($payload) {
$length = ord($payload[1]) & 127;
if($length == 126)
{
$masks = substr($payload, 4, 4);
$data = substr($payload, 8);
}
elseif($length == 127)
{
$masks = substr($payload, 10, 4);
$data = substr($payload, 14);
}
else
{
$masks = substr($payload, 2, 4);
$data = substr($payload, 6);
}
$text = '';
for ($i = 0; $i < strlen($data); ++$i){
$text .= $data[$i] ^ $masks[$i%4];
}
return $text;
}
private function encode($text){
// 0x1 text frame (FIN + opcode)
$b1 = 0x80 | (0x1 & 0x0f);
$length = strlen($text);
if($length <= 125)
{
$header = pack('CC', $b1, $length);
}
elseif($length > 125 && $length < 65536)
{
$header = pack('CCS', $b1, 126, $length);
}
elseif($length >= 65536)
{
$header = pack('CCN', $b1, 127, $length);
}
return $header.$text;
}
private function send($client, $text){
$this->console("Client {$client->get_id()}<<".$text);
$text = $this->encode($text);
if(socket_write($client->get_socket(), $text, strlen($text)) === false) {
$this->console("Unable to write to client #{$client->get_id()}'s socket");
$this->disconnect($client);
}
}
private function start_process(){
$this->console("start a child process");
$pid = pcntl_fork();
if($pid == -1)
{
die('could not fork.');
}
else if($pid)
{
$client->set_pid($pid);
}
else
{
//we are the child
$this->send($client, "something to be sent.");
}
}
private function handshake($client, $headers){//data as headers
$this->console("Getting client WebSocket version...");
if(preg_match("/Sec-WebSocket-Version: (.*)\r\n/", $headers, $match))
{
$version = $match[1];
}
else
{
$this->console("The client doesn't support WebSocket.");
}
$this->console("Client WebSocket version is {$version}, (required: 13)");
if($version == 13)
{
$this->console("Getting headers...");
if(preg_match("/GET (.*) HTTP/", $headers, $match))
{
$root = $match[1];
}
if(preg_match("/Host: (.*)\r\n/", $headers, $match))
{
$host = $match[1];
}
if(preg_match("/Origin: (.*)\r\n/", $headers, $match))
{
$origin = $match[1];
}
if(preg_match("/Sec-WebSocket-Key: (.*)\r\n/", $headers, $match))
{
$key = $match[1];
}
$this->console("client Headers are:");
$this->console("\t- Root: ".$root);
$this->console("\t- Host: ".$host);
$this->console("\t- Origin: ".$origin);
$this->console("\t- Sec-WebSocket-Key: ".$key);
$this->console("Generating Sec-WebSocket-Accept key...");
$acceptKey = $key.'258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
$acceptKey = base64_encode(sha1($acceptKey, true));
$upgrade = "HTTP/1.1 101 Switching Protocols\r\n".
"Upgrade: websocket\r\n".
"Connection: Upgrade\r\n".
"Sec-WebSocket-Accept: $acceptKey".
"\r\n\r\n";
$this->console("sending this response to the client #{$client->get_id()}:\r\n".$upgrade);
socket_write($client->get_socket(), $upgrade);
$client->set_handshake(true);
$this->console("Handshake is successfully done!");
return true;
}
else
{
$this->console("WebSocket version 13 is required (the client supports version {$version})");
return false;
}
}
private function get_client_by_socket($socket){
foreach($this->clients as $client)//get all client objects from the pool and check one by one
{
if($client->get_socket() == $socket)//if socket returned from the client matches with parameter
{
$this->console("client found");
return $client;
}
}
return false;//no such client
}
private function connect($socket){
$this->console("creating client...");
$client_id = uniqid();
while(true){
$stream_socket = #stream_socket_client("tcp://127.0.0.1:10000", $errno, $errstr);
if($stream_socket)
{
$this->console("Apollo client created for client #$client_id.");
break;
}
else
{
$this->console("creation failed. Attempting to recreate Apollo client.");
}
}
$client = new Client($client_id, $socket, $stream_socket);
$this->clients[] = $client; //add the socket as client to be tracked
$this->sockets[] = $socket;//add socket as resource to sockets pool
$this->stream_sockets[] = $stream_socket;//add socket as resource to stream sockets pool
$this->console("Client #{$client->get_id()} is successfully created!");
}
private function console($text, $continue = true){
if(!$continue)
{
die($text);
}
if($this->verbose_mode)
{
echo date('[Y-m-d H:i:s] ').$text."\r\n";
}
}
}
That's because you have to accept the new incoming connection using socket_accept(), passing in the socket you've used for socket_listen().
The resulting socket can be used to check for incoming data as expected.
Edit
It seems that you forgot to add $write = $this->sockets; before your call to socket_select().
I figured out that to enable the socket select to detect data from the client, socket server has to send a dummy message to the client first immediately after handshake.
Related
everyone
I'm a new learner with websocket.
I want to deploy my websocket server on GCE.
However, I got some problem.
environment:
Backend:server.php(use php socket function) run on 8080 port
fronend:client.html(simple javascript)
GCE: Ubuntu 18.04, PHP 7.3, apache 2.4.29
without any google cloud sdk (only 2 program and 1 GCE instance)
problem:
Server can work well at my local machine (windows 10), but on GCE websocket will show "1006" error code.
There is a interesting thing that is client.html can work on GCE when I run server.php at local machine.
Is it normal? What is the connection with my local machine and GCE.
Maybe it's not a firewall blocking problem.
I wonder my websocket doesn't run because of the firewall port blocking at first.
I have set the VPC firewall rule with following GCP document and I also disable ufw on GCE.
I change the apache's port from 80 to 8080 and run apache.
It works successfully and I check netstat -an | grep "LISTEN "
I use telnet GCE_IP 8080 at my local machine but it can't connect.
Here is my 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("localhost", "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://127.0.0.1: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>
on GCE VM:
update
internal IP: .3
external IP: .62
1.
netstat -an | grep "LISTEN " to check if you configured internal IP instead of localhost or/and 127.0.0.1
.
I change my internal IP in the server.php and run.
It looks like working perfectly.
2.
Connection check from the GCE VM to GCE_VM_INTERNAL_IP:8080.
.
Connect successfully!
3.
Connection check from you personal computer to
GCE_VM_EXTERNAL_IP:8080. Also, consider to use curl instead of
telnet
.
It seems that handshaking can work.
I can get websocket message from GCE with internal IP and PC with external IP.
So my problem is maybe like in javascript?
It doesn't send the handshaking frame.
I have a socket chat application script in PHP. I'd like to authenticate the users, but I can't get the cookies from the request headers only at the handshake. This is my script below. How can I get the cookies every time, when the client sends a message? The getallheaders function doesn't working, because it doesn't include the cookies. The socket_recv function gives me only the body of the request.
SocketController doesn't important in this case, it makes only error checking, and it has some field declarations.
<?php namespace Models\Messages;
use Controllers\Messages\SocketController;
use Models\Logs\ErrorLog;
use PDO;
class Socket extends SocketController {
public function SocketHandler() {
$this->clientSocketArray = [$this->socketResource];
while (true) {
$newSocketArray = $this->clientSocketArray;
socket_select($newSocketArray, $null, $null, 0, 10);
if (in_array($this->socketResource, $newSocketArray)) {
$newSocket = socket_accept($this->socketResource);
$this->clientSocketArray[] = $newSocket;
$header = socket_read($newSocket, 1024);
$this->DoHandshake($header, $newSocket, self::HOST, self::PORT);
socket_getpeername($newSocket, $client_ip_address);
$connectionACK = $this->NewConnectionACK($client_ip_address);
$this->Send($connectionACK);
$newSocketIndex = array_search($this->socketResource, $newSocketArray);
unset($newSocketArray[$newSocketIndex]);
}
foreach ($newSocketArray as $newSocketArrayResource) {
//$headers = apache_request_headers();
$headers = getallheaders();
file_put_contents("headers.txt", json_encode($headers), FILE_APPEND);
/*//!!!this is the message sender part!!!, I'd like to get the cookies here*/
while(socket_recv($newSocketArrayResource, $socketData, 1024, 0) >= 1) {
$socketMessage = $this->Unseal($socketData);
$messageObj = json_decode($socketMessage, 1);
$senderID = isset($messageObj["senderID"]) ? (int)$messageObj["senderID"] : 0;
$receiverID = isset($messageObj["receiverID"]) ? (int)$messageObj["receiverID"] : 0;
$messageType = isset($messageObj["messageType"]) ? (int)$messageObj["messageType"] : 1;
$message = isset($messageObj["message"]) ? $messageObj["message"] : "nincs üzenet";
$chat_box_message = $this->CreateChatBoxMessage(
$senderID, $receiverID, $messageType, $message
);
$this->Send($chat_box_message);
break 2;
}
$socketData = #socket_read($newSocketArrayResource, 1024, PHP_NORMAL_READ);
if ($socketData === false) {
socket_getpeername($newSocketArrayResource, $client_ip_address);
$connectionACK = $this->ConnectionDisconnectACK($client_ip_address);
$this->Send($connectionACK);
$newSocketIndex = array_search($newSocketArrayResource, $this->clientSocketArray);
unset($this->clientSocketArray[$newSocketIndex]);
}
}
}
socket_close($this->socketResource);
}
function Send($message) {
// global $clientSocketArray;
$messageLength = strlen($message);
foreach($this->clientSocketArray as $clientSocket) {
#socket_write($clientSocket, $message, $messageLength);
}
return true;
}
function Unseal($socketData) {
$length = ord($socketData[1]) & 127;
if($length == 126) {
$masks = substr($socketData, 4, 4);
$data = substr($socketData, 8);
}
else if($length == 127) {
$masks = substr($socketData, 10, 4);
$data = substr($socketData, 14);
}
else {
$masks = substr($socketData, 2, 4);
$data = substr($socketData, 6);
}
$socketData = "";
for ($i = 0; $i < strlen($data); ++$i) {
$socketData .= $data[$i] ^ $masks[$i%4];
}
return $socketData;
}
function Seal($socketData) {
$b1 = 0x80 | (0x1 & 0x0f);
$length = strlen($socketData);
if($length <= 125)
$header = pack('CC', $b1, $length);
elseif($length > 125 && $length < 65536)
$header = pack('CCn', $b1, 126, $length);
elseif($length >= 65536)
$header = pack('CCNN', $b1, 127, $length);
return $header.$socketData;
}
function DoHandshake($receivedHeader, $clientSocketResource, $hostName, $port) {
$headers = array();
$lines = preg_split("/\r\n/", $receivedHeader);
foreach($lines as $line)
{
$line = chop($line);
if(preg_match('/\A(\S+): (.*)\z/', $line, $matches))
{
$headers[$matches[1]] = $matches[2];
}
}
$secKey = $headers['Sec-WebSocket-Key'];
$secAccept = base64_encode(pack('H*', sha1($secKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
$buffer = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" .
"Upgrade: websocket\r\n" .
"Connection: Upgrade\r\n" .
"WebSocket-Origin: {$hostName}/maganoktatas\r\n" .
"WebSocket-Location: ws://$hostName:$port/maganoktatas/app/ajax/sockeg_messages.php\r\n".
"Sec-WebSocket-Accept:$secAccept\r\n\r\n";
socket_write($clientSocketResource, $buffer, strlen($buffer));
}
function NewConnectionACK($clientIP) {
$message = 'New client ' . $clientIP.' joined';
$messageArray = [
"messageType"=>5,
"message"=>$message
];
$ACK = $this->Seal(json_encode($messageArray));
return $ACK;
}
function ConnectionDisconnectACK($clientIP) {
$message = 'Client ' . $clientIP.' disconnected';
$messageArray = [
"messageType"=>5,
"message"=>$message
];
$ACK = $this->Seal(json_encode($messageArray));
return $ACK;
}
public function GetUserName(string $friendID):string {
$stmt = $this->conn->prepare("SELECT CONCAT(LastName, ' ' , FirstName) as FullName
FROM users WHERE UserID = ?");
$stmt->execute([
$friendID
]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return isset($row["FullName"]) ? $row["FullName"] : "";
}
function CreateChatBoxMessage(int $senderID, int $receiverID, int $messageType,
string $message):string {
$messageArray = [
"messageType"=>$messageType,
"message"=>htmlspecialchars($message),
"receiverID"=>$receiverID,
"senderID"=>$senderID,
"senderName"=>$this->GetUserName($senderID)
];
$chatMessage = $this->Seal(json_encode($messageArray));
return $chatMessage;
}
function __destruct() {
//
}
}
?>
You can't and you don't need.
Browser is sending cookie only during hanshake when connection is made by http protocol. After hanshake, the protocol changes from http to websocket. Your client is connected by tcp connection, so when client sends message you already know from who is this message. Simply save your client data (id, login or sth) together with socket handler after acceppting connection.
I have used WebSocket in PHP. but somehow it will close after some time(so many requests).
Is it anyway to handle it?
We got following error when we try to run it after stop:
socket_bind(): unable to bind address [10013]: An attempt was made to
access a socket in a way forbidden by its access permissions.
in C:\wamp64\www\Websocket\server.php on line 52
Warning: socket_listen(): unable to listen on socket [10022]: An invalid argument was supplied.
in C:\wamp64\www\Websocket\server.php on line 55
Here is my code:
<?php
set_time_limit(0);
error_reporting(E_ALL);
//include 'Triangulate.php';
$filename = 'logs.txt';
class Values
{
public $distance;
public $rasp_id;
public $rssi;
}
$host = '10.37.54.167'; //host
$port = '9000'; //port
$null = null; //null var
$rasp = array(
47 => array(17, 0),
124 => array(1,6.4),
43 => array(1,0),
44 => array(4.6,2));
//Create TCP/IP sream socket
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
//reuseable port
socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1);
//bind socket to specified host
socket_bind($socket, 0, $port);
//listen to port
socket_listen($socket);
//create & add listning socket to the list
$clients = array($socket);
$distance = array();
$totalbicken = array(array());
//start endless loop, so that our script doesn't stop
while (true) {
//manage multipal connections
$changed = $clients;
//returns the socket resources in $changed array
socket_select($changed, $null, $null, 0, 10);
$fp = fopen($filename, 'a+');
//check for new socket
if (in_array($socket, $changed)) {
$socket_new = socket_accept($socket); //accpet new socket
$clients[] = $socket_new; //add socket to client array
$header = socket_read($socket_new, 1024); //read data sent by the socket
perform_handshaking($header, $socket_new, $host, $port); //perform websocket handshake
socket_getpeername($socket_new, $ip); //get ip address of connected socket
$response = mask(json_encode(array('type' => 'system', 'message' => $ip . ' connected'))); //prepare json data
fwrite ($fp,$response . "\n");
// send_message($response); //notify all users about new connection
// send_message_socket($response, $socket_new);
//make room for new socket
$found_socket = array_search($socket, $changed);
unset($changed[$found_socket]);
}
//loop through all connected sockets
foreach ($changed as $changed_socket) {
//check for any incomming data
try {
while (socket_recv($changed_socket, $buf, 1024, 0) >= 1) {
$received_text = unmask($buf); //Unmask Recive data.
fwrite ($fp,"Text receive:" . $received_text . "\n");
$tst_msg = json_decode($received_text); //json decode.
fwrite ($fp,"Json Decord!". "\n");
if (!isset($tst_msg->action)) {
} else {
$user_action = $tst_msg->action; //sender name.
if ($user_action == "distance") { //If action is distance.
$rasp_id = $tst_msg->rasp_id; //get raspberry uniq id.
$beacon_id = $tst_msg->beacon_id; //get beacon id
$distance_temp = $tst_msg->distance; // get distance
$rssi = $tst_msg->rssi; //get RSSI.
fwrite ($fp,isset($distance[$beacon_id]). "\n");
fwrite ($fp,"Get all values!". "\n");
// echo mask(json_encode(array('type'=>'message', 'action'=>'distance', 'beacon_id'=>$beacon_id, 'rasp_id'=>$rasp_id, 'distance'=>$distance)));
if (isset($distance[$beacon_id])) {
$totalbicken[$beacon_id] = count($distance[$beacon_id]); //get total distance of sender beacon
} else {
$totalbicken[$beacon_id] = 0;
}
if (isset($rasp[$rasp_id])) {
echo "\nRequest from: " . $rasp_id;
$myVal = new Values();
$myVal->distance = $distance_temp;
$myVal->rasp_id = $rasp_id;
$myVal->rssi = $rssi;
$flag = false;
if (isset($distance[$beacon_id])) {
if (count($distance[$beacon_id]) > 0) {
for ($i = 0; $i < count($distance[$beacon_id]); $i++) {
$myTempVal = $distance[$beacon_id][$i];
if (($myTempVal->rasp_id) == $rasp_id) {
$flag = true;
$distance[$beacon_id][$i] = $myVal;
break;
}
}
if (!$flag) {
$distance[$beacon_id][$totalbicken[$beacon_id]] = $myVal;
}
} else {
$distance[$beacon_id][$totalbicken[$beacon_id]] = $myVal;
}
} else {
$distance[$beacon_id][$totalbicken[$beacon_id]] = $myVal;
}
if (count($distance[$beacon_id]) > 2) {
/*$data = triangulate($rasp[$distance[$beacon_id][0]->rasp_id], $distance[$beacon_id][0]->distance,
$rasp[$distance[$beacon_id][1]->rasp_id], $distance[$beacon_id][1]->distance,
$rasp[$distance[$beacon_id][2]->rasp_id], $distance[$beacon_id][2]->distance);*/
fwrite ($fp,"Before Triangulation!". "\n");
$data = getTrilateration($rasp[$distance[$beacon_id][0]->rasp_id], $distance[$beacon_id][0]->distance,
$rasp[$distance[$beacon_id][1]->rasp_id], $distance[$beacon_id][1]->distance,
$rasp[$distance[$beacon_id][2]->rasp_id], $distance[$beacon_id][2]->distance);
fwrite ($fp,"End Triangulation!". "\n");
$response_text = mask(json_encode(array('type' => 'message', 'action' => 'distance',
'beacon_id' => $beacon_id, 'distance' => $distance[$beacon_id], 'coordinates' => $data)));
fwrite ($fp,"Response: ". $response_text . "\n");
send_message($response_text);
}
}
}
fwrite ($fp,"Break!: ". "\n");
break 2; //exist this loop*/
}
}
} catch (Exception $e) {
fwrite ($fp,"Exception: done! \n");
fwrite ($fp,"Exception: ". $e->getMessage(). "\n");
echo "Exception:" . $e->getMessage();
}
$buf = #socket_read($changed_socket, 1024, PHP_NORMAL_READ);
if ($buf === false) { // check disconnected client
// remove client for $clients array
try {
$found_socket = array_search($changed_socket, $clients);
socket_getpeername($changed_socket, $ip);
unset($clients[$found_socket]);
//notify all users about disconnected connection
$response = mask(json_encode(array('type' => 'system', 'message' => $ip . ' disconnected')));
send_message($response);
} catch (Exception $e) {
echo "Exception:" . $e;
}
}
}
}
// close the listening socket
socket_close($socket);
$fclose ($fp);
function send_message($msg)
{
global $clients;
foreach ($clients as $changed_socket) {
#socket_write($changed_socket, $msg, strlen($msg));
}
return true;
}
function send_message_socket($msg, $changed_socket)
{
#socket_write($changed_socket, $msg, strlen($msg));
return true;
}
//Unmask incoming framed message
function unmask($text)
{
$length = ord($text[1]) & 127;
if ($length == 126) {
$masks = substr($text, 4, 4);
$data = substr($text, 8);
} elseif ($length == 127) {
$masks = substr($text, 10, 4);
$data = substr($text, 14);
} else {
$masks = substr($text, 2, 4);
$data = substr($text, 6);
}
$text = "";
for ($i = 0; $i < strlen($data); ++$i) {
$text .= $data[$i] ^ $masks[$i % 4];
}
return $text;
}
//Encode message for transfer to client.
function mask($text)
{
$b1 = 0x80 | (0x1 & 0x0f);
$length = strlen($text);
if ($length <= 125) {
$header = pack('CC', $b1, $length);
} elseif ($length > 125 && $length < 65536) {
$header = pack('CCn', $b1, 126, $length);
} elseif ($length >= 65536) {
$header = pack('CCNN', $b1, 127, $length);
}
return $header . $text;
}
//handshake new client.
function perform_handshaking($receved_header, $client_conn, $host, $port)
{
$headers = array();
$lines = preg_split("/\r\n/", $receved_header);
foreach ($lines as $line) {
$line = chop($line);
if (preg_match('/\A(\S+): (.*)\z/', $line, $matches)) {
$headers[$matches[1]] = $matches[2];
}
}
$secKey = $headers['Sec-WebSocket-Key'];
$secAccept = base64_encode(pack('H*', sha1($secKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
//hand shaking header
$upgrade = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" .
"Upgrade: websocket\r\n" .
"Connection: Upgrade\r\n" .
"WebSocket-Origin: $host\r\n" .
"WebSocket-Location: ws://$host:$port/Websocket/shout.php\r\n" .
"Sec-WebSocket-Accept:$secAccept\r\n\r\n";
socket_write($client_conn, $upgrade, strlen($upgrade));
}
Thanks in advance!
I have setup Linux environment and test. It is working fine. It might a firewall issue in windows.
As per my knowledge, the newer version of windows (after service pack-3) has some of the RPC rules for connection (Limit of HTTP requests and connection in second) so, that might be an issue.
Please read this.
As of now, change the operating system is the one of a solution.
Thanks & Regards
Jignesh M. Mehta
Is there any way (other than checking for ping responses) to detect when a client stream (I don't know if a stream would be any different from sockets) becomes unavailable (ie there is no longer any connection but no clean disconnection was made)?
Using this code:
#!/usr/bin/env php
<?php
$socket = stream_socket_server(
'tcp://192.168.1.1:47700',
$errno,
$errstr,
STREAM_SERVER_BIND|STREAM_SERVER_LISTEN,
stream_context_create(
array(),
array()
)
);
if (!$socket) {
echo 'Could not listen on socket'."\n";
}
else {
$clients = array((int)$socket => $socket);
$last = time();
while(true) {
$read = $clients;
$write = null;
$ex = null;
stream_select(
$read,
$write,
$ex,
5
);
foreach ($read as $sock) {
if ($sock === $socket) {
echo 'Incoming on master...'."\n";
$client = stream_socket_accept(
$socket,
5
);
if ($client) {
stream_set_timeout($client, 1);
$clients[(int)$client] = $client;
}
}
else {
echo 'Incoming on client '.((int)$sock)."...\n";
$length = 1400;
$remaining = $length;
$buffer = '';
$metadata['unread_bytes'] = 0;
do {
if (feof($sock)) {
break;
}
$result = fread($sock, $length);
if ($result === false) {
break;
}
$buffer .= $result;
if (feof($sock)) {
break;
}
$continue = false;
if (strlen($result) == $length) {
$continue = true;
}
$metadata = stream_get_meta_data($sock);
if ($metadata && isset($metadata['unread_bytes']) && $metadata['unread_bytes']) {
$continue = true;
$length = $metadata['unread_bytes'];
}
} while ($continue);
if (strlen($buffer) === 0 || $buffer === false) {
echo 'Client disconnected...'."\n";
stream_socket_shutdown($sock, STREAM_SHUT_RDWR);
unset($clients[(int)$sock]);
}
else {
echo 'Received: '.$buffer."\n";
}
echo 'There are '.(count($clients) - 1).' clients'."\n";
}
}
if ($last < (time() - 5)) {
foreach ($clients as $id => $client) {
if ($client !== $socket) {
$text = 'Yippee!';
$ret = fwrite($client, $text);
if ($ret !== strlen($text)) {
echo 'There seemed to be an error sending to the client'."\n";
}
}
}
}
}
}
if ($socket) {
stream_socket_shutdown($socket, STREAM_SHUT_RDWR);
}
and a sockets client on a different computer, I can connect to the server, send and receive data, and disconnect cleanly and everything functions as expected. If, however, I pull the network connection on the client computer, nothing is detected on the server side - the server keeps on listening to the client socket, and also writes to it without any error manifesting itself.
As I understand it, calling feof($stream) will tell you if the remote socket disconnected, but I'm not absolutely certain about that. I'm using ping/pong myself while continuing to research a solution.
You need to set a socket timeout, in which case you get an error if a client does not respond in a timely fashion.
Check PHP's stream_set_timeout function:
http://www.php.net/manual/en/function.stream-set-timeout.php
Also check socket_set_option:
http://php.net/manual/en/function.socket-set-option.php
and finally, check out this great article on how to use sockets in PHP effectively:
"PHP Socket Programming, done the Right Way™"
http://christophh.net/2012/07/24/php-socket-programming/
I wrote a WebSocket Server in PHP, which connects to Firefox well but not to Safari. I think, there's something wrong with the handshake. Safari needs the older draft hybi00... but I can't find the mistake :/
The connection closed in Safari directly after trying to connect.
Thanks in advance for your help!
<?php
error_reporting(E_ALL);
set_time_limit(0);
date_default_timezone_set("Europe/Berlin");
ob_implicit_flush(true);
function debug($text)
{
$file = "log.html";
file_put_contents($file,
"<pre>".$text."</pre>"."<b>Ende der Information</b><p />", 8);
}
//Socket initialisieren
$master = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($master, SOL_SOCKET, SO_REUSEADDR, 1);
//An diesen Computer binden
socket_bind($master, "localhost", 10000) or die();
//Maximale Verbindungsanforderungen: 5
socket_listen($master, 5);
do
{
$client = socket_accept($master);
echo "Neuer Client!\n";
new Client($client);
} while(true);
exit();
//Clientklasse
class Client
{
private $connection;
private $header = array();
private $crypt; //BOOL, hybi00 needs no en-/decryption but hybi06+
function __construct(&$socket)
{
$this->connection = $socket;
$this->SetWebSecKey();
while(true)
{
$q = socket_read($this->connection, 1024);
if(isset($q))
{
echo $this->unmask($q);
}
$welcome = "\x81\x8c\xff\xb8\xbd\xbd\xb7\xdd\xd1\xd1\x90\x98\xea\xd2\x8d\xd4\xd9\x9c";
}
}
/**
* Unmask a received payload
* #param $payload
*
* http://srchea.com/blog/2011/12/build-a-real-time-application-using-html5-websockets/
*/
private function unmask($payload) {
$length = ord($payload[1]) & 127;
if($length == 126) {
$masks = substr($payload, 4, 4);
$data = substr($payload, 8);
}
elseif($length == 127) {
$masks = substr($payload, 10, 4);
$data = substr($payload, 14);
}
else {
$masks = substr($payload, 2, 4);
$data = substr($payload, 6);
}
$text = '';
for ($i = 0; $i < strlen($data); ++$i) {
$text .= $data[$i] ^ $masks[$i%4];
}
return $text;
}
/**
* Encode a text for sending to clients via ws://
* #param $text
*
* http://srchea.com/blog/2011/12/build-a-real-time-application-using-html5-websockets/
*/
private function encode($text)
{
// 0x1 text frame (FIN + opcode)
$b1 = 0x80 | (0x1 & 0x0f);
$length = strlen($text);
if($length <= 125)
$header = pack('CC', $b1, $length);
elseif($length > 125 && $length < 65536)
$header = pack('CCS', $b1, 126, $length);
elseif($length >= 65536)
$header = pack('CCN', $b1, 127, $length);
return $header.$text;
}
private function SetWebSecKey()
{
//Headerrequest in Array teilen
$request = socket_read($this->connection, 1024);
$request = explode("\n", $request);
foreach($request as $key=>$head)
{
$header = explode(":", $head, 2);
$header[0] = trim($header[0]); //Parameter
$header[1] = trim($header[1]); //Wert
$this->header[$header[0]] = $header[1];
}
$handshake = new Handshake($this->header);
$handshake->Send($this->connection);
}
private function SendPlain($text)
{
socket_write($this->connection, $text, strlen($text));
}
}
class Handshake
{
private $header;
private $hybi00;
private $secKey;
public function __construct($headerArray)
{
echo "\nHandshake wird ausgeführt: ";
//Einfacher oder doppelter SecWebKey?
//Einfach: Hybi06+
//Doppelt: Hybi00+
$this->header = $headerArray;
if(isset($this->header["Sec-WebSocket-Key"]))
{
echo "Hybi06+\n";
//Moderner Hybi06+
$this->secKey = $this->InitHybi06();
} elseif(isset($this->header["Sec-WebSocket-Key1"])) {
//Älterer Hybi00+
echo "Hybi00+\n";
$this->secKey = $this->InitHybi00();
} else {
echo "Fehler - Unbekanntes WebSocket-Protokoll\n";
return;
}
}
private function InitHybi06()
{
$this->hybi00 = false;
$key = $this->header["Sec-WebSocket-Key"];
$GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
$key .= $GUID;
return base64_encode(sha1($key, true));
}
private function InitHybi00()
{
$this->hybi00 = true;
$key1 = $this->header["Sec-WebSocket-Key1"];
$key2 = $this->header["Sec-WebSocket-Key2"];
$key1 = $this->Hybi00NumSpace($key1);
$key2 = $this->Hybi00NumSpace($key2);
$data = array_keys($this->header);
$data = $data[count($this->header)-1];
$ctx = hash_init('md5');
hash_update($ctx, pack("N", $key1));
hash_update($ctx, pack("N", $key2));
hash_update($ctx, $data);
$hash_data = hash_final($ctx,true);
return $hash_data;
}
private function Hybi00NumSpace($key)
{
$numbrs = preg_replace('/[^\d]*/', '', $key);
$spaces = strlen(preg_replace('/[^\s][^\s]*/', '', $key));
return $numbrs/$spaces;
}
public function Send($socket, $origin="http://localhost", $location="ws://localhost", $return=false)
{
if(!isset($this->header) || !isset($this->hybi00) || !isset($this->secKey))
{
echo "Fehler: Handshake nicht vollständig!\n"; return;
}
$header = $this->CreateHeader($origin, $location);
debug($header);
$header = explode("\n", $header);
if($return)
return $header;
foreach($header as $line)
{
$line .= "\n";
socket_write($socket, $line, strlen($line));
}
}
private function CreateHeader($origin, $location)
{
$header = "HTTP/1.1 101 WebSocket Protocol Handshake\r\n".
"Upgrade: WebSocket\r\n".
"Connection: Upgrade\r\n".
"Sec-WebSocket-Origin: $origin\r\n";
if($this->hybi00)
{
$header .= "Sec-WebSocket-Location: $location\r\n";
$header .= "\r\n".$this->secKey.chr(0);
} else {
$header .= "Sec-WebSocket-Accept: ".$this->secKey."\r\n\r\n";
}
return $header;
}
}
I dont know how to do this in your code, you can look at PHPDaemon - its non blocking server writing on php, with websocket support.