Akka HTTP 文檔 (非官方漢化)- 導讀
簡介
Akka HTTP 模塊組在 akka-actor 和 akka-stream 的基礎上實現了全HTTP棧( 伺服器- 及客戶- 端 )的功能。它並不是一個 web 框架,而是一個更通用的工具箱以便生成提供或消費基於 http 的網路服務。雖然與瀏覽器進行互動是其功能的組成部分,但這個並不是 Akka HTTP 的主要目的(譯註:簡單來說,別把 akka http 模組只當頁面伺服器)
Akka HTTP 採用的是一個開放式的設計,很多時候會提供不同抽象層次的 API 完成同一件事。 使用者可以選擇更合適相關應用所需抽象度的 API。這裡的意思指,如果當採用一個比較高抽象度的 API 無法實現開發需求的時候,很有可能開發人員可以採用一個比較底抽象度的 API 達到目的,雖然這樣會有更多的靈活型,但也意味著需要寫更多的代碼。
理念
Akka HTTP 的開發一直都有一個很明確的焦點,它是為整合層面提供的一套建設工具,而不是針對應用程序的核心。因而,它更多地把自己定位為一套工具庫而不是一個框架。
當說到一個框架,我們更多會因為其名字聯想到,開發應用的時候會有一個「框」。這個「框」有很多已經被預設的開發決定,同時也提供了一個基礎,這個基礎包括了一些架構能讓開發者可以快速的投入以及交付結果。某種意義上而言,框架就像一個骨架,開發者則是把應用中的「血和肉」掛上去,把應用衍生出來。這些框架的的最佳發揮場景是,開發者在一開始就決定選型並一直嘗試在開發過程中堅持框架帶來的風格。
舉個例子,如果開發者在開發一個面向瀏覽器的 web 應用,那麼,使用一個 web 的框架是一件理所當然的事情。因為這個應用的「核心」是瀏覽器與開發者在伺服器端的代碼進行互動。框架的作者已經挑選了一個「被證明有效」的方式為相關的應用程序開發設計,並讓開發者或多或少地在這個「設計模版」裡面進行「填空」。對開發者而言,可以依賴於已知的最佳實踐架構下進行快速開發有時是很大的資產。
可是,如果開發者的需求並不只是提供一個 web 應用程序(因為這個程序的核心並不是瀏覽器互動),而是一些特別的,甚至是複雜的,業務相關的應用服務,而開發者只是需要通過 REST/HTTP 提供介面連接。那,一個完整的 web 框架就未必是開發者所需要的。這個情景下,應用架構應該取決於什麼對核心更合理而不是對介面層面。而且,這個時候開發者並未能從某些框架中針對瀏覽器端開發的組件獲益,例如,視圖模版,靜態資產管理,JS 和 CSS 的生成器/調解器/縮減器,本地化處理,AJAX 支持,等等。
Akka HTTP 特意為了「不是一個框架」設計,並不是因為我們不喜歡框架,而是為了某些當框架並不是最優解的應用場景。Akka HTTP 是針對建立基於 HTTP 的整合層面,並會盡量「保持邊緣化」。因此,開發者並不需要「基於」 Akka HTTP 開發,而應該使用任何合理基礎開發應用,並只當有 HTTP 整合需求時引入 Akka HTTP。
另一方面而言,如果開發者有意願在一個框架下指導開發應用,可以試試用 Play framework 或者 Lagom,兩者都在其內部使用了 Akka 。
(譯註:上面這句是Lightbend自己的純廣告,Play在最新版才加的akka http, Lagom是它們家最新的微服務框架,完全是為了政治目的加的廣告,稍微鄙視一下)
如何使用 Akka HTTP
Akka HTTP 可以作為一個獨立的 jar 引入,以下是 sbt 的 依賴引入
(譯註:maven 和 gradle 也可以找到相關的依賴,不是一定要用sbt)
"com.typesafe.akka" %% "akka-http" % "10.0.1" n
需要留意一下的是,akka-http 由兩個模塊組成:akka-http 和 akka-http-core。因為 akka-http 依賴於 akka-http-core,開發者並不需要顯式引入後者。如果開發者只依靠底層 API 開發的時候還是要注意可能要顯式引入一下。
HTTP 服務端的路由 DSL
Akka HTTP 在高抽象度的 API 里提供了一套 路由DSL 來描述 HTTP 「路由」 和其相關處理。每個路由可以是一或幾個不同的 Directive 層級組合而成,每個路由則專註處理一個類型的請求。
(譯註:Directive 是 Akka HTTP 自己定義的概念,後面的翻譯會有嘗試進行提供合理的中文對應,這裡作為引讀篇章,暫時不翻譯)
舉個路由通過匹配請求路徑的例子,例如當且只當 path 是 「/hello" 對時候,才處理相關的 HTTP get 請求並 complete 返回相關的字元串文本,該文本會被作為一個 HTTP OK 響應中的正文。
應用程序里關於 請求 和 響應 間正文格式和內容的轉換,是與路由描述分開處理的。這些正文內容可以通過 「magnet」 模式由 marshallers 進行隱式抽取。這意味著開發者可以通過任何類型的對象 complete 一個 請求 的處理,只要在相應範圍內有對應的隱式 marshallers
(譯註:這段文字需要開發人員有三個相關的Scala知識:隱式處理,變數作用域,「magnet」 模式)
默認提供的 marshallers 可以處理如 String 類型或者 ByteString 類型等簡單的對象轉換,開發者也可以使用自定義的 marshallers 例如 JSON類型 等,如果需要 JSON 序列化處理的話,需要額外引入 spray-json 模塊進行相關操作。詳細請參考 JSON 處理
(譯註:marshallers 在這個文章的語境里更接近轉換器,暫時不做中譯,但 marshall 和 un-marshall,會試用生成和抽取)
通過 路由DSL 生成的 Route 對象 必須進行對應的「綁定」才能開始處理不同 HTTP Request
import akka.actor.ActorSystemnimport akka.http.scaladsl.Httpnimport akka.http.scaladsl.model._nimport akka.http.scaladsl.server.Directives._nimport akka.stream.ActorMaterializernimport scala.io.StdInnnobject WebServer {n def main(args: Array[String]) {nn implicit val system = ActorSystem("my-system")n implicit val materializer = ActorMaterializer()nn // 這個在最後的 future flatMap/onComplete 裡面會用到n implicit val executionContext = system.dispatchernn val route =n path("hello") {n get {n complete(HttpEntity(ContentTypes.`text/html(UTF-8)`, "<h1>Say hello to akka-http</h1>"))n }n }nn val bindingFuture = Http().bindAndHandle(route, "localhost", 8080)nn println(s"Server online at http://localhost:8080/nPress RETURN to stop...")n StdIn.readLine() // 等用戶輸入 RETURN 鍵停跑n bindingFuturen .flatMap(_.unbind()) // 放開對埠 8080 的綁定n .onComplete(_ => system.terminate()) // 結束後關掉程序n }n}n
一種比較主流的用法,用相關的 marshaller 把一個模型對象轉換成 JSON 作為 請求 的回應。在以下的例子裡面定義了兩個路由,第一個路由去查詢非同步資料庫,並把對應的 Future[Option[Item]] 結果轉換成 JSON。第二個路由則把 Order 對象從 請求 裡面抽取出來並保存到資料庫里,完成後返回 OK 狀態。
import akka.actor.ActorSystemnimport akka.http.scaladsl.Httpnimport akka.stream.ActorMaterializernimport akka.Donenimport akka.http.scaladsl.server.Routenimport akka.http.scaladsl.server.Directives._nimport akka.http.scaladsl.model.StatusCodesnimport akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._nimport spray.json.DefaultJsonProtocol._nnimport scala.io.StdInnnimport scala.concurrent.Futurennobject WebServer {nn // 模型n final case class Item(name: String, id: Long)n final case class Order(items: List[Item])nn // 生成及抽取 JSON 格式的轉換n implicit val itemFormat = jsonFormat2(Item)n implicit val orderFormat = jsonFormat1(Order)nn // (假的) 非同步資料庫調用 apin def fetchItem(itemId: Long): Future[Option[Item]] = ???n def saveOrder(order: Order): Future[Done] = ???nn def main(args: Array[String]) {nn // 這兩個用於routen implicit val system = ActorSystem()n implicit val materializer = ActorMaterializer()n // 這個在最後的 future map/flatmap 裡面會用n implicit val executionContext = system.dispatchernn val route: Route =n get {n pathPrefix("item" / LongNumber) { id =>n // 有可能 item 沒有 idn val maybeItem: Future[Option[Item]] = fetchItem(id)nn onSuccess(maybeItem) {n case Some(item) => complete(item)n case None => complete(StatusCodes.NotFound)n }n }n } ~n post {n path("create-order") {n entity(as[Order]) { order =>n val saved: Future[Done] = saveOrder(order)n onComplete(saved) { done =>n complete("order created")n }n }n }n }nn val bindingFuture = Http().bindAndHandle(route, "localhost", 8080)n println(s"Server online at http://localhost:8080/nPress RETURN to stop...")n StdIn.readLine() // 等用戶輸入 RETURN 鍵停跑n bindingFuturen .flatMap(_.unbind()) // 放開對埠 8080 的綁定n .onComplete(_ ? system.terminate()) // 結束後關掉程序nn }n}n
在例子里生成和抽取 JSON 的邏輯由 「spray-json」 庫提供。詳細請參考 JSON 處理
Akka HTTP 的其中一個強項是數據的流式處理,流式數據是它的一個核心理念,這意味著 請求 和 響應 的正文內容可以以數據流的形式在伺服器上的進行處理,以達到定量的內存使用率,即使在 請求 和 響應量非常巨大的時候。流式 響應 通過遠程客戶端反壓控制使伺服器不會過多地推送客戶端無法處理的數據,而流式 請求 則意味著伺服器決定遠程客戶端可以多快地推送數據。
以下的例子是向客戶端輸出一個隨機數的數據流,只有客戶端能接受的話:
import akka.actor.ActorSystemnimport akka.stream.scaladsl._nimport akka.util.ByteStringnimport akka.http.scaladsl.Httpnimport akka.http.scaladsl.model.{HttpEntity, ContentTypes}nimport akka.http.scaladsl.server.Directives._nimport akka.stream.ActorMaterializernimport scala.util.Randomnimport scala.io.StdInnnobject WebServer {nn def main(args: Array[String]) {nn implicit val system = ActorSystem()n implicit val materializer = ActorMaterializer()n // 這個在最後的 future flatMap/onComplete 裡面會用到n implicit val executionContext = system.dispatchernn // 數據流是可以被複用的,這裡定義一次之後可以被後來的各個請求使用n val numbers = Source.fromIterator(() =>n Iterator.continually(Random.nextInt()))nn val route =n path("random") {n get {n complete(n HttpEntity(n ContentTypes.`text/plain(UTF-8)`,n // 將數字轉換成一堆位元組碼n numbers.map(n => ByteString(s"$nn"))n )n )n }n }nn val bindingFuture = Http().bindAndHandle(route, "localhost", 8080)n println(s"Server online at http://localhost:8080/nPress RETURN to stop...")n StdIn.readLine() // 等用戶輸入 RETURN 鍵停跑n bindingFuturen .flatMap(_.unbind()) // 放開對埠 8080 的綁定n .onComplete(_ ? system.terminate()) // 結束後關掉程序n }n}n
當一個慢 HTTP 客戶端聯入這個服務的時候,這個客戶端會被反壓,使得下一個隨機數能在伺服器保持定量內存使用率的情況下按需生成。這個例子的效果可以通過 curl 設定速率上限觀察到。
curl --limit-rate 50b 127.0.0.1:8080/randomn
Akka HTTP 路由可以很容易地與 actor 互動。在下面的例子裡面,一個路由既可以用 fire-and-forget 方式處理拍賣下單,而同時第二個路由又可以包括了以 request-reponse 的方式 與 actor 互動。最終的結果則會從 actor 接收到回應後轉換成JSON。
(譯註:fire-and-forget 和 request-repsonse 兩者都是 akka actor 的請求處理模式,這裡可能對讀者要有相關的知識面)
import akka.actor.{Actor, ActorSystem, Props}nimport akka.http.scaladsl.Httpnimport akka.http.scaladsl.model.StatusCodesnimport akka.http.scaladsl.server.Directives._nimport akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._nimport akka.pattern.asknimport akka.stream.ActorMaterializernimport akka.util.Timeoutnimport spray.json.DefaultJsonProtocol._nimport scala.concurrent.duration._nimport scala.io.StdInnnobject WebServer {nn case class Bid(userId: String, bid: Int)n case object GetBidsn case class Bids(bids: List[Bid])nn class Auction extends Actor {n def receive = {n case Bid(userId, bid) => println(s"Bid complete: $userId, $bid")n case _ => println("Invalid message")n }n }nn // 來自 spray-jsonn implicit val bidFormat = jsonFormat2(Bid)n implicit val bidsFormat = jsonFormat1(Bids)nn def main(args: Array[String]) {n implicit val system = ActorSystem()n implicit val materializer = ActorMaterializer()n // 這個在最後的 future flatMap/onComplete 裡面會用到n implicit val executionContext = system.dispatchernn val auction = system.actorOf(Props[Auction], "auction")nn val route =n path("auction") {n put {n parameter("bid".as[Int], "user") { (bid, user) =>n // 下單, fire-and-forgetn auction ! Bid(user, bid)n complete((StatusCodes.Accepted, "bid placed"))n }n }n get {n implicit val timeout: Timeout = 5.secondsnn // 查詢actor現在的狀態n val bids: Future[Bids] = (auction ? GetBids).mapTo[Bids]n complete(bids)n }n }nn val bindingFuture = Http().bindAndHandle(route, "localhost", 8080)n println(s"Server online at http://localhost:8080/nPress RETURN to stop...")n StdIn.readLine() // 等用戶輸入 RETURN 鍵停跑n bindingFuturen .flatMap(_.unbind()) // 放開對埠 8080 的綁定n .onComplete(_ ? system.terminate()) // 結束後關掉程序n }n}n
再次提醒,在例子里抽取和生成 JSON 的邏輯由 「spray-json」 庫提供。
可以在 鏈接 里參考更多的高抽象層API資料
低抽象層的 HTTP 伺服器 API
低抽象層的 Akka HTTP 伺服器 API 可以通過接收 HttpRquest 對象處理不同的連接或請求,並以 HttpResponse 對象返回,這部分功能是由 akka-http-core 模塊提供。以函數形式以及 Flow[HttpRequest, HttpResponse, _] 形式處理類似的 請求-響應 也有相關 API 提供。
import akka.actor.ActorSystemnimport akka.http.scaladsl.Httpnimport akka.http.scaladsl.model.HttpMethods._nimport akka.http.scaladsl.model._nimport akka.stream.ActorMaterializernimport scala.io.StdInnnobject WebServer {nn def main(args: Array[String]) {n implicit val system = ActorSystem()n implicit val materializer = ActorMaterializer()n n // 這個在最後的 future map/flatmap 裡面會用n implicit val executionContext = system.dispatchernn val requestHandler: HttpRequest => HttpResponse = {n case HttpRequest(GET, Uri.Path("/"), _, _, _) =>n HttpResponse(entity = HttpEntity(n ContentTypes.`text/html(UTF-8)`,n "<html><body>Hello world!</body></html>"))nn case HttpRequest(GET, Uri.Path("/ping"), _, _, _) =>n HttpResponse(entity = "PONG!")nn case HttpRequest(GET, Uri.Path("/crash"), _, _, _) =>n sys.error("BOOM!")nn case r: HttpRequest =>n r.discardEntityBytes() // important to drain incoming HTTP Entity streamn HttpResponse(404, entity = "Unknown resource!")n }nn val bindingFuture = Http().bindAndHandleSync(requestHandler, "localhost", 8080)n println(s"Server online at http://localhost:8080/nPress RETURN to stop...")n StdIn.readLine() // 等用戶輸入 RETURN 鍵停跑n bindingFuturen .flatMap(_.unbind()) // 放開對埠 8080 的綁定n .onComplete(_ ? system.terminate()) // 結束後關掉程序n }n}n
可以在 鏈接 里參考更多的低抽象層API資料
HTTP 客戶端 API
客戶端 API 則提供了與 HTTP 伺服器進行通訊的各種方法,同樣基於的 HttpRequest 與 HttpResponse 抽象對象並加入了鏈接池的概念,可以使多個 請求 連接到伺服器時重用同一個 TCP 鏈接,從而使同一個伺服器更高效地處理這些 請求。
一個簡單的例子
import akka.actor.ActorSystemnimport akka.http.scaladsl.Httpnimport akka.http.scaladsl.model._nimport akka.stream.ActorMaterializernnimport scala.concurrent.Futurennimplicit val system = ActorSystem()nimplicit val materializer = ActorMaterializer()nnval responseFuture: Future[HttpResponse] =n Http().singleRequest(HttpRequest(uri = "http://akka.io"))n
可以在 鏈接 里參考更多的客戶端 API 的資料
組成 Akka HTTP 的模塊
Akka HTTP 是由以下的幾個模塊組成:
akka-http
提供高抽象度的功能,例如 抽取/生成 對象,解壓/壓縮數據,以及一個強效的DSL用於定義伺服器端上的基於 HTTP 的 應用程序 API,使用這套 DSL 是用 Akka HTTP 編寫 HTTP 伺服器的推薦用法。詳細的內容請參考 鏈接
akka-http-core
一套完整的,但相對低抽象度的,伺服器- 和 客戶-端 的 HTTP 實現(例如包括:WebSockets)。詳細的內容請參考 鏈接
akka-http-testkit
一套測試套件以及工具,用以驗證伺服器端的開發實現
akka-http-spray-json
一套預設好的膠水代碼用於各種客制類型與JSON格式之間的的轉換( 序列化/反序列化 ),邏輯基於 spray-json,詳細的內容請參考 鏈接
akka-http-xml
一套預設好的膠水代碼用於各種客制類型與XML格式之間的的轉換( 序列化/反序列化 ),邏輯基於 scala-xml,詳細的內容請參考 鏈接
原文 : Akka HTTP Introduction
推薦閱讀:
※雲計算的1024種玩法之零基礎入門
※KaliRouter安裝與使用全指南
※最經典的前端面試題之一,你能答出什麼幺蛾子?
※HTML5的Websocket(理論篇 I)