Akka(37): Http:客戶端操作模式


   Akka-http的客戶端連接模式除Connection-Level和Host-Level之外還有一種非常便利的模式:Request-Level-Api。這種模式免除了連接Connection的概念,任何時候可以直接調用singleRequest來與服務端溝通。下面我們用幾個例子來示范singleRequest的用法:

  (for { response <- Http().singleRequest(HttpRequest(method=HttpMethods.GET,uri="http://localhost:8011/message")) message <- Unmarshal(response.entity).to[String] } yield message).andThen { case Success(msg) => println(s"Received message: $msg") case Failure(err) => println(s"Error: ${err.getMessage}") }.andThen {case _ => sys.terminate()}

這是一個GET操作:用Http().singleRequest直接把HttpRequest發送給服務端uri並獲取返回的HttpResponse。我們看到,整組函數的返回類型都是Future[?],所以用for-comprehension來把所有實際運算包嵌在Future運算模式內(context)。下面這個例子是客戶端上傳數據示范:

 (for { entity <- Marshal("Wata hell you doing?").to[RequestEntity] response <- Http().singleRequest(HttpRequest(method=HttpMethods.PUT,uri="http://localhost:8011/message",entity=entity)) message <- Unmarshal(response.entity).to[String] } yield message).andThen { case Success(msg) => println(s"Received message: $msg") case Failure(err) => println(s"Error: ${err.getMessage}") }.andThen {case _ => sys.terminate()}

以上是個PUT操作。我們需要先構建數據載體HttpEntity。格式轉換函數Marshal也返回Future[HttpEntity],所以也可以包含在for語句內。關注一下這個andThen,它可以連接一串多個monadic運算,在不影響上游運算結果的情況下實現一些副作用計算。值得注意的是上面這兩個例子雖然表現形式很簡潔,但我們無法對數據轉換過程中的異常及response的狀態碼等進行監控。所以我們應該把整個過程拆分成兩部分:先獲取response,再具體處理response,包括核對狀態,處理數據等:

  case class Item(id: Int, name: String, price: Double) def getItem(itemId: Int): Future[HttpResponse] = for { response <- Http().singleRequest(HttpRequest(method=HttpMethods.GET,uri = s"http://localhost:8011/item/$itemId")) } yield response def extractEntity[T](futResp: Future[HttpResponse])(implicit um: Unmarshaller[ResponseEntity,T]) = { futResp.andThen { case Success(HttpResponse(StatusCodes.OK, _, entity, _)) => Unmarshal(entity).to[T] .onComplete { case Success(t) => println(s"Got response entity: ${t}") case Failure(e) => println(s"Unmarshalling failed: ${e.getMessage}") } case Success(_) => println("Exception in response!") case Failure(err) => println(s"Response Failed: ${err.getMessage}") } } extractEntity[Item](getItem(13))

現在這個extractEntity[Item](getItem(13))可以實現全過程的監控管理了。用同樣的模式實現PUT操作:

  def putItem(item: Item): Future[HttpResponse] =
   for { reqEntity <- Marshal(item).to[RequestEntity] response <- Http().singleRequest(HttpRequest(method=HttpMethods.PUT,uri="http://localhost:8011/item",entity=reqEntity)) } yield response extractEntity[Item](putItem(Item(23,"Item#23", 46.0))) .andThen { case _ => sys.terminate()}

當然,我們還是使用了前面幾篇討論里的Marshalling方式來進行數據格式的自動轉換:

import de.heikoseeberger.akkahttpjson4s.Json4sSupport import org.json4s.jackson ... trait JsonCodec extends Json4sSupport { import org.json4s.DefaultFormats import org.json4s.ext.JodaTimeSerializers implicit val serilizer = jackson.Serialization implicit val formats = DefaultFormats ++ JodaTimeSerializers.all } object JsConverters extends JsonCodec ... import JsConverters._ implicit val jsonStreamingSupport = EntityStreamingSupport.json() .withParallelMarshalling(parallelism = 8, unordered = false)

如果我們需要對數據交換過程進行更細致的管控,用Host-Level-Api會更加適合。下面我們就針對Host-Level-Api構建一個客戶端的工具庫:

