Scala 喷洒反向代理:在客户端断开连接后继续传输数据
我试图用Spray/Akka实现一个反向HTTP代理,但遇到了麻烦。我发现在某些情况下,即使在客户端断开连接后,我的代理服务器仍会继续从上游服务器接收数据 下面是我如何实现我的Spray proxy指令(只是对其进行了一点修改): 如果我在客户机和服务器之间放置一个代理层(即,客户机代理到服务器),那么这个反向代理将很好地工作,但是如果我在客户机和服务器之间放置两个代理层,它将有问题。例如,如果我有以下简单的Python HTTP服务器:Scala 喷洒反向代理:在客户端断开连接后继续传输数据,scala,akka,spray,akka-http,Scala,Akka,Spray,Akka Http,我试图用Spray/Akka实现一个反向HTTP代理,但遇到了麻烦。我发现在某些情况下,即使在客户端断开连接后,我的代理服务器仍会继续从上游服务器接收数据 下面是我如何实现我的Spray proxy指令(只是对其进行了一点修改): 如果我在客户机和服务器之间放置一个代理层(即,客户机代理到服务器),那么这个反向代理将很好地工作,但是如果我在客户机和服务器之间放置两个代理层,它将有问题。例如,如果我有以下简单的Python HTTP服务器: import socket from threading
import socket
from threading import Thread, Semaphore
import time
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
from SocketServer import ThreadingMixIn
class MyHTTPHandler(BaseHTTPRequestHandler):
protocol_version = 'HTTP/1.1'
def do_GET(self):
self.send_response(200)
self.send_header('Transfer-Encoding', 'chunked')
self.end_headers()
for i in range(100):
data = ('%s\n' % i).encode('utf-8')
self.wfile.write(hex(len(data))[2:].encode('utf-8'))
self.wfile.write(b'\r\n')
self.wfile.write(data)
self.wfile.write(b'\r\n')
time.sleep(1)
self.wfile.write(b'0\r\n\r\n')
class MyServer(ThreadingMixIn, HTTPServer):
def server_bind(self):
HTTPServer.server_bind(self)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
def server_close(self):
HTTPServer.server_close(self)
if __name__ == '__main__':
server = MyServer(('127.0.0.1', 8080), MyHTTPHandler)
server.serve_forever()
它基本上什么也不做,只是打开一个分块的响应(用于长期运行,以便我们可以检查问题)。如果我以以下方式链接两层代理:
class TestActor(val target: String)(implicit val system: ActorSystem) extends Actor
with HttpService
with ProxyDirectives
{
// we use the enclosing ActorContext's or ActorSystem's dispatcher for our Futures and Scheduler
implicit private def executionContext = actorRefFactory.dispatcher
// the HttpService trait defines only one abstract member, which
// connects the services environment to the enclosing actor or test
def actorRefFactory = context
val serviceRoute: Route = {
get {
proxyTo(target)
}
}
// runs the service routes.
def receive = runRoute(serviceRoute) orElse handleTimeouts
private def handleTimeouts: Receive = {
case Timedout(x: HttpRequest) =>
sender ! HttpResponse(StatusCodes.InternalServerError, "Request timed out.")
}
}
object DebugMain extends App {
val actorName = "TestActor"
implicit val system = ActorSystem(actorName)
// create and start our service actor
val service = system.actorOf(
Props { new TestActor("http://127.0.0.1:8080") },
s"${actorName}Service"
)
val service2 = system.actorOf(
Props { new TestActor("http://127.0.0.1:8081") },
s"${actorName}2Service"
)
IO(Http) ! Http.Bind(service, "::0", port = 8081)
IO(Http) ! Http.Bind(service2, "::0", port = 8082)
}
使用curlhttp://localhost:8082
连接到代理服务器,您将看到Akka系统即使在curl被终止后仍在传输数据(您可以打开调试级别的日志查看详细信息)
我如何处理这个问题?谢谢。事实证明,这是一个非常复杂的问题,而我的解决方案需要将近100行代码 事实上,问题不仅仅存在于我堆叠两层代理时。当我使用单层代理时,问题确实存在,但是没有打印日志,所以我以前没有意识到这个问题 关键问题是当我们使用
IO(Http)!HttpRequest
,它实际上是spray can的主机级API。主机级API的连接由SprayHttpManager
管理,我们的代码无法访问该连接。因此,我们无法对该连接执行任何操作,除非我们向IO(Http)
发送Http.CloseAll
,这将导致所有上游连接关闭
(如果有人知道如何从HttpManager
获取连接,请告诉我)
我们必须使用spray can的连接级API来应对这种情况。所以我想出了这样的办法:
/**
* Proxy to upstream server, where the server response may be a long connection.
*
* @param uri Target URI, where to proxy to.
* @param system Akka actor system.
*/
def proxyToLongConnection(uri: Uri)(implicit system: ActorSystem): Route = {
val io = IO(Http)(system)
ctx => {
val request = reShapeRequest(ctx.request, uri)
// We've successfully opened a connection to upstream server, now start proxying data.
actorRefFactory.actorOf {
Props {
new Actor with ActorLogging {
private var upstream: ActorRef = null
private val upstreamClosed = new AtomicBoolean(false)
private val clientClosed = new AtomicBoolean(false)
private val contextStopped = new AtomicBoolean(false)
// Connect to the upstream server.
{
implicit val timeout = Timeout(FiniteDuration(10, TimeUnit.SECONDS))
io ! Http.Connect(
request.uri.authority.host.toString,
request.uri.effectivePort,
sslEncryption = request.uri.scheme == "https"
)
context.become(connecting)
}
def connecting: Receive = {
case _: Http.Connected =>
upstream = sender()
upstream ! request
context.unbecome() // Restore the context to [[receive]]
case Http.CommandFailed(Http.Connect(address, _, _, _, _)) =>
log.warning("Could not connect to {}", address)
complete(StatusCodes.GatewayTimeout)(ctx)
closeBothSide()
case x: Http.ConnectionClosed =>
closeBothSide()
}
override def receive: Receive = {
case x: HttpResponse =>
ctx.responder ! x.withAck(ContinueSend(0))
case x: ChunkedMessageEnd =>
ctx.responder ! x.withAck(ContinueSend(0))
case x: ContinueSend =>
closeBothSide()
case x: Failure =>
closeBothSide()
case x: Http.ConnectionClosed =>
closeBothSide()
case x =>
// Proxy everything else from server to the client.
ctx.responder ! x
}
private def closeBothSide(): Unit = {
if (upstream != null) {
if (!upstreamClosed.getAndSet(true)) {
upstream ! Http.Close
}
}
if (!clientClosed.getAndSet(true)) {
ctx.responder ! Http.Close
}
if (!contextStopped.getAndSet(true)) {
context.stop(self)
}
}
} // new Actor
} // Props
} // actorOf
} // (ctx: RequestContext) => Unit
}
代码有点长,我怀疑应该有更干净、更简单的实现(实际上我不熟悉Akka)。然而,这段代码是有效的,所以我把这个解决方案放在这里。如果您找到了更好的解决方案,您可以免费发布此问题的解决方案
/**
* Proxy to upstream server, where the server response may be a long connection.
*
* @param uri Target URI, where to proxy to.
* @param system Akka actor system.
*/
def proxyToLongConnection(uri: Uri)(implicit system: ActorSystem): Route = {
val io = IO(Http)(system)
ctx => {
val request = reShapeRequest(ctx.request, uri)
// We've successfully opened a connection to upstream server, now start proxying data.
actorRefFactory.actorOf {
Props {
new Actor with ActorLogging {
private var upstream: ActorRef = null
private val upstreamClosed = new AtomicBoolean(false)
private val clientClosed = new AtomicBoolean(false)
private val contextStopped = new AtomicBoolean(false)
// Connect to the upstream server.
{
implicit val timeout = Timeout(FiniteDuration(10, TimeUnit.SECONDS))
io ! Http.Connect(
request.uri.authority.host.toString,
request.uri.effectivePort,
sslEncryption = request.uri.scheme == "https"
)
context.become(connecting)
}
def connecting: Receive = {
case _: Http.Connected =>
upstream = sender()
upstream ! request
context.unbecome() // Restore the context to [[receive]]
case Http.CommandFailed(Http.Connect(address, _, _, _, _)) =>
log.warning("Could not connect to {}", address)
complete(StatusCodes.GatewayTimeout)(ctx)
closeBothSide()
case x: Http.ConnectionClosed =>
closeBothSide()
}
override def receive: Receive = {
case x: HttpResponse =>
ctx.responder ! x.withAck(ContinueSend(0))
case x: ChunkedMessageEnd =>
ctx.responder ! x.withAck(ContinueSend(0))
case x: ContinueSend =>
closeBothSide()
case x: Failure =>
closeBothSide()
case x: Http.ConnectionClosed =>
closeBothSide()
case x =>
// Proxy everything else from server to the client.
ctx.responder ! x
}
private def closeBothSide(): Unit = {
if (upstream != null) {
if (!upstreamClosed.getAndSet(true)) {
upstream ! Http.Close
}
}
if (!clientClosed.getAndSet(true)) {
ctx.responder ! Http.Close
}
if (!contextStopped.getAndSet(true)) {
context.stop(self)
}
}
} // new Actor
} // Props
} // actorOf
} // (ctx: RequestContext) => Unit
}