Statistical Programming

もがく、WebSocket!!

前回ソリティアを作ろうと思い立ち開発しているさなかで
そうだ、WebSocketを使ってみよう!と決意し調べてみたら見事にはまったのでメモっておきます。
Scalaは他の言語と比べてもその言語仕様も外部ライブラリも変更が頻繁にあるのでそこんとこで
はまりましたね。

さてさて、まずはWebSocketとはなんぞや!?っていうところは割愛するとして
(知らない方はこちらを参考に。 
http://www.atmarkit.co.jp/ait/articles/1111/11/news135.html
Scala&Playでの実装方法についてです。Play本家の方によるwebsocketを利用したchatシステムのコードがGitHubに一応公開されているのですが、あまりシンプルではない上に現時点ではコンパイルすら通らない状況なので別の方のコードを参考にさせていただきました。参考にさせていただいた方の記事はこちらです。

http://blog.controlgroup.com/2013/10/17/simple-websockets-example-play-2-2-0/


いや、本当に簡単すぎてびっくりしましたよ。てかPlayドキュメントにこれいれようよ!って激しく思いましたもん。なんかScalaは成熟してないせいかドキュメントだったり本だったりが初心者向けじゃないんですよねー
まあどうやって開発していけば良いかっていうことを学べる点ではいいんですけど根本的にわかんないよ!的なケースも多い。。。

それはさておきはじめていきますね。と、その前に以下本家のドキュメントです。少な!

WebSocket を扱う
これまでは、標準的な HTTP リクエストを処理し、標準的な HTTP レスポンスを送信するためには Action を使っていました。WebSocket はそれと全く異なるので、通常の Action では扱えません。

WebSocket リクエストを処理するためには、Action の代わりに WebSocket を使います。

def index = WebSocket.using[String] { request => 

// Log events to the console
val in = Iteratee.foreach[String](println).mapDone { _ =>
println("Disconnected")
}

// Send a single 'Hello!' message
val out = Enumerator("Hello!")

(in, out)
}
WebSocket からはリクエストヘッダ (WebSocket 接続を開始するための HTTP リクエストからの。) を参照でき、標準的なヘッダやセッションデータを取得することが可能です。しかし、リクエストボディを参照したり、HTTP レスポンスを返すことはできません。

WebSocket をこの方法で組み上げる場合、in と out の二つのチャンネルを返す必要があります。

in チャンネルは Iteratee[A,Unit] (A はメッセージのタイプで、ここでは String になります) で、各メッセージを受信するたびに通知を受け取ります。またソケットがクライアント側で閉じされた場合には EOF を受け取ります。
out チャンネルは Enumerator[A] で、Web クライアントへ送信するメッセージを生成します。このチャンネルに EOF を送信すると、WebSocket 通信をサーバ側から切断することができます。
この例では、受信した各メッセージを console に出力するだけのシンプルな Iteratee を作成しています。また、メッセージを送信するため、Hello! というメッセージを一回だけ送信する単純なダミーの Enumerator も作成しました。

Tip: WebSocket は http://websocket.org/echo.html でテストすることができます。location に ws://localhost:9000 を設定してください。

次は、入力データを全て捨てつつ、Hello! メッセージを送信した後すぐにソケットを閉じる例を書いてみましょう。

def index = WebSocket.using[String] { request => 

// Just consume and ignore the input
val in = Iteratee.consume[String]()

// Send a single 'Hello!' message and close
val out = Enumerator("Hello!").andThen(Enumerator.eof)

(in, out)
}



Play本家のドキュメントを見ていただくとinとoutがなんなのか、WebSocketとは?なんとなくわかっていただけたかと思いますが現在のバージョンでこれを実行するとエラーになります!それになにやってるのかめっさわかりづらい!というわけでもっとシンプルな方がこちらです。

app/controllers/Application.scala

def index =WebSocket.using[String] { request => 

//Concurernt.broadcast returns (Enumerator, Concurrent.Channel)

val (out,channel) = Concurrent.broadcast[String]
//log the message to stdout and send response back to client

val in = Iteratee.foreach[String] {
msg => println(msg)
//the channel will push to the Enumerator
channel push("RESPONSE: " + msg)
}
(in,out)
}



本家のドキュメントと比べてやってること自体が違うのであれですが根本的に違うところ、てかこのメソッドもう使えないんですけど!?ポイントがいくつかあります笑
まずforeachの後にしていたMapDoneっていうメソッドも非推奨的な扱いなのかwarning的なのがでました。このブログの方によれば今まではEnumerator.imperative() を使っていたんですがPlay的にはConcurrent.broadcast()を使っていきたいらしくそれの兼ね合いでうまくいかなかったりしてるらしいっす。詳しいことはわからないっす。

これは単純にクライアントから何らかのメッセージを受信したらメーセージをprintlnしてchannelへ(RESPONSE:〜)という形でpushする(自分もなんとなくでしかわかんない…)。。。
うーんたぶん、メッセージをクライアントから受け取ったらprintlnをしてそれからそのメッセージをchannelっていうクラスかなんかに入れる動作をpushっていう形でする。そもそもchannelってなによ!?と思ってapiを見たら
A channel for imperative style feeding of input into one or more iteratees.
と。ふんふん。feedってなんやねん!

def
feed[AA >: A](in: Input[E]): Future[Iteratee[E, AA]]
Sends one element of input to the Iteratee and returns a promise containing the new Iteratee. The promise may or may not be completed already when it's returned (the iteratee may use an asynchronous operation to handle the input).
in
input being sent

なるほど。feedとはあるinputをiterateeへ送りそのリターンを新しいIterateeを内包したPromiseを返すんすね。Promiseは返信の約束のことっすね。ぼんやりとわかった気がします。じゃあ結局何してるかっていうと受け取ったメッセージを都合のいいPromiseって形に変換してるんすね〜

val (out,channel)ではそのメッセージを内包したchannelとoutを含んでいるbroadcastを返しているのか…?
わからん…でもなんとなくわかった!笑

とりあえずこれでクライアントから送られたメッセージがなんかそのメッセージを含むいろんなごちゃっとしたやつに内包される形で送り返されるようにできた。たぶん。

つぎにブラウザ開いて http://localhost:9000/ にアクセスしーの、javascriptコンソールを開き
WebSocketを作成する。

var ws = new WebSocket("ws://localhost:9000");

こんな感じで。

んで次はメッセージ受信した際の処理をまとめたfunctionの作成なのかな?

ws.onmessage = function( message ) { console.log( message ); };

weにonmessageしたら以下のファンクションを発動させる!!Stringの引数を受け取りその値をそのままconsole.logに出力させたら準備完了。

最後にクライアント側からメッセージを送信してあげれば

ws.send("test")

こんな感じにわさっとサーバーから返事が返ってくると。

undefined
MessageEvent {ports: Array[0], data: "RESPONSE: test", source: null, lastEventId: "", origin: "ws://localhost:9000"…}
bubbles: false
cancelBubble: false
cancelable: false
clipboardData: undefined
currentTarget: WebSocket
data: "RESPONSE: test"
defaultPrevented: false
eventPhase: 0
lastEventId: ""
origin: "ws://localhost:9000"
ports: Array[0]
returnValue: true
source: null
srcElement: WebSocket
target: WebSocket
timeStamp: 1383994417667
type: "message"
__proto__: MessageEvent

その中に

data: "RESPONSE: test"

ってのがあることからもしっかりメッセージがサーバー側にいっとるいっとる。
もっと勉強せねばな。。。