class PooledClient(host: String, port: Int, poolSettings: ConnectionPoolSettings) (implicit sys: ActorSystem, mat: ActorMaterializer) { import sys.dispatcher private val cnnPool: Flow[(HttpRequest, Int), (Try[HttpResponse], Int), Http.HostConnectionPool] = Http().cachedHostConnectionPool[Int](host = host, port = port, settings = poolSettings) //單一request
  def requestSingleResponse(req: HttpRequest): Future[HttpResponse] = { Source.single(req -> 1) .via(cnnPool) .runWith(Sink.head).flatMap { case (Success(resp), _) => Future.successful(resp) case (Failure(fail), _) => Future.failed(fail) } } //組串request
  def orderedResponses(reqs: Iterable[HttpRequest]): Future[Iterable[HttpResponse]] = { Source(reqs.zipWithIndex.toMap) .via(cnnPool) .runFold(SortedMap[Int, Future[HttpResponse]]()) { case (m, (Success(r), idx)) => m + (idx -> Future.successful(r)) case (m, (Failure(f), idx)) => m + (idx -> Future.failed(f)) }.flatMap { m => Future.sequence(m.values) } } }

下面是一種比較安全的模式:使用了queue來暫存request從而解決因發送方與接收方速率不同所產生的問題:

class QueuedRequestsClient(host: String, port: Int, poolSettings: ConnectionPoolSettings) (qsize: Int = 10, overflowStrategy: OverflowStrategy = OverflowStrategy.dropNew) (implicit sys: ActorSystem, mat: ActorMaterializer) { import sys.dispatcher private val cnnPool: Flow[(HttpRequest,Promise[HttpResponse]),(Try[HttpResponse],Promise[HttpResponse]),Http.HostConnectionPool] = Http().cachedHostConnectionPool[Promise[HttpResponse]](host=host,port=port,settings=poolSettings) val queue = Source.queue[(HttpRequest, Promise[HttpResponse])](qsize, overflowStrategy) .via(cnnPool) .to(Sink.foreach({ case ((Success(resp), p)) => p.success(resp) case ((Failure(e), p))    => p.failure(e) })).run() def queueRequest(request: HttpRequest): Future[HttpResponse] = { val responsePromise = Promise[HttpResponse]() queue.offer(request -> responsePromise).flatMap { case QueueOfferResult.Enqueued    => responsePromise.future case QueueOfferResult.Dropped     => Future.failed(new RuntimeException("Queue overflowed. Try again later.")) case QueueOfferResult.Failure(ex) => Future.failed(ex) case QueueOfferResult.QueueClosed => Future.failed(new RuntimeException("Queue was closed (pool shut down) while running the request. Try again later.")) } } }

下面是這些工具函數的具體使用示范:

  val settings = ConnectionPoolSettings(sys) .withMaxConnections(8) .withMaxOpenRequests(8) .withMaxRetries(3) .withPipeliningLimit(4) val pooledClient = new PooledClient("localhost",8011,settings) def getItemByPool(itemId: Int): Future[HttpResponse] = for { response <- pooledClient.requestSingleResponse(HttpRequest(method=HttpMethods.GET,uri = s"http://localhost:8011/item/$itemId")) } yield response extractEntity[Item](getItemByPool(13)) def getItemsByPool(itemIds: List[Int]): Future[Iterable[HttpResponse]] = { val reqs = itemIds.map { id => HttpRequest(method = HttpMethods.GET, uri = s"http://localhost:8011/item/$id") } val rets = (for { responses <- pooledClient.orderedResponses(reqs) } yield responses) rets } val futResps = getItemsByPool(List(3,5,7)) futResps.andThen { case Success(listOfResps) => { listOfResps.foreach { r => r match { case HttpResponse(StatusCodes.OK, _, entity, _) => Unmarshal(entity).to[Item] .onComplete { case Success(t) => println(s"Got response entity: ${t}") case Failure(e) => println(s"Unmarshalling failed: ${e.getMessage}") } case _ => println("Exception in response!") } } } case _ => println("Failed to get list of responses!") } val queuedClient = new QueuedRequestsClient("localhost",8011,settings)() def putItemByQueue(item: Item): Future[HttpResponse] =
    for { reqEntity <- Marshal(item).to[RequestEntity] response <- queuedClient.queueRequest(HttpRequest(method=HttpMethods.PUT,uri="http://localhost:8011/item",entity=reqEntity)) } yield response extractEntity[Item](putItemByQueue(Item(23,"Item#23", 46.0))) .andThen { case _ => sys.terminate()}

下面是本次討論的示范源代碼:

服務端代碼:

