Edited at
ClojureDay 6

aleph/lamina: Clojureでサーバー/クライアントWebSocket

More than 5 years have passed since last update.

2013年のClojure界隈はcore.asyncが話題ですが、今回は非同期処理にcore.asyncは使いません。

今回はcore.asyncの代わりにalephというライブラリを使って、WebSocketを使った簡単なデモアプリケーションを作ってみようと思います。サーバーとクライアントの両方をClojureで実装します。


aleph

alephは、同じ作者のlaminaという非同期プログラミングライブラリをベースに、nettyを使ってHTTP/WebSocket/TCP/UDPのI/Oをサポートしたライブラリです。

core.asyncと同様に、laminaではchannelと呼ばれるキューにメッセージを出し入れすることで非同期処理をサポートしています。

laminaにはchannel以外にも様々な機能がありますが、今回はchannel以外の機能は使わないので解説しません。(というか解説できるほど私が理解していません。。。

laminaの詳細については公式のwikiに分かりやすく説明が載っているので、そちらを参照してください。

2013/12/6時点でalephの最新のバージョンは0.3.0です。この記事内のコードも0.3.0で動作チェックしています。

またcore.asyncはClojureScriptでも動きますが、alephはその実装上JVM以外でのClojureをサポートしないので注意してください。


aleph + ping = alephing

今回作成するデモは、サーバーが受け取ったメッセージをクライアントにブロードキャストするという、WebSocketのチュートリアルのようなシンプルなアプリケーションです。

ただ、テキストだけのCUIでも十分なのですが、それだとデモとして味気ないので、今回はquilというライブラリ(ProcessingのClojureラッパー)を使って簡単なGUIを追加しました。

アプリケーションのフローとしては、


  1. ウィンドウ内をマウスクリックすると、そのマウス座標をサーバーに送信

  2. サーバー受け取ったマウス座標を現在接続している全クライアントにブロードキャストで送信

  3. クライアントはマウス座標を受け取ると、その座標にオブジェクトをレンダリング

のようになります。簡単に図にまとめると、下のようになります。

今回作成したデモのソースコード類はgithubの下記のレポジトリに置きました。

名前はaleph + pingで alephing にしました。適当です。。。

サーバーのコードはheroku(ws://alephing.herokuapp.com:80)上で動かしているので、クライアントのコードだけをgit cloneしてlein runとするだけでデモを試すことが出来るようになってます。

クライアントを実行すると下のスクリーンショットのようなウィンドウが立ち上がります。

ウィンドウ内を適当にクリックすると「徐々に広がるアニメーションをする円」が表示されると思います。クリックしたマウス座標がブロードキャストで流れてくるので、自分がクリックした座標だけでなく、誰かがクリックした座標にも、円がレンダリングされます。一人で複数のクライアントを立ち上げると分かりやすいです。

クライアントをアイドル状態でしばらく放置しておくと、コネクションが切れてしまうようです。

その際はクライアントを再起動し、サーバーに再接続してください。

では、作っていきましょう。


サーバーを作る

alephを使ってWebSocketサーバーを立ち上げるのは簡単です。

(start-http-server handler {:port port :websocket true})

これだけでWebSocketサーバーが立ち上がります。説明するまでもなくシンプルです。

handlerはクライアントが接続してきた毎に呼ばれる関数で、この中でサーバーの振る舞いを定義します。例えば"ping"という文字列に対して"pong"とレスポンスを返すhandlerは下のように書けます。

(defn handler [ch handshake]

(receive-all ch
(fn [msg] (when (= msg "ping")
(enqueue ch "pong")))))

引数のchはlaminaのchannelです。キューのようなものと考えてください。上で説明したように、このchannelに対して、メッセージを出し入れしてサーバーとクライアントがコミュニケーションをします。

receive-allはイベントリスナーの設定関数のようなもので、chに入ってきたメッセージ毎に指定した関数が呼ばれます。

今回のデモプログラムでは接続してきた全クライアントに対してブロードキャストでメッセージを返すので、


  1. ブロードキャスト用のchannelを1つ作成する

  2. クライアントから来たメッセージが来たら、ブロードキャスト用のchannelにそのメッセージを入れる

  3. ブロードキャスト用のchannelにメッセージが来たら、そのメッセージをそのままクライアントに返す

のようにします。つまりhandlerは下のコードようになります。

(def broadcast-channel (channel))

(defn handler [ch handshake]
(receive-all ch (fn [msg] (enqueue broadcast-channel msg)))
(receive-all broadcast-channel (fn [msg] (enqueue ch msg))))

これだけでブロードキャストのWebSocketサーバーの完成です。

herokuで動かすためのコードなどを加えると、サーバーは下のようなコードになりました。


src/alephing/server/core.clj



(ns alephing.server.core
(:require [clojure.tools.logging :as logging]
[lamina.core :refer :all]
[aleph.http :refer :all]
[environ.core :refer [env]]))

(def broadcast-channel (channel))

(defn handler [ch handshake]
(try
(receive-all ch (fn [msg] (enqueue broadcast-channel msg)))
(receive-all broadcast-channel (fn [msg] (enqueue ch msg)))
(catch Exception e nil)))

(defonce server (atom nil))

(defn start [port]
(let [port (Integer. (or port (env :port) 5000))]
(reset! server
(start-http-server handler
{:port port
:websocket true}))
(logging/info (str "Started with port " port))))

(defn stop []
(when @server
(@server)
(reset! server nil)))

(defn -main [& [port]]
(start port))


stop関数は必要ないですが、REPLでサーバーを再起動させたりしてデバッグするときに便利です。


heroku上で動かす際の注意点

herokuはWebSocketをサポートしていますが、まだexperimentalのようです。

WebSocketをサーバーを使うにはheroku-toolbeltで、

heroku labs:enable websockets

とWebSocketをenableにしないと動かないことに注意しましょう。それ以外は普通のWebアプリケーションと同じです。


クライアントを作る

次にクライアントを作ります。

alephはクライアントがWebSocketサーバーに接続するのも簡単です。

(websocket-client {:url "ws://example.com:80"})

とするだけです。

ただし接続が確立されるまではchannelにメッセージの出し入れが出来ないので、wait-for-result関数で接続確立を待ちます。つまり

(def c (websocket-client {:url "ws://example.com:80"}))

(def ch (wait-for-result c))

のようにします。このchに対してenqueueしたりreceiveしたりすればメッセージのやりとりができます。

まとめると、クライアントのコードは下記のようになりました。

(ns alephing.core

(:require [lamina.core :refer :all]
[aleph.http :refer :all]))

(defn- handler [msg]
(println (read-string msg)))

(defn connect []
(let [c (websocket-client {:url "ws://example.com:80"})
ch (wait-for-result c)]
(receive-all ch handler)
{:client c
:channel ch}))

(defn echo [c data]
(enqueue (:channel c) (pr-str data)))

レポジトリに置いてあるコードには、上記のコードに加えてquilのGUI部分のコードが加わっています。しかし全体のフローはだいたい一緒です。

今回はquilの部分の解説は割愛させて頂きます。非常にシンプルなコードなので、見て頂ければ簡単に理解出来ると思います。


まとめ

今回はWebSocketのサーバーサイドだけでなくクライアントサイドもClojureで実装してみました。

Clojureらしくシンプルに実装できることが伝わったらうれしいです。

alephまたはlaminaには今回紹介したもの以外にも様々な機能があります。laminaのchannelでもsiphonmap*など紹介していない便利な関数があります。興味がある方は調べてみてください。

また、クライアントまでClojure on JVMで書くならTCP/UDPで良いじゃんというツッコミもあると思います。その指摘は私も正しいと思います。

ただWebSocketで実装することで、以下のような利点があると思います。


  • 80番ポートしか開いていないセキュリティ厳しい環境でも動かすことができる

  • 暗号化(SSL)の処理を外部プログラム(ライブラリ)に任せることができる

  • ブラウザ上(JavaScript)で動くWebSocketクライアントと同じサーバーのコードで運用できる

な感じでしょうか。最初はWebSocketで作って、パフォーマンスの問題などが出て来たときにTCP/UDPに切り替えれば良いのかな、と思っています。


すみません。宣伝させてください。

弊社 株式会社テンクーでは、99%をClojureで実装したChrovis(クロビス)という、研究者向けゲノム解析サービスを開発中で、2013年の今冬リリース予定です。

https://chrov.is

Chrovisでは今回紹介したalephも使っており、alephでWebSocket用いたインタラクティブなクライアントアプリケーションを作っています。

また解析結果などのビジュアライゼーションでは、今回のデモではquilを用いましたが、Chrovisではcasmicという社内で作ったOpenGLベースのClojureライブラリを使っています。このcasmicについては近日公開予定です。

そこで現在、弊社ではClojureエンジニアを募集しております。「Clojureを勉強中」や「Clojureに興味がある」でも構いません。短時間やアルバイト、兼業、在宅勤務など柔軟に対応できます。もちろん学生もWelcomeです。年齢や経験は問いません。生物学やバイオインフォマティクスの知識も不要です。

興味がある方はinfo@xcoo.jpまでお気軽にメールをください。

もしくはfederkastenに直接mentionしていただいてもかまいません。

何卒よろしくお願い致します。