OkHttp之OkCurl模块分析

64 阅读4分钟

OkCurl分析模块

模块概述

OkCurl是一个基于OkHttp的curl克隆工具,使用Kotlin编写。它提供了类似curl的命令行接口,但使用OkHttp作为底层HTTP客户端,从而能够利用OkHttp的HTTP引擎(包括HTTP/2)来测试Web服务器。

项目结构

OkCurl项目的主要代码结构如下:

okcurl/
├── src/
│   ├── main/
│   │   └── kotlin/
│   │       └── okhttp3/
│   │           └── curl/
│   │               ├── logging/
│   │               │   ├── LoggingUtil.kt       # 日志配置工具
│   │               │   ├── MessageFormatter.kt  # 消息格式化器
│   │               │   └── OneLineLogFormat.kt  # 单行日志格式
│   │               ├── internal/
│   │               │   └── -MainCommon.kt       # Main类的通用方法实现
│   │               ├── Main.kt                  # 主类,处理命令行参数和执行请求
│   │               └── MainCommandLine.kt       # 程序入口点
│   └── test/
│       └── kotlin/
│           └── okhttp3/
│               └── curl/
│                   └── MainTest.kt              # Main类的单元测试
└── build.gradle.kts                             # 构建配置文件

核心功能

OkCurl提供了以下核心功能:

  1. 命令行参数处理:支持类似curl的命令行选项,如:

    • -X:指定HTTP方法(GET、POST、PUT等)
    • -d:指定POST数据
    • -H:添加HTTP请求头
    • -A:设置User-Agent
    • -e:设置Referer
    • 以及其他超时、重定向、SSL设置等选项
  2. HTTP请求执行

    • 创建和配置OkHttpClient
    • 构建HTTP请求
    • 执行请求并处理响应
    • 输出响应状态、响应头和响应体
  3. 日志记录

    • 支持详细的HTTP请求和响应日志
    • 支持HTTP/2帧的日志
    • 支持SSL调试日志
  4. 安全选项

    • 支持配置SSL证书验证
    • 支持配置主机名验证

实现细节

命令行处理

OkCurl使用Clikt库处理命令行参数。在Main.kt中,各种命令行选项被定义为属性,并使用Clikt的DSL进行配置:

private val method by option("-X", "--request", help = "Specify request command to use")
private val data by option("-d", "--data", help = "HTTP POST data")
private val headers by option("-H", "--header", help = "Custom header to pass to server").multiple()
// 更多选项...

HTTP客户端创建

Main.ktcreateClient方法中,根据命令行选项创建和配置OkHttpClient:

fun createClient(): OkHttpClient {
  val builder = OkHttpClient.Builder()
  // 配置超时
  if (connectTimeout != null) {
    builder.connectTimeout(connectTimeout!!)
  }
  // 配置重定向
  if (followRedirects) {
    builder.followRedirects(true)
    builder.followSslRedirects(true)
  }
  // 配置SSL
  if (insecure) {
    builder.sslSocketFactory(createInsecureSslSocketFactory(), createInsecureTrustManager())
    builder.hostnameVerifier(createInsecureHostnameVerifier())
  }
  // 配置日志
  if (verbose) {
    val logging = HttpLoggingInterceptor(logger)
    logging.level = HttpLoggingInterceptor.Level.HEADERS
    builder.addNetworkInterceptor(logging)
  }
  return builder.build()
}

请求创建

-MainCommon.ktcommonCreateRequest方法中,根据命令行选项创建HTTP请求:

fun commonCreateRequest(): Request {
  // 创建请求构建器
  val requestBuilder = Request.Builder()
  // 设置URL
  requestBuilder.url(url!!)
  // 设置请求方法和请求体
  var requestMethod = method
  if (requestMethod == null) {
    requestMethod = if (data != null) "POST" else "GET"
  }
  // 设置请求头
  for (header in headers) {
    val parts = header.split(':', limit = 2)
    val name = parts[0].trim()
    val value = parts[1].trim()
    if (!isSpecialHeader(name)) {
      requestBuilder.header(name, value)
    }
  }
  // 设置特殊请求头
  if (referer != null) {
    requestBuilder.header("Referer", referer!!)
  }
  if (userAgent != null) {
    requestBuilder.header("User-Agent", userAgent!!)
  } else {
    requestBuilder.header("User-Agent", versionString())
  }
  // 创建请求体
  if (data != null) {
    val contentType = mediaType()
    val requestBody = RequestBody.create(contentType, data!!)
    requestBuilder.method(requestMethod, requestBody)
  } else {
    requestBuilder.method(requestMethod, null)
  }
  return requestBuilder.build()
}

