使用 Swift 搭建一个 HTTP 代理

使用 Swift 搭建一个 HTTP 代理

[[440241]] 媒介

我将通过这篇著作胪陈一下怎样用Swift搭建一个HTTP代理劳动器。本文将使用Hummingbird[1]算作劳动端的基本HTTP框架,以及使用AsyncHTTPClient[2]算作Swift的HTTP客户端来申请目的劳动。

什么是代理劳动器

代理劳动器是一个搭载在客户端和另一个劳动端(背面咱们成为目的劳动端)的中间劳动器,它从客户端转发音书到目的劳动端,而况从目的劳动端获得反映信息传回给客户端。在转发音书之前,它不错以某种方式处理这些音书,通常,它也不错处理复返的反映。

让咱们试着构建一个

在本文中,咱们将构建一个只将HTTP数据包转发到目的劳动的代理劳动器。您不错在这里找到本文的示例代码。

创建模式

咱们使用Hummingbird模板模式[3] 咫尺最低版块适配 Swift5.5 算作咱们劳动的脱手模板。读者不错选用clone这个存储库,或者胜利点击Github模式主页上use this template按钮来创建咱们我方的存储库。用这个模板模式创建一个劳动端而况启动它,不错使用一些界限台选项和文献来成立咱们的运用。详见here[4]

加多 AsyncHTTPClient

咱们将把AsyncHTTPClient算作依赖加入Package.swift以便咱们背面来使用

