2012年5月28日月曜日

WebSocket client with Dart, WebSocket server with Go

ちょっと前の話になるけど、5/12に開催されたDartのハッカソンに参加して、そこでWebSocketを用いてウェブアプリをつくるチームに参加した。自分はここで公開してるSVGアニメーション用のコードがあったので、それを用いたアプリを作ってきた。

その場ではサーバーもクライアントもDartで書いたんだけど、個人的にはサーバーサイドはGoで書きたかったので改めてGoで書きなおしてみた。ちなみにハッカソンではGithubを使って開発したので、それを自分のところへForkして拡張してる。場所はここね。

server.go

基本的にはmanageClientという関数がクライアントに関するリソースを管理するアクターで、serveClient内での各クライアントとのやりとりはすべてこのmanageClientに対して転送される。例えば新規にクライアントがつながってきた場合はAddClientRequestがserveClientからmanageClientに対して送られ、クライアント一覧に相当するmapにクライアントとの接続が格納される。

ちなみにちょっと苦労したのがscriptやらCSSやらをGoのhttpサーバー経由でどうやってホストするのか、という点。それは色々調べたところhttp.ServeFile()という関数を使うとファイルに対するリクエストは大体いい感じに処理してくれることがわかったので、下のコードはそれを採用している

package main

import (
 "code.google.com/p/go.net/websocket"
 "net/http"
 "fmt"
)

type RequestType int
type ResponseType int

var (
 requestQueue = make(chan Request)
 responseQueue = make(chan Response)
)

const (
 // Request
 GENERATE_CLIENT_ID RequestType = 0
 ADD_CLIENT RequestType = 1
 REMOVE_CLIENT RequestType = 2
 BROADCAST RequestType = 3

 // Response
 CLIENT_ID ResponseType = 0
)

type RequestParameter struct {
 clientID int
 Connection *websocket.Conn
}

type DisplayParameter struct {
 Remark string
 Duration string
 Path string
 Rotate string
}

type Request interface {
 Type() RequestType
 ClientID() int
 Process(clientList map[int]*websocket.Conn, queue chan Response, currentClientID *int) map[int]*websocket.Conn
}

type GenerateClientIDRequest struct {
 RequestParameter
}

type BroadcastRequest struct {
 RequestParameter
 DisplayParameter
}

type AddClientRequest struct {
 RequestParameter
}

type RemoveClientRequest struct {
 RequestParameter
}

type Response interface {
 Type() ResponseType
}

type GenerateClientIDResponse struct {
 ClientID int
}

func (gcr GenerateClientIDResponse) Type() ResponseType {
 return CLIENT_ID
}

func (gcm GenerateClientIDRequest) ClientID() int {
 return gcm.clientID
}

func (gcm GenerateClientIDRequest) Type() RequestType {
 return GENERATE_CLIENT_ID
}

func (gcm GenerateClientIDRequest) Process(clientList map[int]*websocket.Conn, queue chan Response, currentClientID *int) map[int]*websocket.Conn {
 res := GenerateClientIDResponse {
  *currentClientID,
 }
 queue <- res
 // proceed to next id
 *currentClientID++
 return clientList
}



func (acm AddClientRequest) ClientID() int {
 return acm.clientID
}

func (acm AddClientRequest) Type() RequestType {
 return ADD_CLIENT
}

func (acm AddClientRequest) Process(clientList map[int]*websocket.Conn, queue chan Response, currentClientID *int) map[int]*websocket.Conn {
 clientList[acm.clientID] = acm.Connection
 fmt.Printf("Added client. %d clients existing.\n", len(clientList))
 return clientList
}

func (bm BroadcastRequest) ClientID() int {
 return bm.clientID
}

func (bm BroadcastRequest) Type() RequestType {
 return BROADCAST
}

func (bm BroadcastRequest) Process(clientList map[int]*websocket.Conn, queue chan Response, currentClientID *int) map[int]*websocket.Conn  {
 // send a text message serialized as JSON.
 for id, client  := range clientList {
  fmt.Printf("Sending to %d\n", id)
  err := websocket.JSON.Send(client, bm.DisplayParameter)
  if err != nil {
   fmt.Printf("error: %s\n",err)
   // remove client
   delete(clientList, id)
  }
 }
 fmt.Println("Broadcasted")
 return clientList
}

func (rcm RemoveClientRequest) ClientID() int {
 return rcm.clientID
}

