Продолжая замечательное путешествие с использованием потрясающей 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 .