Skip to content

HTTP 客户端

HTTP 客户端是 Spire 框架的核心基础设施,提供了高性能、可扩展的 HTTP 通信功能。通过 HttpClientFactory 模式,有效管理 HTTP 请求处理和资源分配,提高应用程序的网络通信性能和可维护性。

快速启动

只需要以下 3 个步骤即可开始使用 HTTP 客户端:

cangjie
import spire_net_http.*
import spire_extensions_injection.*

main() {
    // 1. 创建并配置服务集合
    let services = ServiceCollection()
    services.addHttpClient<HttpClient, HttpClient>()

    // 2. 构建服务提供者并获取工厂
    let provider = services.build()
    let factory = provider.getOrThrow<IHttpClientFactory>()

    // 3. 创建客户端并发送请求
    let client = factory.createClient()
    let content = client.getString("https://httpbin.org/get")
    println(content)
}

HTTP 客户端

手动创建

注册默认客户端,可不使用依赖注入容器,最小化依赖:

cangjie
import spire_net_http.*

main() {

  let client = HttpClient()
  
  println(client.getString("https://httpbin.org/get"))
  
}

注意

生产环境不建议使用单例模式,而是配合容器使用,以防止内存泄漏和DNS缓存问题。

说明

框架内部的DNS缓存清除机制实现了同步DNS重定向,目标域名指向IP地址发生改变时,HTTP请求地址也会随之改变,无须重启服务或改写IP地址编码

基本使用

最常用的 GET 调用与多种响应读取方式:状态、字符串、字节、流

cangjie
import spire_net_http.*

main() {
    let client = HttpClient()

    // 基本 GET 请求
    let response = client.get("https://httpbin.org/get")
    println("Status: ${response.status}")
    println("Content: ${response.content.readAsString()}")

    // 获取字符串响应
    let content = client.getString("https://httpbin.org/json")
    println("String content: ${content}")

    // 获取字节数组响应
    let bytes = client.getByteArray("https://httpbin.org/bytes/100")
    println("Bytes length: ${bytes.size}")

    // 获取流响应
    let stream = client.getStream("https://httpbin.org/stream/3")
    println("Stream response received successfully")
}

内容类型

JsonContent

发送 JSON 正文,请务必设置 Content-Type: application/json

cangjie
import spire_net_http.*
import stdx.serialization.serialization.*

main() {
    let client = HttpClient()

    // POST JSON 
    let request = HttpRequestMessage(HttpMethod.Post, "http://jsonplaceholder.typicode.com/posts")
    
    // 构建一个需要实现序列化接口的数据模型
    let data = TokenModel(accessKey: "xxx", accessSecret: "xxx")
    request.content = JsonContent.create(data)

    let response = client.send(request)
    println("POST Status: ${response.status}")
    println("POST Response: ${response.content.readAsString()}")
}

说明

序列化数据模型实现方式查阅官方文档,或参考如下:

cangjie
public class TokenModel <: Serializable<TokenModel> {
    var accessKey: String = ""
    var accessSecret: String = ""

    public init(accessKey!: String, accessSecret!: String) {
        this.accessKey = accessKey
        this.accessSecret = accessSecret
    }
	
	// 必须实现的序列化方法
    public func serialize(): DataModel {
        return DataModelStruct().add(field<String>("accessKey", accessKey)).add(field<String>("accessSecret", accessSecret))
    }
	// 必须实现的反序列化方法
    public static func deserialize(dm: DataModel): TokenModel {
        let dms = match (dm) {
            case data: DataModelStruct => data
            case _ => throw UnsupportedException("Invalid data model")
        }

        let result = TokenModel(accessKey: "", accessSecret: "")
        result.accessKey = String.deserialize(dms.get("accessKey"))
        result.accessSecret = String.deserialize(dms.get("accessSecret"))
        return result
    }
}

FormUrlContent

发送表单数据,适用于简单字段提交

cangjie
import spire_net_http.*

main() {
    let client = HttpClient()

    // POST 表单数据
    let request = HttpRequestMessage(HttpMethod.Post, "https://httpbin.org/post")
    
    let data = [
        ("username", "john_doe"),
        ("password", "secret123"),
        ("remember", "true")
    ]

    request.content = FormUrlContent(data)
    
    let response = client.send(request)
    println("Form Status: ${response.status}")
}

请求管道(Handlers)