func (rcm RemoveClientRequest) Type() RequestType {
 return REMOVE_CLIENT
}

func (rcm RemoveClientRequest) Process(clientList map[int]*websocket.Conn, queue chan Response, currentClientID *int) map[int]*websocket.Conn  {
 // remove client
 delete(clientList, rcm.clientID)
 fmt.Printf("Removed client ID:%d. %d left\n", rcm.clientID, len(clientList))
 return clientList
}

/**
  * Message loop for the client manager
  */
func manageClient(reqQueue chan Request, resQueue chan Response) {
 var currentClientID int = 0
 clientList := make(map[int]*websocket.Conn, 0)
 fmt.Println("Entering client manage loop")
 for {
  select {
  case msg := <- reqQueue:
   fmt.Printf("client:%d type:%d\n", msg.ClientID(), msg.Type())
   clientList = msg.Process(clientList, resQueue, &currentClientID)
  }
 }
}

/**
  * Get a new client ID from the client manager
  */
func generateClientID(reqQueue chan Request, resQueue chan Response)  int {
 msg := GenerateClientIDRequest {}
 reqQueue <- msg
 res := <- resQueue
 v, _ := res.(GenerateClientIDResponse)
 return v.ClientID
}

/**
  * Function that serves each client
  */
func serveClient(ws *websocket.Conn, reqQueue chan Request, resQueue chan Response) {
 id := generateClientID(reqQueue, resQueue)
 defer func() {
  // remove client when exiting
  msg := RemoveClientRequest {
   RequestParameter {
    id,
    ws,
   },
  }
  reqQueue <- msg
 }()

 // add client first
 msg := AddClientRequest {
  RequestParameter {
   id,
   ws,
  },
 }
 reqQueue <- msg
 fmt.Println("Entering receive loop")
 for {
  var param DisplayParameter
  err := websocket.JSON.Receive(ws, &param)
  if err != nil {
   fmt.Printf("receive error: %s\n",err)
   break
  }
  fmt.Printf("recv:%#v\n", param)

  // broadcast received message
  msg := BroadcastRequest {
   RequestParameter {
    id,
    ws,
   },
   param,
  }
  reqQueue <- msg
 }
}

/**
  * Handler for the websocket json server
  */
func echoJsonServer(ws *websocket.Conn) {
 fmt.Printf("jsonServer %#v\n", ws.Config())
 serveClient(ws, requestQueue, responseQueue)
}

/**
  * Serve all files (html, dart, css)
  */
func mainServer(w http.ResponseWriter, req *http.Request) {
 path := req.URL.Path[1:]
 fmt.Printf("path: %s\n",path)
 http.ServeFile(w, req, path)
}

func main() {
 // start an actor that processes requests from every client
 go manageClient(requestQueue, responseQueue)

 // setup the handlers
 http.Handle("/echo", websocket.Handler(echoJsonServer))
 http.HandleFunc("/", mainServer)
 fmt.Println("serving...")
 port := 8080
 err := http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
 if err != nil {
  panic("ListenAndServer: " + err.Error())
 }
}

client.dart

クライアント側で実行されるDartのコードが以下。いままでのサンプルと違うのは、他のクライアントとポスト内容を共有するためポストボタンをクリックしたら即websocketで内容を送っているところ。実際の表示処理はサーバー側からブロードキャストされてきたメッセージに対してon.messageのハンドラで実行することになる。

#import("dart:html");
#import("dart:json");
#import("RemarkDisplayer.dart");
#import("SVGRemarkDisplayer.dart");
#import("Unit.dart");

void main() {
    // initialize displayers
    final int MAX_NUMBER_OF_REMARKS = 15;
    final int MAX_SPEED = 100;
    var displayer = new SVGRemarkDisplayer();
    displayer.initialize("#stage", MAX_NUMBER_OF_REMARKS);

    // create websocket and add handlers
    WebSocket webSocket = new WebSocket("ws://localhost:8080/echo");
    Element status = document.query("#statusArea");
    webSocket.on.message.add((event) {
            // display remark with given parameters
            var message = event.data;
            Map map = JSON.parse(message);
            DisplayParameter param = new DisplayParameter.map(map);
            displayer.display(param);
        });

    // connect post button with websocket
    TextAreaElement textNode = document.query("#remarkText");
    TextAreaElement pathNode = document.query("#pathString");
    Element postButton = document.query("#postRemark");
    InputElement speedNode = document.query("#displaySpeed");
    InputElement rotateNode = document.query("#rotateRemark");
    postButton.on.click.add((Event e) {
        String message = textNode.value;
        String path = pathNode.value;

        bool rotate = rotateNode.checked;
        var num = speedNode.valueAsNumber;
        var duration = (MAX_SPEED - num.toInt()) * 100;
        var param = new DisplayParameter(message, duration, Unit.Millisecond, path, rotate);
        String paramJson = JSON.stringify(param.toMap());
        webSocket.send(paramJson);
    });
}

