読者です 読者をやめる 読者になる 読者になる

理系学生日記

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

忍者TOOLS

iOS で POST を行うまで

iOS

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.

しかし、このメソッドの仕様(実装?)に問題があり、アンバサンド(&) やプラス(+)、スラッシュ(/)をエスケープしないという謎の動作になってしまいます。

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

- (NSString*)_uriEncodeForString:(NSString *)str {
    return [((NSString*)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault,
                                                                (CFStringRef)str,
                                                                NULL,
                                                                (CFStringRef)@"!*'();:@&=+$,/?%#[]",
                                                                kCFStringEncodingUTF8)) autorelease];
}

POST リクエストの生成

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

- (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) が呼び出されることになります。

- (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 に要求しています。

@class HttpClient;

@protocol HttpClientDelegateProtocol <NSObject>

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

@end