bitterharvest’s diary

A Bitter Harvestは小説の題名。作者は豪州のPeter Yeldham。苦闘の末に勝ちえた偏見からの解放は命との引換になったという悲しい物語

Reactive-banana 紹介(6)

7.NetMonitor

今回の例は、ネットワークのモニタで、入力と出力のパケット数を表示する。簡単なプログラムだが、Reactive-bananaで記述されるプログラムの基本的な構成をとっている。そこで、今回は、Reactive-bananaの基本的な枠組みを紹介しながら、ネットワークをモニタするプログラムを解説する。

7.1 Reactive-bananaの基本的な枠組み

振舞いやイベントについては、理解が進んできたと思うが、イベントがどのように処理されるのかについては、まだ分かっていないことが多いと思う。

Reactive-bananaを構成するモジュールの一つであるReactive.Banana.Combinatorsでは、入力イベントから出力イベントを得る関数をいくつか用意している。CounterとTwoCountersの記事で出てきたunionsはその一つである。そのほかに、filterE(下図参照), collect, spill, accumE, apply, stepper(下図参照)などがある。このように、イベントからイベント(あるいは振舞い)を得るものを、Reactive-bananaでは、イベント・グラフと呼んでいる。
f:id:bitterharvest:20151205094839p:plain
f:id:bitterharvest:20151205095344p:plain
イベント・グラフに加えて、入力と出力を一緒にしたものをイベント・ネットワークと呼んでいる。イベント・ネットワークを構築するためには、イベント・グラフ、入力、出力をモナドMomentの中で記述する。そして、これらからイベント・ネットワークを利用できるようにするためには関数compileを利用する。さらに、イベント・ネットワークを動かすためには、関数actuateを用いる。これにより、ネットワークは、入力イベントを登録し、出力を開始できるようになる。

典型的なプログラムは次のようになる。下記のプログラムで、emouseからedieまでが入力、letの中のbehavior1からevent15までがイベント・グラフ、二つのreactimateが出力である(これまでの例では、reactimateの代わりにsinkを用いた)。

main = do
  -- initialize your GUI framework
  window <- newWindow
  ...

  -- describe the event network
  let networkDescription :: forall t. Frameworks t => Moment t ()
      networkDescription = do
          -- input: obtain  Event  from functions that register event handlers
          emouse    <- fromAddHandler $ registerMouseEvent window
          ekeyboard <- fromAddHandler $ registerKeyEvent window
          -- input: obtain  Behavior  from changes
          btext     <- fromChanges    "" $ registerTextChange editBox
          -- input: obtain  Behavior  from mutable data by polling
          bdie      <- fromPoll       $ randomRIO (1,6)

          -- express event graph
          let
              behavior1 = accumB ...
              ...
              event15 = union event13 event14
  
          -- output: animate some event occurences
          reactimate $ fmap print event15
          reactimate $ fmap drawCircle eventCircle

  -- compile network description into a network
  network <- compile networkDescription
  -- register handlers and start producing outputs
  actuate network

このプログラムの、イベントを入力する部分で使われている関数について説明しておこう。ここでは、三種類のものが使われている。一つは、マウスやキーボードなどが押されたことにより生じるイベントである。また、もう一つは、テキストなどのように変化があった時に生じるイベントである。最後は、変化していくデータからのイベントである。次のようになっている。

fromAddHandlerは、イベントハンドラ(上記では、registerMouseEvent, registerKeyEvent)を登録しておくことで、イベント(この場合には、マウスとキーボード)の検出を可能にする(他の言語では、コールバック関数あるいはリスナーと呼ばれている)。

fromChangesは、変化があったときにイベントハンドラ(上記では、registerTextChange )から知らせてもらう。

fromPollは、変更可能なデータ(たとえば現在時刻)を頻繁にポーリングし、振舞いを得る。上記のプログラムでは乱数の発生をポーリングしている。

出力を定義している部分にあるreactimateは、イベントが発生したときはいつでも、IOの処理を行う。なお、次のことに注意しておくことが必要である。reactimateは、二つのイベントがとても短時間のうちに前後して生じたときは、一方の処理が終了してから、次のイベントを処理するというわけにはいかず、二つのイベントの処理がインターリーブになることがある。

7.2 ユーザ・インタフェース

