前言
我将通过这篇文章详述一下如何用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"),
],
然后在目标依赖也添加一下
代码语言:javascript复制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 中的实现看起来与上面描述的非常相似,应该不会太难。不过我会把它留给别人。
关于我们
Swift社区是由 Swift 爱好者共同维护的公益组织,我们在国内以微信公众号的运营为主,我们会分享以 Swift实战、SwiftUl、Swift基础为核心的技术内容,也整理收集优秀的学习资料。
特别感谢 Swift社区 编辑部的每一位编辑,感谢大家的辛苦付出,为 Swift社区 提供优质内容,为 Swift 语言的发展贡献自己的力量,排名不分先后:张安宇@微软[8]、戴铭@快手[9]、展菲@ESP[10]、倪瑶@Trip.com[11]、杜鑫瑶@新浪[12]、韦弦@Gwell[13]、张浩@讯飞[14]
参考资料
[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
[8]张安宇: https://blog.csdn.net/mobanchengshuang
[9]戴铭: https://ming1016.github.io
[10]展菲: https://github.com/fanbaoying
[11]倪瑶: https://github.com/niyaoyao
[12]杜鑫瑶: https://weibo.com/u/3878455011
[13]韦弦: https://www.jianshu.com/u/855d6ea2b3d1
[14]张浩: https://github.com/zhanghao19920218
- EOF -