dependencies: [     ...     .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.6.0"), ], 

然后在目的依赖也添加一下

targets: [     .executableTarget(name: "App",         dependencies: [             ...             .product(name: "AsyncHTTPClient", package: "async-http-client"),         ], 

咱们将把HTTPClient算作HBApplicatipn的扩张。这么通俗咱们照应HTTPClient的人命周期以及在HTTPClient删除前调用syncShutdown行为。

extension HBApplication {     var httpClient: HTTPClient {         get { self.extensions.get(\.httpClient) }         set { self.extensions.set(\.httpClient, value: newValue) { httpClient in             try httpClient.syncShutdown()         }}     } } 

当HBApplication关闭时辰会调用set内部的闭包。这意味着咱们当咱们援用了HBApplication,即使不使用HTTPClient,咱们也有权限去调用它

加多 middleware[中间件]

咱们将把咱们的代理劳动器算作中间件。中间件将获得一个申请,然后将它发送到目的劳动器而况从目的劳动器获得反映信息。底下使咱们脱手版块的中间件,它需要HTTPClient和目的劳动器的URL两个参数。

struct HBProxyServerMiddleware: HBMiddleware {     let httpClient: HTTPClient     let target: String      func apply(to request: HBRequest, next: HBResponder) -> EventLoopFuture<HBResponse> {         return httpClient.execute(             request: request,             eventLoop: .delegateAndChannel(on: request.eventLoop),             logger: request.logger         )     } } 

咫尺咱们有了HTTPClient和HBProxyServerMiddleware中间件,咱们将它们加入成立文献HBApplication.configure。然后竖立咱们代理劳动地址为http://httpbin.org

func configure(_ args: AppArguments) throws {     self.httpClient = HTTPClient(eventLoopGroupProvider: .shared(self.eventLoopGroup))     self.middleware.add(HBProxyServerMiddleware(httpClient: self.httpClient, target: "http://httpbin.org")) } 
调治类型

当咱们完成上头的体式,构建会表示失败。因为咱们还需要调治Hummingbird和AsyncHTTPClient之间的请乞降反映类型。同期咱们需要湮灭目的劳动的URL到申请里。

申请调治

为了将Hummingbird HBRequest飘浮为AsyncHTTPClient HTTPClient.Request,

原因: 咱们领先需要整理可能仍在加载的HBRequest的body信息,调治经过是异步的

处理决议:是以它需要复返一个包含背面调治扫尾的EventLoopFuture,让咱们将调治函数放到HBRequest内部

extension HBRequest {     func ahcRequest(host: String) -> EventLoopFuture<HTTPClient.Request> {         // consume request body and then construct AHC Request once we have the         // result. The URL for the request is the target server plus the URI from         // the `HBRequest`.         return self.body.consumeBody(on: self.eventLoop).flatMapThrowing { buffer in             return try HTTPClient.Request(                 url: host + self.uri.description,                 method: self.method,                 headers: self.headers,                 body: buffer.map { .byteBuffer($0) }             )         }     } } 
反映信息装换

从HTTPClient.Response到HBResponse的调治尽头简略

extension HTTPClient.Response {     var hbResponse: HBResponse {         return .init(             status: self.status,             headers: self.headers,             body: self.body.map { HBResponseBody.byteBuffer($0) } ?? .empty         )     } } 

咱们咫尺将这两个调治体式加入HBProxyServerMiddleware的apply函数中。同期加入一些日记打印信息

func apply(to request: HBRequest, next: HBResponder) -> EventLoopFuture<HBResponse> {     // log request     request.logger.info("Forwarding \(request.uri.path)")     // convert to HTTPClient.Request, execute, convert to HBResponse     return request.ahcRequest(host: target).flatMap { ahcRequest in         httpClient.execute(             request: ahcRequest,             eventLoop: .delegateAndChannel(on: request.eventLoop),             logger: request.logger         )     }.map { response in         return response.hbResponse     } } 

咫尺应该不错平日编译了。中间件将整理HBRequest的申请体,将它飘浮为HTTPRequest.Request,然后使用HTTPClient将申请转发给目的劳动器。获得的反映信息会飘浮为HBResponse复返给运用。

运走运用,开放网页开放localhost:8080。咱们应该能看到咱们之前竖立代理的httpbin.org网页信息

Streaming[流]

上头的竖立不是相等理思。它会恭候申请竣工加载,然后才将申请转发给目的劳动端。同理反映转发亦然需要恭候反映竣工加载后才会转发。这裁汰了音书发送的成果,通常会导致申请占用多数内存或者反映信息很大。

咱们不错通过流式传输请乞降反映负载来校正这少量。一朝咱们有了它的头部,就脱手将申请发送到目的劳动,并在接受到主体部分时对其进行流式处理。雷同地,一朝咱们有了它的头,在另一个场所脱手发送反映。舍弃对完满申请或反映的恭候将训导代理劳动器的性能。

若是客户端和代理之间的通讯以及代理和目的劳动之间的通讯以不同的速率运行,咱们仍然会遭受内存问题。若是咱们接受数据的速率比处理数据的速率快,数据就会脱手备份。为了幸免这种情况发生,咱们需要或者施加背压以罢手读取稀疏的数据,直到咱们处理了实足多的内存中的数据。有了这个,咱们不错将代理使用的内存量保抓在最低截止。

流式申请

流式传输申请负载是一个尽头简略的经过。本色上,它简化了构造 HTTPClient.Request 的经过因为咱们不需要恭候申请竣工加载。咱们怎样构造 HTTPClient.Request 主体将基于完满的 HBRequest 是否照旧在内存中。若是咱们复返流申请,则会自动运用背压,因为 Hummingbird 劳动器框架会为咱们实行此操作。

func ahcRequest(host: String, eventLoop: EventLoop) throws -> HTTPClient.Request {     let body: HTTPClient.Body?      switch self.body {     case .byteBuffer(let buffer):         body = buffer.map { .byteBuffer($0) }     case .stream(let stream):         body = .stream { writer in             // as we consume buffers from `HBRequest` we write them to             // the `HTTPClient.Request`.             return stream.consumeAll(on: eventLoop) { byteBuffer in                 writer.write(.byteBuffer(byteBuffer))             }         }     }     return try HTTPClient.Request(         url: host + self.uri.description,         method: self.method,         headers: self.headers,         body: body     ) } 
流式反映

流式反映需要一个投诚 HTTPClientResponseDelegate 的class. 这将在 HTTPClient 反映可用时立即从反映中接受数据。反映正文是 ByteBuffers 体式. 咱们不错将这些 ByteBuffers 提供给 HBByteBufferStreamer. 咱们答复的 HBResponse 是由这些流构造,而不是静态的 ByteBuffer。

若是咱们将申请流与反映流代码集结起来,咱们的最终的 apply 函数应该是这么的

func apply(to request: HBRequest, next: HBResponder) -> EventLoopFuture<HBResponse> {     do {         request.logger.info("Forwarding \(request.uri.path)")         // create request         let ahcRequest = try request.ahcRequest(host: target, eventLoop: request.eventLoop)         // create response body streamer. maxSize is the maximum size of object it can process         // maxStreamingBufferSize is the maximum size of data the streamer is allowed to have         // in memory at any one time         let streamer = HBByteBufferStreamer(eventLoop: request.eventLoop, maxSize: 2048*1024, maxStreamingBufferSize: 128*1024)         // HTTPClientResponseDelegate for streaming bytebuffers from AsyncHTTPClient         let delegate = StreamingResponseDelegate(on: request.eventLoop, streamer: streamer)         // execute request         _ = httpClient.execute(             request: ahcRequest,             delegate: delegate,             eventLoop: .delegateAndChannel(on: request.eventLoop),             logger: request.logger         )         // when delegate receives head then signal completion         return delegate.responsePromise.futureResult     } catch {         return request.failure(error)     } } 

你会可贵到在上头的代码中咱们不恭候httpClient.execute. 这是因为若是咱们这么作念了,该函数将在赓续之前恭候系数反映主体在内存中。咱们但愿立即处理反映,因此咱们向交付添加了一个promise: 一朝咱们收到头部信息,就明白过保存头部确定和流到HBResponse来终了。EventLoopFuture这个 promise的是咱们从apply函数传回的。

我莫得在StreamingResponseDelegate这里包含代码,但您不错在完满的示例代码中[5]找到它。

示例代码添加

该示例代码[6]可能在上头的基础上作念了部分修改。

默许绑定地址端口是 8081 而不是 8080。大多数 Hummingbird 示例在 8080 上运行,因此要在这些示例傍边使用代理,它需要绑定到不同的端口。 我添加了一个位置选项,它允许咱们只转发来自特定基本 URL 的申请 我为目的和位置添加了号召行选项,因此不错在不重建运用圭臬的情况下改造这些选项 我删除了 host 标题或申请,以便不错用正确的值填写 若是提供了 content-length 标头,则在调治流申请时,我将其传递给 HTTPClient 流送器,以确保 content-length 为目的劳动器的申请正确竖立标头。 备择决议

咱们不错使用 HummingbirdCore 代替 Hummingbird 算作代理劳动器。这将提供一些稀疏的性能,因为它会删除稀疏的代码层,但会糟跶生动性。添加任何稀疏的路由或中间件需要作念更多的责任。我有只使用HummingbirdCore代理劳动器的示例代码在这里[7]。

虽然,另一种选用是使用 Vapor。我思在 Vapor 中的终了看起来与上头描画的相等相似,应该不会太难。不外我会把它留给别东说念主。

参考贵府

[1]Hummingbird: https://github.com/hummingbird-project/hummingbird

[2]AsyncHTTPClient: https://github.com/swift-server/async-http-client

[3]Hummingbird模板模式: https://github.com/hummingbird-project/template

[4]here: https://opticalaberration.com/2021/12/hummingbird-template.html

[5]示例代码中: https://github.com/hummingbird-project/hummingbird-examples/blob/main/proxy-server/Sources/App/Middleware/StreamingResponseDelegate.swift

[6]示例代码: https://github.com/hummingbird-project/hummingbird-examples/tree/main/proxy-server

[7]在这里: https://github.com/hummingbird-project/hummingbird-examples/tree/main/proxy-server-core