WebSocketのパフォーマンス測定

WebSocketは実際どのくらい速いのか、HTTPとの比較で測定しました。

#動機
クライアントサイドをDartで書いていると、あまりの心地よさに自然とアプリケーションがSPA化していきます。

以前作っていたシステムで、サーバーからプッシュ通知を受け取る必要があったのでWebSocketを使いました。
画面遷移の度にWebSocketをつなぎ直すのはかわいそうなので、全体をSPAにしようとしました。
その時SPAならHTTPではなくWebSocketでRESTした方がいいのではないかと思いました。
よく知られているWebSocketの魅力としてサーバーサイドからプッシュできるという点があります。
しかし、パフォーマス上のメリットもあるのではないかと思い測定しました。

#前提
isyumiさんはWebSocketとHTTPの仕様や実装については無知です。

#測定方法
1000連続でRequestを実行し、全てのResponseが返ってくるまでの時間を計ります。
ブラウザから1~1000の数字を送信し、それを二倍した数をレスポンスします。
前のリクエストが返ってきてから次のリクエストを投げる「直列つなぎテスト」と、
前のリクエストが返ってくる前に次のリクエストを投げる「並列つなぎテスト」に分けて実施します。
事前の調査でDNSルックアップにかかる時間は10ms程度ということがわかっているので無視します。
WebSocketのOpenにかかる時間はWebSocketの通信時間に含めます。

##サーバー
- Amazon EC2
- ap-northeast-1
- t2.micro
- Ubuntu

###コード

```dart:server.dart
library wt_io;
import 'dart:io';
void main(){
  HttpServer.bind('ec2-52-192-212-82.ap-northeast-1.compute.amazonaws.com',80).then(onBind);

}

void onBind(HttpServer server){
  server.listen(onRequest);
}

void onRequest(HttpRequest req){
  if(WebSocketTransformer.isUpgradeRequest(req)){
    WebSocketTransformer.upgrade(req).then(onWebSocket);
  }

  var str_number = req.uri.queryParameters['number'];

  try {
    req.response
      ..headers.add('Access-Control-Allow-Origin','*')
      ..write(twice(str_number))
      ..close();
  }catch(e){
    req.response.close();
  }

}

void onWebSocket(WebSocket ws){
  ws.listen((str) => onWebSocketData(ws ,str));
}

void onWebSocketData(WebSocket ws , String str){
  ws.add(twice(str));
}

String twice(String str_number){
  var int_number = int.parse(str_number);
  return (int_number * 2).toString();
}
```

##クライアント
- MacBook Pro (Retina, 15-inch, Mid 2014)
- プロセッサ 2.2 GHz Intel Core i7
- メモリ 16 GB 1600 MHz DDR3
- OS X EL Capitan
- Chrome 46.0.2490.86 (64-bit)

###コード

```dart:client.dart
library wt_html;

import 'dart:html';


String url = "//ec2-52-192-212-82.ap-northeast-1.compute.amazonaws.com";
int max = 1000;
int sir_http_count = max;
int sir_ws_count = max;
int par_http_count = max;
int par_ws_count = max;

DateTime sir_http_start, sir_http_end, par_http_start, par_http_end,
    sir_ws_start, sir_ws_end, par_ws_start, par_ws_end;


void main() {
  sir_http_start = new DateTime.now();
  timelog(sir_http_start);
  requestSirHttp();
}

void requestSirHttp() {
  if (sir_http_count > 0) {
    HttpRequest.getString(url + "?number=$sir_http_count").then((_) =>
        requestSirHttp());
    sir_http_count --;
  } else {
    var sir_http_end = new DateTime.now();
    timelog(sir_http_end);
    betweenlog(sir_http_end, sir_http_start);
    requestSirWS();
  }
}

void requestSirWS() {
  var ws = new WebSocket("ws:" + url);
  sir_ws_start = new DateTime.now();
  timelog(sir_ws_start);
  ws.onOpen.listen((_) {
    timelog(new DateTime.now());
    ws.send(sir_ws_count --);
  });
  ws.onMessage.listen((_) {
    if (sir_ws_count > 0) {
      ws.send(sir_ws_count --);

    } else {
      sir_ws_end = new DateTime.now();
      timelog(sir_ws_end);
      betweenlog(sir_ws_end, sir_ws_start);
      requestParWS();
    }
  });
}

void requestParWS(){
  par_ws_start = new DateTime.now();
  timelog(par_ws_start);
  var ws = new WebSocket("ws:" + url);
  ws.onOpen.listen((_){
    for(var x = 0 ; x < max ; x ++){
      ws.send(x);
    }
  });
  ws.onMessage.listen((_){
    par_ws_count --;
    if(par_ws_count == 0){
      par_ws_end =  new DateTime.now();
      timelog(par_ws_end);
      betweenlog(par_ws_end , par_ws_start);
      requestParHttp();
    }
  });
}

void requestParHttp(){
  par_http_start = new DateTime.now();
  timelog(par_http_start);

  for(var x = 0 ; x < max ; x ++){
    HttpRequest.getString(url + "?number=$x").then((_){
      par_http_count --;
      if(par_http_count == 0){
        par_http_end =  new DateTime.now();
        timelog(par_http_end);
        betweenlog(par_http_end , par_http_start);
      }
    });
  }
}

void timelog(DateTime dt) {
  document.body.append(new DivElement()
    ..text = dt.toString());
}

void betweenlog(DateTime dt1, DateTime dt2) {
  document.body.append(new DivElement()
    ..text = (dt1.millisecondsSinceEpoch - dt2.millisecondsSinceEpoch)
        .toString());
}
```


#予想
Socketというのだからハンドシェイクをしない分、直列でも並列でもWebSocketの方が早いのではないか。

#結果
||直列|並列|
|:--:|--:|--:|
|HTTP|17553ms|3001ms|
|WebSocket|28841ms|185ms|
##直列
驚いたことに直列の場合はHTTPの方が速かったです。
よく考えたらKeep-Aliveされるのでハンドシェイクとか関係ありませんでした（←やる前に気づけよ）
Keep-Aliveされない設定にして測りなおすか考えましたが、今回は「RESTをするならHTTPかWebSocketか」という問題設定なのでこれがHTTPの実力と判断するべきとしました。
このスピードの差の理由を是非教えてください。
##並列
結構な差でWebSocketの方が速かったです。
これはHTTP2の必要性で議論されている

- 一つのサーバーに対して6本までしかコネクションを張ってはいけない
- 前のレスポンスが返ってくるまで次のリクエストを送らない


というHTTP1.1の仕様上の限界かと思います。
今後前のレスポンスが返ってくる前に次のリクエストを送信しなければいけない場合はWebSocketを選んでいこうと思いました。

#今後
WebSocketの仕様や実装について勉強します。
また、SSLの場合やHTTP2ではどのような差になるのかも試してみたいです。

#補足
前述のシステムですが、一つのWebSocketを引き回そうとするとあまりに複雑になるとわかったので完全SPAにするのはやめました。
具体的に言うと「〇〇を表示する画面を新しいウィンドウで開く」という要件があったので諦めました。
ただ、今後ServiceWorkerを使って再挑戦できると考えています。