请求管道由多个处理器 (DelegatingHandler) 按顺序组成的单向链表组成。最后一个处理器HttpClientHandler负责实际发送请求。我们可以使用处理器来实现单一职责和代码复用。 比如我们可以定义处理器来专门处理授权或者错误。

执行流程

项目架构图

代码演示

我们可以通过实现DelegatingHandler抽象类来定义自定义处理器,例如:

代码示例:

cangjie
import spire_net_http.*

main() {
    // primaryHandler 是真实发请求的末端处理器
    let primary = HttpClientHandler()

    // 创建请求管道
    let pipe = Middleware1(Middleware2(Middleware3(primary)))

    let client = HttpClient(pipe, false)

    let response = client.get("http://httpbin.org/get")
    println("Done: ${response.status}")
}

// 处理器1
public class Middleware1 <: DelegatingHandler {
    init(next: HttpMessageHandler) {
        super(next)
    }

    protected override func send(request: HttpRequestMessage): HttpResponseMessage {
        println("middleware1 start")
        let response = super.send(request)
        println("middleware1 endle")
        return response
    }
}

// 处理器2
public class Middleware2 <: DelegatingHandler {
    init(next: HttpMessageHandler) {
        super(next)
    }

    protected override func send(request: HttpRequestMessage): HttpResponseMessage {
        println("middleware2 start")
        let response = super.send(request)
        println("middleware2 endle")
        return response
    }
}

// 处理器3
public class Middleware3 <: DelegatingHandler {
    init(next: HttpMessageHandler) {
        super(next)
    }

    protected override func send(request: HttpRequestMessage): HttpResponseMessage {
        println("middleware3 start")
        let response = super.send(request)
        println("middleware3 endle")
        return response
    }
}

容器集成

我们通过与容器集成来实现依赖注入的同时,还可以更加便捷的构建请求管道和设置请求管道的生命周期,来避免内存泄漏和DNS缓存失效问题。

注册和解析

利用http客户端工厂将服务集合中的配置好的客户端实例化,工厂模式下创建的默认客户端是互相独立的实例

cangjie
import spire_net_http.*
import spire_extensions_injection.*

main() {
    let services = ServiceCollection()
    
    // 注册 HTTP 客户端
    services.addHttpClientCore()
    
    let provider = services.build()
    let factory = provider.getOrThrow<IHttpClientFactory>()
    
    // 使用工厂创建客户端
    let client1 = factory.createClient()
   
    // 直接解析
    let client2 = provider.getOrThrow<HttpClient>()

}

非命名客户端

cangjie
import spire_net_http.*
import spire_extensions_injection.*

main() {
    let services = ServiceCollection()
    
    // 注册非命名客户端核心与处理器
    services.addHttpClientCore()
    services.addTransient<LoggingHandler, LoggingHandler>()

    // 配置非命名客户端
    services.configureHttpClientDefaults { builder =>
        // 配置生命周期
        builder.setHandlerLifetime(Duration.minute * 2)
        builder.addHttpMessageHandler<LoggingHandler>()  
    }
    
    let provider = services.build()
    let factory = provider.getOrThrow<IHttpClientFactory>()
    
    // 创建非命名客户端
    let client = factory.createClient()
}

class LoggingHandler <: DelegatingHandler {
    protected override func send(request: HttpRequestMessage): HttpResponseMessage {
        println("Request: ${request.method} ${request.requestUri}")
        let response = super.send(request)
        println("Response: ${response.status}")
        return response
    }
}

注意

非命名客户端共享同一个请求管道和配置

命名客户端

命名客户端非常适合多租户的场景下使用

cangjie
import spire_extensions_injection.*

main() {
    let services = ServiceCollection()
    
    // 配置命名客户端(返回 HttpClientBuilder 再配置)
    services.addHttpClient("tenant")
        .setHandlerLifetime(Duration.minute * 2)
    
    let provider = services.build()
    let factory = provider.getOrThrow<IHttpClientFactory>()
    
    // 创建tenant客户端
    let client = factory.createClient("tenant")
}

注意

命名客户端与非命名客户端之间的区别是:命名客户端拥有独立的请求管道和配置

类型化客户端

按领域封装 HttpClient,对外暴露清晰的业务方法

cangjie
import spire_extensions_injection.*
import spire_net_http.*
import stdx.encoding.url.*

