区别于Java EE的传统框架,Spray并没有使用容器(Tomcat, Jetty之类的); 不过它提供了spray-servlet模块来支持Servlet,它实际上是在Servlet API基础上搭建了spray-can(spray server的核心部分),这意味Spray是比较轻量的。 基于此决定研究一下Spray是如何建立TCP连接并且响应HTTP请求的。
在Spray中启动Server服务的基本代码如下:
class RestApi extends HttpServiceActor {
override def receive: Receive = runRoute(
path("test")(complete("simple test"))
)
}
object Boot extends App with ShutdownIfNotBound {
implicit val system = ActorSystem("req-test")
val frontEnd = system.actorOf(Props[RestApi], "frontEnd")
implicit val executionContext = system.dispatcher
implicit val globalTimeout: Timeout = {
val d = Duration("10seconds")
FiniteDuration(d.length, d.unit)
}
IO(Http) ? Http.Bind(frontEnd, interface = "127.0.0.1", port = 8082)
}
(1) IO.apply返回一个用于处理相应层的管理类Actor,spray.can.HttpManager之于HTTP层,spray.can.TcpManager之于TCP层, Spray的层级划分如下:
(2) 项目启动时,HttpManager接受一个Http.Bind绑定信息,目的是让服务器端的Socket监听在指定的InetSocketAddress(IP地址+端口号),当绑定成功之后,就可以开始建立连接了。 下面代码构建了一个简单的spray-client,向服务器发送消息然后接受服务器响应。
object SprayClient {
(1 to 3).foreach { num =>
(IO(Http) ? HttpRequest(uri = "http://localhost:8082/test")).mapTo[HttpResponse]
}
}
具体的流程如下图:
Spray使用Actor来抽象HTTP连接,每一次新的请求就会有一个新的Actor生成,这点可以从命令行打印的日志看出来。 所以官网中提到的spray-can是完全异步的并且能同时处理千计的连接就不难理解了。
# HttpHostConnectionSlot
Dispatching GET request to URL across connection Actor[akka://spray-client/user/IO-HTTP/group-0/0]
Dispatching GET request to URL across connection Actor[akka://spray-client/user/IO-HTTP/group-0/1]
Dispatching GET request to URL across connection Actor[akka://spray-client/user/IO-HTTP/group-0/2]
在阅读的源码时候还有一个比较重要的地方没有研究,那就是Spray的管道操作,在spray.can.server.HttpServerConnection
中对管道的每一部分进行简单的描述,它主要是用于TCP连接建立之后的请求渲染和请求生成的,基本的流程就是在TCP连接管道上进行数据的写入和读取。