Akka HTTP 快速上手

Akka Http 是Akka社区提供的一个 Http服务端/客户端通用工具包,支持 Http 1.0/1.1标准及WebSocket,现在 Http 2 的支持也在紧锣密鼓的实现中。

这篇文章将介绍Akka HTTP Server,我们将介绍Akka Http的常用功能模块及使用方式。Akka Http提供了一套强大、易用、易扩展的route dsl来构建路由。Akka Http Client因还不支持超时功能,现在不建议在产品中使用。

本文代码:akka-http-starter

第一个服务

我们可以从官方提供的 HttpApp 特质开始,它提供了快捷的方式来启动一个Akka HTTP Server。

1
2
3
4
5
6
7
8
9
class WebServer extends HttpApp {
def route: Route =
path("hello") {
get {
import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport._
complete(<h1>Say hello to akka-http</h1>)
}
}
}

path("hello")定义了一个HTTP访问路由,get代表这个路由提供了GET请示,而complete涵数允许我们提供响应结果来完成这个路由定义,这里我们返回了一段文本。Akka Http的路由看起来向声明式的,以一直新颖而又直观的方式来定义HTTP服务。

用户第一次接触这种涵数套涵数(又像树型结构)的代码方式可能不大习惯,其实我们可以换种方式来实现这段代码:

1
2
3
4
5
def traditionRoute: Route = {
val respResult = complete("result") // 响应结果
val hPath = path("hello") // 绑定的HTTP访问路径
hPath(get(result))
}

路径(Http方法(结果)),我们用Java式的风格来实现同样的功能。这样是不是更符合你对代码的预期?

让我们来启动服务:

StartBoot01.scalaStartBoot01.scala
1
2
3
4
5
6
object StartBoot01 {
def main(args: Array[String]): Unit = {
val server = new WebServer
server.startServer("0.0.0.0", 8888)
}
}

通过curl命令来测试下我们的第一个Akka HTTP服务(-i选项可以打印HTTP响应头):

1
2
3
4
5
6
7
8
curl -i http://localhost:8888/hello
HTTP/1.1 200 OK
Server: akka-http/10.0.5
Date: Fri, 31 Mar 2017 17:05:12 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 31
<h1>Say hello to akka-http</h1>

Route

Akka HTTP 提供了一个灵活的DSL,它有很多可组合的元素(Directive 指令)以简洁、易读的方式来构建服务。
让我们来看下面这个示例:

1
2
3
4
5
6
7
8
path("book") {
get {
parameters('name.as[Option[String]], 'isbn.as[Option[String]], 'author.as[Option[String]]) {
(maybeName, maybeIsbn, maybeAuthor) =>
complete(s"name: $maybeName, isbn: $maybeIsbn, author: $maybeAuthor")
}
}
}

对于上面这个定义,类似的 Play 路由定义如:

1
GET /book controller.Page.book(name: Option[String], isbn: Option[String], author: Option[String)

我们可以看到,对一个API路由的定义拆成了几个函数嵌套的形式。path指定访问路径,get决定这个API提供HTTP GET服务,parameters可以抽取请求参数,而complete将一个字符串返回给前端。

JSON

现在大部分的服务都提供JSON格式的数据,Akka HTTP提供了 Mashaller/Unmashaller机制,用户可以基于此灵活的定制自己的序列化/反序列化方式。这里我们使用 Jackson 来解析/处理 JSON。

首选,我们实现自定义的 Mashaller/Unmashaller:

JacksonSupport.scalaJacksonSupport.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
trait JacksonSupport {
private val jsonStringUnmarshaller =
Unmarshaller.byteStringUnmarshaller
.forContentTypes(MediaTypes.`application/json`)
.mapWithCharset {
case (ByteString.empty, _) => throw Unmarshaller.NoContentException
case (data, charset) => data.decodeString(charset.nioCharset.name)
}
// HTTP entity => `A`
implicit def unmarshaller[A](
implicit ct: ClassTag[A],
objectMapper: ObjectMapper = Jackson.defaultObjectMapper
): FromEntityUnmarshaller[A] =
jsonStringUnmarshaller.map(
data => objectMapper.readValue(data, ct.runtimeClass).asInstanceOf[A]
)
// `A` => HTTP entity
implicit def marshaller[A](
implicit objectMapper: ObjectMapper = Jackson.defaultObjectMapper
): ToEntityMarshaller[A] = {
JacksonHelper.marshaller[A](objectMapper)
}
}

实现自定义的 Marshaller/Unmarshaller 后,我们就可以在 Akka HTTP 中提供 Json 支持了。

PageRoute.scalaPageRoute.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import starter.akka.http.json.JacksonSupport._
case class PageInput(title: String, content: String)
class PageRoute {
def route: Route =
path("page") {
post {
entity(as[PageInput]) { pageInput =>
complete(pageInput)
}
}
}
}

Akka HTTP使用了Scala的隐式转换特性来自定义数据序列化,这是一个非侵入式的设计,用户可以在每个模块选择自己的数据序列化方式。

Route类型

Route 是 Akka HTTP 路由 DSL 里的核心概念,用它构建的所有结构,不管是一条线还是很多条线组成,它们都会是这个类型的实例。

1
type Route = RequestContext => Future[RouteResult]

组合路由

Akka HTTP 提供3个基本的操作来让我们创建更复杂的路由链:

  1. 路由转换:它代理一个“内部”的路由,并在这个过程中改变一些请求传入的属性,然后传出响应或两者。
  2. 路由过滤:只允许满足给定条件的请求被传递,并拒绝所有其它访问请求。
  3. 路由链:如果第一个请求被拒绝,将尝试第二个路由。使用 ~ 操作符连接多个路由定义。

Akka HTTP 实现了很多默认的指令 akka.http.scaladsl.server.Directives._ ,你也可以很轻松地创建自己的指令。指令提供了强大和灵活的方式来构建 Akka HTTP。

路由树

当通过嵌套和操作符组合指令和自定义路径时,将构建一个路由结构并形成一颗树。当一个 HTTP 请求进来,它将从根进行这颗树,并以深度优先的方式流过所有分支,直到某个节点完成或全部被拒绝为止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
val route =
a {
b {
c {
... // route 1
} ~
d {
... // route 2
} ~
... // route 3
} ~
e {
... // route 4
}
}

这里由5个指令构建了一个路由树:

  1. 当 a, b, c都通过,才到到达路由 1
  2. 当 a 和 b 通过,但 c 被拒绝且 d 通过,将到达路由 2
  3. 当 a 和 b 通过,但 c 和 d 被拒绝,路由 3 被到达

若路由 3 前面的请求都被拒绝,则它将“捕获”所有请求。这个机制使复杂的过滤逻辑可以很容易的实现。把简单和最具体的放在顶端,一般和普通的放到最后。