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

时间:2016-04-14 14:05:44

标签: scala akka spray akka-http

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

以下是我如何实现我的Spray代理指令(只是对bthuillier's implementation的一点修改):

trait ProxyDirectives {

  private def sending(f: RequestContext ⇒ HttpRequest)(implicit system: ActorSystem): Route = {
    val transport = IO(Http)(system)
    ctx ⇒ transport.tell(f(ctx), ctx.responder)
  }

  /**
    * Re-shape the original request, to match the destination server.
    */
  private def reShapeRequest(req: HttpRequest, uri: Uri): HttpRequest = {
    req.copy(
      uri = uri,
      headers = req.headers.map {
        case x: HttpHeaders.Host => HttpHeaders.Host(uri.authority.host.address, uri.authority.port)
        case x => x
      }
    )
  }

  /**
    * proxy the request to the specified uri
    *
    */
  def proxyTo(uri: Uri)(implicit system: ActorSystem): Route = {
    sending(ctx => reShapeRequest(ctx.request, uri))
  }
}

如果我在客户端和服务器之间放置一个代理层(即客户端< - > proxyTo< - >服务器),这个反向代理将很好用,但是如果我放两个层就会有问题客户端和服务器之间。例如,如果我有以下简单的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)
}

使用curl http://localhost:8082连接到代理服务器,即使在curl被杀死后你也会看到Akka系统继续传输数据(你可以打开DEBUG级别的日志来查看详细信息)。

我该如何处理这个问题?感谢。

1 个答案:

答案 0 :(得分:0)

嗯,事实证明这是一个非常复杂的问题,而我的解决方案需要近100行代码。

实际上,当我堆叠两层代理时,问题不仅存在。当我使用单层代理时,问题确实存在,但没有打印日志,所以我之前没有发现过这个问题。

关键问题是,虽然我们使用 import UIKit class ViewController: UIViewController { @IBOutlet weak var textfield1: UITextField! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { var DestViewController : ViewTwo = segue.destinationViewController as ViewTwo // here is where I get the error } ,但它实际上是来自spray-can的主机级API。主机级API的连接由Spray IO(Http) ! HttpRequest管理,我们的代码无法访问它们。因此,我们无法对该连接执行任何操作,除非我们向HttpManager发送Http.CloseAll,这将导致所有上游连接关闭。

(如果有人知道如何从IO(Http)获取连接,请告诉我。)

我们必须使用spray-can的连接级API来满足这种情况。所以我想出了类似的东西:

HttpManager

代码很长,我怀疑应该有一些更干净和简单的实现(实际上我不熟悉Akka)。不过,这段代码有效,所以我把这个解决方案放在这里。如果您找到了更好的解决方案,可以自由发布解决方案。