感想

Dartで書いたときと比べてGo版はかなりコード量が増えた気がする。大半はmanageClientとどういったメッセージをやりとりするかの定義なので、アクターモデルをやるからにはしょうがないのかもしれない。ただやっぱり厳密に書こうとするとアクターモデルはめんどう。もっと規模が大きくなればアクターモデルの恩恵がわかる、かも?

サーバー側の出力例

サーバー側をビルド・実行して、上記のDartクライアントからGoogleのアスキーアートをPostした場合の出力が下のログ。参考に掲載しておく。

$ go build server.go 
$ ./server 
serving...
Entering client manage loop
path: 
jsonServer &websocket.Config{Location:(*url.URL)(0x188abd80), Origin:(*url.URL)(0x188abdc0), Protocol:[]string(nil), Version:13, TlsConfig:(*tls.Config)(nil), handshakeData:map[string]string(nil)}
client:0 type:0
Entering receive loop
client:0 type:1
Added client. 1 clients existing.
path: favicon.ico
recv:main.DisplayParameter{Remark:"\u3000\u3000\u3000\u3000_,,,,._\u3000\u3000\u3000\u3000\u3000 \u3000 \u3000 \u3000 \u3000 \u3000 \u3000 、-r\n\u3000 \u3000,.','\" ̄`,ゝ\u3000_,,,_\u3000\u3000 _,,,_\u3000\u3000 _,,,,__,. | |\u3000 _,,,,,_\n\u3000\u3000{ {\u3000\u3000 ,___\u3000,'r⌒!゙! ,'r⌒!゙! ,.'r⌒!.!\"| l ,.'r_,,.>〉\n\u3000\u3000ゝヽ、\u3000~]|\u3000ゞ_,.'ノ\u3000ゞ_,.'ノ\u3000ゞ__,.'ノ\u3000| l {,ヽ、__,.\n\u3000\u3000\u3000`ー-‐'\"\u3000\u3000 ~\u3000\u3000\u3000 ~\u3000\u3000〃 `゙,ヽ ̄` `゙'''\"\n\u3000\u3000\u3000\u3000\u3000\u3000\u3000\u3000\u3000\u3000\u3000\u3000\u3000\u3000\u3000\u3000 ゙=、_,.〃 ", Duration:"5000&ms", Path:"M10,150 A15 15 180 0 1 70 140 A15 25 180 0 0 130 130 A15 55 180 0 1 190 120 A15 10 170 0 1 10 150", Rotate:"false"}
client:0 type:3
Sending to 0
Broadcasted

2012年5月4日金曜日

SVG Animation in Dartium

ブラウザ上で何らかのアニメーションをさせようとした場合、選択肢が色々ある。以前はCSS3を使ったし、他にもCanvasを使ったりSVGを使ったりスクリプトベースのアニメーション(要素の位置を定期的に更新する等)もやろうと思えばできる。今回はそのなかでもSVGを用いたアニメーションをDartでやってみた。

SVGでアニメーションする理由

SVGでアニメーションさせようとした理由は主に表現の簡単さから。

SVGはxmlの一種なので、マークアップの形で比較的簡単に文字列を回転させたり指定されたパスの上を通るようなアニメーションを記述することができる。Canvasやスクリプトベースのアニメーションのように1フレームごとにちょっとずつ処理をする、といった下回りを記述する必要もなく、アニメーションの定義だけ書けばあとはブラウザがよろしくやってくれるのはかなり楽。加えてアニメーションの描画に関してもすべてブラウザ側に委託されているので、ブラウザがかしこく描画してくれさえすれば人間が細々チューニングする必要もない。

DartでSVGアニメーション

dartからSVGアニメーションさせようとした場合はいくつか落とし穴があって、そのなかでも一番大きい穴はDartium側のバグだったんだけど、とりあえずうまくいったのでサンプルを示す。