main() {
    let services = ServiceCollection()

    // 使用命名客户端配置管道,名称为第一个泛型参数的完全限定名
    services.addHttpClient<OpenAiClient, OpenAiClient>()
        .configureHttpClient { client =>
            client.baseAddress = URL.parse("https://api.siliconflow.cn/v1")
        }

    let provider = services.build()
   
    let client = provider.getOrThrow<OpenAiClient>()
    let response = client.chat()
    println("Response: ${response.status}")
    println("Response: ${response.content.readAsString()}")
    return 0
}

// 类型化客户端:封装 HttpClient
class OpenAiClient {
    public OpenAiClient(let client: HttpClient) {  }

    public func chat(): HttpResponseMessage {
        let content = StringContent("Hello, This is Spire!")
        return client.post("/chat/completions", content)
    }
}

请求管道

cangjie
import spire_net_http.*
import spire_extensions_injection.*

main() {
    let services = ServiceCollection()

    // 注册处理器
    services.addTransient<Logging1DelegatingHandler, Logging1DelegatingHandler>()
    services.addTransient<Logging2DelegatingHandler, Logging2DelegatingHandler>()

    // 配置请求管道
    services.configureHttpClientDefaults {builder => builder
        .addHttpMessageHandler<Logging1DelegatingHandler>()
        .addHttpMessageHandler<Logging2DelegatingHandler>()
    }
    
    let provider = services.build()
    let factory = provider.getOrThrow<IHttpClientFactory>()
    let client = factory.createClient()
}

class Logging1DelegatingHandler <: DelegatingHandler {
    public override func send(request: HttpRequestMessage) {
        //println("Logging1:start...")
        let response = super.send(request)
        //println("Logging1:end...")
        return response
    }
}

class Logging2DelegatingHandler <: DelegatingHandler {
    public override func send(request: HttpRequestMessage) {
        //println("Logging2:start...")
        let response = super.send(request)
        //println("Logging2:end...")
        return response
    }
}

生命周期

cangjie
import std.runtime.*
import spire_extensions_injection.*
import spire_net_http.*

main() {
    let services = ServiceCollection()
    services.addTransient<LifetimeHandler, LifetimeHandler>()
    
    services.addHttpClient("short-lived")
        .addHttpMessageHandler<LifetimeHandler>()
        .setHandlerLifetime(Duration.second * 5)
    
    let provider = services.build()
    let factory = provider.getOrThrow<IHttpClientFactory>()

    // 弃元通知gc尽快回收    
    let _ = factory.createClient("short-lived")
    gc()
    // 等待生命周期过期
    sleep(Duration.second * 10)
}

class LifetimeHandler <: DelegatingHandler {
    protected override func send(request: HttpRequestMessage): HttpResponseMessage {
        return super.send(request)
    }

    protected override func close(closing: Bool) {
        println("'LifetimeHandler' closed")
        super.close(closing)
    }
}

回收机制

当客户端过期,对其进行标记而不销毁对象,回调外部处理函数将过期客户端从激活队列移出,封装为可跟踪状态的 ExpiredHandlerTrackingEntry(带 WeakRef),并移入过期队列,等待清理程序处理

清理程序会定时检查循环清理,每 5 秒触发一次清理操作

注意

  • 框架通过 WeakRef“观察”对象是否已无强引用;真正的内存回收由运行时 GC 决定。

  • 到期只是进入“过期队列”,不是立即销毁;销毁前提是 WeakRef 指向对象已被 GC 判定为可回收(无强引用)。

最佳实践

性能优化建议

  • 正确设置生命周期:setHandlerLifetime 在复用与刷新间权衡;过短增加建链开销,过长可能滞留资源
  • 避免持有多余强引用:不把 HttpClient 缓存到全局静态结构,交由 DI/工厂管理,利于过期与清理
  • 异步并发要收集句柄:spawn 后收集 Future 并在末尾 get(),确保任务完成与异常可见

架构设计

  • 使用命名客户端隔离配置:不同后端/租户/功能使用不同名称,配置独立(基址、处理器、超时、生命周期)
  • 类型化客户端封装领域 API:业务代码依赖服务(类型化客户端)而非裸 HttpClient,便于测试与复用
  • 统一默认配置再按需覆盖:用 configureHttpClientDefaults 设定全局默认;命名客户端按需覆盖,避免重复
  • 处理器用 DI 注册:自定义 DelegatingHandler 先注册到容器,再通过 addHttpMessageHandler<T>() 引入

开发建议

  • 基址与相对路径:相对路径需配置 baseAddress;传字符串 URL 时请用完整 URL,避免解析差异