import akka.actor._ import akka.stream._ import akka.http.scaladsl.Http import akka.http.scaladsl.server.Directives._ import de.heikoseeberger.akkahttpjson4s.Json4sSupport import org.json4s.jackson trait JsonCodec extends Json4sSupport { import org.json4s.DefaultFormats import org.json4s.ext.JodaTimeSerializers implicit val serilizer = jackson.Serialization implicit val formats = DefaultFormats ++ JodaTimeSerializers.all } object JsConverters extends JsonCodec object TestServer extends App with JsonCodec { implicit val httpSys = ActorSystem("httpSystem") implicit val httpMat = ActorMaterializer() implicit val httpEC = httpSys.dispatcher import JsConverters._ case class Item(id: Int, name: String, price: Double) val messages = path("message") { get { complete("hello, how are you?") } ~ put { entity(as[String]) {msg => complete(msg) } } } val items = (path("item" / IntNumber) & get) { id =>
       get { complete(Item(id, s"item#$id", id * 2.0)) } } ~ (path("item") & put) { entity(as[Item]) {item => complete(item) } } val route = messages ~ items val (host, port) = ("localhost", 8011) val bindingFuture = Http().bindAndHandle(route,host,port) println(s"Server running at $host $port. Press any key to exit ...") scala.io.StdIn.readLine() bindingFuture.flatMap(_.unbind()) .onComplete(_ => httpSys.terminate()) }

客戶端源代碼: 