请求执行和响应处理

-MainCommon.ktcommonRun方法中,执行请求并处理响应:

fun commonRun(): Int {
  // 创建HTTP客户端
  val client = createClient()
  // 创建请求
  val request = createRequest()
  // 执行请求
  val response = client.newCall(request).execute()
  // 处理响应
  if (showHeaders) {
    println("${response.protocol} ${response.code} ${response.message}")
    for ((name, value) in response.headers) {
      println("$name: $value")
    }
    println()
  }
  // 输出响应体
  response.body!!.source().use { source ->
    val sink = Okio.buffer(Okio.sink(System.out))
    sink.writeAll(source)
    sink.emit()
  }
  // 关闭资源
  response.body!!.close()
  client.close()
  return 0
}

日志记录

LoggingUtil.kt中,配置日志记录:

fun configureLogging(debug: Boolean, showHttp2Frames: Boolean, sslDebug: Boolean) {
  if (debug) {
    val rootLogger = getLogger("")
    rootLogger.level = Level.FINE
    val handler = ConsoleHandler()
    handler.formatter = OneLineLogFormat()
    handler.level = Level.FINE
    rootLogger.addHandler(handler)
  }
  if (showHttp2Frames) {
    val http2Logger = getLogger("okhttp3.internal.http2")
    http2Logger.level = Level.FINE
    val handler = ConsoleHandler()
    handler.formatter = MessageFormatter
    handler.level = Level.FINE
    http2Logger.addHandler(handler)
  }
  if (sslDebug) {
    val sslLogger = getLogger("javax.net.ssl")
    sslLogger.level = Level.FINE
    val handler = ConsoleHandler()
    handler.formatter = MessageFormatter
    handler.level = Level.FINE
    sslLogger.addHandler(handler)
  }
}

依赖关系

OkCurl项目的主要依赖包括:

  1. OkHttp:作为底层HTTP客户端
  2. Okio:用于I/O操作
  3. Clikt:用于命令行解析
  4. Kotlin标准库:Kotlin语言支持

构建配置

OkCurl使用Gradle构建系统,主要配置包括:

  1. 插件

    • Kotlin JVM插件
    • Dokka(Kotlin文档生成工具)
    • Maven发布插件
    • Shadow JAR插件(用于创建包含所有依赖的可执行JAR)
  2. 任务

    • 复制资源模板文件,并替换变量(如项目版本)
  3. JAR配置

    • 设置模块名为okhttp3.curl
    • 设置主类为okhttp3.curl.MainCommandLineKt
  4. GraalVM配置(如果Java版本>=11):

    • 配置本地镜像名称为okcurl
    • 设置主类为okhttp3.curl.MainCommandLineKt

测试方法

OkCurl使用JUnit和assertk库进行单元测试。测试用例包括:

  1. 简单GET请求:验证默认请求方法和URL
  2. PUT请求:验证PUT方法和请求体
  3. 带数据的POST请求:验证POST方法、Content-Type和请求体
  4. 带数据的PUT请求:验证PUT方法、Content-Type和请求体
  5. 带Content-Type头的请求:验证自定义Content-Type
  6. 带Referer头的请求:验证Referer头
  7. 带User-Agent头的请求:验证User-Agent头
  8. 默认User-Agent:验证默认User-Agent头
  9. 带日期格式的头:验证带日期格式的头

总结

OkCurl是一个功能完善的curl克隆工具,它利用OkHttp的强大功能提供了类似curl的命令行接口。它支持各种HTTP方法、请求头、请求体、超时设置、重定向、SSL设置等功能,并提供了详细的日志记录功能。通过使用Kotlin语言和现代库(如Clikt、OkHttp、Okio),OkCurl提供了一个简洁、高效的HTTP客户端工具。