Authenticator
以下は、Mac OS X 10.4.11 上の Xcode 2.5 で説明しています。
1 アプリケーション
このサンプルは、コマンド行ツールです。Xcode 上でビルドした後で、作られた実行ファイル(build/Development/Authenticator)は、Xcode 上で起動引数を設定し実行できますが、このサンプルでは同時に並列して実行されるないとサンプルがうまく働きません。そこで、ターミナルを使います。ターミナルまで実行ファイルをドラッグします。するとパスが入力されるのでそのままリターンを押して実行します。
$ /Authenticator
usage: /Authenticator server // to run as server
usage: /Authenticator client // to run as client
usage: One of the two arguments must be specified.
このように引数なしだと、使用法を示した文字列が表示されて終了します。次に引数 server を追加してみます。
$ /Authenticator server
/Authenticator server: started
サーバーが起動したというメッセージが表示され、そのまま待機状態になります。ここで、ターミナルで「ファイル」>「新規シェル」を選び新しいシェルを開きます。そこで今度は引数 client でサンプルを実行します。
$ /Authenticator client
2007-12-26 21:09:58.764 Authenticator[645] description:
2007-12-26 21:09:58.767 Authenticator[645] isKindOfClass NSObject? YES
2007-12-26 21:09:58.768 Authenticator[645] Done. Messages sent successfully.
このようにサーバーへのメッセージ送信が成功したことを示すメッセージが表示され終了します。この Authenticator は、「認証を行うもの」という意味で、サーバーは送られてきたメッセージを調べて認証します。
2 ファイル構成
プロジェクトを構成しているファイルは以下のようになっています。
非常に簡単です。main.m ファイルしかありません。Foundation フレームワークにリンクされていることに注意してください。main.m ファイル内には、main 関数のほかに、Authenticator クラスのインターフェイス宣言と実装も収められています。まず、このクラスから見てみましょう。
3 Authenticator クラス
まず、インターフェイスから見てみます。main.m の最初の方に宣言があります。
main.m > Authenticator > @interface インスタンス変数宣言
@interface Authenticator : NSObject
// このクラスのインスタンスはNSConnectionの委任として動作することになる
// NSConnectionの委任はメッセージを認証する他にいろいろなことができるが
// ここでは単に認証だけに興味がある
- (NSData *)authenticationDataForComponents:(NSArray *)components;
// 任意の認証情報を収容するNSDataを計算して返す
// 事実上、これは要素配列に対する「署名」である
- (BOOL)authenticateComponents:(NSArray *)components withData:(NSData *)signature;
// NSData内の認証情報が有効でメッセージ要素に一致するかを検証
@end
インスタンス変数なしの単純なクラスです。まず最初のメソッドの実装から見てみます。
main.m > Authenticator > authenticationDataForComponents:
- (NSData *)authenticationDataForComponents:(NSArray *)components {
unsigned int idx1, idx2;
unsigned char checksum = 0;
unsigned int count = [components count];
// 与えられた配列内の要素に対して認証データを計算
// NSPortとNSDataという2つのタイプの要素がある
// 理解できないタイプの要素は無視したほうがいい
// ここでは、配列内のNSDataオブジェクト内のバイト全体の
// 簡単な1バイトのチェックサムを計算する
for (idx1 = 0; idx1 < count; idx1++) {
// 配列の要素ごとに繰り返し
id item = [components objectAtIndex:idx1];
if ([item isKindOfClass:[NSData class]]) {
const unsigned char *bytes = [item bytes];
unsigned int length = [item length];
for (idx2 = 0; idx2 < length; idx2++) {
// NSData内のバイトごとに繰り返し
checksum ^= bytes[idx2];
// XOR をとって代入
}
}
}
// NSData内にチェックサムバイトを挿入し、それを返す
// これはメッセージ要素に対する認証データ
return [NSData dataWithBytes:&checksum length:1];
}
ここでは簡単な XOR 演算によるチェックサムを使っています。16 ビットにして 1 の補数で計算するような方法もよくとられます。奇数長の場合は最後に 0 を追加します。
全体的には最初に要素配列の数を得て、配列内の要素をその分繰り返しして1つずつ処理します。各要素ごとに NSData クラスだった場合だけ取り出して、データの長さを調べて、先頭から1バイトずつ取り出して、チェックサムとの XOR をとっています。そうして計算したチェックサムを最後に返しています。
つぎに認証を行うメソッドを見てみます。この逆になると予想されるので理解も簡単でしょう。
main.m > Authenticator > authenticateComponents: withData:
- (BOOL)authenticateComponents:(NSArray *)components withData:(NSData *)signature {
// 要素に対する認証データを検証する
// 良い認証は再計算することなく署名を検証する方法を持っているだろう
// この例ではそうせず、単に再計算する
NSData *recomputedSignature = [self authenticationDataForComponents:components];
// 2つのNSDataが等しくないなら、認証は失敗!
if (![recomputedSignature isEqual:signature]) {
NSLog(@"received signature %@ doesn't match computed signature %@",
signature, recomputedSignature);
return NO;
}
return YES;
}
単に先ほどのメソッドを使って、認証データを再計算しているだけです。
さて、このクラスはインターフェイス宣言の最初にコメントされていたように、main 関数内で NSConnection の委任にされます。そのため、NSConnection の委任メソッドが実装されています。それは、親の接続が新しい接続を許可するかどうかを返すものです。
main.m > Authenticator > connection: shouldMakeNewConnection:
- (BOOL)connection:(NSConnection *)ancestor
shouldMakeNewConnection:(NSConnection *)conn
{
// 認証関連でない委任メソッド
// (クライアントごとの)子の接続のすべてが同じ委任を得ることを確実にする
[conn setDelegate:[ancestor delegate]];
return YES;
}
単に子の接続の委任を親の委任に設定して、全部 OK しているだけです。
4 main 関数
さて、Authenticator クラスが理解できたので、main 関数を調べてみます。main 関数は、引数によって処理を分けています。その部分は後で説明することにして、おおまかな構造を見てみます。
main.m > main
int main(int argc, const char *argv[]) {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
if (argc < 2) {
fprintf(stderr, "usage: %s server // to run as server\n", argv[0]);
fprintf(stderr, "usage: %s client // to run as client\n", argv[0]);
fprintf(stderr, "usage: One of the two arguments must be specified.\n");
exit(1);
}
if (0 == strcmp(argv[1], "server")) {
サーバー時の処理
} else if (0 == strcmp(argv[1], "client")) {
クライアント時の処理
} else {
fprintf(stderr, "Unknown argument '%s'. Must be 'client' or 'server'.\n",
argv[1]);
exit(1);
}
[pool release];
return 0;
}
まず、Foundation 利用時に必ずしなければいけないものとして自動解放プールを作成しています。これは Foundation や AppKit 内のクラスなどを使う前に最初にしなければなりません。AppKit の NSApplication では、これは自動的に作られます。そして終了する前にそれを解放します。このサンプルは単純なのでこれでいいですが、複雑な処理の(特に繰り返しなど一時的に多量のオブジェクトが割り当てられ解放される)場合、さらにローカルな自動解放プールを作って、後で解放して負担を軽くします。
それから引数の数をチェックして、引数がない場合は、使用法を示すメッセージを出力しています。これはコマンド行ツールではよくある仕様です。引数配列の一番最初の自身のコマンド名を使っていることに注意してください。
引数があれば、文字列を比較して何が渡されたかを調べ、それによって処理を分けています。未知の引数が渡された場合は、エラーメッセージを出力して終了します。
まずサーバー時の処理を見てましょう。
main.m > main > サーバー時の処理
// 分散オブジェクト経由でオブジェクト発行に使われる一般NSConnectionを作成
NSConnection *conn = [[NSConnection alloc] init];
// 分散オブジェクト経由で発行される一般オブジェクトを作成
// 通常これは「サーバー」として何か意味のあることを実際に行うオブジェクト
NSObject *object = [[NSObject alloc] init];
// サーバーに入ってきたメッセージを認証する Authenticator オブジェクト作成
// クライアントとサーバーは同じ認証方法を使う必要があるが、
// 同じクラスを使う必要はないだろう
Authenticator *authenticator = [[Authenticator alloc] init];
// 接続を設定
[conn setDelegate:authenticator];
[conn setRootObject:object];
// ルートオブジェクトの名前を設定
if (![conn registerName:CONNECTION_NAME]) {
fprintf(stderr,
"%s server: could not register server. Is one already running?\n",
argv[0]);
exit(1);
}
fprintf(stderr, "%s server: started\n", argv[0]);
// 入ってきたメッセージを処理させながら永久に実行ループを実行させる
[[NSRunLoop currentRunLoop] run];
// オブジェクトの後片付け、この場合実際には必要ではない
[authenticator release];
[object release];
[conn release];
単純でコメントも多いので、理解するのは簡単だと思います。まず最初に一般的な NSConnection オブジェクトを作成しています。このサンプルでは同一ホスト内での分散だけが考えられているので、このようなデフォルトの初期化で十分機能します。もしホスト間の接続を確立することが必要なら、NSSocketPort のようにマシン間での通信をサポートしているポートを使い、+ connectionWithReceivePort:sendPort: や – initWithReceivePort:sendPort: などで、受信・送信ポートを指定して接続を作成する必要があります。
つぎに NSObject を作っています。これは接続のルートオブジェクトとして機能させるためのものです。ふつうの場合、何か意味のあることを行わなければならないので、これは相手方が送るメッセージに対応できる独自クラスになります。このサンプルでは委任を使ってメッセージの認証を行うものの、それ以上の実際の作業は何も行わないので、単に NSObject を作成しています。
つぎに委任として認証を行うことになる Authenticator クラスのインスタンスを作成しています。
それから、今作ったインスタンスを接続の委任として設定し、先ほど作った NSObject をルートオブジェクトに設定しています。ルートオブジェクトを設定することで、このオブジェクトが相手方に発行されることになります。
つぎにルートオブジェクトの名前を設定しています。- registerName: はローカルホスト上のデフォルトの名前サーバーに名前を登録します。ネットワーク経由でソケットポートを使って分散を行うなら、- registerName: withNameServer: を使い、[NSSocketPortNameServer sharedInstance sharedInstance] を引数の名前サーバーとして与えて登録することになるでしょう。名前として与えている定数 CONNECTION_NAME は、main.m の最初で以下のように定義されています。
main.m > main > CONNECTION_NAME
#define CONNECTION_NAME @"authentication test"
registerName: は名前の登録に成功すると YES を返します。もし NO が返されたら、エラーメッセージを出力して終了します。ここでは、ありうる可能性のひとつとして、すでにサーバーとして実行されていないか示唆しています。
うまく名前が登録できたら、サーバーとして動作する準備が整いました。実行を始める前に、開始を告げるメッセージを出力しておきます。それから現在の実行ループに実行を指示しています。
NSConnection は、初期化された時に現在の実行ループに自身を登録します。AppKit を利用した通常のアプリケーションの場合、すでに実行ループが実行されているため、この作業は不必要です。接続は実行ループ内でメッセージ監視を行うことになります。別スレッドで接続を実行したいなら、別スレッドを分離した後、そちらで接続を作成するか、runInNewThread メソッドで新しいスレッドを分離して実行させます。主スレッドと二次スレッド間の通信に分散を使う場合は、二次スレッドの開始メソッド内で接続を作ることになります。これについては『マルチスレッドプログラミングトピック』の「分散オブジェクトによる通信」を見てください。
最後に後片付けをしています。コメントでは実際に必要ではない云々とありますが、実行ループがひたすら実行されているため、終了時でないかぎり、この部分のコードが呼ばれることはなく、プログラム終了時にプロセスに割り当てられたメモリが廃棄されるため、解放を行わなくても実際には問題ないからです。
このように、サーバーは実行ループを実行し続け、外部から接続要求が入ってくれば、委任メソッドが呼び出されることになります。つぎはクライアントの方を見てみましょう。
main.m > main > クライアント時の処理
// サーバーに送るメッセージを認証するAuthenticatorオブジェクトを作成
// クライアントとサーバーは同じ認証方法を使う必要があるが、
// 同じクラスを使う必要はないだろう
Authenticator *authenticator = [[Authenticator alloc] init];
NSDistantObject *proxy;
// サーバー接続を検索する
NSConnection *conn
= [NSConnection connectionWithRegisteredName:CONNECTION_NAME host:nil];
if (!conn) {
fprintf(stderr,
"%s server: could not find server.
You need to start one on this machine first.\n",
argv[0]);
exit(1);
}
// NSConnectionの委任として認証オブジェクトを設定
// ルート代理を探す最初のものも含め、すべての以降のメッセージは
// 認証オブジェクトを経由することになる
[conn setDelegate:authenticator];
proxy = [conn rootProxy];
if (!proxy) {
fprintf(stderr,
"%s server: could not get proxy. This should not happen.\n",
argv[0]);
exit(1);
}
// これはサンプルなので、サーバーオブジェクトが実際に何を行うかについて気にしない
// 単にそれにメッセージを送ることができるだけである
// 単なるNSObjectなので、何らかのNSObjectメッセージを送信する
// 認証が成功しなかったら、NSFailedAuthenticationException例外が発生する
NSLog(@"description: %@", [proxy description]);
NSLog(@"isKindOfClass NSObject? %@",
[proxy isKindOfClass:[NSObject self]] ? @"YES" : @"NO");
NSLog(@"Done. Messages sent successfully.");
まず認証に使うオブジェクトを作成しています。その後、サーバーで登録したのと同じ名前を使って接続を探しています。ホスト名に nil が渡されているので、ローカルホストが検索されます。見つからなければ、エラーを表示して終了します。ここでは委任を設定するため、接続を探していますが直接ルートプロキシを探すこともできます。返される接続は、探された元の接続が作る、このクライアント用の子の接続です。
つぎに見つかった接続(このクライアント用の子の接続)に対してクライアント側で委任を設定しています。それからルートオブジェクトの代理を取得します。取得に失敗すれば、エラーを出力して終了します。
あとは取得した代理を使っているだけです。サーバーが発行したルートオブジェクトは NSObject だったので、ここでは NSObject のメソッドを使っていますが、通常は、発行された独自オブジェクトが理解できるメソッドを使い、もっとさまざまな作業を行うことになるでしょう。得られたオブジェクトを解放したり後片付けしていないことに気をつけてください。接続オブジェクト等は保持されていません。
管理人:神吉 秀典 E-mail: