トラフィックをWebSocketでリアルタイムに描写してみた

[NetOpsCoding AdventCalender](http://qiita.com/advent-calendar/2015/netopscoding)の11日目の記事です．

今回はネットワークオペレーションをする上で，必ずといっていいほど確認するトラフィックグラフを，*WebSocket*という技術を用いて実装してみます．
デモとソースコードは最後に貼り付けています．

**尚，この実装手法は[Cedec2015](http://cedec.cesa.or.jp/2015/)において@yuyarinさんが監視のために使っていたツール参考にして実装しています．**

1. はじめに
=
トラフィックをモニタするツールとしてはCacti,Zabbix,MRTG等が有名です．
多くの場合，SNMPの取得間隔は5分程度だと思います．
基本はそれで確認は満足できますが，新しい回線の開通/トラフィック調整などの実作業時には，*その瞬間*のトラフィックを見たい場合が多いです．
機器によってはCLIベースでリアルタイムなトラフィックを監視できますが，CLIでは複数箇所の状態変化は気付きにくく，異常検知が遅れてしまうこともありませんか？

そんな悩みを解決すべく，*視覚的にわかりやすく，かつ短時間のトラフィック変化をリアルタイムに捉えることができる*グラフを作ってみます．

2. Web上でリアルタイムに描写するには 
=
Webの画面上で動的にグラフを描写する場合，主に二通りの方法があります．

 * 定期的にブラウザ側から更新データを取りに行く方法(Ajaxによるポーリング)
 * サーバが投げるデータを捉えて反映させる方法(WebSocketによる双方向通信)

WebSocketは比較的新しい技術です．
データの送受信のタイミングや，通信のやりとりの方法は自由に決定できます．
一度セッションを貼ってしまえば，簡単に双方向でデータの受け渡しができちゃいます．

グラフ描写はどちらでも実現可能ですが，どうせなら他のツール実装でも応用がききそうなWebSocketを使ってみます！！
では，何はともあれ早速使ってみましょう！

3. 実行環境
=

* Python 2.7.6
  * [tornado](http://www.tornadoweb.org/en/stable/index.html) 4.3
* nginx 1.8.0
* jquery 1.8.2
* [HighCharts](http://www.highcharts.com/)

Web上で表示するため，nginxやapache等のプラットフォームが必要になります．
今回のデモサーバーではnginxを用います．また，WSの通信に今回は8080番を使っています．

WebSocketのライブラリは[tornado](http://www.tornadoweb.org/en/stable/index.html)を使っています．Gevent-WebSocket , ws4py等ありますが，自分にはtornadoが一番しっくり来ました．

また，グラフ描写ツールとしてHighChartsを使っていますが，動的に追加できるグラフツールであれば他のものでも問題ありません．
また，HightChartsは個人利用以外は有料なので，気にされる方は他のツールを使ったほうが良いでしょう．
[ccchart](http://ccchart.com/)などはサンプルも豊富でオススメです．

5. WebSocket-Server側の実装
=

サーバサイドを実装してみましょう．
WebSocketのサーバーサイドの実装は「非同期処理」という点のみ抑えておけば難しくありません．
特定のURIで待ち受け，クライアントが接続されると，お互いのデータやり取りを自由にできるようになるので，SNMPを取得した結果を送信できるようになります．
SNMPはsubprocessによるコマンド実行で取得し，結果を正規表現で抽出しています．
また，今回は自サーバのNICのCounterを見るOIDを指定しています．

下記のようなコードを実装後，pythonコマンドで実行するだけでWebSocketのサーバーの構築は完了です！

```ws_traffic_server.py
#! /usr/bin/env python
# -*- coding: utf-8 -*-
import tornado.ioloop
import tornado.web
import tornado.websocket
from tornado.options import define, options ,parse_command_line
from datetime import datetime
import shlex, subprocess , time ,re , json ,threading

SLEEP_TIME = 20 # SNMPを取得する間隔

COMMUNITY = 'dev'
IP = '127.0.0.1'
IFMIB_OID = '1.3.6.1.2.1.31.1.1.1'

snmp_command_in = 'snmpget -v 2c -c %s %s %s.6.2'%(COMMUNITY,IP,IFMIB_OID)
snmp_command_out = 'snmpget -v 2c -c %s %s %s.10.2'%(COMMUNITY,IP,IFMIB_OID)

#WebSocketがListenするポートを指定
define("port", default = 8080,type = int)

class SendWebSocket(tornado.websocket.WebSocketHandler):

    #コネクションが確保されると呼び出されるイベント
    def open(self):
        print 'Session Opened. IP:' + self.request.remote_ip

    #ブラウザが閉じられた場合等，切断イベントが発生した場合のイベント
    def on_close(self):
        print "Session closed"

    #クライアントからメッセージが送られてくると呼び出されるイベント
    def on_message(self, message):
        if message == 'hello':
            pre_counter_in = int(self.exe_snmp(snmp_command_in))
            pre_counter_out = int(self.exe_snmp(snmp_command_out))

            #WebSocketではtime.timerは使えない．下記変数のCallBackで遅延を再現
            #SLEE_TIME秒後に後半のSNMPと通信処理を開始する
            tornado.ioloop.IOLoop.instance().call_later(SLEEP_TIME,self.snmp_second_half,{'in_counter':pre_counter_in,'out_counter':pre_counter_out})

    #SLEEP_TIME秒後のSNMPを取得
    def snmp_second_half(self,pre_counters):
        result = {}
        pos_counter_in = int(self.exe_snmp(snmp_command_in))
        pos_counter_out = int(self.exe_snmp(snmp_command_out))

        #指定秒数とのcounter差分からトラフィック算出
        traffic_in = (pos_counter_in - pre_counters['in_counter'])  / SLEEP_TIME
        traffic_out = (pos_counter_out - pre_counters['out_counter'])  / SLEEP_TIME
        #JSON化したトラフィックデータをWebクライアントへ送信
        try:
            result.update({'traffic_in' : traffic_in , 'traffic_out' : traffic_out})
            result.update({'timestamp' : time.mktime(datetime.now().timetuple())})
            self.write_message(json.dumps(result))
        except:
            print "Client is already disconnectted."

    #SNMP実行結果のValueのみを返す
    #e.g. [IF-MIB::ifHighSpeed.21 = Gauge32: 1000] -> [1000]
    def exe_snmp(self,snmp_command):
        split_command = shlex.split(snmp_command)
        exec_output = subprocess.check_output(split_command)
        r = re.compile("(.*)(: )(.*)")
        snmp_result = r.match(exec_output).group(3)
        return snmp_result

    #Trueにしないと明示されたホストからしか通信を受け付けない
    def check_origin(self, origin):
        return True

#指定したURIでWSへの接続要求を待ち受ける
app = tornado.web.Application([
    (r"/ws/ifmon/", SendWebSocket),
])

if __name__ == "__main__":
    parse_command_line()
    app.listen(options.port)
    mainloop = tornado.ioloop.IOLoop.instance()
    mainloop.start() #WebSocketServer起動
```

ここでハマりやすいのは，ネットワーク機器のトラフィックをSNMPから取得して計算するまでの過程です．
SNMPによるトラフィック量計算は
`( 現在のIFcounter - n秒後のIFcounter ) / n`
というように数十秒待たなければ算出できないので，n秒の間はプログラムでも待たなければなりません．
しかし標準ライブラリにある`time.sleep()`は，WebSocketのような非同期通信時に使うことは出来ません．プロセス全体がSleepしてしまうため，他の閲覧者にも影響が出てしまいます．

これはtornado内で用意されている`IOLoop.call_later()`という遅延後にコールバックができる関数によって解決しました．

プログラムはちょっと無理やり作っている感があるので，正しくはこう書くべきとかあればコメントいただけると嬉しいです．

6. クライアント側の実装
=
クライアント側の処理は，Javascriptによって行います．
クライアント側が，サーバから送られてきたトラフィックデータを捌くためには，4つのイベントハンドラを知っておく必要があります．

|Event|発生タイミング|
|---|---|
|onopen|サーバとの接続が完了した際に呼ばれるイベント|
|onmessage|データを受け取った時に発生するイベント(メインな所)|
|onclose|サーバとの接続が切断された際に呼ばれるイベント|
|onerror|エラー発生時に呼ばれるイベント|

サーバへの接続が完了し，`onopen`のイベントが発生したら，SNMPを取り始めるよう`hello`という合図をサーバに送ります．
`hello`を送信すると，サーバ側が反応し，トラフィックのデータを送信します．
サーバからのデータの受信を検知すると，`onmessage`イベントが発生するので，グラフ描写はこのイベント内で行います．
ブラウザを閉じるなど，WebSocketを切断するような動作を捉えると，`onclose`イベントが発生し，そこで通信終了になります．

グラフの設定等については長いので割愛します．
WebSocket処理の該当箇所は下記のような実装にしました．

```ws_client.js
var WS_URL = 'ws://www5445uo.sakura.ne.jp:8080/ws/ifmon/'

var init_ws_Traffic = function(){
  var traffic_chart =Traffic_Chart('traffic_chart', {}); //Highhartグラフのコンストラクタ
  var ws = new WebSocket(WS_URL);
  console.log('----- WebSocket Open -----')

  ws.onopen = function() {//WS接続確立
    ws.send('hello');
  };

  ws.onerror = function(event){
    $("p#error").text('Failed join to the server');
    console.log('Connection faild....')
  };

  ws.onmessage = function(event) { //トラフィックデータ処理
    var data = JSON.parse(event.data);
    var timestamp = data['timestamp']*1000 + 60 * 9 * 60 * 1000;
    var value = data['traffic_in']
    document.getElementById('traffic_in').innerHTML = convert_bps(value); //テキストでも表示させる
    traffic_chart.series[0].addPoint([timestamp, value], true, true); //InboundTrafficのグラフAddはここで行う
    value = data['traffic_out']
    document.getElementById('traffic_out').innerHTML = convert_bps(value);
    traffic_chart.series[1].addPoint([timestamp, value], true, true);//OutboundTrafficのグラフAddはここで行う
    ws.send('hello'); // 次のトラフィック要求
  };
  ws.onclose = function () {
    ws.send('close');
    ws.close();
  };
  window.onbeforeunload = function () {
    ws.send('close');
    ws.close()
  };
}

```

以上2つがWebSocket通信で必要なものになります．
実際に作ったものをWeb画面で確認するとこんな感じになります．
<img width="1276" alt="スクリーンショット 2015-12-11 1.19.25.png" src="https://qiita-image-store.s3.amazonaws.com/0/76968/0accb3f3-c8a5-9bc4-91fd-3c7ef6d6fd86.png">
ちゃんとグラフになってますね・・・！

これで作業時用に10秒間隔のトラフィックグラフを取得することができるようになりました！
もし同じようなものを作っていて，より良い手法があったら教えて下さい．

7. おまけ(デモとソースコード)
=
リアルタイムで更新されていく感が無いとイマイチ分からないと思うので，
作ったものをデモサーバーに上げておきました．
期間限定かもしれませんが，どんなもんか見てみてください．
20秒程待ってると，何か変化が起こると思います．

**デモ設置のサーバーが消え去ったので、実働のデモはありません**

また，デモで使っているコードは下記に上がっています．
https://github.com/Mabuchin/wstraffic

8. 最後に
=
作業時に使うためのトラフィックグラフの実装方法について紹介しました！
少し書き換えれば1つのグラフの中に複数機器のトラフィックを描写できるようにする等，みなさんの用途にあった改造をしてもらうともっと便利に使えると思います．
もし何か使う機会があれば是非使ってみてください！