例によって、ユーザ・インタフェースを定義する。送ったパケットの数と受けたそれとを表示する必要があるので、次のようになる。

    f        <- frame    [text := "Network Monitor"]
    out1     <- staticText f []
    out2     <- staticText f []
    
    set f [layout := minsize (sz 250 70) $ margin 10 $
            column 10 [label "TCP network statistics",
                       grid 5 5 [[label "Packets sent: ", widget out1]
                                ,[label "Packets received: ", widget out2]]
                      ]
          ]

初期画面は次のようになる。
f:id:bitterharvest:20151204082711p:plain

7.3 イベント・ネットワークの構築

イベント・ネットワークを構築する前に、タイマーの準備をしておく。これは、ネットワークをモニタするための間隔を与えるタイマーである。ここでは、500ミリ秒ごとにモニタすることにする。

    t <- timer f [ interval := 500 ] -- timer every 500 ms

それでは、入力、イベント・グラフ、出力からなるイベント・ネットワークを構築しよう。

入力

入力は、ネットワークの状況を得ることである。これは、後で定義する関数getoNetworkStatisticsから、得ることにすると、入力は次のようになる。

            bnetwork <- fromPoll getNetworkStatistics

イベント・グラフ

イベント・グラフは、タイマーからモニタする時間が来たというイベントである。これは次のようになる。

            etick <- event0 t command

出力

最後に出力であるが、これはreactimateの代わりにsinkを用いる。これは、出力でGUIのためのウィジェットを利用することによる。次のようになる。

            let showSent     = maybe "parse error" show . fst
                showReceived = maybe "parse error" show . snd
        
            sink out1 [ text :== showSent     <$> bnetwork ]
            sink out2 [ text :== showReceived <$> bnetwork ]

ネットワークの状況は、システムプロセスのnetstatを用いて次のように得ることができる。

getNetworkStatistics :: IO NetworkStatistics
getNetworkStatistics = do
    s <- readProcess "netstat" ["-s", "-p","tcp"] ""
    return (readField "packets sent" s
           ,readField "packets received" s)

readField :: String -> String -> Maybe Int
readField fieldname = id
    . fmap (read . filter isDigit) . listToMaybe
    . filter (fieldname `isSuffixOf`) . lines

後はcompile,actuateして実行すればよいことになる。

7.4 プログラムの全体

プログラムはこのホームページで公表されているが、転載する。

{-----------------------------------------------------------------------------
    reactive-banana-wx
    
    Example: Minuscule network monitor
------------------------------------------------------------------------------}

import Data.Char
import Data.List
import Data.Maybe

import System.Process

import Graphics.UI.WX hiding (Event)
import Reactive.Banana
import Reactive.Banana.WX

{-----------------------------------------------------------------------------
    Main
------------------------------------------------------------------------------}
main :: IO ()
main = start $ do
    f        <- frame    [text := "Network Monitor"]
    out1     <- staticText f []
    out2     <- staticText f []
    
    set f [layout := minsize (sz 250 70) $ margin 10 $
            column 10 [label "TCP network statistics",
                       grid 5 5 [[label "Packets sent: ", widget out1]
                                ,[label "Packets received: ", widget out2]]
                      ]
          ]
    
    t <- timer f [ interval := 500 ] -- timer every 500 ms

    let networkDescription :: MomentIO ()
        networkDescription = do
            -- The network statistics are polled when and only when
            -- the event network handles an event.
            bnetwork <- fromPoll getNetworkStatistics
            -- That's why we need a timer that generates regular events to handle.
            etick    <- event0 t command
        
            let showSent     = maybe "parse error" show . fst
                showReceived = maybe "parse error" show . snd
        
            sink out1 [ text :== showSent     <$> bnetwork ]
            sink out2 [ text :== showReceived <$> bnetwork ]
    
    network <- compile networkDescription
    actuate network

-- Obtain network statistics from the  netstat  utility
type NetworkStatistics = (Maybe Int, Maybe Int)

getNetworkStatistics :: IO NetworkStatistics
getNetworkStatistics = do
    s <- readProcess "netstat" ["-s", "-p","tcp"] ""
    return (readField "packets sent" s
           ,readField "packets received" s)

readField :: String -> String -> Maybe Int
readField fieldname = id
    . fmap (read . filter isDigit) . listToMaybe
    . filter (fieldname `isSuffixOf`) . lines