WIP WebSocket Server for Pathfinder.
- Enables Map data push to clients (Browser)
This commit is contained in:
30
README.md
30
README.md
@@ -1,9 +1,27 @@
|
||||
### Prototype ChatApp
|
||||
### [WIP] WebSocket server for [Pathfinder](https://github.com/exodus4d/pathfinder)
|
||||
|
||||
**Demo:**
|
||||
- https://www.test.pathfinder-w.space/
|
||||
####Install:
|
||||
1. Install [Composer](https://getcomposer.org/download/)
|
||||
2. Install Composer dependencies from `composer.json` file:
|
||||
- `php composer.phar install` (edit composer.phar path to your Composer installation)
|
||||
3. Start WebSocket server `php cmd.php`
|
||||
|
||||
**Info:**
|
||||
####Default Configuration
|
||||
**Clients (WebBrowser) listen for connections**
|
||||
- Host:`0.0.0.0.` (=> any client can connect)
|
||||
- Port:`8020`
|
||||
- URI:`127.0.0.1:8020` (Your WebServer (e.g. Nginx) should pass all WebSocket connections to this source)
|
||||
|
||||
**TCP Socket connection (Internal use fore WebServer <=> WebSocket communication)**
|
||||
- Host:`127.0.0.1` (=> Assumed WebServer and WebSocket Server running on the same machine)
|
||||
- Port:`5555`
|
||||
- URI: `tcp://127.0.0.1:5555`
|
||||
|
||||
**[Optional]**
|
||||
The default configuration should be fine for most installations.
|
||||
You can change/overwrite the default **Host** and **Port** configuration by adding additional CLI parameters when starting the WebSocket server:
|
||||
|
||||
`php cmd.php --pf_listen_host [CLIENTS_HOST] --pf_listen_port [CLIENTS_PORT] --pf_host [TCP_HOST] --pf_port [TCP_PORT]`
|
||||
|
||||
####Info:
|
||||
- [*Ratchet*](http://socketo.me/) - "WebSockets for PHP"
|
||||
- [*Web Sockets API*](https://www.w3.org/TR/2009/WD-websockets-20091222/)
|
||||
- [*Web Workers*](https://www.w3.org/TR/workers/) - (SharedWorker)
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
<?php
|
||||
namespace Exodus4D\Socket\Main;
|
||||
|
||||
|
||||
use Ratchet\MessageComponentInterface;
|
||||
use Ratchet\ConnectionInterface;
|
||||
|
||||
class Chat implements MessageComponentInterface {
|
||||
protected $clients;
|
||||
|
||||
public function __construct() {
|
||||
$this->clients = new \SplObjectStorage;
|
||||
echo "__construct() \n";
|
||||
}
|
||||
|
||||
public function onOpen(ConnectionInterface $conn) {
|
||||
//store the new connection
|
||||
$this->clients->attach($conn);
|
||||
echo "NEW connection! ({$conn->resourceId})\n";
|
||||
}
|
||||
|
||||
public function onMessage(ConnectionInterface $from, $msg) {
|
||||
$numRecv = count($this->clients) - 1;
|
||||
echo sprintf('Connection %d sending to %d other connection%s' . "\n"
|
||||
, $from->resourceId, $numRecv, $numRecv == 1 ? '' : 's');
|
||||
|
||||
foreach ($this->clients as $client) {
|
||||
// if ($from !== $client) {
|
||||
$client->send($msg);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
public function onClose(ConnectionInterface $conn) {
|
||||
$this->clients->detach($conn);
|
||||
|
||||
echo "Connection {$conn->resourceId} has disconnected\n";
|
||||
}
|
||||
|
||||
public function onError(ConnectionInterface $conn, \Exception $e) {
|
||||
echo "An error has occurred: {$e->getMessage()}\n";
|
||||
|
||||
$conn->close();
|
||||
}
|
||||
}
|
||||
494
app/Main/mapupdate.php
Normal file
494
app/Main/mapupdate.php
Normal file
@@ -0,0 +1,494 @@
|
||||
<?php
|
||||
/**
|
||||
* Created by PhpStorm.
|
||||
* User: Exodus
|
||||
* Date: 02.12.2016
|
||||
* Time: 22:29
|
||||
*/
|
||||
|
||||
namespace Exodus4D\Socket\Main;
|
||||
|
||||
use Ratchet\MessageComponentInterface;
|
||||
use Ratchet\ConnectionInterface;
|
||||
|
||||
class MapUpdate implements MessageComponentInterface {
|
||||
|
||||
/**
|
||||
* expire time for map access tokens (seconds)
|
||||
* @var int
|
||||
*/
|
||||
protected $mapAccessExpireSeconds = 30;
|
||||
|
||||
/**
|
||||
* character access tokens for clients
|
||||
* -> tokens are unique and expire onSubscribe!
|
||||
* @var
|
||||
*/
|
||||
|
||||
protected $characterAccessData;
|
||||
/**
|
||||
* access tokens for clients grouped by mapId
|
||||
* -> tokens are unique and expire onSubscribe!
|
||||
* @var
|
||||
*/
|
||||
protected $mapAccessData;
|
||||
|
||||
/**
|
||||
* connected characters
|
||||
* @var
|
||||
*/
|
||||
protected $characters;
|
||||
/**
|
||||
* valid client connections subscribed to maps
|
||||
* @var array
|
||||
*/
|
||||
protected $subscriptions;
|
||||
|
||||
/**
|
||||
* internal socket for response calls
|
||||
* @var null | \React\ZMQ\SocketWrapper
|
||||
*/
|
||||
protected $internalSocket = null;
|
||||
|
||||
public function __construct($socket) {
|
||||
$this->internalSocket = $socket;
|
||||
$this->characterAccessData = [];
|
||||
$this->mapAccessData = [];
|
||||
$this->characters = [];
|
||||
$this->subscriptions = [];
|
||||
|
||||
echo "__construct() \n";
|
||||
}
|
||||
|
||||
public function onOpen(ConnectionInterface $conn) {
|
||||
echo "NEW connection! ({$conn->resourceId})\n";
|
||||
}
|
||||
|
||||
public function onMessage(ConnectionInterface $conn, $msg) {
|
||||
$msg = json_decode($msg, true);
|
||||
|
||||
if(
|
||||
isset($msg['task']) &&
|
||||
isset($msg['load'])
|
||||
){
|
||||
$task = $msg['task'];
|
||||
$load = $msg['load'];
|
||||
|
||||
switch($task){
|
||||
case 'subscribe':
|
||||
$this->subscribe($conn, $load);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function onClose(ConnectionInterface $conn) {
|
||||
$this->unSubscribeConnection($conn);
|
||||
|
||||
echo "Connection {$conn->resourceId} has disconnected\n";
|
||||
}
|
||||
|
||||
public function onError(ConnectionInterface $conn, \Exception $e) {
|
||||
$this->unSubscribeConnection($conn);
|
||||
$conn->close();
|
||||
|
||||
echo "An error has occurred: {$e->getMessage()}\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* subscribes a connection to valid accessible maps
|
||||
* @param ConnectionInterface $conn
|
||||
* @param $subscribeData
|
||||
*/
|
||||
private function subscribe(ConnectionInterface $conn, $subscribeData){
|
||||
$characterId = (int)$subscribeData['id'];
|
||||
$characterToken = $subscribeData['token'];
|
||||
|
||||
if($characterId && $characterToken){
|
||||
|
||||
// check if character access token is valid (exists and not expired in $this->characterAccessData
|
||||
if( $this->checkCharacterAccess($characterId, $characterToken) ){
|
||||
$this->characters[$characterId][$conn->resourceId] = $conn;
|
||||
|
||||
// valid character -> check map access
|
||||
foreach((array)$subscribeData['mapData'] as $data){
|
||||
$mapId = (int)$data['id'];
|
||||
$mapToken = $data['token'];
|
||||
|
||||
if($mapId && $mapToken){
|
||||
// check if token is valid (exists and not expired) in $this->mapAccessData
|
||||
if( $this->checkMapAccess($characterId, $mapId, $mapToken) ){
|
||||
// valid map subscribe request
|
||||
$this->subscriptions[$mapId][$characterId] = $characterId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* subscribes an active connection from maps
|
||||
* @param ConnectionInterface $conn
|
||||
*/
|
||||
private function unSubscribeConnection(ConnectionInterface $conn){
|
||||
$characterIds = $this->getCharacterIdsByConnection($conn);
|
||||
|
||||
foreach($characterIds as $characterId){
|
||||
$this->unSubscribeCharacterId($characterId, $conn);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* unSubscribe a $characterId from ALL maps
|
||||
* -> if $conn is set -> just unSub the $characterId from this $conn
|
||||
* @param int $characterId
|
||||
* @param null $conn
|
||||
* @return bool
|
||||
*/
|
||||
private function unSubscribeCharacterId($characterId, $conn = null){
|
||||
if($characterId){
|
||||
|
||||
// unSub from $this->characters -------------------------------------------------------
|
||||
if($conn){
|
||||
// just unSub a specific connection (e.g. single browser window)
|
||||
$resourceId = $conn->resourceId;
|
||||
if( isset($this->characters[$characterId][$resourceId]) ){
|
||||
unset($this->characters[$characterId][$resourceId]);
|
||||
|
||||
if( !count($this->characters[$characterId]) ){
|
||||
// no connection left for this character
|
||||
unset($this->characters[$characterId]);
|
||||
}
|
||||
}
|
||||
}else{
|
||||
// unSub ALL connections from a character (e.g. multiple browsers)
|
||||
if( isset($this->characters[$characterId]) ){
|
||||
unset($this->characters[$characterId]);
|
||||
}
|
||||
}
|
||||
|
||||
// unSub from $this->subscriptions ----------------------------------------------------
|
||||
foreach($this->subscriptions as $mapId => $characterIds){
|
||||
if(array_key_exists($characterId, $characterIds)){
|
||||
unset($this->subscriptions[$mapId][$characterId]);
|
||||
|
||||
if( !count($this->subscriptions[$mapId]) ){
|
||||
// no characters left on this map
|
||||
unset($this->subscriptions[$mapId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* delete mapId from subscriptions and broadcast "delete msg" to clients
|
||||
* @param string $task
|
||||
* @param int $mapId
|
||||
* @return int
|
||||
*/
|
||||
private function deleteMapId($task, $mapId){
|
||||
$connectionCount = $this->broadcastMapData($task, $mapId, $mapId);
|
||||
|
||||
// remove map from subscriptions
|
||||
if( isset($this->subscriptions[$mapId]) ){
|
||||
unset($this->subscriptions[$mapId]);
|
||||
}
|
||||
|
||||
return $connectionCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ConnectionInterface $conn
|
||||
* @return int[]
|
||||
*/
|
||||
private function getCharacterIdsByConnection(ConnectionInterface $conn){
|
||||
$characterIds = [];
|
||||
$resourceId = $conn->resourceId;
|
||||
|
||||
foreach($this->characters as $characterId => $resourceIDs){
|
||||
if(
|
||||
array_key_exists($resourceId, $resourceIDs) &&
|
||||
!in_array($characterId, $characterIds)
|
||||
){
|
||||
$characterIds[] = $characterId;
|
||||
}
|
||||
}
|
||||
return $characterIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $mapId
|
||||
* @return array
|
||||
*/
|
||||
private function getCharacterIdsByMapId($mapId){
|
||||
$characterIds = [];
|
||||
|
||||
if( !empty($this->subscriptions[$mapId]) ){
|
||||
$characterIds = array_values( (array)$this->subscriptions[$mapId]);
|
||||
}
|
||||
|
||||
return $characterIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* get connection objects by characterIds
|
||||
* @param int[] $characterIds
|
||||
* @return \SplObjectStorage
|
||||
*/
|
||||
private function getConnectionsByCharacterIds($characterIds){
|
||||
$connections = new \SplObjectStorage;
|
||||
|
||||
foreach($characterIds as $characterId){
|
||||
if($charConnections = (array)$this->characters[$characterId] ){
|
||||
foreach($charConnections as $conn){
|
||||
if( !$connections->contains($conn) ){
|
||||
$connections->attach($conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $connections;
|
||||
}
|
||||
|
||||
/**
|
||||
* check character access against $this->characterAccessData whitelist
|
||||
* @param $characterId
|
||||
* @param $characterToken
|
||||
* @return bool
|
||||
*/
|
||||
private function checkCharacterAccess($characterId, $characterToken){
|
||||
$access = false;
|
||||
if( !empty($characterAccessData = (array)$this->characterAccessData[$characterId]) ){
|
||||
foreach($characterAccessData as $i => $data){
|
||||
$deleteToken = false;
|
||||
// check expire for $this->characterAccessData -> check ALL characters and remove expired
|
||||
if( ((int)$data['expire'] - time()) > 0 ){
|
||||
// still valid -> check token
|
||||
if($characterToken === $data['token']){
|
||||
$access = true;
|
||||
$deleteToken = true;
|
||||
}
|
||||
}else{
|
||||
// token expired
|
||||
$deleteToken = true;
|
||||
}
|
||||
|
||||
if($deleteToken){
|
||||
unset($this->characterAccessData[$characterId][$i]);
|
||||
// -> check if tokens for this charId is empty
|
||||
if( empty($this->characterAccessData[$characterId]) ){
|
||||
unset($this->characterAccessData[$characterId]);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $access;
|
||||
}
|
||||
|
||||
/**
|
||||
* check map access against $this->mapAccessData whitelist
|
||||
* @param $characterId
|
||||
* @param $mapId
|
||||
* @param $mapToken
|
||||
* @return bool
|
||||
*/
|
||||
private function checkMapAccess($characterId, $mapId, $mapToken){
|
||||
$access = false;
|
||||
if( !empty($mapAccessData = (array)$this->mapAccessData[$mapId][$characterId]) ){
|
||||
foreach($mapAccessData as $i => $data){
|
||||
$deleteToken = false;
|
||||
// check expire for $this->mapAccessData -> check ALL characters and remove expired
|
||||
if( ((int)$data['expire'] - time()) > 0 ){
|
||||
// still valid -> check token
|
||||
if($mapToken === $data['token']){
|
||||
$access = true;
|
||||
$deleteToken = true;
|
||||
}
|
||||
}else{
|
||||
// token expired
|
||||
$deleteToken = true;
|
||||
}
|
||||
|
||||
if($deleteToken){
|
||||
unset($this->mapAccessData[$mapId][$characterId][$i]);
|
||||
// -> check if tokens for this charId is empty
|
||||
if( empty($this->mapAccessData[$mapId][$characterId]) ){
|
||||
unset($this->mapAccessData[$mapId][$characterId]);
|
||||
// -> check if map has no access tokens left for characters
|
||||
if( empty($this->mapAccessData[$mapId]) ){
|
||||
unset($this->mapAccessData[$mapId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $access;
|
||||
}
|
||||
|
||||
/**
|
||||
* broadcast data ($load) to $connections
|
||||
* @param ConnectionInterface[] | \SplObjectStorage $connections
|
||||
* @param $task
|
||||
* @param $load
|
||||
* @param int[] $characterIds optional, recipients (e.g if multiple browser tabs are open)
|
||||
*/
|
||||
private function broadcastData($connections, $task, $load, $characterIds = []){
|
||||
$response = [
|
||||
'task' => $task,
|
||||
'characterIds' => $characterIds,
|
||||
'load' => $load
|
||||
];
|
||||
|
||||
foreach($connections as $conn){
|
||||
$conn->send( json_encode($response) );
|
||||
}
|
||||
}
|
||||
|
||||
// custom calls ===============================================================================
|
||||
|
||||
/**
|
||||
* receive data from TCP socket (main App)
|
||||
* -> send response back
|
||||
* @param $data
|
||||
*/
|
||||
public function receiveData($data){
|
||||
$data = (array)json_decode($data, true);
|
||||
$load = $data['load'];
|
||||
$task = $data['task'];
|
||||
$response = false;
|
||||
|
||||
switch($data['task']){
|
||||
case 'characterLogout':
|
||||
$response = $this->unSubscribeCharacterId($load);
|
||||
break;
|
||||
case 'mapConnectionAccess':
|
||||
$response = $this->setConnectionAccess($load);
|
||||
break;
|
||||
case 'mapAccess':
|
||||
$response = $this->setAccess($task, $load);
|
||||
break;
|
||||
case 'mapUpdate':
|
||||
$response = $this->broadcastMapUpdate($task, $load);
|
||||
break;
|
||||
case 'mapDeleted':
|
||||
$response = $this->deleteMapId($task, $load);
|
||||
break;
|
||||
case 'healthCheck':
|
||||
$response = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
$this->internalSocket->send($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $task
|
||||
* @param array $mapData
|
||||
* @return int
|
||||
*/
|
||||
private function broadcastMapUpdate($task, $mapData){
|
||||
$mapId = (int)$mapData['config']['id'];
|
||||
|
||||
return $this->broadcastMapData($task, $mapId, $mapData);
|
||||
}
|
||||
|
||||
/**
|
||||
* send map data to ALL connected clients
|
||||
* @param string $task
|
||||
* @param int $mapId
|
||||
* @param mixed $load
|
||||
* @return int
|
||||
*/
|
||||
private function broadcastMapData($task, $mapId, $load){
|
||||
$characterIds = $this->getCharacterIdsByMapId($mapId);
|
||||
$connections = $this->getConnectionsByCharacterIds($characterIds);
|
||||
|
||||
$this->broadcastData($connections, $task, $load, $characterIds);
|
||||
return count($connections);
|
||||
}
|
||||
|
||||
/**
|
||||
* set/update map access for allowed characterIds
|
||||
* @param string $task
|
||||
* @param array $accessData
|
||||
* @return int count of connected characters
|
||||
*/
|
||||
private function setAccess($task, $accessData){
|
||||
$NewMapCharacterIds = [];
|
||||
|
||||
if($mapId = (int)$accessData['id']){
|
||||
$characterIds = (array)$accessData['characterIds'];
|
||||
$currentMapCharacterIds = array_values((array)$this->subscriptions[$mapId]);
|
||||
|
||||
// check all charactersIds that have access... ----------------------------------------
|
||||
foreach($characterIds as $characterId){
|
||||
// ... for it least ONE active connection ...
|
||||
if( !empty($this->characters[$characterId]) ){
|
||||
// ... add characterId to new subscriptions for a map
|
||||
$NewMapCharacterIds[$characterId] = $characterId;
|
||||
}
|
||||
}
|
||||
|
||||
// broadcast "map delete" to no longer valid characters -------------------------------
|
||||
$removedMapCharacterIds = array_diff($currentMapCharacterIds, array_values($NewMapCharacterIds) );
|
||||
$removedMapCharacterConnections = $this->getConnectionsByCharacterIds($removedMapCharacterIds);
|
||||
$this->broadcastData($removedMapCharacterConnections, $task, $mapId, $removedMapCharacterIds);
|
||||
|
||||
// update map subscriptions -----------------------------------------------------------
|
||||
if( !empty($NewMapCharacterIds) ){
|
||||
// set new characters that have map access (overwrites existing subscriptions for that map)
|
||||
$this->subscriptions[$mapId] = $NewMapCharacterIds;
|
||||
}else{
|
||||
// no characters (left) on this map
|
||||
unset($this->subscriptions[$mapId]);
|
||||
}
|
||||
}
|
||||
return count($NewMapCharacterIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* set map access data (whitelist) tokens for map access
|
||||
* @param $connectionAccessData
|
||||
* @return bool
|
||||
*/
|
||||
private function setConnectionAccess($connectionAccessData) {
|
||||
$response = false;
|
||||
$characterId = (int)$connectionAccessData['id'];
|
||||
$characterToken = $connectionAccessData['token'];
|
||||
|
||||
if(
|
||||
$characterId &&
|
||||
$characterToken
|
||||
){
|
||||
// expire time for character and map tokens
|
||||
$expireTime = time() + $this->mapAccessExpireSeconds;
|
||||
|
||||
// tokens for character access
|
||||
$this->characterAccessData[$characterId][] = [
|
||||
'token' => $characterToken,
|
||||
'expire' => $expireTime
|
||||
];
|
||||
|
||||
foreach((array)$connectionAccessData['mapData'] as $mapData){
|
||||
$mapId = (int)$mapData['id'];
|
||||
|
||||
$this->mapAccessData[$mapId][$characterId][] = [
|
||||
'token' => $mapData['token'],
|
||||
'expire' => $expireTime
|
||||
];
|
||||
}
|
||||
|
||||
$response = 'OK';
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -12,23 +12,51 @@ use Ratchet\Server\IoServer;
|
||||
use Ratchet\Http\HttpServer;
|
||||
use Ratchet\WebSocket\WsServer;
|
||||
|
||||
use React;
|
||||
|
||||
class WebSockets {
|
||||
|
||||
function __construct(){
|
||||
$this->startChat();
|
||||
protected $dns;
|
||||
protected $wsListenPort;
|
||||
protected $wsListenHost;
|
||||
|
||||
function __construct($dns, $wsListenPort, $wsListenHost){
|
||||
$this->dns = $dns;
|
||||
$this->wsListenPort = (int)$wsListenPort;
|
||||
$this->wsListenHost = $wsListenHost;
|
||||
|
||||
$this->startMapSocket();
|
||||
}
|
||||
|
||||
private function startChat(){
|
||||
$server = IoServer::factory(
|
||||
private function startMapSocket(){
|
||||
$loop = React\EventLoop\Factory::create();
|
||||
|
||||
// Listen for the web server to make a ZeroMQ push after an ajax request
|
||||
$context = new React\ZMQ\Context($loop);
|
||||
|
||||
$pull = $context->getSocket(\ZMQ::SOCKET_REP);
|
||||
// Binding to 127.0.0.1 means, the only client that can connect is itself
|
||||
$pull->bind( $this->dns );
|
||||
|
||||
// main app -> inject socket for response
|
||||
$mapUpdate = new Main\MapUpdate($pull);
|
||||
// "onMessage" listener
|
||||
$pull->on('message', [$mapUpdate, 'receiveData']);
|
||||
|
||||
// Set up our WebSocket server for clients wanting real-time updates
|
||||
$webSock = new React\Socket\Server($loop);
|
||||
// Binding to 0.0.0.0 means remotes can connect (Web Clients)
|
||||
$webSock->listen($this->wsListenPort, $this->wsListenHost);
|
||||
new IoServer(
|
||||
new HttpServer(
|
||||
new WsServer(
|
||||
new Main\Chat()
|
||||
$mapUpdate
|
||||
)
|
||||
),
|
||||
8020
|
||||
$webSock
|
||||
);
|
||||
|
||||
$server->run();
|
||||
$loop->run();
|
||||
}
|
||||
|
||||
}
|
||||
32
cmd.php
32
cmd.php
@@ -3,4 +3,34 @@ require 'vendor/autoload.php';
|
||||
|
||||
use Exodus4D\Socket;
|
||||
|
||||
new Socket\WebSockets();
|
||||
if(PHP_SAPI === 'cli'){
|
||||
// optional CLI params
|
||||
$options = getopt('', [
|
||||
'pf_listen_host:',
|
||||
'pf_listen_port:',
|
||||
'pf_host:',
|
||||
'pf_port:'
|
||||
]);
|
||||
|
||||
/**
|
||||
* WebSocket connection (for WebClients => Browser)
|
||||
* default WebSocket URI: ws://127.0.0.1:8020
|
||||
*
|
||||
* pf_client_ip '0.0.0.0' <-- any client can connect
|
||||
* pf_ws_port 8020 <-- any client can connect
|
||||
*/
|
||||
$wsListenHost = (!empty($options['pf_listen_host'])) ? $options['pf_listen_host'] : '0.0.0.0' ;
|
||||
$wsListenPort = (!empty($options['pf_listen_port'])) ? (int)$options['pf_listen_port'] : 8020 ;
|
||||
|
||||
$host = (!empty($options['pf_host'])) ? $options['pf_host'] : '127.0.0.1' ;
|
||||
$port = (!empty($options['pf_port'])) ? (int)$options['pf_port'] : 5555 ;
|
||||
|
||||
$dns = 'tcp://' . $host . ':' . $port;
|
||||
|
||||
new Socket\WebSockets($dns, $wsListenPort, $wsListenHost);
|
||||
}else{
|
||||
echo "Script need to be called by CLI!";
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
{
|
||||
"require": {
|
||||
"cboden/ratchet": "dev-master"
|
||||
},
|
||||
"name": "exodus4d/pathfinder_websocket",
|
||||
"description": "WebSocket extension for 'Pathfinder'",
|
||||
"minimum-stability": "stable",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Mark Friedrich",
|
||||
"email": "pathfinder@exodus4d.de"
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Exodus4D\\Socket\\": "app/"
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"php-64bit": ">=7.0",
|
||||
"ext-zmq": "1.1.*",
|
||||
"cboden/ratchet": "0.3.*",
|
||||
"react/zmq": "0.3.*"
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#wrapper {
|
||||
width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#leave-room {
|
||||
margin-bottom: 10px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
#user-container {
|
||||
width: 500px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#main-container {
|
||||
width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#messages {
|
||||
height: 300px;
|
||||
width: 500px;
|
||||
border: 1px solid #ccc;
|
||||
padding: 20px;
|
||||
text-align: left;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
#msg-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#msg {
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.user {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.msg {
|
||||
margin-bottom: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.time {
|
||||
float: right;
|
||||
color: #939393;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.details {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
#socket-status,
|
||||
#notification-status {
|
||||
font-size: 40px;
|
||||
}
|
||||
.red{
|
||||
color: red;
|
||||
}
|
||||
|
||||
.green{
|
||||
color: green;
|
||||
}
|
||||
53
index.html
53
index.html
@@ -1,53 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Chat</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/3.0.3/handlebars.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.2/moment.min.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrapper">
|
||||
Status: <span id="socket-status" class="red">●</span>
|
||||
Notification: <span id="notification-status" class="red">●</span>
|
||||
|
||||
<div id="user-container">
|
||||
<label for="user">What's your name?</label>
|
||||
<input type="text" id="user" name="user">
|
||||
<button type="button" id="join-chat">Join Chat</button>
|
||||
</div>
|
||||
|
||||
<div id="main-container" class="hidden">
|
||||
<button type="button" id="leave-room">Leave</button>
|
||||
<button type="button" id="toggle-notification">Notification</button>
|
||||
<div id="messages">
|
||||
|
||||
</div>
|
||||
|
||||
<div id="msg-container">
|
||||
<input type="text" id="msg" name="msg">
|
||||
<button type="button" id="send-msg">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script id="messages-template" type="text/x-handlebars-template">
|
||||
{{#each messages}}
|
||||
<div class="msg">
|
||||
<div class="time">{{time}}</div>
|
||||
<div class="details">
|
||||
<span class="user">{{user}}</span>: <span class="text">{{text}}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</script>
|
||||
|
||||
<script src="js/worker/message.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
162
js/app.js
162
js/app.js
@@ -1,162 +0,0 @@
|
||||
var domain = location.host;
|
||||
var workerProtocol = (window.location.protocol === 'https:') ? 'wss:' : 'ws:';
|
||||
var workerUri = workerProtocol + '//' + domain + '/ws/chat';
|
||||
|
||||
window.onload = function (){
|
||||
|
||||
var msgWorker = this.msgWorker;
|
||||
|
||||
var worker = new SharedWorker('js/worker/chat.js', 'worker_name');
|
||||
|
||||
worker.port.addEventListener('message', function(e){
|
||||
let load = e.data;
|
||||
load.__proto__ = msgWorker.prototype;
|
||||
|
||||
switch(load.command){
|
||||
case 'ws:open':
|
||||
// WebSocket in SharedWorker is open
|
||||
setSocketStatus(true);
|
||||
initChat();
|
||||
break;
|
||||
case 'ws:send':
|
||||
updateMessages(load.data());
|
||||
break;
|
||||
case 'ws:closed':
|
||||
setSocketStatus(false);
|
||||
break;
|
||||
}
|
||||
|
||||
// show webSocket info
|
||||
console.info(load.socket);
|
||||
}, false);
|
||||
|
||||
worker.onerror = function(e){
|
||||
console.error('SharedWorker onerror:');
|
||||
};
|
||||
|
||||
worker.port.start();
|
||||
|
||||
var msgWorkerInit = new msgWorker('ws:init');
|
||||
msgWorkerInit.data({
|
||||
uri: workerUri
|
||||
});
|
||||
worker.port.postMessage(msgWorkerInit);
|
||||
|
||||
|
||||
// Chat init ==================================================================================
|
||||
var user;
|
||||
var messages = [];
|
||||
|
||||
var messages_template = Handlebars.compile($('#messages-template').html());
|
||||
|
||||
var initChat = function(){
|
||||
|
||||
$('#join-chat').click(function(){
|
||||
user = $('#user').val();
|
||||
$('#user-container').addClass('hidden');
|
||||
$('#main-container').removeClass('hidden');
|
||||
|
||||
var msgWorkerSend = new msgWorker('ws:send');
|
||||
msgWorkerSend.data({
|
||||
'user': user,
|
||||
'text': user + ' entered the room',
|
||||
'time': moment().format('hh:mm a')
|
||||
});
|
||||
|
||||
worker.port.postMessage(msgWorkerSend);
|
||||
|
||||
$('#user').val('');
|
||||
});
|
||||
|
||||
$('#send-msg').click(function(){
|
||||
var text = $('#msg').val();
|
||||
|
||||
var msgWorkerSend = new msgWorker('ws:send');
|
||||
msgWorkerSend.data({
|
||||
'user': user,
|
||||
'text': text,
|
||||
'time': moment().format('hh:mm a')
|
||||
});
|
||||
|
||||
worker.port.postMessage(msgWorkerSend);
|
||||
|
||||
$('#msg').val('');
|
||||
});
|
||||
|
||||
$('#leave-room').click(function(){
|
||||
var msgWorkerSend = new msgWorker('ws:send');
|
||||
msgWorkerSend.data({
|
||||
'user': user,
|
||||
'text': user + ' has left the room',
|
||||
'time': moment().format('hh:mm a')
|
||||
});
|
||||
|
||||
worker.port.postMessage(msgWorkerSend);
|
||||
|
||||
$('#messages').html('');
|
||||
messages = [];
|
||||
|
||||
$('#main-container').addClass('hidden');
|
||||
$('#user-container').removeClass('hidden');
|
||||
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
var setSocketStatus = function(status){
|
||||
$('#socket-status').toggleClass('red', !status).toggleClass('green', status);
|
||||
};
|
||||
|
||||
var updateMessages = function(msg){
|
||||
messages.push(msg);
|
||||
var messages_html = messages_template({'messages': messages});
|
||||
$('#messages').html(messages_html);
|
||||
$("#messages").animate({ scrollTop: $('#messages')[0].scrollHeight}, 1000);
|
||||
};
|
||||
|
||||
// Notification init ==========================================================================
|
||||
|
||||
var updateNotification = function(status){
|
||||
$('#notification-status').toggleClass('red', !status).toggleClass('green', status);
|
||||
};
|
||||
|
||||
var notifyMe = function(){
|
||||
var msgWorkerNotify = new msgWorker('ws:notify');
|
||||
|
||||
if (Notification.permission === 'granted'){
|
||||
msgWorkerNotify.data({
|
||||
status: true
|
||||
});
|
||||
worker.port.postMessage(msgWorkerNotify);
|
||||
|
||||
updateNotification(true);
|
||||
}else{
|
||||
Notification.requestPermission(function (permission) {
|
||||
msgWorkerNotify.data({
|
||||
status: permission === 'granted'
|
||||
});
|
||||
worker.port.postMessage(msgWorkerNotify);
|
||||
|
||||
updateNotification(permission === 'granted');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$('#toggle-notification').on('click', notifyMe);
|
||||
|
||||
// ============================================================================================
|
||||
/*
|
||||
window.onbeforeunload = function() {
|
||||
var msgWorkerClose = new msgWorker('ws:close');
|
||||
worker.port.postMessage(msgWorkerClose);
|
||||
|
||||
//console.log('test close');
|
||||
//worker.port.close();
|
||||
|
||||
return 'sdf';
|
||||
};
|
||||
*/
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
'use strict';
|
||||
self.importScripts('message.js');
|
||||
|
||||
var socket = null;
|
||||
var ports = [];
|
||||
var notifications = false;
|
||||
|
||||
var initSocket = function(uri){
|
||||
var msgWorkerOpen = new msgWorker('ws:open');
|
||||
|
||||
if(socket === null){
|
||||
socket = new WebSocket(uri);
|
||||
|
||||
socket.onopen = function(e){
|
||||
msgWorkerOpen.socket = this;
|
||||
|
||||
ports[ports.length - 1].postMessage(msgWorkerOpen);
|
||||
|
||||
socket.onmessage = function(e){
|
||||
let load = JSON.parse(e.data);
|
||||
|
||||
let msgWorkerSend = new msgWorker('ws:send');
|
||||
msgWorkerSend.socket = this;
|
||||
|
||||
msgWorkerSend.data(load);
|
||||
|
||||
for (let i = 0; i < ports.length; i++) {
|
||||
ports[i].postMessage(msgWorkerSend);
|
||||
}
|
||||
|
||||
if(notifications){
|
||||
new Notification('Message: ' + load.text);
|
||||
}
|
||||
};
|
||||
|
||||
socket.onclose = function(){
|
||||
let msgWorkerWsClosed = new msgWorker('ws:closed');
|
||||
msgWorkerWsClosed.socket = this;
|
||||
|
||||
console.log(socket.readyState);
|
||||
for (let i = 0; i < ports.length; i++) {
|
||||
ports[i].postMessage(msgWorkerWsClosed);
|
||||
}
|
||||
};
|
||||
|
||||
socket.onerror = function(){
|
||||
console.error('ws: onerror()');
|
||||
};
|
||||
}
|
||||
}else{
|
||||
// socket still open
|
||||
ports[ports.length - 1].postMessage(msgWorkerOpen);
|
||||
}
|
||||
};
|
||||
|
||||
self.addEventListener('connect', function (event){
|
||||
let port = event.ports[0];
|
||||
ports.push(port);
|
||||
|
||||
port.addEventListener('message', function (e){
|
||||
let load = e.data;
|
||||
load.__proto__ = msgWorker.prototype;
|
||||
|
||||
switch(load.command){
|
||||
case 'ws:init':
|
||||
initSocket(load.data().uri);
|
||||
break;
|
||||
case 'ws:send':
|
||||
socket.send(JSON.stringify(load.data()));
|
||||
break;
|
||||
case 'ws:close':
|
||||
closeSocket();
|
||||
break;
|
||||
case 'ws:notify':
|
||||
notifications = load.data().status;
|
||||
break;
|
||||
}
|
||||
}, false);
|
||||
|
||||
port.start();
|
||||
}, false);
|
||||
|
||||
|
||||
// Util ================================================================
|
||||
var closeSocket = function(){
|
||||
// only close if active
|
||||
if(socket.readyState === socket.OPEN){
|
||||
socket.close();
|
||||
}
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
var msgWorker = class MessageWorker {
|
||||
|
||||
constructor(cmd){
|
||||
// message properties
|
||||
this.cmd = cmd;
|
||||
this.msgBody = null;
|
||||
|
||||
// webSocket props
|
||||
this.ws = {
|
||||
url: undefined,
|
||||
readyState: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
set socket(socket){
|
||||
this.ws.url = socket.url;
|
||||
this.ws.readyState = socket.readyState;
|
||||
}
|
||||
|
||||
get socket(){
|
||||
return this.ws;
|
||||
}
|
||||
|
||||
get command(){
|
||||
return this.cmd;
|
||||
}
|
||||
|
||||
|
||||
data(data) {
|
||||
if(data){
|
||||
this.msgBody = data;
|
||||
}
|
||||
|
||||
return this.msgBody;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user