Dartで始めるWebSocket


# 概要

今週からDartを始めたのでとりあえずWebSocketを扱ってみました。
内容としては軽くDartについて触れたあと、WebSocket、Redisのコードを中心にまとめます。
あと、UnitTestの環境も構築して動くようにしました。

# Dartって何？

GoogleがJavaScriptの代替に考えた言語
node.jsのようにシングルスレッドで動作。I/Oの処理はノンブロッキングで動作。

# そろそろDart来るかも？

サーバースクリプトとして問題なく使えるレベル。
ECMAの標準規格になったので近い将来ブラウザに搭載されるかも。

# 習得コスト

JavaやC#をやってた方は習得コスト低め
構文や、概念が似てる

# Dartを覚える

株式会社クレスさんが配布している資料(PDF)がおすすめ
[プログラミング言語Dartの基礎](
http://www.cresc.co.jp/tech/java/Google_Dart/DartLanguageGuide_about.html)


なお、サーバーサイドは`dart:io`、ブラウザは`dart:html`と、使用するライブラリが異なる

# Dartを使おうと思った理由

JavaScriptの可読性がよくない。ちょっとした処理を書くのに便利だが、そこそこのサイズになると微妙。
なのでDartがイケてるのか試そう！

# クライアントの挙動がおかしい？

UnityとCocos2dxでクライアント作ろうとしたら
UnityのWebSocketは動くが、Cocos2dxの標準のライブラリではDartだとコネクションに失敗します。
node.jsだと両方動くんだけどね。。
サードパーティのライブラリ試してみようと思うけど、有志の方が直してくれるのを待つべきか。。

# WebSocketを題材にした理由

Socket.ioがバージョン間でプロトコル問題あって使い勝手が微妙なのでWebSocket単体でいいんでね？
基本RFC 6455でまとまってるし。

# Redis環境構築

KVS。ここでは異なるサーバー間でのデータストアとして利用。

* Install && サーバー起動 && 監視

```
$ brew install redis
$ redis-server 
$ redis-cli subscribe "chan0" 
```

# UnitTestにブラウザ必要なんですね

`dart:html`をUnitTestするには一手間必要。
下記の処理にはテストコード一行もないけど動く環境だけ作ったよ！

# Dart環境構築

[Getting and Installing Dart Is Easy!](https://www.dartlang.org/tools/download.html)

Dart EditorをダウンロードすればDart SDKがもれなくついてくる
Dart EditorはEclipseベース

Sublimeでも構文チェックをしてくれるのでEditorが必要ない方はSDKだけでもよい。
Package Control から DartをInstallすればよい。
Preferences -> Package Settings -> Dart -> Settings – User

```
// 構文チェックのON .. 重くて使えん
"dart_linter_active" : true

// SDKのパスを通す
"dart_sdk_path": "/path/to/dart-sdk"
```

# Code

* ファイル構成

```
root
  |- pubspec.yaml
  |- bin/socket.dart
  |- test
      |- socket_client_tests.dart
      |- socket_client_tests.html
```

* パッケージ管理

```Yaml:pubspec.yaml
name: sockettest
description: A sample command-line application
dev_dependencies:
  unittest: any
  redis_client: any
  browser: any
```

* サーバーコード

```Dart:socket.dart
import 'dart:io';
//import 'dart:async';
import "package:redis_client/redis_client.dart";

Map<String,Client> _client;

main(List<String> args) {

  RedisClient client;
  RedisClient client1;

  String port = (args.length > 0) ? args[0] : "8080";
  HttpServer.bind('127.0.0.1', int.parse(port))
    .then((HttpServer server) {
      server
        .where((request) => request.uri.path == '/ws') 
        .transform(new WebSocketTransformer()) 
        .listen((WebSocket ws){
          redisHandler(ws);
        });
    print("Echo server started");
  });
}

class Client {
  String userId;
  RedisClient pub;
  RedisClient sub;
  WebSocket ws;
  Client(this.userId,this.ws,this.sub,this.pub){}
}

wsHandler(Client client) {

  if (_client == null) 
    _client = {};

  _client[client.userId] = client;

  client.sub.subscribe(["chan0"],(Receiver message){
    message.receiveMultiBulkStrings().then((List<String> message){
      print(message);
      client.ws.add(message[2]);
    });
  });

  client.ws.listen((message) {
    message += " " + client.userId;
    print('message is ${message}');
    client.pub.publish("chan0",message);
    client.ws.add('Echo: ${message}');
  }, onDone: (() {
    client.pub.close();
    client.sub.close();
    _client.remove(client.userId);
    print('connection ${client.ws.hashCode} closed with ${client.ws.closeCode} for ${client.ws.closeReason}');
  }));
    
}

redisHandler(WebSocket ws) {
  print('new connection : ${ws.hashCode}');

  RedisClient rcSub;
  RedisClient rcPub;
  
  RedisClient.connect("127.0.0.1:6379")
    .then((c) {
        rcSub = c;
        //rcSub.flushall();
    }).then((_){
        RedisClient.connect("127.0.0.1:6379")
        .then((c1){
          rcPub = c1;
        }).then((_) => rcPub.incrby("global:nextUserId",1))
          .then((num userNum){
            String userId = "uid:" + userNum.toString();
            var client = new Client(userId,ws,rcSub,rcPub);
            wsHandler(client);
        });
    });
}
```

* クライアントコード(UnitTest用)

```Dart:socket_client_tests.dart
library map_tests;

import 'package:unittest/unittest.dart';
import "package:unittest/html_individual_config.dart";
import 'dart:html';

WebSocket webSocket;

var wsUri = "ws://localhost:8000/ws";
main(){
  
  group("test()", () {
    test("should properly parse info strings", () {
      
    });
  });
  print("wsurl: ${wsUri}");
  webSocket = new WebSocket(wsUri);

  if (webSocket != null && webSocket.readyState == WebSocket.OPEN) {
	webSocket.send("aaaaa");
  } else {
    print('WebSocket not connected, message aa not sent');
  }

  webSocket.onOpen.listen((Event evt){
  	print("open");
  	webSocket.send("Hello World");
  });
  webSocket.onMessage.listen((MessageEvent e) {
  	print("onMessage");
  });

  webSocket.onError.listen((Event evt){
  	print("onError");
  });

  webSocket.onClose.listen((CloseEvent evt){
  	print("onClose");
  });
}
```

```Html:socket_client_tests.html
<html>
<head>
  <title>Hipster Test Suite</title>
  <script type="application/dart" src="socket_client_tests.dart"></script>
  <script type="text/javascript" src="packages/browser/dart.js"></script>
</head>

<body>
<h1>Test!</h1>

</body>
</html>
```

# 実行コマンド

* Dartサーバー起動

```
$ dart bin/fuji_dart.dart 8000
```

* UnitTest

`$ pub serve ` 実行して `http://localhost:8080/socket_client_tests.html` にブラウザから繋ぐ。

# Redisが別ポートで共有出来ているかの確認

複数ポートでサーバー起動して、クライアントも複数ポートで立ち上げれば検証できます。

# よかったこと

* コードが簡潔で見やすくなった。
* すごく書きやすい

# だめだったこと

* 括弧の閉じが何処か解らんことがよくある
* Cocos2dxのWebSocketがDartで動かんw
* ブラウザ通さないと`dart:html`のUnitTest動かんw
* QiitaでDartのシンタックスきかない！pygmentsみたら[Supported languages](http://pygments.org/languages/)で対応きてるよ

# よく解っていないこと

* RedisのPub/Subはコネクションを分けて持たないとだめなの？

# 参考資料

* [redisドキュメント日本語訳](http://redis.shibu.jp/)
* [Dartチートシート](http://dartlangfr.net/dart-cheat-sheet/core.html)
* [redis_client library](http://dartist.github.io/redis_client/api/redis_client.html)