Помните мой последний пост о WebSockets и AngularJs? Сегодня мы будем играть с чем-то похожим. Я хочу создать интерфейс ключ-значение для игры с веб-сокетами. Позвольте мне объяснить это немного.
Сначала мы увидим бэкэнд. Одно приложение Silex с двумя маршрутами: получить один и отправить один:
<?php include __DIR__ . '/../../vendor/autoload.php'; include __DIR__ . '/SqlLiteStorage.php'; use Silex\Application; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Silex\Provider\DoctrineServiceProvider; $app = new Application([ 'debug' => true, 'ioServer' => 'http://localhost:3000', 'httpServer' => 'http://localhost:3001', ]); $app->after(function (Request $request, Response $response) { $response->headers->set('Access-Control-Allow-Origin', '*'); }); $app->register(new G\Io\EmitterServiceProvider($app['httpServer'])); $app->register(new DoctrineServiceProvider(), [ 'db.options' => [ 'driver' => 'pdo_sqlite', 'path' => __DIR__ . '/../../db/app.db.sqlite', ], ]); $app->register(new G\Io\Storage\Provider(new SqlLiteStorage($app['db']))); $app->get('conf', function (Application $app, Request $request) { $chanel = $request->get('token'); return $app->json([ 'ioServer' => $app['ioServer'], 'chanel' => $chanel ]); }); $app->get('/{key}', function (Application $app, $key) { return $app->json($app['gdb.get']($key)); }); $app->post('/{key}', function (Application $app, Request $request, $key) { $content = json_decode($request->getContent(), true); $chanel = $content['token']; $app->json($app['gdb.post']($key, $content['value'])); $app['io.emit']($chanel, [ 'key' => $key, 'value' => $content['value'] ]); return $app->json(true); }); $app->run();
Как мы видим, мы регистрируем одного поставщика услуг:
$app->register(new G\Io\Storage\Provider(new SqlLiteStorage($app['db'])));
Этому провайдеру нужен экземпляр StorageIface
namespace G\Io\Storage; interface StorageIface { public function get($key); public function post($key, $value); }
Наша реализация использует SqlLite, но довольно просто перейти на другое хранилище базы данных или даже базу данных NoSql.
use Doctrine\DBAL\Connection; use G\Io\Storage\StorageIface; class SqlLiteStorage implements StorageIface { private $db; public function __construct(Connection $db) { $this->db = $db; } public function get($key) { $statement = $this->db->executeQuery('select value from storage where key = :KEY', ['KEY' => $key]); $data = $statement->fetchAll(); return isset($data[0]['value']) ? $data[0]['value'] : null; } public function post($key, $value) { $this->db->beginTransaction(); $statement = $this->db->executeQuery('select value from storage where key = :KEY', ['KEY' =>; $key]); $data = $statement->fetchAll(); if (count($data) > 0) { $this->db->update('storage', ['value' => $value], ['key' => $key]); } else { $this->db->insert('storage', ['key' => $key, 'value' => $value]); } $this->db->commit(); return $value; } }
Мы также регистрируем другого поставщика услуг:
$app->register(new G\Io\EmitterServiceProvider($app['httpServer']));
Ответственность этого поставщика заключается в том, чтобы уведомлять сервер веб-сокета о любых изменениях в хранилище:
namespace G\Io; use Pimple\Container; use Pimple\ServiceProviderInterface; class EmitterServiceProvider implements ServiceProviderInterface { private $server; public function __construct($url) { $this->server = $url; } public function register(Container $app) { $app['io.emit'] = $app->protect(function ($chanel, $params) use ($app) { $s = curl_init(); curl_setopt($s, CURLOPT_URL, '{$this->server}/emit/?' . http_build_query($params) . '&_chanel=' . $chanel); curl_setopt($s, CURLOPT_RETURNTRANSFER, true); $content = curl_exec($s); $status = curl_getinfo($s, CURLINFO_HTTP_CODE); curl_close($s); if ($status != 200) throw new \Exception(); return $content; }); } }
Сервер Websocket — это простой сервер socket.io, а также сервер Express для обработки триггеров бэкэнда.
var express = require('express'), expressApp = express(), server = require('http').Server(expressApp), io = require('socket.io')(server, {origins: 'localhost:*'}) ; expressApp.get('/emit', function (req, res) { io.sockets.emit(req.query._chanel, req.query); res.json('OK'); }); expressApp.listen(3001); server.listen(3000);
Наше клиентское приложение — это приложение AngularJs:
<!doctype html> <html ng-app="app"> <head> <script src="//localhost:3000/socket.io/socket.io.js"></script> <script src="assets/angularjs/angular.js"></script> <script src="js/app.js"></script> <script src="js/gdb.js"></script> </head> <body> <div ng-controller="MainController"> <input type="text" ng-model="key"> <button ng-click="change()">change</button> </div> </body> </html>
angular.module('app', ['Gdb']) .run(function (Gdb) { Gdb.init({ server: 'http://localhost:8080/gdb', token: '4b96716bcb3d42fc01ff421ea2cfd757' }); }) .controller('MainController', function ($scope, Gdb) { $scope.change = function () { Gdb.set('key', $scope.key).then(function() { console.log("Value set"); }); }; Gdb.get('key').then(function (data) { $scope.key = data; }); Gdb.watch('key', function (value) { console.log("Value updated"); $scope.key = value; }); }) ;
Как мы видим, приложение AngularJs использует одну небольшую библиотеку под названием Gdb для обработки связи с бэкендом и WebSockets:
angular.module('Gdb', []) .factory('Gdb', function ($http, $q, $rootScope) { var socket, gdbServer, token, watches = {}; var Gdb = { init: function (conf) { gdbServer = conf.server; token = conf.token; $http.get(gdbServer + '/conf', {params: {token: token}}).success(function (data) { socket = io.connect(data.ioServer); socket.on(data.chanel, function (data) { watches.hasOwnProperty(data.key) ? watches[data.key](data.value) : null; $rootScope.$apply(); }); }); }, set: function (key, value) { var deferred = $q.defer(); $http.post(gdbServer + '/' + key, {value: value, token: token}).success(function (data) { deferred.resolve(data); }); return deferred.promise; }, get: function (key) { var deferred = $q.defer(); $http.get(gdbServer + '/' + key, {params: {token: token}}).success(function (data) { deferred.resolve(JSON.parse(data)); }); return deferred.promise; }, watch: function (key, closure) { watches[key] = closure; } }; return Gdb; });
И это все. Вы можете увидеть весь проект на GitHub .