2

前言

我将通过这篇文章详述一下如何用 Swift 搭建一个 HTTP 代理服务器。本文将使用 Hummingbird 作为服务端的基本HTTP框架,以及使用 AsyncHTTPClient 作为 Swift 的 HTTP 客户端来请求目标服务。

什么是代理服务器

代理服务器是一个搭载在客户端和另一个服务端(后面我们成为目标服务端)的中间服务器,它从客户端转发消息到目标服务端,并且从目标服务端获取响应信息传回给客户端。在转发消息之前,它可以以某种方式处理这些消息,同样,它也可以处理返回的响应。

让我们试着构建一个

在本文中,我们将构建一个只将 HTTP 数据包转发到目标服务的代理服务器。您可以在这里找到本文的示例代码。

创建项目

我们使用 Hummingbird 模板项目 目前最低版本适配Swift5.5作为我们服务的初始模板。读者可以选择clone这个存储库,或者直接点击Github项目主页上 use this template 按钮来创建我们自己的存储库。用这个模板项目创建一个服务端并且启动它,可以使用一些控制台选项和文件来配置我们的应用。详见 here

增加 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
        )
    }
}

现在我们有了 HTTPClientHBProxyServerMiddleware 中间件,我们将它们加入配置文件 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"))
}

转换类型

当我们完成上面的步骤,构建会显示失败。因为我们还需要转换 HummingbirdAsyncHTTPClient 之间的请求和响应类型。同时我们需要合并目标服务的 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.ResponseHBResponse 的转换相当简单

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

我们现在将这两个转换步骤加入 HBProxyServerMiddlewareapply 函数中。同时加入一些日志打印信息

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 这里包含代码,但您可以在完整的示例代码中找到它。

示例代码添加

示例代码可能在上面的基础上做了部分修改。

  1. 默认绑定地址端口是 8081 而不是 8080。大多数 Hummingbird 示例在 8080 上运行,因此要在这些示例旁边使用代理,它需要绑定到不同的端口。
  2. 我添加了一个位置选项,它允许我们只转发来自特定基本 URL 的请求
  3. 我为目标和位置添加了命令行选项,因此可以在不重建应用程序的情况下更改这些选项
  4. 我删除了 host 标题或请求,以便可以用正确的值填写
  5. 如果提供了 content-length 标头,则在转换流请求时,我将其传递给 HTTPClient 流送器,以确保 content-length 为目标服务器的请求正确设置标头。

备择方案

我们可以使用 HummingbirdCore 代替 Hummingbird 作为代理服务器。这将提供一些额外的性能,因为它会删除额外的代码层,但会牺牲灵活性。添加任何额外的路由或中间件需要做更多的工作。我有只使用 HummingbirdCore 代理服务器的示例代码在这里

当然,另一种选择是使用 Vapor。我想在 Vapor 中的实现看起来与上面描述的非常相似,应该不会太难。不过我会把它留给别人。

关于我们

Swift社区是由 Swift 爱好者共同维护的公益组织,我们在国内以微信公众号的运营为主,我们会分享以 Swift实战SwiftUlSwift基础为核心的技术内容,也整理收集优秀的学习资料。

特别感谢 Swift社区 编辑部的每一位编辑,感谢大家的辛苦付出,为 Swift社区 提供优质内容,为 Swift 语言的发展贡献自己的力量。


Swift社区
11.9k 声望3.8k 粉丝

我们希望做一个最专业最权威的 Swift 中文社区,我们希望更多的人学习和使用Swift。我们会分享以 Swift 实战、SwiftUI、Swift 基础为核心的技术干货,欢迎您的关注与支持。