html

まえつくったCSS3のアニメーションとほぼ同等の動きをSVGで再現したつもり。一点違うのがスライダーで速度の調整をできるようにした点。ちなみに、bodyの閉じタグの前に入ってるscriptは現状のDartiumのバグを回避するためのもの。

<!doctype html>
<head>
    <meta charset="utf-8">
    <title>dartlang test</title>
    <link rel="stylesheet" type="text/css" href="./css/bootstrap.min.css" />
    <link rel="stylesheet" type="text/css" href="./css/client.css" />
</head>
<body>
    <script type="application/dart"  src="./script/svgtest.dart"></script>
    <div class="navbar">
        <div class="navbar-inner">
            <div class="container">
                <a class="brand" href="#">dartlang test</a>
            </div>
        </div>
    </div>
    
    <div class="container-fluid">
        <div class="row-fluid" >
            <div class="span4">
                <textarea class="input-xxlarge" rows="23" id="remarkText">
        ___|二ニー-、、;:;:;:;:;:;:;:;:;:;:;:;:;:;:;:;:;:;:|;::;:;:;:;:;:;:;:;:;:;:;:;:l
        /rヽ三三三三三─‐-- 、;:;:;:;:;:;:;:|;:;:;:;:;:;:;:;:;:;:;:;:;:;l
        ',i ,-三三三三三、   _,.ニ、ー-、!;: -‐二 ̄彡′
        ',、、ヾ三三'" ̄ ̄   `ー‐"    ヾ-'"  .〉′
        ヽ ヽヾ三,'    :::..,. -‐- 、     _,,..-‐、、,'
         `ー',ミミ     ::.弋ラ''ー、   i'"ィ'之フ l
         /:l lミミ     ::::.. 二フ´   l ヽ、.ノ ,'     
      ,.-‐フ:::::| |,ミ             l      /       
     /r‐'":::::::::| |ヾ        /__.   l    /      
 _,. -‐"i .|::::::::::::::::::',.',. \        ⌒ヽ、,ノ   /ヽ,_             
"    l ヽ:::::::::::::::::ヽヽ. \   _,_,.、〃  /l |    ___,. -、
     ',\\:::::::::::::::ヽ\  \  、. ̄⌒" ̄/:::::| |    ( ヽ-ゝ _i,.>-t--、
     \\\;::::::::::::\\  `、.__  ̄´ ̄/::::::::::l |    `''''フく _,. -ゝ┴-r-、
       ヽ \`ー-、::::::ヽ ヽ    ̄フフ::::::::::::::ノ ./   ,.-''"´ / ̄,./´ ゝ_'ヲ
          `ー-二'‐┴┴、__/‐'‐´二ー'".ノ   / _,. く  / ゝ_/ ̄|
               ̄`ー─--─‐''" ̄      / にニ'/,.、-t‐┴―'''''ヽ
                              /  /  .(_ヽ-'__,.⊥--t-⊥,,_
                              /  /  /   ̄   )  ノ__'-ノ
                             /      /    ゝニ--‐、‐   |
                            /           /‐<_   ヽ  |ヽ </textarea>

                <!-- setting panel -->
                <div class="setting">
                    <ul>
                        <li>
                            <label>Display speed</label>
                            <input type="range"  min="0" max="99" value="50" step="1" id="displaySpeed">
                        </li>
                    </ul>
                    <button class="btn-primary" id="postRemark" type="button">Post</button>
                </div>
            </div>

            <div class="span8 offset4">
                <div id="stage">
                </div>
            </div>
        </div>
    </div>
    <!-- workaround for SVG animation bug http://code.google.com/p/dart/issues/detail?id=2856 -->
    <script>navigator.webkitStartDart()</script>
</body>
</html>

svgtest.dart

mainにあたる部分。ここでは別のファイルに定義されているクラスを生成して、あとはボタンのハンドラに表示処理をつなげている。

#import("dart:html");
#import("RemarkDisplayer.dart");
#import("SVGRemarkDisplayer.dart");
#import("Unit.dart");

void main() {
    final int MAX_NUMBER_OF_REMARKS = 10;
    final int MAX_SPEED = 100;

    var displayer = new SVGRemarkDisplayer();
    displayer.initialize("#stage", MAX_NUMBER_OF_REMARKS);

    var postButton = document.query("#postRemark");
    InputElement speed = document.query("#displaySpeed");
    TextAreaElement textNode = document.query("#remarkText");
    postButton.on.click.add((Event e) {
            var num = speed.valueAsNumber;
            var duration = (MAX_SPEED - num.toInt()) * 100;
            var param = new DisplayParameter(textNode.value, duration, Unit.Millisecond);
            displayer.display(param);
    });
}

SVGRemarkDisplayer.dart

SVGでの表示処理を行うクラス。initializeで各文字列の親にあたるg要素を生成しておいて、実際に表示を行うdisplayでは渡された文字列をtspanで複数行に分解して、アニメーションをくっつけて表示している。アニメーション自体は左から右に要素を移動させる単純なもの。スライダーによって指定された秒数でアニメーションの速度を調整することができる。

#library("SVGRemarkDisplayer");
#import("dart:html");
#import("RemarkDisplayer.dart");

/**
 * Class that displays remarks via SVG
 */
class SVGRemarkDisplayer implements RemarkDisplayer {
    SVGRemarkDisplayer() {
        _remarkList = new List<SVGElement>();
        _currentRemark = 0;
        _numberOfRemarks = 0;
    }

    /**
     * Create remark nodes under the tag with the given ID
     */
    void initialize(String stageID, int numberOfRemarks) {
        _numberOfRemarks = numberOfRemarks;
        var stage = document.query(stageID);

        var svg = new SVGSVGElement();
        for (int i=0; i<numberOfRemarks; i++) {
            SVGGElement g = new SVGElement.svg("<g></g>");
            _remarkList.add(g);
            svg.nodes.add(g);
        }

       stage.nodes.add(svg);
       _root = svg;
    }

    /**
     * Display given remark at the current node
     */
    void display(DisplayParameter parameter) {
        var node = _remarkList[_currentRemark];

        // delete any nodes we already have
        node.nodes.clear();

        // convert text to svg
        var lines = parameter.remark.split("\n");
        SVGTextElement text = new SVGElement.svg("<text font-family='IPA モナーPゴシック' y='300'></text>");
        for (int i=0; i<lines.length; i++) {
            var line = lines[i];
            SVGTSpanElement span = new SVGElement.svg("<tspan x='0' dy='15'>${line}</tspan>");
            text.nodes.add(span);
        }
        
        node.nodes.add(text);
        var duration = "${parameter.duration}${parameter.durationUnit.toString()}";
        SVGAnimationElement animation
            = new SVGElement.svg("<animateTransform attributeName='transform' type='translate' from='0' to='1300' dur='${duration}' fill='freeze' begin='indefinite' />");
        node.nodes.add(animation);
        animation.beginElement();

        // proceed to next remark
        _currentRemark++;
        if (_currentRemark >= _numberOfRemarks) {
            _currentRemark = 0;
        }
    }

    int _numberOfRemarks;
    List<SVGElement> _remarkList;
    int _currentRemark;
    SVGSVGElement _root;
}

RemarkDisplayer.dart

文字列の表示をつかさどるクラスのインタフェース定義。CSS3を使ってアニメーションするクラスの方(今回は未使用)もおなじインタフェースを実装している。

#library("RemarkDisplayer");
#import("dart:html");
#import("Unit.dart");

/**
 * Parameter for displaying
 */
class DisplayParameter {
    DisplayParameter(this.remark, this.duration, this.durationUnit);
    String remark;
    int duration;
    Unit durationUnit;
}

/**
 * Class that manages remark display
 */
interface RemarkDisplayer {
    /**
     * Display given remark
     */
    void display(DisplayParameter param);
}

Unit.dart

表示単位を隠蔽するためのクラス。本当はenumを使用したいものの、現状のdartにはenumが無いためconstつきコンストラクタで代用している。

#library("Unit");

/**
  * Unit for animation
  */
class Unit {
    const Unit(int id) : _id = id;

    String toString() {
        switch (_id)
        {
        case PixelID:
            return "px";
        case MillisecID:
            return "ms";
        default:
            return "px";
        }
    }

    final int _id;

    static final int PixelID = 0;
    static final int MillisecID = 100;
    static final Pixel = const Unit(PixelID);
    static final Millisecond = const Unit(MillisecID);
}

結果

PostをクリックするとSVGアニメーションが左から右へ流れる。またスライダーを左に寄せるとゆっくり、右に寄せると速いアニメーションで表示することができる。