2016-04-14 35 views
0

我试图用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连接到代理服务器,你会看到阿卡系统保持数据传输卷曲已被杀害即使(你可以打开详细的DEBUG级别的日志)。

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

回答

0

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

实际上,这个问题不仅存在于我堆叠两层代理的时候。当我使用一个图层代理时,问题确实存在,但没有打印日志,所以我以前没有意识到这个问题。

关键问题是,虽然我们使用IO(Http) ! HttpRequest,但它实际上是spray-can的主机级API。主机级API的连接由Spray HttpManager管理,我们的代码无法访问它。因此,除非我们发送Http.CloseAllIO(Http),这将导致所有上游连接关闭,所以我们无法处理该连接。

(如果有人知道如何从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 
} 

的代码是有点长,我怀疑应该有一些更干净和简单的实现(其实我不熟悉的阿卡)。不过,这段代码很有用,所以我在这里提供了这个解决方案如果你找到了更好的解决方案,你可以自由发布你的解决方案。

相关问题