httpclient4cj:支持HTTP/2的高效HTTP客户端,具备连接池与响应缓存功能

一个高效的HTTP客户端

分支10Tags18
文件最后提交记录最后更新时间
1 年前
4 年前
1 年前
1 个月前
1 年前
1 个月前
3 年前
1 年前
1 个月前
1 个月前

Httpclient4cj

介绍

Httpclient4cj 是一个高效的 HTTP 客户端,代码参考:https://github.com/square/okhttp/tree/parent-3.14.9

特性

  • 🚀 支持HTTP/2,允许所有同一个主机地址的请求共享同一个 socket 连接

  • 🌍 连接池减少请求延时

  • 💪 缓存响应内容,避免一些完全重复的请求

路线

软件架构

架构图

源码目录

.
├── README.md
├── doc
│   ├── assets
│   ├── cjcov     
│   ├── feature_api.md  
│   └── design.md
└── src
  • doc是库的设计文档、提案、库的使用文档、LLT 覆盖报告(由于未支持服务端的对接,部分拦截器连接后处理逻辑暂时无法覆盖)
  • src是库源码目录

接口说明

主要是核心类和成员函数说明,详情见 API

编译执行

编译

两种编译方式

  1. 使用脚本编译
    1. 下载配置编译脚本
    2. ciTest build
  2. 使用包管理器编译
    1. 该三方库依赖stdx,请参考stdx文档配置CANGJIE_STDX_PATH环境变量
    2. cjpm build

示例

注意:以下示例请将url地址换成实际可用地址。

获取HttpClient对象并配置超时时间

import httpclient4cj
import httpclient4cj.*
import std.time.*

main() {
    let client: HttpClient = HttpClient.builder()
        .connectTimeout(Duration.second)
        .readTimeout(Duration.second)
        .writeTimeout(Duration.second)
        .build()
}

同步 GET 请求方法示例

import httpclient4cj
import httpclient4cj.*
import std.time.*
import stdx.encoding.url.*

main() {
    let url = URL.parse("http://127.0.0.1:50060")
    let client: HttpClient = HttpClient.builder()
        .connectTimeout(Duration.second)
        .readTimeout(Duration.second)
        .writeTimeout(Duration.second)
        .build()
    let request = Request.builder().url(url).build()
    let call = client.newCall(request)
    let response = call.execute()
    println(response.getBody().getOrThrow().getString())
}

同步 Post 发送json字符串

import httpclient4cj
import httpclient4cj.*
import std.time.*

main() {
    let client: HttpClient = HttpClient.builder()
        .connectTimeout(Duration.second)
        .readTimeout(Duration.second)
        .writeTimeout(Duration.second)
        .build()
    let request = Request.builder()
        .url("http://127.0.0.1:50060")
        .post(RealRequestBody.create(MediaType.get("application/json; charset=utf-8"), "{\"id\": \"123\"}"))
        .build()
    let response = client.newCall(request).execute()
    println(response.getBody().getOrThrow().getString())
}

同步 Post 发送文件内容

import httpclient4cj
import httpclient4cj.*
import std.time.*
import std.fs.*

main() {
    let client: HttpClient = HttpClient.builder()
        .connectTimeout(Duration.second)
        .readTimeout(Duration.second)
        .writeTimeout(Duration.second)
        .build()
    var arr: Array<Byte> = Array<Byte>(1024, {i => UInt8(i % 256)})
    File.writeTo("./body.txt", arr)
    let file = File("./body.txt", OpenOption.Open(true, false))
    let body = RealRequestBody.create(MediaType.get("text/plain"), file)
    let request: httpclient4cj.Request = httpclient4cj.Request.builder()
        .url("http://127.0.0.1:50060")
        .post(body)
        .build()
    let response: httpclient4cj.Response = client.newCall(request).execute()
    println(response.getBody().getOrThrow().getString())
    remove("./body.txt")
}

multipart 发送数据以及多上传文件

import httpclient4cj
import httpclient4cj.*
import std.time.*
import std.fs.*

