はじめに
iPhoneプログラミングに関する筆者の前回の記事では、iPhoneアプリケーション内からWebサービスを利用(consume)し、そこから返されるXMLデータを解析する方法について考察しました。Webサービスは今とても流行っているものの、Webサービスを利用するために必要とされるペイロードはかなり大きく、わずかなデータを取り出したいだけの場合は無駄が多いように感じられます。問題は、SOAPパケット自体が多くのバイト数を消費することです。そこで、これに代わる方法としてソケットを利用することを考えます。ソケットを使えば、余分なXMLペイロードなしに情報をやり取りすることが可能です。また、サーバとの接続が確立した状態を維持できるので、アプリケーションを非同期で動かして、送られてきたデータを必要なときだけ受け取るようなことも可能です。
本稿では、TCP/IPを使用してサーバと通信する方法を学びます。また、筆者が以前書いた記事の中で述べたアイデアを使って、簡単なチャットアプリを作ります。
本稿のサンプルプロジェクトでは、Xcodeを使用し、新規のView-based ApplicationプロジェクトをNetworkという名前で作成します。
ストリームによるネットワーク通信
ネットワーク上でソケットを使用して通信するときはNSStreamクラスを使うのが簡単です。NSStreamはストリームを表す抽象クラスで、これを使ってデータを読み書きできます。このクラスはメモリ、ファイル、ネットワークに対して有効です。NSStreamクラスを使うと、NSStreamオブジェクトに対してデータを読み書きするだけでサーバと通信できます。
Mac OS Xでサーバとの接続を確立するには、NSHostオブジェクトとNSStreamオブジェクトを次のように使います。
NSInputStream *iStream; NSOutputStream *oStream; uint portNo = 500; NSURL *website = [NSURL URLWithString:urlStr]; NSHost *host = [NSHost hostWithName:[website host]]; [NSStream getStreamsToHost:host port:portNo inputStream:&iStream outputStream:&oStream];
ご存じのように、NSStreamクラスにはgetStreamsToHost:port:inputStream:outputStream:
というクラスメソッドがあり、これでサーバに対して入力ストリームと出力ストリームを作成してデータを読み書きできます。しかし、問題はgetStreamsToHost:port:inputStream:outputStream:
メソッドがiPhone OSでサポートされていないことです。そのため、上記のコードはiPhoneアプリでは動きません。
この問題は、既存のNSStreamクラスにカテゴリを追加してgetStreamsToHost:port:inputStream:outputStream:
メソッドによる機能を置き換えれば解決できます。具体的には、Xcode内でClassesグループを右クリックし、新規のファイルをNSStreamAdditions.mという名前で追加し、NSStreamAdditions.hファイルに次のコードを追加します。
#import @interface NSStream (MyAdditions) + (void)getStreamsToHostNamed:(NSString *)hostName port:(NSInteger)port inputStream:(NSInputStream **)inputStreamPtr outputStream:(NSOutputStream **)outputStreamPtr; @end
また、NSStreamAdditions.mファイルに、リスト1のコードを追加します。
上のコードにより、getStreamsToHostNamed:port:inputStream:outputStream:
というクラスメソッドがNSStreamクラスに追加され、iPhoneアプリからこのメソッドを使ってサーバとのTCP接続を行えるようになります。
ここに示したカテゴリのコードは、AppleのTechnical Q&A1652を手本にしています。
#import "NSStreamAdditions.h" @implementation NSStream (MyAdditions) + (void)getStreamsToHostNamed:(NSString *)hostName port:(NSInteger)port inputStream:(NSInputStream **)inputStreamPtr outputStream:(NSOutputStream **)outputStreamPtr { CFReadStreamRef readStream; CFWriteStreamRef writeStream; assert(hostName != nil); assert( (port > 0) && (port < 65536) ); assert( (inputStreamPtr != NULL) || (outputStreamPtr != NULL) ); readStream = NULL; writeStream = NULL; CFStreamCreatePairWithSocketToHost( NULL, (CFStringRef) hostName, port, ((inputStreamPtr != nil) ? &readStream : NULL), ((outputStreamPtr != nil) ? &writeStream : NULL) ); if (inputStreamPtr != NULL) { *inputStreamPtr = [NSMakeCollectable(readStream) autorelease]; } if (outputStreamPtr != NULL) { *outputStreamPtr = [NSMakeCollectable(writeStream) autorelease]; } } @end
NetworkViewController.mファイルに、以下のステートメントを挿入します。
#import "NetworkViewController.h" #import "NSStreamAdditions.h" @implementation NetworkViewController NSMutableData *data; NSInputStream *iStream; NSOutputStream *oStream;
connectToServerUsingStream:portNo:
メソッドを定義します。これでサーバに接続して、入力ストリームと出力ストリームのオブジェクトを作成できます。
-(void) connectToServerUsingStream:(NSString *)urlStr portNo: (uint) portNo { if (![urlStr isEqualToString:@""]) { NSURL *website = [NSURL URLWithString:urlStr]; if (!website) { NSLog(@"%@ is not a valid URL"); return; } else { [NSStream getStreamsToHostNamed:urlStr port:portNo inputStream:&iStream outputStream:&oStream]; [iStream retain]; [oStream retain]; [iStream setDelegate:self]; [oStream setDelegate:self]; [iStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; [oStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; [oStream open]; [iStream open]; } } }
入力ストリームと出力ストリームの両方が、実行ループ上でイベントを受け取るようにスケジュールしてあります。これで、ストリームにデータがなくなってもコードが中断されなくなります。また、両方のストリームオブジェクトのデリゲート(delegate)をself
に設定しているのは、ストリーム上のデータを受け取るメソッドをこの同じクラスに実装するからです。