mike-neckのブログ

JavaかJavaFXかJavaEE(なんかJava8が多め)

URLSession による HTTP 接続と libcurl

f:id:mike_neck:20180609045321p:plain

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 の組み合わせによる、一見非同期に見えるこの処理は本当に非同期なのか、それとも非同期っぽい同期コードなのかを知りたい。ここで、

  • ちゃんと非同期
    • select ないしは kqueue を使って、イベントループがネットワークのイベントを拾ってから処理をおこなっている
    • 大量のリクエストを出しても性能が劣化しない(いくつものリクエストをさばける)
    • Java で NIO 使った場合はこちらになる
  • 非同期っぽい同期
    • 同期的なネットワーク呼び出しを GCD でディスパッチして、バックグラウンドのスレッドに固まってもらうことで、 main スレッドは固まらないようにしている
    • 準備したスレッドプールの数を超えるリクエストを出すと、全体としての性能が劣化する
    • Java で NIO 使わない場合は、こちらになる

とする。3連休は自宅でダラダラ過ごしながら、どんな感じの非同期なのか疑問に思った我々は swift-corelibs-foundation と swift-corelibs-libdispatch に挑んだ。


結論: ちゃんと非同期

結論としては、前者であるが、 selectkqueue を直接使っているわけではない。

具体的には URLSession の通信部分は libcurl をつかっていて、一つひとつのリクエストは libcurl の easy handle API を利用している。それらを URLSession が保持している libcurl の multi handle API で処理している形である。


以下、読んだ内容をざっくりと…(C言語が読めないし、 libcurl よく知らないし、 swift も大して読めないので不正確かもしれない)

  • URLSession
    • 複数のリクエストもふくめて、 リクエスト処理をあらわす URLSessionTask(_TaskRegistry で管理されている) やそれをハンドルする DispatchQueue を管理しているクラス
    • インスタンス生成時に、その URLSession が管理する multi handle API を表す _MultiHandleインスタンスを生成する
    • dataTask などのメソッドでは URLSessionTaskインスタンスと、 completion handler をラップした _TaskRegistry._Behaviour_TaskRegistry に加える
  • URLSessionTask
    • リクエストの処理をユーザーが制御するためのクラスで、 ファイルを伴わないリクエスト -> URLSessionDataTask 、 Multi Part でファイルをアップロードするリクエスト -> URLSessionUploadTask 、 ファイルをダウンロードするリクエスト -> URLSessionDownloadTask などのサブクラスがある
    • これらのクラスはイニシャライザーでリクエストの URL に基づき URLProtocol を生成・保持する
    • URLSessionTaskresume 関数にて、 URLProtocolstartLoading 関数を経由して、 HTTPURLProtocolconfigureEasyHandle 関数にて libcurl の easy handle API のラッパーである _EasyHandle にリクエストのパラメーターを渡していく
  • URLProtocol
    • 多くの場合は HTTPURLProtocolインスタンスが生成されるが、ほとんどの関数はスーパークラスである _NativeProtocol のものが使われている
    • リダイレクトの場合の制御や、データ読み込み完了の際に呼び出される関数が提供されている
    • これらのコールバックが提供されているのは、 _NativeProtocol_EasyHandleDelegate プロトコルに準拠しているため
  • _MultiHandle
    • selectkqueue のような役割を提供している
    • ソケットのステータスが変更された場合に呼び出されるコールバック関数を提供する
    • curl multi API に監視対象の _SocketSources が登録されていない場合には、新しい _SocketSources を生成・登録する
    • ソケットの状態に基づき、 _ReadSource_WriteSource を登録する
    • _MultiHandle の中にイベントループがあり、 curl multi API から curl_multi_info_readを呼び出す。その関数で _EasyHandle が見つかった場合(つまり読み込み完了した場合)は _EasyHandlecompletedTransfer(withError:) を呼び出す。これは URLProtocol(_NativeProtocol) の transferCompleted(withError:) にディスパッチされて、パラメーターの error: NSError の有無によって、 failWith(:error, request:)(エラーの場合)または comleteTask() または redirectFor(request:) が呼び出される。 completeTask が呼ばれた場合は _ProtocolClienturlProtocolDidFinishLoading(_: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 で何個でもリクエストを投げられるようになっている