main() {
    let client: HttpClient = HttpClient.builder()
        .connectTimeout(Duration.second)
        .readTimeout(Duration.second)
        .writeTimeout(Duration.second)
        .build()
    var arr: Array<Byte> = Array<Byte>(1024, {i => UInt8(i % 256)})
    File.writeTo("./body1.txt", arr)
    File.writeTo("./body2.txt", arr)
    let body1 = RealRequestBody.create(MediaType.get("text/plain"), File("./body1.txt", OpenOption.Open(true, false)))
    let body2 = RealRequestBody.create(MediaType.get("text/plain"), File("./body2.txt", OpenOption.Open(true, false)))
    let mu = MultipartBody.builder()
        .addFormDataPart("name", "huluwa")
        .addFormDataPart("age", "18")
        .addFormDataPart("file1", "body1.txt", body1)
        .addFormDataPart("file2", "body2.txt", body2)
        .build()
    let request = Request.builder().url("http://127.0.0.1:50060").post(mu).build()
    let response: httpclient4cj.Response = client.newCall(request).execute()
    println(response.getBody().getOrThrow().getString())
    remove("./body1.txt")
    remove("./body2.txt")
}

chunked分块传输请求

import httpclient4cj
import httpclient4cj.*
import std.time.*
import std.io.*

main() {
    let client: HttpClient = HttpClient.builder().build()
    let request = httpclient4cj.Request.builder().url("http://127.0.0.1:50060").post(MyRequestBody()).build()
    let response = client.newCall(request).execute()
    println(response.getBody().getOrThrow().getString())
}
class MyRequestBody <: RequestBody {
    public func contentType(): Option<MediaType> {
        return MediaType.parse("application/json")
    }
    public func getContentLength(): Int64 {
        // 声明的getContentLength -1,客户端拦截器默认添加"Transfer-Encoding", "chunked"头,使用ChunkedSink读取body
        return -1
    }
    public func writeTo(sink: Sink): Unit {
        // 每次调用write都会产生一个传输块
        sink.write("hello".toArray())
        sink.write("world".toArray())
    }
    public func get(): InputStream {
        return ByteBuffer()
    }
}

异步请求以及回调

import httpclient4cj
import httpclient4cj.*
import std.time.*

main() {
    let client: HttpClient = HttpClient.builder()
        .connectTimeout(Duration.second)
        .readTimeout(Duration.second)
        .writeTimeout(Duration.second)
        .build()
    let request = Request.builder()
        .url("http://127.0.0.1:50060")
        .header("User-Agent", "Httpclient4cj-Example")
        .build()
    for (i in 0..5) {
        let async = client.newCall(request)
        async.enqueue(MyCallbacks())
    }
    sleep(Duration.second)
}
public class MyCallbacks <: Callback {
    public func onFailure(call: Call, e: Exception): Unit {
        println("fail, ${e.message}")
    }
    public func onResponse(call: Call, response: Response): Unit {
        println(response)
    }
}

拦截器使用

import httpclient4cj
import httpclient4cj.*
import std.time.*

main() {
    let client: HttpClient = HttpClient.builder()
        .addNetworkInterceptor(MyInterceptor())
        .connectTimeout(Duration.second)
        .readTimeout(Duration.second)
        .writeTimeout(Duration.second)
        .build()
    let request = Request.builder().url("http://127.0.0.1:50060").build()
    let response: Response = client.newCall(request).execute()
    println(response.getBody().getOrThrow().getString())
    return 0
}
// 自定义拦截器打印route地址
class MyInterceptor <: Interceptor {
    public func intercept(chain: Chain): httpclient4cj.Response {
        let conn = chain.getConnection().getOrThrow()
        println("route = ${conn.getRoute().getInetSocketAddress()}")
        return chain.proceed(chain.getRequest())
    }
}

cookie管理

import stdx.encoding.url.*
import httpclient4cj
import httpclient4cj.*
import std.time.*

main() {
    let url: URL = URL.parse("http://127.0.0.1:50060")
    let jar = getSimpleCookieJar()
    let client: HttpClient = HttpClient.builder()
        .cookieJar(jar)
        .connectTimeout(Duration.second)
        .readTimeout(Duration.second)
        .writeTimeout(Duration.second)
        .build()
    let request = httpclient4cj.Request.builder().url(url).build()
    let call = client.newCall(request)
    let response: httpclient4cj.Response = call.execute()
    for (cookie in client.getCookieJar().loadForRequest(url)) {
        println(cookie.toSetCookieString())
    }
    return 0
}

监听器使用

