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