swift で HTTP でAPIを呼び出すときなどは URLSession#dataTask(with:,completionHandler:)
を使うのが一般的らしい。
guard let url = URL(string: "http://localhost:8080/api") else { throw MyURLError.invalidURL } URLSession.shared.dataTask(with: url) {(data, response, error) in if let e = error { print("error: \(e)") } if let r = response as? HTTPURLResponse { print("status: \(r.statusCode)") print("Content-Type: \(r.allHeaderFields["Content-Type"] ?? "")") } if let d = data { let body = String(data: d, encoding: String.Encoding.utf8) ?? "" print("body: \(body)") } }
疑問: URLSession
ってどんな非同期なの?
URLSession
+ URLSessionDataTask
の組み合わせによる、一見非同期に見えるこの処理は本当に非同期なのか、それとも非同期っぽい同期コードなのかを知りたい。ここで、
- ちゃんと非同期
- 非同期っぽい同期
とする。3連休は自宅でダラダラ過ごしながら、どんな感じの非同期なのか疑問に思った我々は swift-corelibs-foundation と swift-corelibs-libdispatch に挑んだ。
結論: ちゃんと非同期
結論としては、前者であるが、 select
や kqueue
を直接使っているわけではない。
具体的には URLSession
の通信部分は libcurl をつかっていて、一つひとつのリクエストは libcurl の easy handle API を利用している。それらを URLSession
が保持している libcurl の multi handle API で処理している形である。
以下、読んだ内容をざっくりと…(C言語が読めないし、 libcurl よく知らないし、 swift も大して読めないので不正確かもしれない)
URLSession
URLSessionTask
- リクエストの処理をユーザーが制御するためのクラスで、 ファイルを伴わないリクエスト ->
URLSessionDataTask
、 Multi Part でファイルをアップロードするリクエスト ->URLSessionUploadTask
、 ファイルをダウンロードするリクエスト ->URLSessionDownloadTask
などのサブクラスがある - これらのクラスはイニシャライザーでリクエストの URL に基づき
URLProtocol
を生成・保持する URLSessionTask
はresume
関数にて、URLProtocol
のstartLoading
関数を経由して、HTTPURLProtocol
のconfigureEasyHandle
関数にて libcurl の easy handle API のラッパーである_EasyHandle
にリクエストのパラメーターを渡していく
- リクエストの処理をユーザーが制御するためのクラスで、 ファイルを伴わないリクエスト ->
URLProtocol
_MultiHandle
select
やkqueue
のような役割を提供している- ソケットのステータスが変更された場合に呼び出されるコールバック関数を提供する
- curl multi API に監視対象の
_SocketSources
が登録されていない場合には、新しい_SocketSources
を生成・登録する - ソケットの状態に基づき、
_ReadSource
、_WriteSource
を登録する _MultiHandle
の中にイベントループがあり、 curl multi API からcurl_multi_info_read
を呼び出す。その関数で_EasyHandle
が見つかった場合(つまり読み込み完了した場合)は_EasyHandle
のcompletedTransfer(withError:)
を呼び出す。これはURLProtocol
(_NativeProtocol
) のtransferCompleted(withError:)
にディスパッチされて、パラメーターのerror: NSError
の有無によって、failWith(:error, request:)
(エラーの場合)またはcomleteTask()
またはredirectFor(request:)
が呼び出される。completeTask
が呼ばれた場合は_ProtocolClient
のurlProtocolDidFinishLoading(_:URLProtocol)
が呼び出されて、URLSession
が抱えているDispatchQueue
上でURLSessionTask
から取得した completion handler(つまりユーザーがdataTask
に与えたハンドラー) にレスポンスを渡す。処理が終わったあとは_TaskRegistry
からURLSessionTask
を取り除く
_EasyHandleDelegate
protocol _EasyHandleDelegate { // レスポンスを受け取ったとき func didReceive(data: Data) -> _EasyHandle._Action // レスポンスを受け取ったとき func didReceive(headerData data: Data, contentLength: Int64) -> _EasyHandle._Action // POST のボディを書き込めるとき func fill(writeBuffer buffer: UnsafeMutableBufferPointer<Int8>) -> _EasyHandle._WriteBufferResult // 書き込み完了後 func transferCompleted(withError error: NSError?) // input stream のシーキング(? CURLOPT_SEEKFUNCTION で渡すオプションのコールバック e.g. https://curl.haxx.se/libcurl/c/CURLOPT_SEEKFUNCTION.html) func seekInputStream(to position: UInt64) throws // プログレスを更新 (? CURLOPT_XFERINFOFUNCTION で渡すオプションのコールバック e.g. https://curl.haxx.se/libcurl/c/CURLOPT_XFERINFOFUNCTION.html) func updateProgressMeter(with propgress: _EasyHandle._Progress) }
以上から、動作は libcurl の multi handle API をベースにしたイベント駆動の作りになっており、一つの URLSession
で何個でもリクエストを投げられるようになっている