26日目、@adachi_c です。こんにちは〜。
今日は2台のiPhoneで通信して、JSONとMessagePackのシリアライズ、デシリアライズの速度を計る計測アプリ作ったんで、見てってください。
シリアライズとはなにか
シリアライズというのは何かというと、他の環境で復元(デシリアライズ)できるように、環境依存なデータ構造を、システム間で相互理解できる形式に変換することです。
システムごとに、いろんなOS、言語、新旧の違いなどがあるかと思いますが、それぞれが相互運用性を維持するために、共通のメッセージ表現手法が必要です。それがJSONだとか、XMLのDOMやSAXに該当します。シリアライズ形式によっては、型情報を持っていることもあったりします。
OSや言語が異なるシステム間でAPIを設計するとき、このシリアライズを使うことが有効になるわけです。
どのシリアライズ方式をとるか
では、様々な種類のメッセージ表現手法のうち、どういうシリアライズ方式を選べばよいのでしょうか?
この話をするまえに、まず、次の論文はすごい目から鱗というか、シリアライズ関係する人はとりあえず今日中に1~3章だけでも必読だと思いました。日本語で書いてあるし。
http://syuki.skr.jp/files/201204041/furuhashi-master-last-iso-pdfa.pdf
これによると、シリアライズ選定では、次の3点がポイントになるそうな。
** サービスはいろんな言語がくみ合わさってできてる=>多言語に対応する
** スケールアウト型の分散システムは通信量がめちゃめちゃ多い=>圧縮効率が大事
** 古いシステムとも通信したい=>実装が簡単じゃないとダメ
という点で、考えると、今あるほとんどのシリアライズ方式には問題があるみたいで、
例えばJSONだと実装は簡単だけど、遅いし、型情報をもたない、という問題があります。Thriftとかだと効率はいいけど、IDLを別途実装しないとダメ。IDLというのは、型情報を相手に通知するための言語です。早さというのと、実装の難しさというのはトレードオフ関係にあるみたいですね。
で、MessagePackというのがこの論文の方が考えたフォーマットで、DQ10のバックエンドなどでも使われているという話も聞きました。型情報もってるし、早いという。これだけ聞いたら素晴らしいですね。
というわけで、今回はiPhone対iPhoneで簡単な通信サーバ、クライアントを実装して、このMessagePackと、JSONで送受信をやって、ふーんやっぱりね。って結果が出るのを確認し、そのあとでDQ10を起動してMessagePackのありがたみを体感したような気分になりたいと思います。
ソースの場所
というわけで、1Mバイトの文字列が入ったNSDictionaryをシリアライズし、サーバに送信して、デシリアライズしてNSDictionaryを取り出すという簡単なアプリを作りました。
ソース:
https://github.com/adachic/Seriarize_compere
サーバ、クライアント実装
GCDAsyncSocketで簡単に実装。
下記のように、アプリは一つのViewControllerにGCDAsyncSocketを持って、socketのデリゲートをViewControllerに指定
- (void)viewDidLoad
{
[super viewDidLoad];
self.resultLog = [NSMutableString string];
[self.cl_button addTarget:self
action:@selector(exec_cl:) forControlEvents:UIControlEventTouchUpInside];
[self.sv_button addTarget:self
action:@selector(exec_sv:) forControlEvents:UIControlEventTouchUpInside];
self.sum = 0;
/*デリゲートを設定*/
self.socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
}
```
送受信時、データが大きいと一度のチャンクで受信しきれないことがあるので、パケットの先頭にレングス4バイトをもうけて、送るのが大事。
クライアント動作時(クライアントボタン押す):
````Objective-C:ViewController.m
- (void)exec_cl:(UIButton*)button
{
[self.sv_button setEnabled:NO];
self.label.text = @"running in Client mode";
NSError *err;
/*相手のiPhoneにつなぐ*/
if(![self.socket connectToHost:DEV_SV_HOST onPort:DEV_SV_PORT error:&err] && !times){
NSLog(@"connection error : %@",err);
}
/*テストデータの生成*/
:
:
NSInteger datalen = [packed length];
NSMutableData *d = [NSMutableData dataWithLength:0];
/*データの長さを先頭4バイトに書く*/
[d appendBytes:&datalen length:4];
[d appendData:packed];
/*サーバに送信*/
[self.socket writeData:d withTimeout:-1 tag:TAG_WRITE];
times++;
}
```
サーバ動作時(サーバボタン押す):
````Objective-C:ViewController.m
- (void)exec_sv:(UIButton*)button
{
[self.cl_button setEnabled:NO];
self.label.text = @"running in Server mode: ready";
NSError *error;
/*listen,ポート番号で待ち受ける*/
if (![self.socket acceptOnPort:DEV_SV_PORT error:&error])
{
NSLog(@"I goofed: %@", error);
}
}
/*accept時のハンドラ*/
- (void)socket:(GCDAsyncSocket *)sender didAcceptNewSocket:(GCDAsyncSocket *)newSocket
{
/*acceptしてnewSocketが得られるので、newSocketそこから先頭4バイト読む*/
[newSocket readDataToLength:4 withTimeout:-1 buffer:self.payload bufferOffset:0 tag:TAG_READ_LENGTH];
}
/*読み出し時のハンドラ*/
- (void)socket:(GCDAsyncSocket *)sender didReadData:(NSData *)data withTag:(long)tag
{
int size = 0;
/*先頭4バイト読み取りに成功*/
if (tag == TAG_READ_LENGTH){
[data getBytes:&size length: sizeof(size)];
NSLog(@"recvd size=%d",size);
/*本体の長さがわかったので、読み取る*/
[sender readDataToLength:size withTimeout:-1 buffer:self.payload bufferOffset:0 tag:TAG_READ_DONE];
return;
}
/*本体の読み取りに成功*/
if (tag == TAG_READ_DONE){
NSDate *startDate = [NSDate date];
:
/*以下、受信処理*/
```
## シリアライズ、デシリアライズ実装
以下の4メソッドがシリアライズ、デシリアライズを担当する。
````Objective-C:ViewController.m
- (NSMutableArray *) decodeDataJSON:(NSData *)data
{
return [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
}
- (NSMutableArray *) decodeDataMessagePack:(NSData *)data
{
return [data messagePackParse];
}
- (NSData *) encodeDataJSON:(NSMutableDictionary *)dict
{
return [NSJSONSerialization dataWithJSONObject:dict options:0 error:nil];
}
- (NSData *) encodeDataMessagePack:(NSMutableArray *)dict
{
return [dict messagePack];
}
```
あとはテストデータを作って、これらのメソッドにぶち込み、その前後の時間をはかる。これは送信側。シリアライズ時間をとり、ログ表示し、平均表示する。通信にかかる時間は環境によって変わると思うので、今回はやっていない。
##シリアライズ時間計測実装
````Objective-C:ViewController.m
- (void)exec_cl:(UIButton*)button
{
[self.sv_button setEnabled:NO];
self.label.text = @"running in Client mode";
NSError *err;
if(![self.socket connectToHost:DEV_SV_HOST onPort:DEV_SV_PORT error:&err] && !times){
NSLog(@"connection error : %@",err);
}
/*テストデータの生成:1Mバイトの文字列をディクショナリにぶちこむ*/
NSMutableDictionary *mdic = [NSMutableDictionary dictionary];
[mdic setObject:[self makeStringAZ:300000] forKey:@"key1"];
[mdic setObject:[self makeStringAZ:300000] forKey:@"key2"];
[mdic setObject:[self makeStringAZ:400000] forKey:@"key3"];
NSDate *startDate = [NSDate date];
#ifdef TEST_MESSAGEPACK
NSData *packed = [self encodeDataMessagePack:mdic];
#elif TEST_JSON
NSData *packed = [self encodeDataJSON:mdic];
#endif
NSTimeInterval interval = [[NSDate date] timeIntervalSinceDate:startDate];
self.sum += interval;
/*アベレージの表示*/
self.label.text = [NSString stringWithFormat:@"ave:%lf",self.sum/(times+1)];
NSLog(@"%02d:encode time is %lf ",times, interval);
[self.resultLog appendString:[NSString stringWithFormat:@"%02d:encode time:%lf\n",times, interval]];
self.textview.text = self.resultLog;
NSInteger datalen = [packed length];
NSMutableData *d = [NSMutableData dataWithLength:0];
[d appendBytes:&datalen length:4];
[d appendData:packed];
[self.socket writeData:d withTimeout:-1 tag:TAG_WRITE];
times++;
}
/* aからzまでを繰り返す文字列を作る */
- (NSString *)makeStringAZ:(NSInteger)length
{
NSMutableString *str = [NSMutableString string];
unichar a = 0;
for (int i=0; i<length; i++) {
a = 'a'+i%26;
[str appendString:[NSString stringWithCharacters:&a length:1]];
}
return str;
}
```
##デシリアライズ時間計測実装
次にデシリアライズ時間。受信側で、ログ表示し、平均表示する。
````Objective-C:ViewController.m
- (void)socket:(GCDAsyncSocket *)sender didReadData:(NSData *)data withTag:(long)tag
{
int size = 0;
if (tag == TAG_READ_LENGTH){
[data getBytes:&size length: sizeof(size)];
NSLog(@"recvd size=%d",size);
[sender readDataToLength:size withTimeout:-1 buffer:self.payload bufferOffset:0 tag:TAG_READ_DONE];
return;
}
if (tag == TAG_READ_DONE){
NSDate *startDate = [NSDate date];
#ifdef TEST_MESSAGEPACK
NSLog(@"recv MessagePack");
NSMutableDictionary *dict = [self decodeDataMessagePack:data];
#elif TEST_JSON
NSLog(@"recv Json");
NSMutableDictionary *dict = [self decodeDataJSON:data];
#endif
NSTimeInterval interval = [[NSDate date] timeIntervalSinceDate:startDate];
self.sum += interval;
self.label.text = [NSString stringWithFormat:@"ave:%lf",self.sum/(times+1)];
NSLog(@"%02d:decode time is %lf ",times, interval);
[self.resultLog appendString:[NSString stringWithFormat:@"%02d:encode time:%lf\n",times, interval]];
self.textview.text = self.resultLog;
times++;
[sender readDataToLength:4 withTimeout:-1 buffer:self.payload bufferOffset:0 tag:TAG_READ_LENGTH];
return;
}
}
```
## 結果(iPhone4Sで計測)
このアプリをつかって、1Mバイトのシリアライズ、デシリアライズをそれぞれ5回やって平均をとった。以下のような結果になった。
*JSON
-encode
0.032196s
-decode
0.022391s
*msgpack
-encode
0.009353s
-decode
0.021298s
## おわりに
今回はJSONとMessagePackだけでしたが、性能的にはMessagePackが優れてそうです。が、MessagePackはエンコード早いが、デコードが結構遅いこともわかりました。
Thriftまでやりたかったんですが、そっちはちょっと難航しまして..今後実装追加したいと思います。
ではでは、26日目、おつきあいくださいましてありがとうございました!