ちょっと前の話になるけど、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, ¤tClientID)
}
}
}
/**
* 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, ¶m)
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
0 件のコメント:
コメントを投稿