Scala 喷洒反向代理:在客户端断开连接后继续传输数据

Scala 喷洒反向代理:在客户端断开连接后继续传输数据,scala,akka,spray,akka-http,Scala,Akka,Spray,Akka Http,我试图用Spray/Akka实现一个反向HTTP代理,但遇到了麻烦。我发现在某些情况下,即使在客户端断开连接后,我的代理服务器仍会继续从上游服务器接收数据 下面是我如何实现我的Spray proxy指令(只是对其进行了一点修改): 如果我在客户机和服务器之间放置一个代理层(即,客户机代理到服务器),那么这个反向代理将很好地工作,但是如果我在客户机和服务器之间放置两个代理层,它将有问题。例如,如果我有以下简单的Python HTTP服务器: import socket from threading

我试图用Spray/Akka实现一个反向HTTP代理,但遇到了麻烦。我发现在某些情况下,即使在客户端断开连接后,我的代理服务器仍会继续从上游服务器接收数据

下面是我如何实现我的Spray proxy指令(只是对其进行了一点修改):

如果我在客户机和服务器之间放置一个代理层(即,客户机代理到服务器),那么这个反向代理将很好地工作,但是如果我在客户机和服务器之间放置两个代理层,它将有问题。例如,如果我有以下简单的Python HTTP服务器:

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的连接由Spray
HttpManager
管理,我们的代码无法访问该连接。因此,我们无法对该连接执行任何操作,除非我们向
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
}