Ответ 1
Примечание. Мой ответ здесь не содержит ссылок на 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 JSON and expected data etc...
$data = json_decode(subscription->getId());
// Validate the users id and token together against the db values
// Now, let 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 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 **/
}
Это в основном это. Основные моменты здесь:
- Уникальный токен, идентификатор пользователя и идентификатор соединения предоставляют уникальную комбинацию, необходимую для обеспечения того, чтобы один пользователь не мог видеть другие пользовательские данные.
- Уникальный токен означает, что если один и тот же пользователь открывает другую страницу и просит подписаться, у них будет свой собственный идентификатор соединения + токена, так что у одного и того же пользователя не будет двойной подписки на той же странице (в основном, каждое соединение имеет собственные данные).
Extension
Вы должны обеспечить, чтобы все данные были проверены, а не попытка взлома, прежде чем что-либо сделать с ним. Зарегистрируйте все попытки подключения, используя что-то вроде Monolog, и настройте пересылку электронной почты, если произойдет какое-либо критическое событие (например, сервер перестает работать, потому что кто-то является ублюдком и пытается для взлома вашего сервера).
Точки закрытия
- Проверить все. Я не могу это подчеркнуть. Ваш уникальный токен, который изменяется при каждом запросе, важно.
- Помните, что если вы повторно генерируете токен в каждом HTTP-запросе и вы отправляете запрос POST перед попыткой подключения через веб-сайты, вам нужно будет передать повторно сгенерированный токен на свой JavaScript, прежде чем пытаться подключиться ( в противном случае ваш токен будет недействительным).
- Записать все. Ведите учет всех, кто подключается, спрашивает, какую тему, и отключается. Монолог отлично подходит для этого.