理系学生日記

おまえはいつまで学生気分なのか

iOS で POST を行うまで

iPhone から某 WebService のフォーム認証をパスするまでのコードを書く上で HTTP POST を行う処理を実装する必要があったので、その流れを少しメモって行こうとおもいます。 なお実装は、https://github.com/kishikawakatsumi/ldr-touch をかなり参考にしています。

** POST を行うには HTTP 通信等の通信を行うために、Foundation フレームワークは NSURLConnection や NSURLRequest, NSURLResponse といったクラスを中核とする URL Loading System を提供しています。 手っ取り早く POST を行うためにはこれらのクラスを使用する必要がありますが、NSURLRequest や Response はその名称の通り Request/Response を抽象化したクラスであり、NSURLConnection はデータ送受信とその各フックポイント (delegate method) を提供するのに特化したクラスになっています。 この枠組みの中で、POST は API として提供されていないため、POST するための HTTP ヘッダの生成や URIEncode などは自力で実装する必要があります。

**URIEncode URI Encode を行うためのメソッドとして、NSString クラスには stringByAddingPercentEscapesUsingEncoding: メソッドが用意されています。

Returns a representation of the receiver using a given encoding to determine the percent escapes necessary to convert the receiver into a legal URL string. << しかし、このメソッドの仕様(実装?)に問題があり、アンバサンド(&) やプラス(+)、スラッシュ(/)をエスケープしないという謎の動作になってしまいます。 - ref: http://madebymany.com/blog/url-encoding-an-nsstring-on-ios

したがって、URI Encode を行おうと思うと、以下のような CFURLCreateStringByAddingPercentEscapes を使う方法が定石となっているようです。

|objc| - (NSString)_uriEncodeForString:(NSString )str { return [*1 autorelease]; } ||<

**POST リクエストの生成 POST するためには、まず HTTP BODY を作成する必要があります。 フォーム認証の場合は、普通ユーザ ID とパスワードの key-value ペアは必須でしょうから、これを作成するメソッドを作成します。パラメータは NSDictionary (Perl でいうハッシュ、Java でいう Map のようなクラスです) とします。

|objc| - (NSString)_buildParameters:(NSDictionary )params { NSMutableString *s = [NSMutableString string];

NSString *key;
for ( key in params ) {
    NSString *uriEncodedValue = [self _uriEncodeForString:[params objectForKey:key]];
    [s appendFormat:@"%@=%@&", key, uriEncodedValue];
}

if ( [s length] > 0 ) {
    [s deleteCharactersInRange:NSMakeRange([s length]-1, 1)];
}
return s;

} ||<

これで HTTP Body が作成できたので、あとは HTTP Request を作成し、対象 URL へ送りつければ良いことになります。 HTTP Request に最低限設定すべきなのは以下でしょうか。 - HTTP メソッドとして POST を設定 - Content-Type として "application/x-www-form-urlencoded" を指定 - Content-Length に HTTP Body の長さを指定 - HTTP Body を設定

このようにして作成した HTTP Request を NSURLConnection に渡すと、非同期でリクエストが送信され、各コールバック(delegate method) が呼び出されることになります。

|objc| - (void)post:(NSURL )url withParameters:(NSDictionary )params { // BODY の作成 NSString bodyString = [self _buildParameters:params]; NSData httpBody = [bodyString dataUsingEncoding:NSUTF8StringEncoding];

NSMutableURLRequest *req = [[NSMutableURLRequest alloc] initWithURL:url
                                                        cachePolicy:NSURLRequestReloadIgnoringCacheData
                                                    timeoutInterval:HTTP_TIMEOUT];
// POST の HTTP Request を作成
[req setHTTPMethod:@"POST"];
[req setValue:@"application/x-www-form-urlencoded"                 forHTTPHeaderField:@"Content-Type"];
[req setValue:[NSString stringWithFormat:@"%d", [httpBody length]] forHTTPHeaderField:@"Content-Length"];
[req setHTTPBody:httpBody];
[req setHTTPShouldHandleCookies:YES];

// POST 送信
NSLog(@"sending [%@] (%d bytes) to %@ ...", bodyString, [httpBody length], url);
self.conn = [[NSURLConnection alloc] initWithRequest:req delegate:self];
if ( self.conn ) {
    self.receivedData = [NSMutableData data];
}
else {
    NSLog(@"creating NSURLConnection failed: in %s", __FUNCTION__);
}
[req release];

} ||<

なお、(このへんは好き好きかと思いますが)ぼくは delegate クラスには明示的にプロトコル実装を要求する方が好きなので、こんなメソッドの実装を delegate に要求しています。

|objc| @class HttpClient;

@protocol HttpClientDelegateProtocol

  • (void)httpClientFailed:(HttpClient )client withError:(NSError )error;
  • (void)httpClientSucceedWithResponse:(NSURLResponse )response withData:(NSData )data;

@end ||<

*1:NSString)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (CFStringRef)str, NULL, (CFStringRef)@"!'();:@&=+$,/?%#[]", kCFStringEncodingUTF8