Jetty7のWebSocketをScalaから使う
HTML5っ!WebSocketっ!サーバーからプッシュでっ!
やりますよscalaで。とはいえ、Javaでも同じなので、Javaでやってみようって人にも参考になるかも?
サーバーの実装
サーバ側の実装は、要点をまとめるとこんな感じです。
- org.eclipse.jetty.websocket.WebSocketServletを継承したServletを作ります。
- protected abstract WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) を実装しますよ
- クライアントからws:/hostname/でWebSocketの接続要求が来たら、このdoWebSocketConnectが呼ばれます。
- doWebSocketConnectでは、org.eclipse.jetty.websocket.WebSocketを実装したオブジェクトを生成して返すようにします。
- WebSocketの実装クラスでは
- クライアントとの接続が確立したら、void onConnect(WebSocket.Outbound outbound) が呼び出されます。
- WebSocket.Outboundオブジェクトってのは、クライアントとのSocketみたいなものだとおもっておけばよいです
- クライアントにメッセージを送るには、WebSocket.OutboundオブジェクトのsendMessage(byte frame, String data) を呼び出せばよいです。
- クライアントからメッセージを受信したら、void onMessage(byte frame, String data) が呼び出されます。
- 接続が切られた場合は、void onDisconnect() が呼び出されます。
簡単ですね。実際はこんな感じのコードです。
package com.yuroyoro.websocket import scala.collection.mutable.Set import javax.servlet.http._ import org.eclipse.jetty.websocket._ import org.eclipse.jetty.websocket.WebSocket.Outbound class ChatServlet extends WebSocketServlet { val clients = Set.empty[ChatWebSocket] override def doGet(req:HttpServletRequest, res:HttpServletResponse ) = getServletContext.getNamedDispatcher("default").forward(req, res) override def doWebSocketConnect(req:HttpServletRequest, protocol:String ) = new ChatWebSocket class ChatWebSocket extends WebSocket { var outbound:Outbound = _ override def onConnect(outbound:Outbound ) = { this.outbound = outbound clients += this onMessage( 0, "WebSocket is success!!!"); } override def onMessage(frame:Byte, data:Array[Byte], offset:Int, length:Int ) = {} override def onMessage(frame:Byte, data:String ) = clients.foreach{ c => c.outbound.sendMessage( frame, data ) } override def onDisconnect = clients -= this } }
クライアント
論よりソースってことで。
var ws = new WebSocket("ws://localhost:8080/"); ws.onmessage = function(m) { $('#content').prepend(m.data ); }; $('#hashtag').change(function(){ ws.send(this.value); }); $(window).unload(function(){ ws.close(); });
- サーバと接続するにはWebSocketオブジェクトを作ります。引数はURLです。
- メッセージを送るには、WebSocketオブジェクトのsend( value )を呼ぶだけ
- サーバからメッセージを受け取るには、WebSocketオブジェクトのonmessageにcallbackをつっこんでおくだけ
- 接続をきるには、WebSocketオブジェクトのclose
ほんと簡単ですね。
今のところWebSocketをサポートしてるのはGoogle ChromeだけなのでChromeでやってね★
動かしかた
Jettyは、7.0.1.v20091125が必要です。あと、jetty-websocket-7.0.1.v20091125.jarとかがいります。このへんの必要そうなのはJettyのサイトから落っことしてください。
俺は面倒なのでmavenでやりました。pom.xmlの参考にどぞ
あとは、普通のServletアプリケーションのように、WebSocketServletを継承したServlet作ってweb.xmlに入れておけばいいです。
mavenでやる場合の注意点として、どうも今時点ではmvn jetty:runで起動した場合はWebSocketはうまく動作しないようです。(F.Y.I Re: [jetty-users] Re: NPE in WebSocketServlet)
なので、JettyのLauncher作りました。
scala-websocket/hashtag/src/main/scala/com/yuroyoro/websocket/JettyLauncher.scala at master · yuroyoro/scala-websocket · GitHub
サンプル
サンプルを2つほど作りました。おなじみのChatと、Twitterのハッシュタグ検索をサーバからプッシュするヤツ。ScalaのActorとWebSocketでこういうリアルタイムプッシュができてうれしいですよね?
GitHubに置いてあります。
yuroyoro/scala-websocket · GitHub
サンプルその1 チャット
Scalaで書いてます。ソースはさっきはったので省略。
scala-websocket/chat at master · yuroyoro/scala-websocket · GitHub
サンプルその2 Twitterハッシュタグ検索
ScalaのActorで更新があったらプッシュします。
scala-websocket/hashtag at master · yuroyoro/scala-websocket · GitHub
package com.yuroyoro.websocket import javax.servlet.http._ import org.eclipse.jetty.websocket._ import org.eclipse.jetty.websocket.WebSocket.Outbound import scala.actors._ import scala.actors.Actor._ import scala.xml.XML import scala.io.Source class HashTagServlet extends WebSocketServlet { override def doGet(req:HttpServletRequest, res:HttpServletResponse ) = getServletContext.getNamedDispatcher("default").forward(req, res) override def doWebSocketConnect(req:HttpServletRequest, protocol:String ) = new HashTagWebSocket class HashTagWebSocket extends WebSocket { var outbound:Outbound = _ object SearchActor extends Actor{ def act() = { react { case Search( frame, tag, sinceId) => { val url = "http://search.twitter.com/search.atom?q=%%23%s&since_id=%s".format( tag, sinceId ) val l = XML.loadString( Source.fromURL( url, "utf-8").getLines.mkString) \\ "entry" println( "fetch:%s".format( url )) l.map{ e => val c = (e \\ "content" toString) .replace("<", "<").replace(">", ">") .replace(""", "\"").replace("&", "&") val Some(img_url) = e \ "link" find { e => e \ "@rel" == "image" } """<div class="status"> <span class="profile_img"><img src="%s"/></span> <span class="user_name">%s</span> <span class="message">%s</span> </div> """.format( img_url \ "@href" text, e \\ "author" \\ "name" text,c ) }.reverse.foreach{ e => println(e);outbound.sendMessage( frame , e.toString ) } Thread.sleep(1000 * 30 ) val lastId = ( sinceId /: l.map{ e => ( e \ "id" text).split(":").last.toLong }){ (n, m) => if( n > m ) n else m } println( "lastId:%s".format( lastId)) SearchActor ! Search( frame, tag , lastId) act() } case Dispose => } } } override def onConnect(outbound:Outbound ) = this.outbound = outbound override def onMessage(frame:Byte, data:Array[Byte], offset:Int, length:Int ) = {} override def onMessage(frame:Byte, data:String ) = { SearchActor.start SearchActor ! Search( frame, data, 0 ) } override def onDisconnect = SearchActor ! Dispose } } case class Search( frame:Byte, tag:String , sinceId:Long) case class Dispose()