我即将发布的解决方案涵盖了在 Web 浏览器上从服务器到客户端通信的整个过程,包括使 Websocket 服务器在后台运行的方法(使用或不使用 docker)。
Step 1:
假设您已经通过 Composer 安装了 Ratchet,请在项目中创建一个名为 bin 的文件夹,并将文件命名为“startwebsocketserver.php”(或您想要的任何名称)
Step 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();
请注意,在我的示例中,我使用引导文件来加载数据库。如果您不使用数据库或其他方法,请忽略它。为了这个答案的目的,我将假设 Doctrine 2 作为数据库。
这段代码的作用是在同一代码库中同时创建一个 HTTP 服务器和一个 WebSocket 服务器。我正在使用$app->route
方法,因为您可以为 HTTP 服务器添加进一步的路由来组织 API 调用,以从 PHP Web 服务器与 WebSocket 服务器通信。
$loop 变量包括应用程序循环中的 Websocket 服务器以及 HTTPServer。
Step 3:
在您的项目目录中创建一个名为 websockets 的文件夹。在其中创建另一个名为 WebSocketApp 的文件夹。现在在其中创建 3 个空文件。
客户端事件.php
html服务器.php
Websocket服务器.php
接下来我们将一一研究这些文件。未能按此顺序创建这些目录将导致 Composer Autoload PSR-0 无法找到它们。
您可以更改名称,但请确保相应地编辑您的作曲家文件。
Step 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,重要的部分是“自动加载”部分。这一部分尤其重要。
"psr-0": {
"WebSocketApp":"websockets"
},
这将自动加载 WebSocketApp 命名空间中 websockets 文件夹中的所有内容。 psr-0 假设代码将按命名空间的文件夹进行组织,这就是为什么我们必须在 websockets 内添加另一个名为 WebSocketApp 的文件夹。
Step 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 的事件系统并通过查看查询字符串并将其转换为 JSON 字符串来将消息传播到 WebSocket 服务器。如果您愿意,它也可以保留为数组,但就我而言,我需要 JSON 字符串。
Step 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 假设 JSON 字符串为'{"command":"updatestatus","userid":"2"}'
您可以根据自己的喜好进行设置。
Step 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();
}
}
这是需要解释的最复杂的文件。首先,有一个受保护的变量客户端,用于存储与该棘轮 Websocket 服务器建立的每个连接。它是在 onOpen 事件中创建的。
接下来,Web 浏览器客户端将在 onMessage 事件中注册自己以接收消息。我使用 JSON 协议完成此操作。一个例子是我使用的格式的代码,特别是我使用他们的 cookie 中的令牌来识别我的系统中的用户以及简单的注册消息。
我简单地在这个函数中查看数据库,看看是否有 authToken 与 cookie 一起使用。
如果数据库中的 Socket 表有写入 $from->resourceId
这是棘轮用来跟踪该特定连接编号的编号。
接下来,在 onClose 方法中请注意,我们必须确保在连接关闭时删除我们创建的条目,以便数据库不会填充不必要的额外数据。
最后注意 updateClient 函数是一个 symfony 事件,它是从我们之前做的 HtmlServer 触发的。
这就是实际将消息发送到客户端 Web 浏览器的内容。首先,如果用户打开了许多网络浏览器来创建不同的连接,我们会循环访问与该用户相关的所有已知套接字。 Doctrine 通过 $user->getSockets() 使这变得容易,你必须决定最好的方法来做到这一点。
然后您只需输入 $client->send($message) 即可将消息发送到 Web 浏览器。
Step 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 来回传递信息的简单方法。
Step 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 消息。
Step 10:
没有第10步你就完成了!好吧,不完全是......为了在后台运行网络服务器,我使用 Docker,这使得它很容易。只需使用以下命令执行网络服务器即可。
docker exec -itd draftandpermit_web_1 bash -c "cd /var/www/callcenter/livesite; php bin/startwebsocketserver.php"
或与您的情况相当的东西。这里的关键是我正在使用的 -d 选项,它在后台运行它。即使您再次运行该命令,它也不会生成两个实例,这很漂亮。关闭服务器超出了本文的范围,但如果您找到了一个好的方法来做到这一点,请修改或评论这个答案。
另外,不要忘记在 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"
请记住 8080 由 WebSockets 使用,因此它必须完全通过。
如果您对实体和数据库结构感到好奇,我在这里使用的是附图。