美文网首页KotlinKotlin官方文档Kotlin
Kotlin网络库Fuel的设计之道

Kotlin网络库Fuel的设计之道

作者: 老梁写代码 | 来源:发表于2018-04-18 23:44 被阅读73次

    使用场景

    一个“朴素”的 url 完全可以用一个字符串来表示(例如 "https://www.youzan.com"),我们可以利用 Kotlin 语言本身的特性为 String 类型添加一个扩展函数 httpGet(),然后借此发起 http 请求:

    "https://www.youzan.com".httpGet()
    

    但是,对于不是朴素字符串的对象来说,我们可以让其实现一个接口:

    interface PathStringConvertible {
        val path: String
    }
    

    然后,将“计算”过后的 path 通过一个 String 类型提供出来,例如:

    enum class HttpsBin(relativePath: String) : Fuel.PathStringConvertible {
        USER_AGENT("user-agent"),
        POST("post"),
        PUT("put"),
        PATCH("patch"),
        DELETE("delete");
    
        override val path = "https://httpbin.org/$relativePath"
    }
    

    但是,也会存在一种情况,所有的 url 可能会共享一个 base url,或者是其他公用参数,那么还需有一个地方来存储这些通用配置,这个地方的幕后老大就叫 FuelManager

    StringPathStringConvertible 最终也会调用到 FuelManager

    +----------+
    |  String  |------------->----+
    +----------+                  |    +------+    +-------------+
                                  |--->| Fuel |--->| FuelManager |
    +-------------------------+   |    +------+    +-------------+
    |  PathStringConvertible  |->-+
    +-------------------------+
    

    除了通过 String 或者 PathStringConvertiable 来发起请求,我们还可以直接用一个 Request,因此 Fuel 还提供了转换 Request 的接口:
    除了通过 String 或者 PathStringConvertiable 来发起请求,我们还可以直接用一个 Request,因此 Fuel 还提供了转换 Request 的接口:

    interface RequestConvertible {
        val request: Request
    }
    

    综上来看,发起一个 http 请求可以有如下四种方式:

    1. 一个字符串
    2. PathStringConvertible 变量
    3. RequestConvertible 变量
    4. 直接使用 Fuel 伴生对象提供的方法

    代码实现

    对外提供服务的 Fuel

    首先 Fuel 作为对外的接口提供方(类似 Facade 模式),通过一个伴生对象(companion object)提供服务(以 get 方法为例):

    companion object {
      @JvmStatic @JvmOverloads
      fun get(path: String, parameters: List<Pair<String, Any?>>? = null): Request =
              request(Method.GET, path, parameters)
    
      @JvmStatic @JvmOverloads
      fun get(convertible: PathStringConvertible, parameters: List<Pair<String, Any?>>? = null): Request =
              request(Method.GET, convertible, parameters)
    
      private fun request(method: Method, path: String, parameters: List<Pair<String, Any?>>? = null): Request =
              FuelManager.instance.request(method, path, parameters)
    
      private fun request(method: Method, convertible: PathStringConvertible, parameters: List<Pair<String, Any?>>? = null): Request =
              request(method, convertible.path, parameters)
    }
    

    Fuel 类通过伴生对象提供的 http 方法有 get/post/put/patch/delete/download/upload/head,这些方法最终会路由到 FuleManager 的实例(instance)。

    同时,Fule.kt 源文件为 StringPathStringConvertible 定义了扩展,以支持这些 http 方法(以 get 方法为例):

    @JvmOverloads
    fun String.httpGet(parameters: List<Pair<String, Any?>>? = null): Request = Fuel.get(this, parameters)
    
    @JvmOverloads
    fun Fuel.PathStringConvertible.httpGet(parameter: List<Pair<String, Any?>>? = null): Request = Fuel.get(this, parameter)
    

    幕后老大 FuleManager

    FuleManager 利用伴生对象实现了单例模式:

    companion object {
      //manager
      var instance by readWriteLazy { FuelManager() }
    }
    

    同时利用代理属性实现了单例的懒加载。

    readWriteLazy 是一个函数,它的返回值是一个 ReadWriteProperty,代码比较容易,具体可见 Delegates.kt

    也就是说,当我们第一次访问 FuelManager 时,一个具体的实例会被创建出来,这个实例担负了存储公用配置和发起请求的重任,首先来看它的属性:

    var client: Client
    var proxy: Proxy?
    var basePath: String?
    
    var baseHeaders: Map<String, String>?
    var baseParams: List<Pair<String, Any?>>
    
    var keystore: KeyStore?
    var socketFactory: SSLSocketFactory
    
    var hostnameVerifier: HostnameVerifier
    

    Client 是一个接口,通过它我们可以自定义 http 引擎。

    interface Client {
      fun executeRequest(request: Request): Response
    }
    
    +---------+     +--------+     +----------+
    | Request | ==> | Client | ==> | Response |
    +---------+     +--------+     +----------+
                         |
                        \|/                   +--------------------+
                  +------------+              | HttpURLConnection  |
                  | HttpClient | --based on-- +--------------------+
                  +------------+              | HttpsURLConnection |
                                              +--------------------+
    

    Fuel 默认提供的 Http 引擎是 HttpClient,它是基于 HttpURLConnection 的实现。
    Fuel 默认提供的 Http 引擎是 HttpClient,它是基于 HttpURLConnection 的实现。

    basePathbaseHeadersbaseParams 存储了请求的公用配置,我们可以通过 FuleManager.instance 为其赋值:

    FuelManager.instance.apply {
      basePath = "http://httpbin.org"
      baseHeaders = mapOf("Device" to "Android")
      baseParams = listOf("key" to "value")
    }
    

    keystore 用于构建 socketFactory,再加上 hostnameVerifier,它们用于 https 请求,在 HttpClient 中有用到:

    private fun establishConnection(request: Request): URLConnection {
      val urlConnection = if (proxy != null) request.url.openConnection(proxy) else request.url.openConnection()
      return if (request.url.protocol == "https") {
        val conn = urlConnection as HttpsURLConnection
        conn.apply {
          sslSocketFactory = request.socketFactory // socketFactory
          hostnameVerifier = request.hostnameVerifier // hostnameVerifier
        }
      } else {
        urlConnection as HttpURLConnection
      }
    }
    

    如果要深入了解 HTTPS 证书,可参考 「HTTPS 精读之 TLS 证书校验」。

    FuelManager 在发起请求时会用这些参数构建一个 Request

    fun request(method: Method, path: String, param: List<Pair<String, Any?>>? = null): Request {
      val request = request(Encoding(
            httpMethod = method,
            urlString = path,
            baseUrlString = basePath,
            parameters = if (param == null) baseParams else baseParams + param
      ).request)
    
      request.client = client
      request.headers += baseHeaders.orEmpty()
      request.socketFactory = socketFactory
      request.hostnameVerifier = hostnameVerifier
      request.executor = createExecutor()
      request.callbackExecutor = callbackExecutor
      request.requestInterceptor = requestInterceptors.foldRight({ r: Request -> r }) { f, acc -> f(acc) }
      request.responseInterceptor = responseInterceptors.foldRight({ _: Request, res: Response -> res }) { f, acc -> f(acc) }
      return request
    }
    

    关于 requestInterceptorresponseInterceptor,原理与 OkHttp 实现的拦截器一致,只不过这里利用了 Kotlin 的高阶函数,代码实现非常简单,具体细节可参考 「Kotlin实战之Fuel的高阶函数」。

    跟其他网络库一样,一次完整的请求,必然包含两个实体—— Request & Response,先来看 Request

    请求实体 Request

    class Request(
      val method: Method,
      val path: String,
      val url: URL,
      var type: Type = Type.REQUEST,
      val headers: MutableMap<String, String> = mutableMapOf(),
      val parameters: List<Pair<String, Any?>> = listOf(),
      var name: String = "",
      val names: MutableList<String> = mutableListOf(),
      val mediaTypes: MutableList<String> = mutableListOf(),
      var timeoutInMillisecond: Int = 15000,
      var timeoutReadInMillisecond: Int = timeoutInMillisecond) : Fuel.RequestConvertible
    

    它支持三种类型的请求:

    enum class Type {
      REQUEST,
      DOWNLOAD,
      UPLOAD
    }
    

    针对每个类型都有对应的任务(task):

    //underlying task request
    internal val taskRequest: TaskRequest by lazy {
      when (type) {
        Type.DOWNLOAD -> DownloadTaskRequest(this)
        Type.UPLOAD -> UploadTaskRequest(this)
        else -> TaskRequest(this)
      }
    }
    

    涉及到上传下载的 DownloadTaskRequestUploadTaskRequest 都继承自 TaskRequest,它们会处理文件和流相关的东西,关于此可参考 IO 哥写的 一些「流与管道」的小事 以及 OK, IO

    FuelManager 在构造 Request 时用到了一个类——Encoding

    class Encoding(
      val httpMethod: Method,
      val urlString: String,
      val requestType: Request.Type = Request.Type.REQUEST,
      val baseUrlString: String? = null,
      val parameters: List<Pair<String, Any?>>? = null) : Fuel.RequestConvertible
    

    Encoding 也是继承自 Fuel.RequestConvertible,它完成了对 Request 参数的组装编码,并产生了一个 Request

    Encoding 组装 query parameter 的方式可以说赏心悦目,贴出来欣赏一下:

    private fun queryFromParameters(params: List<Pair<String, Any?>>?): String = params.orEmpty()
      .filterNot { it.second == null }
      .map { (key, value) ->  URLEncoder.encode(key, "UTF-8") to URLEncoder.encode("$value", "UTF-8") }
      .joinToString("&") { (key, value) -> "$key=$value" }
    

    请求返回结果 Response

    class Response(
      val url: URL,
      val statusCode: Int = -1,
      val responseMessage: String = "",
      val headers: Map<String, List<String>> = emptyMap(),
      val contentLength: Long = 0L,
      val dataStream: InputStream = ByteArrayInputStream(ByteArray(0))
    

    Response 的属性可以看出,它所携带的仍然是一个流(Stream),我们先看 Response 是如何与 Request 串联起来的。

    Deserializable.kt 文件为 Request 定了名称为 response 的扩展函数:

    private fun <T : Any, U : Deserializable<T>> Request.response(
      deserializable: U,
      success: (Request, Response, T) -> Unit,
      failure: (Request, Response, FuelError) -> Unit): Request {
    
        val asyncRequest = AsyncTaskRequest(taskRequest)
    
        asyncRequest.successCallback = { response ->
          val deliverable = Result.of { deserializable.deserialize(response) }
          callback {
            deliverable.fold({
              success(this, response, it)
            }, {
              failure(this, response, FuelError(it))
            })
          }
        }
    
        asyncRequest.failureCallback = { error, response ->
          callback {
            failure(this, response, error)
          }
        }
    
        submit(asyncRequest)
        return this
    }
    

    扩展函数 response 的参数中,deserializable 负责反序列化操作,successfailure 用于处理请求结果。

    Fuel 提供了两个 Deserializable 的实现:StringDeserializer 以及 ByteArrayDeserializer,它们用于反序列化 response 的 stream。

    异步请求

    Deserializable.ktRequest 定义的扩展函数 response 在执行异步操作时用到了一个 AsnycTaskRequest,其实它本身并不提供异步实现,而是交由一个 ExecutorService 去执行,而这个 ExecutorService 恰由 FuelManager 定义,并在构造 Request 时传入给它。

    FuleManager.kt

    //background executor
    var executor: ExecutorService by readWriteLazy {
      Executors.newCachedThreadPool { command ->
        Thread(command).also { thread ->
          thread.priority = Thread.NORM_PRIORITY
          thread.isDaemon = true
        }
      }
    }
    

    AsyncTaskRequestUploadTaskRequestDownloadTaskRequest 一样,都是继承自 TaskRequest,只不过它多了两个异步调用的回调:

    var successCallback: ((Response) -> Unit)? = null
    var failureCallback: ((FuelError, Response) -> Unit)? = null
    

    请求图例

    至此,请求、回复,异步调用,对外接口都了解过了,一个基本的网络库框架已经成型。

             +------------------------+
             | https://www.youzan.com |
             +------------------------+
                         |
                         |
                        \|/
                      +------+
                      | Fuel |
                      +------+
                         |
                         |
                        \|/
                  +-------------+
                  | FuelManager |
                  +-------------+
                         |
                         |
                        \|/
    +---------+      +--------+      +----------+
    | Request | ===> | Client | ===> | Response |
    +---------+      +--------+      +----------+
    

    虽然Fuel 的复杂度不可与 OkHttp 相提并论,但是依赖 Kotlin 语言本身的灵活性,它的代码却比 OkHttp 要简洁的多,特别是关于高阶函数和扩展函数的运用,极大地提升了代码的可读性。

    参考资料

    相关文章

      网友评论

      • wenzb:你好,请问下这个框架是否是自动处理cookie,比如登录接口返回cookie,再请求其他接口时候会不会自动携带这个cookie的信息,还是需要自己手动取出返回的cookie,再添加到其他请求的头部信息里面

      本文标题:Kotlin网络库Fuel的设计之道

      本文链接:https://www.haomeiwen.com/subject/zdhikftx.html