用户棘轮存储连接并在服务器实例外部发送消息
Ratchet Store Connection of User & Send Message Outside of Server Instance
我一直在学习教程 here 并让棘轮服务器正常工作。
我的聊天 class 目前与教程或多或少相同,所以这里没有必要展示,因为我的问题更多是关于 实施策略 .
在我附加的问题中,用户正在寻找如何获取特定用户的连接对象。在最佳答案解决方案中,跟踪资源 ID 似乎是执行此操作的方法。
例如创建连接时有这段代码。
public function onOpen(ConnectionInterface $conn) {
// Store the new connection to send messages to later
$this->clients[$conn->resourceId] = $conn;
echo "New connection! ({$conn->resourceId})\n";
}
这会创建一个成员变量 clients
来存储所有连接,您现在只需通过 ID 引用它即可发送消息。此客户端 然而是 ConnectionInterface $conn
的一个实例
然后要发送消息,您只需使用下面的代码输入客户端 ID 作为数组键。很简单。
$client = $this->clients[{{insert client id here}}];
$client->send("Message successfully sent to user.");
正如我们所知,棘轮在服务器上作为脚本在一个永无止境的事件循环中运行。
我是 运行 一个 Symfony 项目,其中 在服务器实例之外 运行 当用户在system 我需要它向连接到服务器的特定客户端发送消息。
我不确定如何执行此操作,因为客户端 是 ConnectionInterface 的实例,并且是在用户首次通过 WebSockets 连接时创建的。如何以这种方式向特定客户端发送消息?
这是我想要实现的目标的视觉效果。
参考文献:
how to get the connection object of a specific user?
我即将 post 的解决方案涵盖了在 Web 浏览器上从服务器到客户端通信的整个过程,包括在后台创建 Websocket 服务器 运行 的方法(使用和没有 docker).
第 1 步:
假设你已经通过 composer 安装了 ratchet,在你的项目中创建一个名为 bin 的文件夹并将文件命名为 "startwebsocketserver.php"(或任何你想要的)
第 2 步:
将下面的代码复制进去。
<?php
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use React\Socket\Server;
use React\EventLoop\Factory;
use WebSocketApp\Websocketserver;
use WebSocketApp\Htmlserver;
use WebSocketApp\Clientevent;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Ratchet\App;
require dirname(__DIR__) . '/vendor/autoload.php';
require_once dirname(__DIR__) . '/bootstrap/bootstrap.php';
$websocketserver = new Websocketserver();
$dispatcher = new EventDispatcher(); //@JA - This is used to maintain communication between the websocket and HTTP Rest API Server
$dispatcher->addListener('websocketserver.updateclient', array($websocketserver, 'updateClient'));
//// 1. Create the event loop
$loop = Factory::create();
//// 2. Create websocket servers
$webSock = new Server($loop);
new IoServer(
new HttpServer(
new WsServer( $websocketserver )
),
$webSock
);
$webSock->listen('8080', '0.0.0.0');
$app = new App( 'localhost', 6677, '0.0.0.0',$loop );
$app->route( '/', new Htmlserver(), [ '*' ] );//@JA - Allow any origins for last parameter
$app->run();
请注意,在我的示例中,我使用 bootstrap 文件来加载数据库。 如果您没有使用数据库或其他方法,请忽略它。 为了这个答案的目的,我将假设 Doctrine 2 作为数据库。
此代码的作用是在同一代码库中同时创建一个 HTTP 服务器和一个 WebSocket 服务器。我正在使用 $app->route
方法,因为您可以为 HTTP 服务器添加更多路由以组织 API 调用以从您的 PHP Web 服务器与 WebSocket 服务器通信。
$loop 变量包括应用程序循环中的 Websocket 服务器以及 HTTPServer。
第 3 步:
在您的项目目录中创建一个名为 websockets 的文件夹。在其中创建另一个名为 WebSocketApp 的文件夹。现在在其中创建 3 个空文件。
Clientevent.php
Htmlserver.php
Websocketserver.php
接下来我们将逐一逐一研究这些文件。 未能按此顺序创建这些目录将导致 composer Autoload PSR-0 无法找到它们。
您可以更改名称,但请确保相应地编辑您的作曲家文件。
第 4 步:
在你的 composer.json 文件中确保它看起来像这样。
{
"require": {
"doctrine/orm": "^2.5",
"slim/slim": "^3.0",
"slim/twig-view": "^2.1",
"components/jquery": "*",
"components/normalize.css": "*",
"robloach/component-installer": "*",
"paragonie/random_compat": "^2.0",
"twilio/sdk": "^5.5",
"aws/aws-sdk-php": "^3.22",
"mailgun/mailgun-php": "^2.1",
"php-http/curl-client": "^1.7",
"guzzlehttp/psr7": "^1.3",
"cboden/ratchet": "^0.3.6"
},
"autoload": {
"psr-4": {
"app\":"app",
"Entity\":"entities"
},
"psr-0": {
"WebSocketApp":"websockets"
},
"files": ["lib/utilities.php","lib/security.php"]
}
}
在我的例子中,我使用的是 doctrine & slim,重要的部分是 "autoload" 部分。这部分特别重要。
"psr-0": {
"WebSocketApp":"websockets"
},
这将自动加载 WebSocketApp 命名空间中的文件夹 websockets 中的任何内容。 psr-0 假定代码将按名称空间的文件夹组织,这就是为什么我们必须在 websockets 中添加另一个名为 WebSocketApp 的文件夹。
第 5 步:
在 htmlserver.php 文件中放这个...
<?php
namespace WebSocketApp;
use Guzzle\Http\Message\RequestInterface;
use Guzzle\Http\Message\Response;
use Guzzle\Http\Message\Request;
use Ratchet\ConnectionInterface;
use Ratchet\Http\HttpServerInterface;
class Htmlserver implements HttpServerInterface {
protected $response;
public function onOpen( ConnectionInterface $conn, RequestInterface $request = null ) {
global $dispatcher;
$this->response = new Response( 200, [
'Content-Type' => 'text/html; charset=utf-8',
] );
$query = $request->getQuery();
parse_str($query, $get_array);//@JA - Convert query to variables in an array
$json = json_encode($get_array);//@JA - Encode to JSON
//@JA - Send JSON for what you want to do and the token representing the user & therefore connected user as well.
$event = new ClientEvent($json);
$dispatcher->dispatch("websocketserver.updateclient",$event);
$this->response->setBody('{"message":"Successfully sent message to websocket server")');
echo "HTTP Connection Triggered\n";
$this->close( $conn );
}
public function onClose( ConnectionInterface $conn ) {
echo "HTTP Connection Ended\n";
}
public function onError( ConnectionInterface $conn, \Exception $e ) {
echo "HTTP Connection Error\n";
}
public function onMessage( ConnectionInterface $from, $msg ) {
echo "HTTP Connection Message\n";
}
protected function close( ConnectionInterface $conn ) {
$conn->send( $this->response );
$conn->close();
}
}
此文件的目的是通过基本 HTTP 简化与 WebSocket 服务器的通信,稍后我将演示如何从 PHP Web 服务器使用 cURL。我设计它是为了使用 Symfony 的事件系统将消息传播到 WebSocket 服务器,并通过查看查询字符串并将其转换为 JSON 字符串。如果您愿意,它也可以保存为一个数组,但在我的例子中,我需要 JSON 字符串。
第 6 步:
接下来在clientevent.php放这段代码...
<?php
namespace WebSocketApp;
use Symfony\Component\EventDispatcher\Event;
use Entity\User;
use Entity\Socket;
class Clientevent extends Event
{
const NAME = 'clientevent';
protected $user; //@JA - This returns type Entity\User
public function __construct($json)
{
global $entityManager;
$decoded = json_decode($json,true);
switch($decoded["command"]){
case "updatestatus":
//Find out what the current 'active' & 'busy' states are for the userid given (assuming user id exists?)
if(isset($decoded["userid"])){
$results = $entityManager->getRepository('Entity\User')->findBy(array('id' => $decoded["userid"]));
if(count($results)>0){
unset($this->user);//@JA - Clear the old reference
$this->user = $results[0]; //@JA - Store refernece to the user object
$entityManager->refresh($this->user); //@JA - Because result cache is used by default, this will make sure the data is new and therefore the socket objects with it
}
}
break;
}
}
public function getUser()
{
return $this->user;
}
}
请注意,User 和 Socket 实体是我根据 Doctrine 2 创建的实体。您可以使用您喜欢的任何数据库。在我的例子中,我需要根据来自数据库的登录令牌从 PHP Web 服务器向特定用户发送消息。
Clientevent 假定 '{"command":"updatestatus","userid":"2"}'
的 JSON 字符串
不过您可以根据自己的喜好进行设置。
第 7 步:
在 Websocketserver.php 文件中放这个...
<?php
namespace WebSocketApp;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
use Symfony\Component\EventDispatcher\Event;
use Entity\User;
use Entity\Authtoken;
use Entity\Socket;
class Websocketserver implements MessageComponentInterface {
protected $clients;
public function updateClient(Event $event)
{
$user = $event->getUser();//@JA - Get reference to the user the event is for.
echo "userid=".$user->getId()."\n";
echo "busy=".($user->getBusy()==false ? "0" : "1")."\n";
echo "active=".($user->getActive()==false ? "0" : "1")."\n";
$json["busy"] = ($user->getBusy()==false ? "0" : "1");
$json["active"] = ($user->getActive()==false ? "0" : "1");
$msg = json_encode($json);
foreach($user->getSockets() as $socket){
$connectionid = $socket->getConnectionid();
echo "Sending For ConnectionID:".$connectionid."\n";
if(isset($this->clients[$connectionid])){
$client = $this->clients[$connectionid];
$client->send($msg);
}else{
echo "Client is no longer connected for this Connection ID:".$connectionid."\n";
}
}
}
public function __construct() {
$this->clients = array();
}
public function onOpen(ConnectionInterface $conn) {
// Store the new connection to send messages to later
$this->clients[$conn->resourceId] = $conn;
echo "New connection! ({$conn->resourceId})\n";
}
public function onMessage(ConnectionInterface $from, $msg) {
global $entityManager;
echo sprintf('Connection %d sending message "%s"' . "\n", $from->resourceId, $msg);
//@JA - First step is to decode the message coming from the client. Use token to identify the user (from cookie or local storage)
//@JA - Format is JSON {token:58d8beeb0ada3:4ffbd272a1703a59ad82cddc2f592685135b09f2,message:register}
$json = json_decode($msg,true);
//echo 'json='.print_r($json,true)."\n";
if($json["message"] == "register"){
echo "Registering with server...\n";
$parts = explode(":",$json["token"]);
$selector = $parts[0];
$validator = $parts[1];
//@JA - Look up records in the database by selector.
$tokens = $entityManager->getRepository('Entity\Authtoken')->findBy(array('selector' => $selector, 'token' => hash('sha256',$validator)));
if(count($tokens)>0){
$user = $tokens[0]->getUser();
echo "User ID:".$user->getId()." Registered from given token\n";
$socket = new Socket();
$socket->setUser($user);
$socket->setConnectionid($from->resourceId);
$socket->setDatecreated(new \Datetime());
$entityManager->persist($socket);
$entityManager->flush();
}else{
echo "No user found from the given cookie token\n";
}
}else{
echo "Unknown Message...\n";
}
}
public function onClose(ConnectionInterface $conn) {
global $entityManager;
// The connection is closed, remove it, as we can no longer send it messages
unset($this->clients[$conn->resourceId]);
//@JA - We need to clean up the database of any loose ends as well so it doesn't get full with loose data
$socketResults = $entityManager->getRepository('Entity\Socket')->findBy(array('connectionid' => $conn->resourceId));
if(count($socketResults)>0){
$socket = $socketResults[0];
$entityManager->remove($socket);
$entityManager->flush();
echo "Socket Entity For Connection ID:".$conn->resourceId." Removed\n";
}else{
echo "Was no socket info to remove from database??\n";
}
echo "Connection {$conn->resourceId} has disconnected\n";
}
public function onError(ConnectionInterface $conn, \Exception $e) {
echo "An error has occurred: {$e->getMessage()}\n";
$conn->close();
}
}
这是解释起来最复杂的文件。首先有一个受保护的变量 clients,它存储与这个棘轮 websocket 服务器建立的每个连接。它是在 onOpen 事件中创建的。
接下来的 onMessage 事件是 Web 浏览器客户端将自己注册以接收消息的地方。我使用 JSON 协议做到了这一点。一个例子是我特别使用的格式代码,其中我使用他们 cookie 中的令牌来识别它在我的系统中的用户以及一个简单的注册消息。
我在这个函数中简单地查看了数据库,看看是否有 authToken 与 cookie 一起使用。
如果在你的数据库中有写入 Socket table 的 $from->resourceId
这是 ratchet 用来跟踪特定连接号的号码。
接下来在 onClose 方法中注意,我们必须确保在连接关闭时删除我们创建的条目,这样数据库就不会被不必要的和额外的数据填满。
最后请注意,updateClient 函数是一个 symfony 事件,由我们之前做的 HtmlServer 触发。
这是将消息实际发送到客户端 Web 浏览器的内容。首先,如果用户打开了许多 Web 浏览器以创建不同的连接,我们将遍历与该用户相关的所有已知套接字。 Doctrine 使用 $user->getSockets() 使这很容易,你必须决定最好的方法来做到这一点。
然后您只需说 $client->send($msg) 即可将消息发送到网络浏览器。
第 8 步:
最后在您的 javascript 浏览器中输入类似这样的内容。
var hostname = window.location.hostname; //@JA - Doing it this way will make this work on DEV and LIVE Enviroments
var conn = new WebSocket('ws://'+hostname+':8080');
conn.onopen = function(e) {
console.log("Connection established!");
//@JA - Register with the server so it associates the connection ID to the supplied token
conn.send('{"token":"'+$.cookie("ccdraftandpermit")+'","message":"register"}');
};
conn.onmessage = function(e) {
//@JA - Update in realtime the busy and active status
console.log(e.data)
var obj = jQuery.parseJSON(e.data);
if(obj.busy == "0"){
$('.status').attr("status","free");
$('.status').html("Free");
$(".unbusy").css("display","none");
}else{
$('.status').attr("status","busy");
$('.status').html("Busy");
$(".unbusy").css("display","inline");
}
if(obj.active == "0"){
$('.startbtn').attr("status","off");
$('.startbtn').html("Start Taking Calls");
}else{
$('.startbtn').attr("status","on");
$('.startbtn').html("Stop Taking Calls");
}
};
我的演示展示了使用 JSON 来回传递信息的简单方法。
第 9 步:
为了从 PHP Web 服务器发送消息,我在辅助函数中做了类似的事情。
function h_sendWebsocketNotificationToUser($userid){
//Send notification out to Websocket Server
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://localhost/?command=updatestatus&userid=".$userid);
curl_setopt($ch, CURLOPT_PORT, 6677);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($ch);
curl_close($ch);
}
这将尝试随时为特定用户发送 updateStatus 消息。
第 10 步:
没有第10步你完成了!好吧,不完全是...对于 运行 我在后台使用 Docker 的网络服务器,这很容易。只需使用以下命令执行网络服务器。
docker exec -itd draftandpermit_web_1 bash -c "cd /var/www/callcenter/livesite; php bin/startwebsocketserver.php"
或与您的情况相当的东西。这里的关键是我在后台使用的 -d 选项 运行s。即使您再次 运行 命令,它也不会产生两个实例,这很漂亮。关闭服务器不在本文讨论范围之内,但如果您找到一个好的方法,请修改或评论此答案。
另外不要忘记在 docker-compose 文件上正确打开端口。我为我的项目做了类似的事情。
ports:
- "80:80"
- "8080:8080"
- "6060:80"
- "443:443"
- "6677:6677"
#This is used below to test on local machines, just portforward this on your router.
- "8082:80"
记住 WebSockets 使用 8080,所以它必须完全通过。
如果你对实体和数据库结构感到好奇,我在这里使用的是附图。
我一直在学习教程 here 并让棘轮服务器正常工作。
我的聊天 class 目前与教程或多或少相同,所以这里没有必要展示,因为我的问题更多是关于 实施策略 .
在我附加的问题中,用户正在寻找如何获取特定用户的连接对象。在最佳答案解决方案中,跟踪资源 ID 似乎是执行此操作的方法。
例如创建连接时有这段代码。
public function onOpen(ConnectionInterface $conn) {
// Store the new connection to send messages to later
$this->clients[$conn->resourceId] = $conn;
echo "New connection! ({$conn->resourceId})\n";
}
这会创建一个成员变量 clients
来存储所有连接,您现在只需通过 ID 引用它即可发送消息。此客户端 然而是 ConnectionInterface $conn
然后要发送消息,您只需使用下面的代码输入客户端 ID 作为数组键。很简单。
$client = $this->clients[{{insert client id here}}];
$client->send("Message successfully sent to user.");
正如我们所知,棘轮在服务器上作为脚本在一个永无止境的事件循环中运行。
我是 运行 一个 Symfony 项目,其中 在服务器实例之外 运行 当用户在system 我需要它向连接到服务器的特定客户端发送消息。
我不确定如何执行此操作,因为客户端 是 ConnectionInterface 的实例,并且是在用户首次通过 WebSockets 连接时创建的。如何以这种方式向特定客户端发送消息?
这是我想要实现的目标的视觉效果。
参考文献:
how to get the connection object of a specific user?
我即将 post 的解决方案涵盖了在 Web 浏览器上从服务器到客户端通信的整个过程,包括在后台创建 Websocket 服务器 运行 的方法(使用和没有 docker).
第 1 步:
假设你已经通过 composer 安装了 ratchet,在你的项目中创建一个名为 bin 的文件夹并将文件命名为 "startwebsocketserver.php"(或任何你想要的)
第 2 步:
将下面的代码复制进去。
<?php
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use React\Socket\Server;
use React\EventLoop\Factory;
use WebSocketApp\Websocketserver;
use WebSocketApp\Htmlserver;
use WebSocketApp\Clientevent;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Ratchet\App;
require dirname(__DIR__) . '/vendor/autoload.php';
require_once dirname(__DIR__) . '/bootstrap/bootstrap.php';
$websocketserver = new Websocketserver();
$dispatcher = new EventDispatcher(); //@JA - This is used to maintain communication between the websocket and HTTP Rest API Server
$dispatcher->addListener('websocketserver.updateclient', array($websocketserver, 'updateClient'));
//// 1. Create the event loop
$loop = Factory::create();
//// 2. Create websocket servers
$webSock = new Server($loop);
new IoServer(
new HttpServer(
new WsServer( $websocketserver )
),
$webSock
);
$webSock->listen('8080', '0.0.0.0');
$app = new App( 'localhost', 6677, '0.0.0.0',$loop );
$app->route( '/', new Htmlserver(), [ '*' ] );//@JA - Allow any origins for last parameter
$app->run();
请注意,在我的示例中,我使用 bootstrap 文件来加载数据库。 如果您没有使用数据库或其他方法,请忽略它。 为了这个答案的目的,我将假设 Doctrine 2 作为数据库。
此代码的作用是在同一代码库中同时创建一个 HTTP 服务器和一个 WebSocket 服务器。我正在使用 $app->route
方法,因为您可以为 HTTP 服务器添加更多路由以组织 API 调用以从您的 PHP Web 服务器与 WebSocket 服务器通信。
$loop 变量包括应用程序循环中的 Websocket 服务器以及 HTTPServer。
第 3 步:
在您的项目目录中创建一个名为 websockets 的文件夹。在其中创建另一个名为 WebSocketApp 的文件夹。现在在其中创建 3 个空文件。
Clientevent.php Htmlserver.php Websocketserver.php
接下来我们将逐一逐一研究这些文件。 未能按此顺序创建这些目录将导致 composer Autoload PSR-0 无法找到它们。
您可以更改名称,但请确保相应地编辑您的作曲家文件。
第 4 步:
在你的 composer.json 文件中确保它看起来像这样。
{
"require": {
"doctrine/orm": "^2.5",
"slim/slim": "^3.0",
"slim/twig-view": "^2.1",
"components/jquery": "*",
"components/normalize.css": "*",
"robloach/component-installer": "*",
"paragonie/random_compat": "^2.0",
"twilio/sdk": "^5.5",
"aws/aws-sdk-php": "^3.22",
"mailgun/mailgun-php": "^2.1",
"php-http/curl-client": "^1.7",
"guzzlehttp/psr7": "^1.3",
"cboden/ratchet": "^0.3.6"
},
"autoload": {
"psr-4": {
"app\":"app",
"Entity\":"entities"
},
"psr-0": {
"WebSocketApp":"websockets"
},
"files": ["lib/utilities.php","lib/security.php"]
}
}
在我的例子中,我使用的是 doctrine & slim,重要的部分是 "autoload" 部分。这部分特别重要。
"psr-0": {
"WebSocketApp":"websockets"
},
这将自动加载 WebSocketApp 命名空间中的文件夹 websockets 中的任何内容。 psr-0 假定代码将按名称空间的文件夹组织,这就是为什么我们必须在 websockets 中添加另一个名为 WebSocketApp 的文件夹。
第 5 步:
在 htmlserver.php 文件中放这个...
<?php
namespace WebSocketApp;
use Guzzle\Http\Message\RequestInterface;
use Guzzle\Http\Message\Response;
use Guzzle\Http\Message\Request;
use Ratchet\ConnectionInterface;
use Ratchet\Http\HttpServerInterface;
class Htmlserver implements HttpServerInterface {
protected $response;
public function onOpen( ConnectionInterface $conn, RequestInterface $request = null ) {
global $dispatcher;
$this->response = new Response( 200, [
'Content-Type' => 'text/html; charset=utf-8',
] );
$query = $request->getQuery();
parse_str($query, $get_array);//@JA - Convert query to variables in an array
$json = json_encode($get_array);//@JA - Encode to JSON
//@JA - Send JSON for what you want to do and the token representing the user & therefore connected user as well.
$event = new ClientEvent($json);
$dispatcher->dispatch("websocketserver.updateclient",$event);
$this->response->setBody('{"message":"Successfully sent message to websocket server")');
echo "HTTP Connection Triggered\n";
$this->close( $conn );
}
public function onClose( ConnectionInterface $conn ) {
echo "HTTP Connection Ended\n";
}
public function onError( ConnectionInterface $conn, \Exception $e ) {
echo "HTTP Connection Error\n";
}
public function onMessage( ConnectionInterface $from, $msg ) {
echo "HTTP Connection Message\n";
}
protected function close( ConnectionInterface $conn ) {
$conn->send( $this->response );
$conn->close();
}
}
此文件的目的是通过基本 HTTP 简化与 WebSocket 服务器的通信,稍后我将演示如何从 PHP Web 服务器使用 cURL。我设计它是为了使用 Symfony 的事件系统将消息传播到 WebSocket 服务器,并通过查看查询字符串并将其转换为 JSON 字符串。如果您愿意,它也可以保存为一个数组,但在我的例子中,我需要 JSON 字符串。
第 6 步:
接下来在clientevent.php放这段代码...
<?php
namespace WebSocketApp;
use Symfony\Component\EventDispatcher\Event;
use Entity\User;
use Entity\Socket;
class Clientevent extends Event
{
const NAME = 'clientevent';
protected $user; //@JA - This returns type Entity\User
public function __construct($json)
{
global $entityManager;
$decoded = json_decode($json,true);
switch($decoded["command"]){
case "updatestatus":
//Find out what the current 'active' & 'busy' states are for the userid given (assuming user id exists?)
if(isset($decoded["userid"])){
$results = $entityManager->getRepository('Entity\User')->findBy(array('id' => $decoded["userid"]));
if(count($results)>0){
unset($this->user);//@JA - Clear the old reference
$this->user = $results[0]; //@JA - Store refernece to the user object
$entityManager->refresh($this->user); //@JA - Because result cache is used by default, this will make sure the data is new and therefore the socket objects with it
}
}
break;
}
}
public function getUser()
{
return $this->user;
}
}
请注意,User 和 Socket 实体是我根据 Doctrine 2 创建的实体。您可以使用您喜欢的任何数据库。在我的例子中,我需要根据来自数据库的登录令牌从 PHP Web 服务器向特定用户发送消息。
Clientevent 假定 '{"command":"updatestatus","userid":"2"}'
不过您可以根据自己的喜好进行设置。
第 7 步:
在 Websocketserver.php 文件中放这个...
<?php
namespace WebSocketApp;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
use Symfony\Component\EventDispatcher\Event;
use Entity\User;
use Entity\Authtoken;
use Entity\Socket;
class Websocketserver implements MessageComponentInterface {
protected $clients;
public function updateClient(Event $event)
{
$user = $event->getUser();//@JA - Get reference to the user the event is for.
echo "userid=".$user->getId()."\n";
echo "busy=".($user->getBusy()==false ? "0" : "1")."\n";
echo "active=".($user->getActive()==false ? "0" : "1")."\n";
$json["busy"] = ($user->getBusy()==false ? "0" : "1");
$json["active"] = ($user->getActive()==false ? "0" : "1");
$msg = json_encode($json);
foreach($user->getSockets() as $socket){
$connectionid = $socket->getConnectionid();
echo "Sending For ConnectionID:".$connectionid."\n";
if(isset($this->clients[$connectionid])){
$client = $this->clients[$connectionid];
$client->send($msg);
}else{
echo "Client is no longer connected for this Connection ID:".$connectionid."\n";
}
}
}
public function __construct() {
$this->clients = array();
}
public function onOpen(ConnectionInterface $conn) {
// Store the new connection to send messages to later
$this->clients[$conn->resourceId] = $conn;
echo "New connection! ({$conn->resourceId})\n";
}
public function onMessage(ConnectionInterface $from, $msg) {
global $entityManager;
echo sprintf('Connection %d sending message "%s"' . "\n", $from->resourceId, $msg);
//@JA - First step is to decode the message coming from the client. Use token to identify the user (from cookie or local storage)
//@JA - Format is JSON {token:58d8beeb0ada3:4ffbd272a1703a59ad82cddc2f592685135b09f2,message:register}
$json = json_decode($msg,true);
//echo 'json='.print_r($json,true)."\n";
if($json["message"] == "register"){
echo "Registering with server...\n";
$parts = explode(":",$json["token"]);
$selector = $parts[0];
$validator = $parts[1];
//@JA - Look up records in the database by selector.
$tokens = $entityManager->getRepository('Entity\Authtoken')->findBy(array('selector' => $selector, 'token' => hash('sha256',$validator)));
if(count($tokens)>0){
$user = $tokens[0]->getUser();
echo "User ID:".$user->getId()." Registered from given token\n";
$socket = new Socket();
$socket->setUser($user);
$socket->setConnectionid($from->resourceId);
$socket->setDatecreated(new \Datetime());
$entityManager->persist($socket);
$entityManager->flush();
}else{
echo "No user found from the given cookie token\n";
}
}else{
echo "Unknown Message...\n";
}
}
public function onClose(ConnectionInterface $conn) {
global $entityManager;
// The connection is closed, remove it, as we can no longer send it messages
unset($this->clients[$conn->resourceId]);
//@JA - We need to clean up the database of any loose ends as well so it doesn't get full with loose data
$socketResults = $entityManager->getRepository('Entity\Socket')->findBy(array('connectionid' => $conn->resourceId));
if(count($socketResults)>0){
$socket = $socketResults[0];
$entityManager->remove($socket);
$entityManager->flush();
echo "Socket Entity For Connection ID:".$conn->resourceId." Removed\n";
}else{
echo "Was no socket info to remove from database??\n";
}
echo "Connection {$conn->resourceId} has disconnected\n";
}
public function onError(ConnectionInterface $conn, \Exception $e) {
echo "An error has occurred: {$e->getMessage()}\n";
$conn->close();
}
}
这是解释起来最复杂的文件。首先有一个受保护的变量 clients,它存储与这个棘轮 websocket 服务器建立的每个连接。它是在 onOpen 事件中创建的。
接下来的 onMessage 事件是 Web 浏览器客户端将自己注册以接收消息的地方。我使用 JSON 协议做到了这一点。一个例子是我特别使用的格式代码,其中我使用他们 cookie 中的令牌来识别它在我的系统中的用户以及一个简单的注册消息。
我在这个函数中简单地查看了数据库,看看是否有 authToken 与 cookie 一起使用。
如果在你的数据库中有写入 Socket table 的 $from->resourceId
这是 ratchet 用来跟踪特定连接号的号码。
接下来在 onClose 方法中注意,我们必须确保在连接关闭时删除我们创建的条目,这样数据库就不会被不必要的和额外的数据填满。
最后请注意,updateClient 函数是一个 symfony 事件,由我们之前做的 HtmlServer 触发。
这是将消息实际发送到客户端 Web 浏览器的内容。首先,如果用户打开了许多 Web 浏览器以创建不同的连接,我们将遍历与该用户相关的所有已知套接字。 Doctrine 使用 $user->getSockets() 使这很容易,你必须决定最好的方法来做到这一点。
然后您只需说 $client->send($msg) 即可将消息发送到网络浏览器。
第 8 步:
最后在您的 javascript 浏览器中输入类似这样的内容。
var hostname = window.location.hostname; //@JA - Doing it this way will make this work on DEV and LIVE Enviroments
var conn = new WebSocket('ws://'+hostname+':8080');
conn.onopen = function(e) {
console.log("Connection established!");
//@JA - Register with the server so it associates the connection ID to the supplied token
conn.send('{"token":"'+$.cookie("ccdraftandpermit")+'","message":"register"}');
};
conn.onmessage = function(e) {
//@JA - Update in realtime the busy and active status
console.log(e.data)
var obj = jQuery.parseJSON(e.data);
if(obj.busy == "0"){
$('.status').attr("status","free");
$('.status').html("Free");
$(".unbusy").css("display","none");
}else{
$('.status').attr("status","busy");
$('.status').html("Busy");
$(".unbusy").css("display","inline");
}
if(obj.active == "0"){
$('.startbtn').attr("status","off");
$('.startbtn').html("Start Taking Calls");
}else{
$('.startbtn').attr("status","on");
$('.startbtn').html("Stop Taking Calls");
}
};
我的演示展示了使用 JSON 来回传递信息的简单方法。
第 9 步:
为了从 PHP Web 服务器发送消息,我在辅助函数中做了类似的事情。
function h_sendWebsocketNotificationToUser($userid){
//Send notification out to Websocket Server
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://localhost/?command=updatestatus&userid=".$userid);
curl_setopt($ch, CURLOPT_PORT, 6677);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($ch);
curl_close($ch);
}
这将尝试随时为特定用户发送 updateStatus 消息。
第 10 步:
没有第10步你完成了!好吧,不完全是...对于 运行 我在后台使用 Docker 的网络服务器,这很容易。只需使用以下命令执行网络服务器。
docker exec -itd draftandpermit_web_1 bash -c "cd /var/www/callcenter/livesite; php bin/startwebsocketserver.php"
或与您的情况相当的东西。这里的关键是我在后台使用的 -d 选项 运行s。即使您再次 运行 命令,它也不会产生两个实例,这很漂亮。关闭服务器不在本文讨论范围之内,但如果您找到一个好的方法,请修改或评论此答案。
另外不要忘记在 docker-compose 文件上正确打开端口。我为我的项目做了类似的事情。
ports:
- "80:80"
- "8080:8080"
- "6060:80"
- "443:443"
- "6677:6677"
#This is used below to test on local machines, just portforward this on your router.
- "8082:80"
记住 WebSockets 使用 8080,所以它必须完全通过。
如果你对实体和数据库结构感到好奇,我在这里使用的是附图。