Продолжая замечательное путешествие с использованием потрясающей Play Framework и языка Scala , я хотел бы поделиться еще одной интересной реализацией построения графиков в реальном времени: на этот раз с использованием легких серверных событий вместо полнодуплексной технологии WebSockets, описанной ранее в этом посте . В самом деле, если вам не нужна двунаправленная связь, а требуется только передача на сервер, события на стороне сервера выглядят как вполне естественное соответствие. И если вы используете Play Framework , это действительно легко сделать.
Давайте попробуем охватить один и тот же вариант использования, поэтому будет справедливо сравнить обе реализации: у нас есть несколько хостов, и мы хотели бы наблюдать за использованием ЦП на каждом из них в режиме реального времени (на графике). Давайте начнем с создания простого приложения Play Framework (выбрав Scala в качестве основного языка):
|
1
|
play new play-sse-example |
Теперь, когда макет нашего приложения будет готов, наш следующий шаг — создать начальную веб-страницу (используя безопасный шаблонный движок Play Framework ) и присвоить ей имя views / dashboard.scala.html . Вот как это выглядит:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
@(title: String, hosts: List[Host])<!DOCTYPE html><html> <head> <title>@title</title> <link rel="stylesheet" media="screen" href="@routes.Assets.at("stylesheets/main.css")"> <link rel="shortcut icon" type="image/png" href="@routes.Assets.at("images/favicon.png")"> <script src="@routes.Assets.at("javascripts/jquery-1.9.0.min.js")" type="text/javascript"></script> <script src="@routes.Assets.at("javascripts/highcharts.js")" type="text/javascript"></script> </head> <body> <div id="hosts"> <ul class="hosts"> @hosts.map { host => <li> <a href="#" onclick="javascript:show( '@host.id' )"><b>@host.name</b></a> </li> } </ul> </div> <div id="content"> </div> </body></html><script type="text/javascript">function show( hostid ) { $('#content').trigger('unload'); $("#content").load( "/host/" + hostid, function( response, status, xhr ) { if (status == "error") { $("#content").html( "Sorry but there was an error:" + xhr.status + " " + xhr.statusText); } } )}</script> |
Шаблон выглядит точно так же, как в примере с WebSockets , за исключением одной строки, цель которой будет объяснена чуть позже.
|
1
|
$('#content').trigger('unload'); |
Результатом этой веб-страницы является простой список хостов. Всякий раз, когда пользователь нажимает на ссылку хоста, специфичное для хоста представление будет выбираться с сервера (используя AJAX ) и отображаться. Следующий шаблон является наиболее интересным, views / host.scala.html , и содержит много важных деталей:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
@(host: Host)( implicit request: RequestHeader )<div id="content"> <div id="chart"></div> <script type="text/javascript"> var charts = [] charts[ '@host.id' ] = new Highcharts.Chart({ chart: { renderTo: 'chart', defaultSeriesType: 'spline' }, xAxis: { type: 'datetime' }, series: [{ name: "CPU", data: [] } ] }); </script> </div><script type="text/javascript"> if( !!window.EventSource ) { var event = new EventSource("@routes.Application.stats( host.id )"); event.addEventListener('message', function( event ) { var datapoint = jQuery.parseJSON( event.data ); var chart = charts[ '@host.id' ]; chart.series[ 0 ].addPoint({ x: datapoint.cpu.timestamp, y: datapoint.cpu.load }, true, chart.series[ 0 ].data.length >= 50 ); } ); $('#content').bind('unload',function() { event.close(); }); } </script> |
Основным компонентом пользовательского интерфейса является простая диаграмма, построенная с использованием библиотеки Highcharts . Блок сценария внизу пытается создать объект EventSource, который является реализацией событий на стороне сервера на стороне браузера. Если браузер поддерживает события на стороне сервера, будет создано соответствующее соединение с конечной точкой на стороне сервера, и диаграмма будет обновляться для каждого сообщения, полученного от сервера (прослушиватель ‘message’ ). Настало время объяснить назначение этой конструкции (и ее аналога $ (‘# content’). Trigger (‘unload’), упомянутого выше):
|
1
2
3
|
$('#content').bind('unload',function() { event.close();}); |
Всякий раз, когда пользователь нажимает на разные хосты, предыдущий поток событий должен быть закрыт, и должен быть создан новый. Невыполнение этого требования приводит к тому, что создается все больше и больше потоков событий, а в браузере появляется все больше и больше слушателей событий. Чтобы преодолеть это, мы привязываем метод unload к элементу div с содержимым id и вызываем его все время, когда пользователь нажимает на хост. Этим мы закрываем поток событий все время, прежде чем открывать новый. Достаточно интерфейса, давайте перейдем к концу.
Таблица маршрутизации и в основном весь код остаются прежними, за исключением только двух небольших изменений метода, Statistics.attach и Application.stats . Давайте посмотрим, как реализация серверной статистики процессора хоста с использованием событий на стороне сервера реализована на стороне контроллера (и сопоставлена с / stats /: id URL):
|
01
02
03
04
05
06
07
08
09
10
11
|
def stats( id: String ) = Action { request => Hosts.hosts.find( _.id == id ) match { case Some( host ) => Async { Statistics.attach( host ).map { enumerator => Ok.stream( enumerator &> EventSource() ).as( "text/event-stream") } } case None => NoContent }} |
Очень короткий кусок кода, который делает много вещей. После нахождения соответствующего хоста по его идентификатору мы «привязываемся» к нему, получая экземпляр Enumerator : непрерывный поток данных статистики процессора. Ok.stream (enumerator &> EventSource ()) .as («текст / поток событий») преобразует этот непрерывный поток статистических данных в поток событий, которые клиент может использовать, используя события на стороне сервера .
Чтобы закончить с изменениями на стороне сервера, давайте посмотрим, как выглядит «присоединение» к потоку статистики хоста:
|
1
2
3
4
5
|
def attach( host: Host ): Future[ Enumerator[ JsValue ] ] = { ( actor( host.id ) ? Connect( host ) ).map { case Connected( enumerator ) => enumerator }} |
Это так же просто, как вернуть Enumerator , и, поскольку мы используем акторы Akka , становится немного сложнее с Future и асинхронными вызовами. Вот и все!
В действии наше простое приложение выглядит следующим образом (с использованием Mozilla Firefox ), в качестве примера используются только Host 1 и Host 2 :
Очень красиво и просто, и еще раз, большое спасибо ребятам из Play Framework и сообществу. Полный исходный код доступен на GitHub .