import stdx.encoding.url.*
import httpclient4cj
import httpclient4cj.*
import stdx.net.tls.*
import std.collection.*
import std.net.*
import std.time.*

main() {
    let url = URL.parse("http://127.0.0.1:50060")
    let client: HttpClient = HttpClient.builder()
        .eventListener(MyEventListener())
        .connectTimeout(Duration.second)
        .readTimeout(Duration.second)
        .writeTimeout(Duration.second)
        .build()
    let request = Request.builder().url(url).build()
    let call = client.newCall(request)
    let response = call.execute()
    println(response.getBody().getOrThrow().getString())
    return 0
}
class MyEventListener <: EventListener {
    public func callStart(call: Call): Unit {
        println("callStart!!!")
    }
    public func callEnd(call: Call): Unit {
        println("callEnd!!!")
    }
    public func dnsStart(call: Call, domainName: String): Unit {
        println("dnsStart!!!")
    }
    public func dnsEnd(call: Call, domainName: String, allRoutes: ArrayList<Route>): Unit {
        println("dnsEnd!!!")
    }
    public func connectStart(call: Call, socketAddress: SocketAddress): Unit {
        println("connectStart!!!")
    }
    public func connectEnd(call: Call, socketAddress: SocketAddress, protocol: Protocol): Unit {
        println("connectEnd!!!")
    }
    public func secureConnectStart(call: Call): Unit {
        println("secureConnectStart!!!")
    }
    public func secureConnectEnd(call: Call, tlsConfig: TlsClientConfig): Unit {
        println("secureConnectEnd!!!")
    }
    public func connectFailed(call: Call, socketAddress: SocketAddress, protocol: Protocol, e: HttpException): Unit {
        println("connectFailed!!!")
    }
    public func requestHeadersStart(call: Call): Unit {
        println("requestHeadersStart!!!")
    }
    public func requestHeadersEnd(call: Call): Unit {
        println("requestHeadersEnd!!!")
    }
    public func requestBodyStart(call: Call): Unit {
        println("requestBodyStart!!!")
    }
    public func requestBodyEnd(call: Call, byteCount: Int64): Unit {
        println("requestBodyEnd!!!")
    }
    public func requestFailed(call: Call, e: HttpException): Unit {
        println("requestFailed!!!")
    }
    public func responseHeadersStart(call: Call): Unit {
        println("responseHeadersStart!!!")
    }
    public func responseHeadersEnd(call: Call, response: Response): Unit {
        println("responseHeadersEnd!!!")
    }
    public func responseBodyStart(call: Call): Unit {
        println("responseBodyStart!!!")
    }
    public func responseBodyEnd(call: Call, byteCount: Int64): Unit {
        println("responseBodyEnd!!!")
    }
    public func responseFailed(call: Call, e: HttpException): Unit {
        println("responseFailed!!!")
    }
}

使用http2协议

import stdx.encoding.url.*
import httpclient4cj
import httpclient4cj.*
import stdx.net.tls.*
import std.collection.*
import std.time.*

main() {
    let url = URL.parse("http://127.0.0.1:50060")
    // 需要用户自己配置CA证书验证公钥,如果不配置CA证书将不进行服务端证书校验,将使用不安全的连接
    // H2_PRIOR_KNOWLEDGE 使用明文http2协议, 不支持https
    // .protocols(ArrayList<String>([Protocol.get("H2_PRIOR_KNOWLEDGE").toString()]))
    // 使用apln协商协议,服务端响应支持的协议,list中前面的协议会被优先选择
    let client: HttpClient = HttpClient.builder()
        .tlsConfig(TlsClientConfig())
        .protocols(ArrayList<String>([Protocol.get("HTTP/2.0").toString(), Protocol.get("HTTP/1.1").toString()]))
        .connectTimeout(Duration.second)
        .readTimeout(Duration.second)
        .writeTimeout(Duration.second)
        .build()
    let request = Request.builder().url(url).build()
    let call = client.newCall(request)
    let response = call.execute()
    println(response)
    return 0
}

开源协议

本项目基于 Apache-2.0 license,请自由享受和参与开源

参与贡献

主要写参与贡献的人以及个人主页链接

欢迎给我们提交PR,欢迎给我们提交Issue,欢迎参与任何形式的贡献。

项目介绍

一个高效的HTTP客户端

定制我的领域