( ꒪⌓꒪) ゆるよろ日記

( ゚∀゚)o彡°オパーイ!オパーイ! ( ;゚皿゚)ノシΣ フィンギィィーーッ!!!

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

画面はこんな感じ。
f:id:yuroyoro:20100316192227p:image

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("&lt;", "<").replace("&gt;", ">")
                  .replace("&quot;", "\"").replace("&amp;", "&")
                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()