はじめに
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に設定しているのは、ストリーム上のデータを受け取るメソッドをこの同じクラスに実装するからです。
