Храповик PHP WAMP-React / ZeroMQ-специфичная передача пользователя
Примечание: это не аналогично этот вопрос С
MessageComponentInterface
. Я используюWampServerInterface
вместо этого, поэтому этот вопрос относится именно к этой части. Мне нужен ответ с примерами кода и объяснением, поскольку я вижу, что это полезно для других в будущем.
попытка зацикленных толчков для отдельных пользователей
я использую часть WAMP храповика и ZeroMQ, и я в настоящее время имейте рабочую версию учебник по интеграции push.
Я пытаюсь выполнить следующее:
- сервер zeromq запущен и работает, готов регистрировать подписчиков и отписывающихся
- пользователь подключается в своем браузере по протоколу websocket
- A цикл запускается, который отправляет данные в конкретные кто просил его
- когда пользователь отключается, цикл для данных этого пользователя остановлен
у меня есть пункты (1) и (2), однако проблема у меня с третьим:
во-первых: как я могу отправлять данные только каждому конкретному пользователю? Broadcast отправляет его всем, если только "темы"не становятся индивидуальными идентификаторами пользователей?
во-вторых: у меня большая проблема безопасности. если я отправляю, какой идентификатор пользователя хочет подписаться на стороне клиента, который похоже, мне нужно, тогда пользователь может просто изменить переменную на идентификатор другого пользователя, и вместо этого возвращаются их данные.
в-третьих: мне нужно запустить отдельный PHP-скрипт содержащий код для zeromq, чтобы начать цикл. Я не уверен, что это лучший способ сделать это, и я бы предпочел, чтобы это работало полностью в кодовой базе, а не в отдельном файле php. Это важная область, в которой мне нужно разобраться.
следующий код показывает, что у меня сейчас есть.
сервер, который работает только из консоли
я буквально типа php bin/push-server.php
для выполнения этого. Подписки и подписки ООН выводятся на этот терминал для целей отладки.
$loop = ReactEventLoopFactory::create();
$pusher = Pusher;
$context = new ReactZMQContext($loop);
$pull = $context->getSocket(ZMQ::SOCKET_PULL);
$pull->bind('tcp://127.0.0.1:5555');
$pull->on('message', array($pusher, 'onMessage'));
$webSock = new ReactSocketServer($loop);
$webSock->listen(8080, '0.0.0.0'); // Binding to 0.0.0.0 means remotes can connect
$webServer = new RatchetServerIoServer(
new RatchetWebSocketWsServer(
new RatchetWampWampServer(
$pusher
)
),
$webSock
);
$loop->run();
толкатель, который отправляет данные через websockets
Я опустил бесполезный материал и сосредоточился на onMessage()
и onSubscribe()
методы.
public function onSubscribe(ConnectionInterface $conn, $topic)
{
$subject = $topic->getId();
$ip = $conn->remoteAddress;
if (!array_key_exists($subject, $this->subscribedTopics))
{
$this->subscribedTopics[$subject] = $topic;
}
$this->clients[] = $conn->resourceId;
echo sprintf("New Connection: %s" . PHP_EOL, $conn->remoteAddress);
}
public function onMessage($entry) {
$entryData = json_decode($entry, true);
var_dump($entryData);
if (!array_key_exists($entryData['topic'], $this->subscribedTopics)) {
return;
}
$topic = $this->subscribedTopics[$entryData['topic']];
// This sends out everything to multiple users, not what I want!!
// I can't send() to individual connections from here I don't think :S
$topic->broadcast($entryData);
}
скрипт для начала использования вышеуказанного кода толкателя в петля
это моя проблема - это отдельный файл php, который, надеюсь, может быть интегрирован в другой код в будущем, но в настоящее время я не уверен, как использовать это правильно. Должен ли я получить идентификатор пользователя из сеанса? Мне все еще нужно отправить его с клиентской стороны...
// Thought sessions might work here but they don't work for subscription
session_start();
$userId = $_SESSION['userId'];
$loop = ReactEventLoopFactory::create();
$context = new ZMQContext();
$socket = $context->getSocket(ZMQ::SOCKET_PUSH, 'my pusher');
$socket->connect("tcp://localhost:5555");
$i = 0;
$loop->addPeriodicTimer(4, function() use ($socket, $loop, $userId, &$i) {
$entryData = array(
'topic' => 'subscriptionTopicHere',
'userId' => $userId
);
$i++;
// So it doesn't go on infinitely if run from browser
if ($i >= 3)
{
$loop->stop();
}
// Send stuff to the queue
$socket->send(json_encode($entryData));
});
наконец, на стороне клиента js подписаться с
$(document).ready(function() {
var conn = new ab.Session(
'ws://localhost:8080'
, function() {
conn.subscribe('topicHere', function(topic, data) {
console.log(topic);
console.log(data);
});
}
, function() {
console.warn('WebSocket connection closed');
}
, {
'skipSubprotocolCheck': true
}
);
});
вывод
вышеуказанное работает, но мне действительно нужно выяснить следующий:
как я могу отправлять отдельные сообщения отдельным пользователям? Когда они посещают страницу, которая запускает соединение websocket в JS, должен ли я также запускать скрипт, который пихает вещи в очередь в PHP (zeromq)? Это то, что я сейчас делаю вручную, и это просто неправильно.
подписываясь на пользователя из JS, не может быть безопасно захватить идентификатор пользователя из сеанса и отправить его из клиентский. Это может быть подделкой. Скажите, пожалуйста, есть более простой способ, и если да, то как?
2 ответов
Примечание: мой ответ здесь не включить ссылки на ZeroMQ, так как я больше не использую его. Тем не менее, я уверен, что вы сможете выяснить, как использовать ZeroMQ с этим ответом, если вам нужно.
использовать JSON
прежде всего,Websocket RFC и WAMP Spec укажите, что тема для подписки должна быть строка. Я немного жульничаю, но ... все еще придерживаясь спецификации: вместо этого я передаю JSON.
{
"topic": "subject here",
"userId": "1",
"token": "dsah9273bui3f92h3r83f82h3"
}
JSON по-прежнему является строкой, но она позволяет мне передавать больше данных вместо "темы", и для PHP просто сделать json_decode()
на другом конце. Конечно, вы должны проверить, что вы действительно получаете JSON, но это зависит от вашей реализации.
- темы
тема тему пользователь подписывается на. Вы используете это, чтобы решить, какие данные вы передаете пользователю.
- UserId
очевидно идентификатор пользователя. Вы должны убедитесь, что этот пользователь существует и может подписаться, используя следующую часть:
- маркер
это должно быть использовать случайно сгенерированный токен, сгенерированный в вашем PHP и переданный Переменной JavaScript. Когда я говорю "одно использование", я имею в виду каждый раз, когда вы перезагружаете страницу (и, по расширению, по каждому HTTP-запросу), ваша переменная JavaScript должна иметь новый токен. Этот маркер должен храниться в базе данных по идентификатору пользователя.
затем, как только запрос websocket сделан, вы сопоставляете токен и идентификатор пользователя с теми, кто в базе данных, чтобы убедиться, что пользователь действительно тот, о ком они говорят, и они не возились с JS переменная.
Примечание: в обработчике событий вы можете использовать
$conn->remoteAddress
чтобы получить IP-адрес соединения, поэтому, если кто-то пытается подключиться злонамеренно, вы можете заблокировать их (войти в них или что-то еще).
почему это работает?
это работает, потому что каждый раз, когда приходит новое соединение,уникальный маркер гарантирует, что ни один пользователь не будет иметь доступа к чужим данным подписки.
в Сервер
вот что я использую для запуска цикла и обработчика событий. Я создаю цикл, делая все создание объекта стиля декоратора и передавая мой EventHandler (к которому я скоро приду) с циклом там тоже.
$loop = Factory::create();
new IoServer(
new WsServer(
new WampServer(
new EventHandler($loop) // This is my class. Pass in the loop!
)
),
$webSock
);
$loop->run();
Обработчик События
class EventHandler implements WampServerInterface, MessageComponentInterface
{
/**
* @var \React\EventLoop\LoopInterface
*/
private $loop;
/**
* @var array List of connected clients
*/
private $clients;
/**
* Pass in the react event loop here
*/
public function __construct(LoopInterface $loop)
{
$this->loop = $loop;
}
/**
* A user connects, we store the connection by the unique resource id
*/
public function onOpen(ConnectionInterface $conn)
{
$this->clients[$conn->resourceId]['conn'] = $conn;
}
/**
* A user subscribes. The JSON is in $subscription->getId()
*/
public function onSubscribe(ConnectionInterface $conn, $subscription)
{
// This is the JSON passed in from your JavaScript
// Obviously you need to validate it's JSON and expected data etc...
$data = json_decode(subscription->getId());
// Validate the users id and token together against the db values
// Now, let's subscribe this user only
// 5 = the interval, in seconds
$timer = $this->loop->addPeriodicTimer(5, function() use ($subscription) {
$data = "whatever data you want to broadcast";
return $subscription->broadcast(json_encode($data));
});
// Store the timer against that user's connection resource Id
$this->clients[$conn->resourceId]['timer'] = $timer;
}
public function onClose(ConnectionInterface $conn)
{
// There might be a connection without a timer
// So make sure there is one before trying to cancel it!
if (isset($this->clients[$conn->resourceId]['timer']))
{
if ($this->clients[$conn->resourceId]['timer'] instanceof TimerInterface)
{
$this->loop->cancelTimer($this->clients[$conn->resourceId]['timer']);
}
}
unset($this->clients[$conn->resourceId]);
}
/** Implement all the extra methods the interfaces say that you must use **/
}
это в основном все. Основные моменты здесь:
- уникальный токен, идентификатор пользователя и идентификатор соединения обеспечивают уникальную комбинацию, необходимую для обеспечения одного пользователя не удается увидеть данные другого пользователя.
- уникальный токен означает, что если один и тот же пользователь открывает другую страницу и запрашивает подписку, у них будет свой собственный идентификатор соединения + комбинация токенов, чтобы один и тот же пользователь не имел двойных подписок на одной странице (в основном, каждое соединение имеет свои собственные индивидуальные данные).
расширение
вы должны убедиться, что все данные проверены, а не попытка взлома, прежде чем что-либо с ним делать. Журнал все соединения попытки использовать что-то вроде монолог, и настроить переадресацию электронной почты, если происходят какие-либо критические (например, сервер перестает работать, потому что кто-то ублюдок и пытается взломать ваш сервер).
Закрытие Пунктов
- Проверить Все. Я не могу подчеркнуть это достаточно. Ваш уникальный токен, который изменяется при каждом запросе, -важно.
- помните, если вы повторно генерируете токен на каждом HTTP запрос, и вы делаете запрос POST перед попыткой подключения через websockets, вам придется передать повторно сгенерированный токен на ваш JavaScript перед попыткой подключения (в противном случае ваш токен будет недействительным).
- журнал все. Ведите учет всех, кто подключается, спрашивает, какая тема, и отключается. Монолог отлично подходит для этого.
чтобы отправить определенным пользователям, вам нужен шаблон маршрутизатора-дилера вместо pub-SUB. Это объясняется в руководстве, в главе 3. Безопасность, если вы используете ZMQ v4.0, обрабатывается на уровне провода, поэтому вы не видите его в приложении. Это все еще требует некоторой работы, если вы не используете привязку CZMQ, которая предоставляет структуру аутентификации (zauth).
в основном, для аутентификации вы устанавливаете обработчик на inproc: / / zeromq.убить.01, и отвечать на запросы через сокет. Гуглить ZeroMQ ZAP для RFC; существует также тестовый случай в ядре libzmq/tests / test_security_curve.программа СРР.