import akka.actor._ import akka.http.scaladsl.settings.ConnectionPoolSettings import akka.stream._ import akka.stream.scaladsl._ import akka.http.scaladsl.Http import akka.http.scaladsl.model._ import scala.util._ import de.heikoseeberger.akkahttpjson4s.Json4sSupport import org.json4s.jackson import scala.concurrent._ import akka.http.scaladsl.unmarshalling.Unmarshal import akka.http.scaladsl.unmarshalling._ import akka.http.scaladsl.marshalling.Marshal import scala.collection.SortedMap import akka.http.scaladsl.common._ trait JsonCodec extends Json4sSupport { import org.json4s.DefaultFormats import org.json4s.ext.JodaTimeSerializers implicit val serilizer = jackson.Serialization implicit val formats = DefaultFormats ++ JodaTimeSerializers.all } object JsConverters extends JsonCodec class PooledClient(host: String, port: Int, poolSettings: ConnectionPoolSettings) (implicit sys: ActorSystem, mat: ActorMaterializer) { import sys.dispatcher private val cnnPool: Flow[(HttpRequest, Int), (Try[HttpResponse], Int), Http.HostConnectionPool] = Http().cachedHostConnectionPool[Int](host = host, port = port, settings = poolSettings) def requestSingleResponse(req: HttpRequest): Future[HttpResponse] = { Source.single(req -> 1) .via(cnnPool) .runWith(Sink.head).flatMap { case (Success(resp), _) => Future.successful(resp) case (Failure(fail), _) => Future.failed(fail) } } def orderedResponses(reqs: Iterable[HttpRequest]): Future[Iterable[HttpResponse]] = { Source(reqs.zipWithIndex.toMap) .via(cnnPool) .runFold(SortedMap[Int, Future[HttpResponse]]()) { case (m, (Success(r), idx)) => m + (idx -> Future.successful(r)) case (m, (Failure(f), idx)) => m + (idx -> Future.failed(f)) }.flatMap { m => Future.sequence(m.values) } } } class QueuedRequestsClient(host: String, port: Int, poolSettings: ConnectionPoolSettings) (qsize: Int = 10, overflowStrategy: OverflowStrategy = OverflowStrategy.dropNew) (implicit sys: ActorSystem, mat: ActorMaterializer) { import sys.dispatcher private val cnnPool: Flow[(HttpRequest,Promise[HttpResponse]),(Try[HttpResponse],Promise[HttpResponse]),Http.HostConnectionPool] = Http().cachedHostConnectionPool[Promise[HttpResponse]](host=host,port=port,settings=poolSettings) val queue = Source.queue[(HttpRequest, Promise[HttpResponse])](qsize, overflowStrategy) .via(cnnPool) .to(Sink.foreach({ case ((Success(resp), p)) => p.success(resp) case ((Failure(e), p))    => p.failure(e) })).run() def queueRequest(request: HttpRequest): Future[HttpResponse] = { val responsePromise = Promise[HttpResponse]() queue.offer(request -> responsePromise).flatMap { case QueueOfferResult.Enqueued    => responsePromise.future case QueueOfferResult.Dropped     => Future.failed(new RuntimeException("Queue overflowed. Try again later.")) case QueueOfferResult.Failure(ex) => Future.failed(ex) case QueueOfferResult.QueueClosed => Future.failed(new RuntimeException("Queue was closed (pool shut down) while running the request. Try again later.")) } } } object ClientRequesting extends App { import JsConverters._ implicit val sys = ActorSystem("sysClient") implicit val mat = ActorMaterializer() implicit val ec = sys.dispatcher implicit val jsonStreamingSupport = EntityStreamingSupport.json() .withParallelMarshalling(parallelism = 8, unordered = false) case class Item(id: Int, name: String, price: Double) def extractEntity[T](futResp: Future[HttpResponse])(implicit um: Unmarshaller[ResponseEntity,T]) = { futResp.andThen { case Success(HttpResponse(StatusCodes.OK, _, entity, _)) => Unmarshal(entity).to[T] .onComplete { case Success(t) => println(s"Got response entity: ${t}") case Failure(e) => println(s"Unmarshalling failed: ${e.getMessage}") } case Success(_) => println("Exception in response!") case Failure(err) => println(s"Response Failed: ${err.getMessage}") } } (for { response <- Http().singleRequest(HttpRequest(method=HttpMethods.GET,uri="http://localhost:8011/message")) message <- Unmarshal(response.entity).to[String] } yield message).andThen { case Success(msg) => println(s"Received message: $msg") case Failure(err) => println(s"Error: ${err.getMessage}") } //.andThen {case _ => sys.terminate()}
 (for { entity <- Marshal("Wata hell you doing?").to[RequestEntity] response <- Http().singleRequest(HttpRequest(method=HttpMethods.PUT,uri="http://localhost:8011/message",entity=entity)) message <- Unmarshal(response.entity).to[String] } yield message).andThen { case Success(msg) => println(s"Received message: $msg") case Failure(err) => println(s"Error: ${err.getMessage}") } //.andThen {case _ => sys.terminate()}
 def getItem(itemId: Int): Future[HttpResponse] = for { response <- Http().singleRequest(HttpRequest(method=HttpMethods.GET,uri = s"http://localhost:8011/item/$itemId")) } yield response extractEntity[Item](getItem(13)) def putItem(item: Item): Future[HttpResponse] =
   for { reqEntity <- Marshal(item).to[RequestEntity] response <- Http().singleRequest(HttpRequest(method=HttpMethods.PUT,uri="http://localhost:8011/item",entity=reqEntity)) } yield response extractEntity[Item](putItem(Item(23,"Item#23", 46.0))) .andThen { case _ => sys.terminate()} val settings = ConnectionPoolSettings(sys) .withMaxConnections(8) .withMaxOpenRequests(8) .withMaxRetries(3) .withPipeliningLimit(4) val pooledClient = new PooledClient("localhost",8011,settings) def getItemByPool(itemId: Int): Future[HttpResponse] = for { response <- pooledClient.requestSingleResponse(HttpRequest(method=HttpMethods.GET,uri = s"http://localhost:8011/item/$itemId")) } yield response extractEntity[Item](getItemByPool(13)) def getItemsByPool(itemIds: List[Int]): Future[Iterable[HttpResponse]] = { val reqs = itemIds.map { id => HttpRequest(method = HttpMethods.GET, uri = s"http://localhost:8011/item/$id") } val rets = (for { responses <- pooledClient.orderedResponses(reqs) } yield responses) rets } val futResps = getItemsByPool(List(3,5,7)) futResps.andThen { case Success(listOfResps) => { listOfResps.foreach { r => r match { case HttpResponse(StatusCodes.OK, _, entity, _) => Unmarshal(entity).to[Item] .onComplete { case Success(t) => println(s"Got response entity: ${t}") case Failure(e) => println(s"Unmarshalling failed: ${e.getMessage}") } case _ => println("Exception in response!") } } } case _ => println("Failed to get list of responses!") } val queuedClient = new QueuedRequestsClient("localhost",8011,settings)() def putItemByQueue(item: Item): Future[HttpResponse] =
    for { reqEntity <- Marshal(item).to[RequestEntity] response <- queuedClient.queueRequest(HttpRequest(method=HttpMethods.PUT,uri="http://localhost:8011/item",entity=reqEntity)) } yield response extractEntity[Item](putItemByQueue(Item(23,"Item#23", 46.0))) .andThen { case _ => sys.terminate()} }

 

 

 

 

 

 

 

 

 

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM