akka-http
Akka-http.
akka-http 설치
akka-http 는 akka module에서 분리 되어 독립적인 배포 사이클을 가진다. 현제 10.1.1 version 에 akka 2.5.13, scala 2.12.6 version, 으로 진행 한다.
akka-http 는 akka-http-core와 akka-http 로 되어 있있는데 akka-http가 akka-http-core 를 dependency하게 되어 있으므로 akka-http 만 선언해도 된다.
이는 g8 template project 가 있어 쉽게 설치 할 수 있다.
sbt -Dsbt.version=1.0.4 new https://github.com/akka/akka-http-scala-seed.g8
Akka-Http route
Akka-http route API 는 DSL를 제공하며, 이는 1개 이상의 Directive 로 구성되어 요청에 대한 route 처리를 기술하게 한다.
아래에 예제는 akka-io 에 나와 있는 예제 이다.
object WebServer {
def main(args: Array[String]): Unit = {
implicit val system = ActorSystem("myFirstHttpServer")
implicit val mat = ActorMaterializer()
implicit val ec = system.dispatcher
val route = path("hellow") {
get {
complete(HttpEntity(ContentTypes.`text/html(UTF-8)`, "<h1>hellow world akka-http</h>"))
}
}
val bindFuture: Future[Http.ServerBinding] = Http().bindAndHandle(route, "localhost", 8080)
StdIn.readLine()
bindFuture.flatMap(serverBinding => serverBinding.unbind()).onComplete(_ => system.terminate())
}
}
그리고 다음과 같은 명령으로 server를 띄운다.
Macintosh:akka-http sslee$ sbt "runMain com.sslee.http.intro.WebServer"
그리고 command에서 다음과 같은 명령을 하면 결과는 아래와 같이 나온다. (난 HTTPie 를 설치한 상황이므로)
Macintosh:akka-http sslee$ http GET localhost:8080/hellow
HTTP/1.1 200 OK
Content-Length: 30
Content-Type: text/html; charset=UTF-8
Date: Sat, 30 Jun 2018 04:56:29 GMT
Server: akka-http/10.1.1
<h1>hellow world akka-http</h>
akka-http spray-json
akka-http 는 client, server 간의 form data => scala type(Unmarshalling), 혹은 scala type => json string(Marshalling)으로 spray-json module를 사용한다.
아래 예는 Item(scala type) => Json String 으로의 marshalling과 , form json data => Order(scala type)으로의 unmarshalling을 보여 준다.
object SimpleMarshallWebServer {
case class Item(name: String, id: Long)
case class Order(items: List[Item])
var mockDatabase = List.empty[Item]
//route 실행 필수
implicit val system = ActorSystem("SimpleMarshallWebServer")
implicit val mat = ActorMaterializer()
//Future에 필요
implicit val ec = system.dispatcher
//fake select database
def getItem(id: Long): Future[Option[Item]] = Future {
mockDatabase.find(item => item.id == id)
}
//fake insert database
def saveOrder(order: Order): Future[Done] = {
order match {
case Order(items) =>
mockDatabase = items ::: mockDatabase
case _ => mockDatabase
}
Future { Done }
}
def main(args: Array[String]): Unit = {
//spray-json 을 이용한 marshall, unmarshall시 필요한 암시자
implicit val itemFormatter = jsonFormat2(Item)
implicit val orderFormatter = jsonFormat1(Order)
val route: Route = get {
pathPrefix("item" / LongNumber) { id =>
val item = getItem(id)
onSuccess(item) {
//marshalling
case Some(item) => complete(item)
case None => complete(StatusCodes.NotFound)
}
}
} ~ post {
path("createOrder") {
//unmarshalling
entity(as[Order]) { order =>
val saved: Future[Done] = saveOrder(order)
onComplete(saved) { done =>
complete("order created")
}
}
}
}
val bindFuture: Future[Http.ServerBinding] = Http().bindAndHandle(route, "localhost", 8080)
StdIn.readLine()
bindFuture.flatMap(serverBinding => serverBinding.unbind()).onComplete(_ => system.terminate())
}
}
서버 기동
sbt "runMain com.sslee.http.intro.SimpleMarshallWebServer"
그리고 Order(items: List[Item]) 으로 Unmarshalling될 요청 값을 json.txt file로 만들어 HTTPie를 이용하여 실행을 해보자.
//json.txt file
{"items":[{"name":"akka-book","id":45}]}
//요청실행 Order 등록
Macintosh:akka-http sslee$ http POST localhost:8080/createOrder < ./json.txt
HTTP/1.1 200 OK
Content-Length: 13
Content-Type: text/plain; charset=UTF-8
Date: Sat, 30 Jun 2018 06:37:38 GMT
Server: akka-http/10.1.1
order created
등록값 조회
Macintosh:akka-http sslee$ http GET localhost:8080/item/45
HTTP/1.1 200 OK
Content-Length: 28
Content-Type: application/json
Date: Sat, 30 Jun 2018 06:38:13 GMT
Server: akka-http/10.1.1
{
"id": 45,
"name": "akka-book"
}
akka http 와 akka-stream
akka-http에서 akka-stream을 쉽게 이용할 수 있으며, 이를 통해 첨부파일이나 memory size를 넘는 대량의 데이터를 처리하더라도 OOM이 발생하지 않는다. 이는 reactive stream 구조의 akka-stream 덕분이다. 아래의 예제를 통해 client browser가 Sink, server에서 무한 Random 생성기가 Source로 이는 무한이지만, 절대 OOM 이 발생되지 않음을 볼 수 있으며, 이는 client의 즉 Sink쪽에서 Source쪽으로의 backpressure 를 통해 (water mark) async 로 수위를 조절한다. 이는 akka-stream에서 이야기 한 부분이다.
소스는 akkak-io site 에 있다.
object ExampleStreamWebServer {
def main(args: Array[String]) {
//route 실행에 필수, Stream
implicit val system = ActorSystem("ExampleStreamWebSystem")
implicit val mat = ActorMaterializer()
//Future 에 필요
implicit val ec = system.dispatcher
val randomSource = Source.fromIterator(() => Iterator.continually(Random.nextInt))
val route = path("random") {
get {
complete {
HttpEntity(
ContentTypes.`text/plain(UTF-8)`, randomSource.map(n => ByteString(s"$n\n")))
}
}
}
val bindFuture: Future[Http.ServerBinding] = Http().bindAndHandle(route, "localhost", 8080)
println(s"Server online at http://localhost:8080/\nPress RETURN to stop...")
StdIn.readLine()
bindFuture.flatMap(serverBinding => serverBinding.unbind()).onComplete(_ => system.terminate())
}
}
실행시 curl로 요청 rate 를 이용하여 요청수위를 늦춘다.
Macintosh:akka-http sslee$ curl --limit-rate 50b 127.0.0.1:8080/random
-1090963985
-472554423
193702927
1496244304
....중략
akka-http 와 Actor
akka-http 는 Actor 하고 쉽게 상화 작용할 수 있다. 이때 client에서 요청은 route 를 거처 Acotor에게 호출하고 있는 즉 Actor의 응답을 기다리지 않고 바로 client에게 응답결과를 보낸다(아래 예제의 put). 또한 Actor의 ask 요청을 하고 Future을 받으며 이 Future의 결과가 완료시 client 에게 응답이 보내진다.(아래 예제 get)
아래 예제는 akka.io 에 있는 예제를 약간 수정만 했다.
GET요청시 actor의 receive method에서 고의로 Thread.sleep 를 주어 client화면에 이의 영향을 받지 않고 바고 응답결과를 받는지 확인 할 수 있다.
또한 PUT의 요청을 처리하는 부분도 Thread.sleep를 주었지만, 비동기로 요청의 응답을 Future로 받기 때문에 ###### 문자열이 바로 console 에 출력됨을 알 수 있다.
object ExampleViaActorWebServer {
case class Event(eventId: String, ticket: Int)
case object GetEvents
case class Events(xs: List[Event])
class EventActor extends Actor with ActorLogging {
var events = List.empty[Event]
def receive = {
case ev @ Event(eventId, ticket) =>
Thread.sleep(3000L)
events = ev :: events
case GetEvents =>
Thread.sleep(3000L)
sender() ! Events(events)
}
}
def main(args: Array[String]) = {
implicit val system = ActorSystem("ViaActorSystem")
implicit val mat = ActorMaterializer()
implicit val ec = system.dispatcher
val actor = system.actorOf(Props[EventActor], "eventActor")
implicit val eventFormater = jsonFormat2(Event)
implicit val eventsFormater = jsonFormat1(Events)
val route = path("event") {
put {
parameter("eventId", "ticket".as[Int]) { (eventId, ticket) =>
actor ! Event(eventId, ticket)
complete((StatusCodes.Accepted, "create event success"))
}
} ~
get {
implicit val timeout: Timeout = 5 seconds
val events: Future[Events] = (actor ? GetEvents).mapTo[Events]
println("########################")
complete(events)
}
}
val serverBind: Future[Http.ServerBinding] = Http().bindAndHandle(route, "localhost", 8080)
println(s"Server started host localhost port 8080, Do you want shutdown server? and then press RETURN")
StdIn.readLine()
serverBind.flatMap(serverB => serverB.unbind()).onComplete(_ => system.terminate())
}
}
호출
Macintosh:akka-http sslee$ http PUT localhost:8080/event eventId==Metallica ticket==10
HTTP/1.1 202 Accepted
Content-Length: 20
Content-Type: text/plain; charset=UTF-8
Date: Thu, 19 Jul 2018 14:42:28 GMT
Server: akka-http/10.1.1
create event success
Macintosh:akka-http sslee$ http GET localhost:8080/event
HTTP/1.1 200 OK
Content-Length: 44
Content-Type: application/json
Date: Thu, 19 Jul 2018 14:42:40 GMT
Server: akka-http/10.1.1
{
"xs": [
{
"eventId": "Metallica",
"ticket": 10
}
]
}
web server console
[success] Total time: 78 s, completed Jul 19, 2018 11:41:58 PM
Macintosh:akka-http sslee$ sbt "runMain com.sslee.http.intro.ExampleViaActorWebServer"
...중략
ace-http/akka-http/target/scala-2.12/akka-http_2.12-0.1-SNAPSHOT.jar ...
[info] Done packaging.
[info] Running com.sslee.http.intro.ExampleViaActorWebServer
Server started host localhost port 8080, Do you want shutdown server? and then press RETURN
########################