2016-04-14 13 views
0

私はSpray/AkkaでリバースHTTPプロキシを実装しようとしていますが、問題が発生します。クライアントが切断された後でも、プロキシサーバーはアップストリームサーバーからデータを受信し続けることがあることがわかっています。スプレイリバースプロキシ:クライアントが切断された後にデータを転送し続ける

は、ここで私は私のスプレープロキシディレクティブ(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 < - > server)ですが、クライアントとサーバーの間に2つのレイヤーを置くと問題が発生します。たとえば、私は次のような単純な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行のコードを必要としますが、これは非常に複雑な問題です。

実際には、プロキシの2つのレイヤーを積み重ねているときに問題が存在するだけではありません。 1つのレイヤープロキシを使用しているときに問題は存在しますが、ログは出力されませんので、以前はこの問題を認識していませんでした。

重要な問題は、IO(Http) ! HttpRequestを使用している間は、実際にスプレー缶のホストレベルのAPIであることです。ホストレベルのAPIの接続は、コードでアクセスできないスプレーHttpManagerによって管理されます。したがって、Http.CloseAllIO(Http)に送信しないと、すべてのアップストリーム接続が閉じられます。

(接続方法を知っている人は、HttpManagerから教えてください)。

この状況では、スプレー缶の接続レベルの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 
} 

コードは少し長いです、と私はいくつかのよりクリーンでシンプルな実装(実際に私はアッカに慣れていないよ)があるはずです疑います。それにもかかわらず、このコードは機能するので、私はこの解決策をここに入れます。いくつかの問題が見つかった場合は、この問題に自由にソリューションを投稿することができます。

関連する問題