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()
GAE/JでScalaとSlim3使って恵方を知ることができるサービス作ったよ(誰得?)
Google App EngineでScalaとSlim3使ってヘンなサービス作りました。
なんと! 今年の恵方だけでなく、去年や来年の恵方までわかってしまう画期的なサービスです!!
Google App EngineでScalaとSlim3の話は、appengine ja night #6 Beer Talk : ATNDでLTする予定です。
slim3-genのscala版作ったりでちょっと頑張りました。
たぶん、来年の節分の頃には、こんなサービス作ったことすら忘れているでしょう。
NetBeansとScalaを使ってAppEngineたんといちゃいちゃする方法
俺「新しいアプリだよ。さぁ、デプロイするからAppSlotを解放するんだ…!」
appengineたん「で、でぷろい…ですか…?こんなおっきなあぷり…は、入るかな…?」
俺「今日はScalaを使ったアプリケーションなんだよ」
appengineたん「Scalaなんて…そんな変態的なこと…で、できません ///」
俺「もう遅いよ。どうだ? どんどんアプリがアップロードされていくぞ!」
appengineたん「は、入りました…。こんなおっきなアプリケーション…あついです…」
俺「よしテストだ。どんどんリクエストをおくってやるからな」
appengineたん「そ、そんなにリクエストされたら…らめぇっ!!SpinUpしちゃうぅっ!!」
俺「まだまだいくぞ。おらっ!データストアにputしてやるっ!」
appengineたん「らめぇぇ!あっ、あふれちゃうっ!!データが…quotaからあふれちゃうよぉぉっ!!」
自分の頭の悪さに絶望しました。どうみても手遅れです本当にあり(ry
ねたはさておき、Scala2.8 + NetBeans6.8で、Google App Engine/Javaの環境の作り方についてまとめました。
NetBeanでScala
NetBean6.8入れる
まずはNetBean6.8を入れますよ。ダウンロー丼してインスコれ!!
http://netbeans.org/downloads/
NetBeans Scala plugin
次に、NetBeans Scala pluginを入れます。
http://wiki.netbeans.org/Scala68v1#Install_with_NetBeans_6.8
ダウンロードしたファイルを適当なとこに解凍しておきます。
次に、NetBeansのメニューから[ツール]→[プラグイン]を選び、[ダウンロード済み]のタブを開きます。[プラグインの追加]ボタンを押して、先ほど解凍したNetBeans Scala pluginのフォルダを表示させ、*.nbmファイルを全て選択します。
これでインストールを押すと、プラグインがインストールされます。
MaxOSXの人は、Scala実行環境のパスを環境変数に設定しておく必要があります。
NetBeansをインストールしたフォルダ(通常は"/Applications/NetBeans/NetBeans 6.8.app/Contents/MacOS")に、environment.plistと言うファイルを以下の内容で作ります。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>SCALA_HOME</key> <string>/Users/ozaki/dev/Scala/scala-2.8.0.Beta1-prerelease/rt</string> <key>PATH</key> <string>/opt/local/bin:/opt/local/sbin:/bin:/sbin:/usr/bin:/usr/sbin:/Users/ozaki/dev/Scala/scala-2.8.0.Beta1-prerelease/rt/bin</string> </dict> </plist>
SCALA_HOMEには、Scala2.8をインスコしたパスを、PATHには、Scala2.8のbinディレクトリを絶対パスで追加します。このファイルを作成したら、一度ログアウトしないと有効になりませんので注意が必要です。
Google App Engin Pluginを入れる
Google App Engine SDK
まず、Google App Engine SDKが無ければダウンロー丼しておくべし。
Google App Engine Plugin
[ツール]→[プラグイン]を選び、[設定]のタブを開きます。[追加]ボタンを押して、"http://kenai.com/projects/nbappengine/downloads/download/Latest_NetBeans68/updates.xml"を追加します。
[使用可能なプラグイン]タブを開いて[カタログを再読込]すると、GoogleAppEngineのプラグインが一覧に表示されますので、全て選択して[インストール]します。
プラグインのインストール後に、GoogleAppEngineのサーバーをサービスに追加します。
左側の[サービス]タブを表示し、"サーバー"を右クリックして[サーバーの追加]を選びます。
"Google App Engine"を選択して次へ。
"Installation Location"には、Google App Engine SDKを解凍したディレクトリを指定します。
Google App Engine Libraryの追加
[ツール]→[ライブラリ]を選び、"新規ライブラリ"を選びます。
名前に"AppEngineLibs"と入力し、"jar/フォルダを追加"を押して、Google App Engine SDKを解凍したディレクトリにある"user/appengine-api-1.0-sdk.jar"を選択して追加します。
サンプルプロジェクトを動かしてみる
Google App Engine SDKに付属しているサンプルアプリを動かしてみましょう。
[プロジェクト]→[新規作成]を選び、"サンプル"の中にある"Google App Engine"のGuest Bookを選びましょう。
できたプロジェクトを実行すると、ブラウザで動作が確認できます。
ScalaでApp Engineを動かしてみる
Webアプリケーションプロジェクト
では、ScalaでApp Engineを動かしてみましゅ。
[ファイル]→[新規プロジェクト]を選んで、"Java Web"の"Webアプリケーション"を選択します。
プロジェクト名などを入力して、"サーバーと設定"のところでサーバに"Google App Engine"を選んで完了です。
Scalaプロジェクト
Scalaを使う場合、Webアプリケーションプロジェクトとは別にScalaプロジェクトを用意します。このプロジェクトを先ほど作ったWebアプリケーションプロジェクトから参照させる形です。
[ファイル]→[新規プロジェクト]を選んで、"Scala"の"Scala Class Library"を選択します。
出来たプロジェクトのライブラリを右クリックして[ライブラリを追加]を選び、"AppEngineLibs"と"Scala28"を追加します。
プロジェクトを→クリックして、[新規]→[その他]から"Scala"の"Scala Class"を選んで新規作成します。
内容をこんな感じにします。
package testapp class FirstScala { def someMethod() = { "This comes from Scala!!" } }
Webアプリケーションプロジェクトを変更する
Webアプリケーションプロジェクトのライブラリを右クリックして[ライブラリを追加]を選び、"AppEngineLibs"と"Scala28"を追加します。
次に、Webアプリケーションプロジェクトのライブラリを右クリックして、[プロジェクトを追加]を選びScalaプロジェクトを追加します。
"Webページ"にあるindex.jspを修正します。
<%@page contentType="text/html" pageEncoding="UTF-8"%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <%@page import="testapp.FirstScala"%> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>JSP Page</title> </head> <body> <h1>Hello World!</h1> <p>Test: <%= new FirstScala().someMethod() %></p> </body> </html>
実行
Webアプリケーションプロジェクトを実行すると、ブラウザに画面が表示されます。
もしここで、SCALA_HOMEが読み込めないというAntのエラーが出る場合は、 Scalaプロジェクトの"nbproject/build-impl.xml"を開き、-init-privateターゲットないでscala.homeを絶対パスで設定するようにしてみてください。
<target depends="-pre-init" name="-init-private"> <property file="nbproject/private/config.properties"/> <property file="nbproject/private/configs/${config}.properties"/> <property file="nbproject/private/private.properties"/> <property environment="env"/> <condition property="scala.home" value="${env.SCALA_HOME}"> <isset property="env.SCALA_HOME"/> </condition> <property name="scala.home" value="/Users/ozaki/dev/Scala/scala-2.8.0.Beta1-prerelease/rt"/> <property name="scala.lib" value="${scala.home}/lib"/> <fail unless="scala.home"> You must set SCALA_HOME or environment property and append "-J-Dscala.home=scalahomepath" property to the end of "netbeans_default_options" in NetBeansInstallationPath/etc/netbeans.conf to point to Scala installation directory. </fail> <property name="scala.compiler" value="${scala.lib}/scala-compiler.jar"/> <property name="scala.library" value="${scala.lib}/scala-library.jar"/> <taskdef resource="scala/tools/ant/antlib.xml"> <classpath> <pathelement location="${scala.compiler}"/> <pathelement location="${scala.library}"/> </classpath> </taskdef> </target>
Scala hack-a-thon #2 開催しました。
Scala hack-a-thon #2、無事終了しました。
参加された皆さん、本当に有り難うございました。
やっぱり、誰もしゃべらず黙々とコード書いたり資料読んだりで、まじで「絶対にしゃべってはいけない Scala hack-a-thon」でした。
しかし、みなさんそれなり楽しんでいただけたようで何よりです。
当日#scalahackでのついったーの発言をまとめました。
Scala hack-a-thonまとめ - Togetter
会場は、Oracleさんにご提供頂きました。無線LAN電源完備で、無限コーヒーと青山の夜景つきというすばらしい会場で、本当に感謝しております。
今回は、発表の時間を2時間ほどとりました。当初予定していた方以外に、飛び込みのLTの応募をたくさん頂いて、ほんとにうれしかったです。もっと時間枠を取ればよかったかな?
当日発表して頂いた方たちです。本当に有り難うございました。
- play frameworkとsienaで簡単GAEアプリ作成 (@k_nishijimaさん)
- PEGパターンマッチングライブラリpegexのデモ(@kmizuさん)
- scalaからappengineのapiをたたく話 (@marblejenkaさん)
- 夢とDreamControllerについて(@_Relmさん)
- 痛車の話( @regtan )
3回目は、グループ分けしてハンズオンしたり、発表枠をもう少し取ったりしようかなと思います。
3回目の日程はまだ未定ですが、そのうちやりますのでぜひご参加ください。
今回使用した資料は、前回からあまり更新されてません><
Genricsについて少し書いたのと、Githubの方にサンプルとしてEchoサーバとFeedからマルコフ連鎖するやつを追加しています。
yuroyoro/scala-hackathon · GitHub
Dropbox - 404
ってことで、繰り返しになりますが、皆さん本当に有り難うございました。
Scala hack-a-thon #2に参加される皆様へ
えと、もう明日なんですが、2月20日(土)に、Scala hack-a-thon #2が開催される予定となっております。
つきましてはですね、主催である私目から皆様へ、どのような趣旨のイベントなのかと、いくつかのお願いをこの場を借りてさせて頂きたい所存でございますことよ。
(相変わらず日本語がトチ狂っているのは無視してください><)
コンセプト
このイベントのコンセプトは、「みんなで集まってコードを書く」これにつきます。
各自がそれぞれのペースでコードを書いて、わからないところは誰かに聞く。参加者の自主性を重んじるイベントと、あいなっております。
とはいえ、当日初めてScalaに触れる方もいらっしゃるでしょうし、それなりの資料は用意するつもりです。
死霊はこちらです。都度更新する予定です。
当日の内容
基本は、各自でコード書いてもらうんですが、初めてScalaに触れる方のためにハンズオンをします。大体2時間くらいです。
それがおわったら、用意した死霊にそってやってもらうなど各自の自主性に任せます。
あと、 17:00くらいから発表もやる予定です。
今のところ、以下の方に発表して頂く予定ですが、当日の飛び入り参加も全然おけですよ!
プレゼン資料なんて飾りです。エライ人にはそれがわからんのですよ!!
- Liftについて(俺)
- play frameworkとsienaで簡単GAEアプリ作成 (@k_nishijimaさん)
- PEGパターンマッチングライブラリpegexのデモ(@kmizuさん)
お願い
Scalaの実行環境とAPIドキュメントは、当日USBメモリで配布するつもりですが、できれば事前に準備しておいてもらえるとありがたいです。
環境の作り方については、こちらで資料を用意しています。
http://github.com/yuroyoro/scala-hackathon/raw/master/doc/source/setup.pdf
会場は、Oracleさんのご厚意でご提供頂いております。電源、無線LAN完備のすばらしいところです。キレイに使いましょう。
ゴミは持ち帰りましょう。
感想をBlogで書いてもらえると、感激のあまり体中の穴という穴からなんらかの汁があふれ出してしまうかもしれないです。
最後に、とっても大事なことですが。
ついったーだけじゃなくリアルでも会話しましょう
最後に
開催に当たって、できる限りの準備をするつもりですが、いろいろ至らない点も多々あるかと思います。
皆様のご協力をお願いいたします。
この機会に、Scalaを使ってくれる人が増えること、そして、当日集まって頂いた皆さんと、今後オン/オフ問わず、交流できていけばいいなぁと思っています。
では、当日は宜しくお願いいたします。
草生やす関数
草生やしたいときに
def www( s:String ):String = { import scala.util.Random val rnd = new Random( ) val ws = List( "w","W","w" ) def w(n:Int):String = ( 0 - n) until rnd.nextInt(3) map { i => ws( rnd.nextInt( ws.size -1 ) ) } mkString ("ちょ" + w(1) + "おま" + w(1) ) + ("" /: s ){ ( c,e ) => c + e + w(0) } + w(0) }
実行結果:
scala> www("誰得よこんなん") res1: String = ちょWWおまw誰得よWWこんwなんWwW
ちょっとした問題。Listから要素を検索して残りの要素数を返す。どう書く?
ちょっと面白い問題を見つけたので、俺も解いてみました。
仕様はこうです。
- あるListから要素を探し、該当する要素を除いた残りの要素数を返す。
- ただし、要素がListに存在しない場合は-1を返す。
1の仕様だけなら、単純にList#dropWhileしてlengthを返すだけでしょうが、2を考えるとそうも行きません。
存在しない場合もListの末尾に該当する場合もともに結果が0になってしまうからです。
実行結果はこんな感じになります。
scala> val list = List("World", "is", "not", "enough") list: List[java.lang.String] = List(World, is, not, enough) scala> remainsLength( list , "is") res1: Int = 2 scala> remainsLength( list , "foo") res2: Int = -1 scala> remainsLength( list , "enough") res3: Int = 0
俺はScalaで解きましたが、他の言語でやってみるのも面白いかとおもいます。
ストレートにやってみる
List#dropWhileだけではダメなら、最初に要素が存在するかList#findで検索して、結果のOption型をmatchで分けてやればよいと考えたのが以下のものです。
def remainsLength[T]( l:List[T],v:T ) = l.find{ v == } match { case Some(_) => l.dropWhile{ v!= }.size - 1 case None => -1 }
よく考えたら、List#indexOfでいんじゃまいかと思って書いたのが以下のものです。考え方自体はfindと変わってないです。
def remainsLength[T]( l:List[T],v:T ) = l.indexOf( v ) match { case -1 => -1 case n => l.drop( n + 1 ).length }
ただ、どっちも matchを使ってるんでいまいちな感じ。
再帰でやってみる
再帰でやるなら、Listのパターンマッチで、先頭要素が該当するまで再帰で頭からListを喰っていけばよいはずです。
def remainsLength[T]( l:List[T],v:T ):Int = l match { case Nil => -1 case `v` ::xs => xs.length case x::xs => search( xs , v) }
caseに書くセレクタで`(バッククォート)で囲むと、パターンマッチ変数にならずに評価されるというのは初めて知りました。
このパターンマッチは以外と有用かも?
追記
@ymnkさんにもっとエレガントな回答を頂きました。
def search[T](l: Seq[T], s:T)=l.dropWhile(s!=_).length-1
dropWhileで返す要素が、検索対象を含む場合は最小で1、含まない場合は0だから、単純にlength-1をするだけでおけと。
追記その2 無理矢理なimplicit conversionでOptionを拡張してみた
Optionに、Someだったら中身を引数の関数に渡して、Noneだったらdefaultを返すような関数が無かったので、無理矢理implicit conversionで対応してみたのがこれ。
class MapOrElseOption[A]( o:Option[A] ){ def mapOrElse[B]( default : => B)( f:(A) => B ) = o match { case None => default case Some(x) => f(x) } } implicit def option2MapOrElseOption[A]( o:Option[A] ) = new MapOrElseOption( o ) def remainsLength[T]( l:List[T],v:T ) = l.find{ v == }.mapOrElse( -1 ){ _ => l.dropWhile{ v!= }.size - 1 }