Статьи

Облегченные графики в реальном времени с Play Framework и Scala с использованием событий на стороне сервера

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

сс-host1

сс-host2

Очень красиво и просто, и еще раз, большое спасибо ребятам из Play Framework и сообществу. Полный исходный код доступен на GitHub .