使用 Swift 搭建一个 HTTP 代理

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

前言

我将通过这篇文章详述一下如何用 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 中的实现看起来与上面描述的非常相似,应该不会太难。不过我会把它留给别人。

原文转载: Optical Aberration

关于我们

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

欢迎关注公众号:Swift社区,后台点击进群,可以进入我们社区的各种交流讨论群。希望我们 Swift社区 是大家在网络空间中的另一份共同的归属。

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

目录
相关文章
|
5月前
|
安全 网络协议 网络安全
IP代理的三大协议:HTTP、HTTPS与SOCKS5的区别
**HTTP代理**适用于基本网页浏览,简单但不安全;**HTTPS代理**提供加密,适合保护隐私;**SOCKS5代理**灵活强大,支持TCP/UDP及认证,适用于绕过限制。选择代理协议应考虑安全、效率及匿名需求。
|
5月前
|
Web App开发 缓存 JavaScript
使用TypeScript创建高效HTTP代理请求
使用TypeScript创建高效HTTP代理请求
|
3月前
|
缓存 负载均衡 安全
|
3月前
|
安全 Java Shell
【内网—内网转发】——http协议代理转发_reGeorg代理转发
【内网—内网转发】——http协议代理转发_reGeorg代理转发
105 3
|
4月前
|
数据采集 缓存 安全
2024年最佳http 代理 IP选择及其价格分析
2024年,多家服务商如快代理、123Proxy、巨量代理、IPIDEA等提供不同类型的代理IP,以满足数据采集、跨境电商等多种需求。
2024年最佳http 代理 IP选择及其价格分析
|
4月前
|
数据采集 缓存 负载均衡
实测 | 芝麻代理,快代理、熊猫代理、豌豆代理HTTP代理质量测试
哈喽大家,欢迎来到本期知识分享!我们将探讨HTTP代理的质量分析方法,无论新手还是资深用户都能从中受益。首先介绍了HTTP代理的基本概念及其重要性。接着,我们通过两个关键指标——响应时间和可用性来评估代理质量。响应时间可通过`curl`命令测试并计算平均值;可用性则需设置定时任务持续检测,比如使用Python脚本。最后,通过具体案例分析了几家知名代理供应商的表现,其中青果网络在各项指标上表现突出,是进行数据采集等活动的优质选择。记得选择最适合自己的代理服务哦!
实测 | 芝麻代理,快代理、熊猫代理、豌豆代理HTTP代理质量测试
|
5月前
|
数据采集 安全 大数据
http代理一般受众于哪些人群?
HTTP代理主要适用于三类人群:数据采集专业人士,如网络爬虫开发者;网络兼职者,例如游戏试玩、电商优化者,利用代理IP提高工作效率;以及网络推广者,借助代理发布广告帖子以提升品牌知名度。代理提供安全、效率和稳定性支持。
79 3
http代理一般受众于哪些人群?
|
5月前
|
安全 网络安全 数据安全/隐私保护
http代理的带宽会受到什么因素的影响?
在数字化时代,网络安全与HTTP代理服务密切相关。代理服务器的硬件性能、安全机制,以及同时使用人数都会影响带宽。此外,IP质量、并发数也是决定带宽的关键因素。选择高性价比的HTTP代理服务商能确保更快的速度和更广泛的覆盖。HTTP代理在网络营销等领域中扮演重要角色,用户应根据具体需求选择合适的代理类型。
48 1
http代理的带宽会受到什么因素的影响?
|
5月前
|
数据采集
http代理IP能开展哪些应用?
在互联网时代,HTTP代理IP广泛应用于营销推广、排名优化和数据采集。对于营销,它帮助企业在社交媒体上切换IP以确保推广活动顺利进行;在排名优化中,代理IP避免了因同一IP多账号操作导致的账号暂停,提升企业产品曝光;在数据采集时,通过不断切换IP,提高数据抓取效率,确保任务的高效执行。
38 2
http代理IP能开展哪些应用?
|
5月前
|
数据采集 安全
短效http代理为什么比长效http代理用的人多?
在信息化时代,互联网广泛应用,http代理IP成为许多业务的必需。短效HTTP代理因其安全(及时切换保护用户信息)、广泛的应用场景(如数据采集、广告投放)、较低的成本和更高的访问效率,故使用人数超过长效HTTP代理。了解这些差异,有助于选择合适的代理服务。
56 1
短效http代理为什么比长效http代理用的人多?