IO docs, in progress
This commit is contained in:
parent
a5611e88b2
commit
1fc35a04ad
3 changed files with 291 additions and 0 deletions
216
akka-docs/scala/code/akka/docs/io/HTTPServer.scala
Normal file
216
akka-docs/scala/code/akka/docs/io/HTTPServer.scala
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2011 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
package akka.docs.io
|
||||||
|
|
||||||
|
import akka.actor._
|
||||||
|
import akka.util.{ ByteString, ByteStringBuilder }
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
|
||||||
|
class HttpServer(port: Int) extends Actor {
|
||||||
|
|
||||||
|
val state = IO.IterateeRef.Map.async[IO.Handle]()(context.dispatcher)
|
||||||
|
|
||||||
|
override def preStart {
|
||||||
|
IOManager(context.system) listen new InetSocketAddress(port)
|
||||||
|
}
|
||||||
|
|
||||||
|
def receive = {
|
||||||
|
|
||||||
|
case IO.NewClient(server) ⇒
|
||||||
|
val socket = server.accept()
|
||||||
|
state(socket) flatMap (_ ⇒ HttpServer.processRequest(socket))
|
||||||
|
|
||||||
|
case IO.Read(socket, bytes) ⇒
|
||||||
|
state(socket)(IO Chunk bytes)
|
||||||
|
|
||||||
|
case IO.Closed(socket, cause) ⇒
|
||||||
|
state(socket)(IO EOF None)
|
||||||
|
state -= socket
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
object HttpServer {
|
||||||
|
import HttpIteratees._
|
||||||
|
|
||||||
|
def processRequest(socket: IO.SocketHandle): IO.Iteratee[Unit] =
|
||||||
|
IO repeat {
|
||||||
|
for {
|
||||||
|
request ← readRequest
|
||||||
|
} yield {
|
||||||
|
val rsp = request match {
|
||||||
|
case Request("GET", "ping" :: Nil, _, _, headers, _) ⇒
|
||||||
|
OKResponse(ByteString("<p>pong</p>"),
|
||||||
|
request.headers.exists { case Header(n, v) ⇒ n.toLowerCase == "connection" && v.toLowerCase == "keep-alive" })
|
||||||
|
case req ⇒
|
||||||
|
OKResponse(ByteString("<p>" + req.toString + "</p>"),
|
||||||
|
request.headers.exists { case Header(n, v) ⇒ n.toLowerCase == "connection" && v.toLowerCase == "keep-alive" })
|
||||||
|
}
|
||||||
|
socket write OKResponse.bytes(rsp).compact
|
||||||
|
if (!rsp.keepAlive) socket.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//#request-class
|
||||||
|
case class Request(meth: String, path: List[String], query: Option[String], httpver: String, headers: List[Header], body: Option[ByteString])
|
||||||
|
case class Header(name: String, value: String)
|
||||||
|
//#request-class
|
||||||
|
|
||||||
|
//#constants
|
||||||
|
object HttpConstants {
|
||||||
|
val SP = ByteString(" ")
|
||||||
|
val HT = ByteString("\t")
|
||||||
|
val CRLF = ByteString("\r\n")
|
||||||
|
val COLON = ByteString(":")
|
||||||
|
val PERCENT = ByteString("%")
|
||||||
|
val PATH = ByteString("/")
|
||||||
|
val QUERY = ByteString("?")
|
||||||
|
}
|
||||||
|
//#constants
|
||||||
|
|
||||||
|
//#read-request
|
||||||
|
object HttpIteratees {
|
||||||
|
import HttpConstants._
|
||||||
|
|
||||||
|
def readRequest =
|
||||||
|
for {
|
||||||
|
requestLine ← readRequestLine
|
||||||
|
(meth, (path, query), httpver) = requestLine
|
||||||
|
headers ← readHeaders
|
||||||
|
body ← readBody(headers)
|
||||||
|
} yield Request(meth, path, query, httpver, headers, body)
|
||||||
|
//#read-request
|
||||||
|
|
||||||
|
//#read-request-line
|
||||||
|
def ascii(bytes: ByteString): String = bytes.decodeString("US-ASCII").trim
|
||||||
|
|
||||||
|
def readRequestLine =
|
||||||
|
for {
|
||||||
|
meth ← IO takeUntil SP
|
||||||
|
uri ← readRequestURI
|
||||||
|
_ ← IO takeUntil SP // ignore the rest
|
||||||
|
httpver ← IO takeUntil CRLF
|
||||||
|
} yield (ascii(meth), uri, ascii(httpver))
|
||||||
|
//#read-request-line
|
||||||
|
|
||||||
|
//#read-request-uri
|
||||||
|
def readRequestURI = IO peek 1 flatMap {
|
||||||
|
case PATH ⇒
|
||||||
|
for {
|
||||||
|
path ← readPath
|
||||||
|
query ← readQuery
|
||||||
|
} yield (path, query)
|
||||||
|
case _ ⇒ sys.error("Not Implemented")
|
||||||
|
}
|
||||||
|
//#read-request-uri
|
||||||
|
|
||||||
|
//#read-path
|
||||||
|
def readPath = {
|
||||||
|
def step(segments: List[String]): IO.Iteratee[List[String]] = IO peek 1 flatMap {
|
||||||
|
case PATH ⇒ IO drop 1 flatMap (_ ⇒ readUriPart(pathchar) flatMap (segment ⇒ step(segment :: segments)))
|
||||||
|
case _ ⇒ segments match {
|
||||||
|
case "" :: rest ⇒ IO Done rest.reverse
|
||||||
|
case _ ⇒ IO Done segments.reverse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step(Nil)
|
||||||
|
}
|
||||||
|
//#read-path
|
||||||
|
|
||||||
|
//#read-query
|
||||||
|
def readQuery: IO.Iteratee[Option[String]] = IO peek 1 flatMap {
|
||||||
|
case QUERY ⇒ IO drop 1 flatMap (_ ⇒ readUriPart(querychar) map (Some(_)))
|
||||||
|
case _ ⇒ IO Done None
|
||||||
|
}
|
||||||
|
//#read-query
|
||||||
|
|
||||||
|
//#read-uri-part
|
||||||
|
val alpha = Set.empty ++ ('a' to 'z') ++ ('A' to 'Z') map (_.toByte)
|
||||||
|
val digit = Set.empty ++ ('0' to '9') map (_.toByte)
|
||||||
|
val hexdigit = digit ++ (Set.empty ++ ('a' to 'f') ++ ('A' to 'F') map (_.toByte))
|
||||||
|
val subdelim = Set('!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=') map (_.toByte)
|
||||||
|
val pathchar = alpha ++ digit ++ subdelim ++ (Set(':', '@') map (_.toByte))
|
||||||
|
val querychar = pathchar ++ (Set('/', '?') map (_.toByte))
|
||||||
|
|
||||||
|
def readUriPart(allowed: Set[Byte]): IO.Iteratee[String] = for {
|
||||||
|
str ← IO takeWhile allowed map ascii
|
||||||
|
pchar ← IO peek 1 map (_ == PERCENT)
|
||||||
|
all ← if (pchar) readPChar flatMap (ch ⇒ readUriPart(allowed) map (str + ch + _)) else IO Done str
|
||||||
|
} yield all
|
||||||
|
|
||||||
|
def readPChar = IO take 3 map {
|
||||||
|
case Seq('%', rest @ _*) if rest forall hexdigit ⇒
|
||||||
|
java.lang.Integer.parseInt(rest map (_.toChar) mkString, 16).toChar
|
||||||
|
}
|
||||||
|
//#read-uri-part
|
||||||
|
|
||||||
|
//#read-headers
|
||||||
|
def readHeaders = {
|
||||||
|
def step(found: List[Header]): IO.Iteratee[List[Header]] = {
|
||||||
|
IO peek 2 flatMap {
|
||||||
|
case CRLF ⇒ IO takeUntil CRLF flatMap (_ ⇒ IO Done found)
|
||||||
|
case _ ⇒ readHeader flatMap (header ⇒ step(header :: found))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step(Nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
def readHeader =
|
||||||
|
for {
|
||||||
|
name ← IO takeUntil COLON
|
||||||
|
value ← IO takeUntil CRLF flatMap readMultiLineValue
|
||||||
|
} yield Header(ascii(name), ascii(value))
|
||||||
|
|
||||||
|
def readMultiLineValue(initial: ByteString): IO.Iteratee[ByteString] = IO peek 1 flatMap {
|
||||||
|
case SP ⇒ IO takeUntil CRLF flatMap (bytes ⇒ readMultiLineValue(initial ++ bytes))
|
||||||
|
case _ ⇒ IO Done initial
|
||||||
|
}
|
||||||
|
//#read-headers
|
||||||
|
|
||||||
|
//#read-body
|
||||||
|
def readBody(headers: List[Header]) =
|
||||||
|
if (headers.exists(header ⇒ header.name == "Content-Length" || header.name == "Transfer-Encoding"))
|
||||||
|
IO.takeAll map (Some(_))
|
||||||
|
else
|
||||||
|
IO Done None
|
||||||
|
//#read-body
|
||||||
|
}
|
||||||
|
|
||||||
|
//#ok-response
|
||||||
|
object OKResponse {
|
||||||
|
import HttpConstants.CRLF
|
||||||
|
|
||||||
|
val okStatus = ByteString("HTTP/1.1 200 OK")
|
||||||
|
val contentType = ByteString("Content-Type: text/html; charset=utf-8")
|
||||||
|
val cacheControl = ByteString("Cache-Control: no-cache")
|
||||||
|
val date = ByteString("Date: ")
|
||||||
|
val server = ByteString("Server: Akka")
|
||||||
|
val contentLength = ByteString("Content-Length: ")
|
||||||
|
val connection = ByteString("Connection: ")
|
||||||
|
val keepAlive = ByteString("Keep-Alive")
|
||||||
|
val close = ByteString("Close")
|
||||||
|
|
||||||
|
def bytes(rsp: OKResponse) = {
|
||||||
|
new ByteStringBuilder ++=
|
||||||
|
okStatus ++= CRLF ++=
|
||||||
|
contentType ++= CRLF ++=
|
||||||
|
cacheControl ++= CRLF ++=
|
||||||
|
date ++= ByteString(new java.util.Date().toString) ++= CRLF ++=
|
||||||
|
server ++= CRLF ++=
|
||||||
|
contentLength ++= ByteString(rsp.body.length.toString) ++= CRLF ++=
|
||||||
|
connection ++= (if (rsp.keepAlive) keepAlive else close) ++= CRLF ++= CRLF ++= rsp.body result
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
case class OKResponse(body: ByteString, keepAlive: Boolean)
|
||||||
|
//#ok-response
|
||||||
|
|
||||||
|
object Main extends App {
|
||||||
|
val port = Option(System.getenv("PORT")) map (_.toInt) getOrElse 8080
|
||||||
|
val system = ActorSystem()
|
||||||
|
val server = system.actorOf(Props(new HttpServer(port)))
|
||||||
|
}
|
||||||
|
|
@ -22,5 +22,6 @@ Scala API
|
||||||
stm
|
stm
|
||||||
agents
|
agents
|
||||||
transactors
|
transactors
|
||||||
|
io
|
||||||
testing
|
testing
|
||||||
extending-akka
|
extending-akka
|
||||||
|
|
|
||||||
74
akka-docs/scala/io.rst
Normal file
74
akka-docs/scala/io.rst
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
.. _io-scala:
|
||||||
|
|
||||||
|
IO (Scala)
|
||||||
|
==========
|
||||||
|
|
||||||
|
.. sidebar:: Contents
|
||||||
|
|
||||||
|
.. contents:: :local:
|
||||||
|
|
||||||
|
Introduction
|
||||||
|
------------
|
||||||
|
|
||||||
|
This documentation is in progress. More to come.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
|
||||||
|
Http Server
|
||||||
|
^^^^^^^^^^^
|
||||||
|
|
||||||
|
Some commonly used constants:
|
||||||
|
|
||||||
|
.. includecode:: code/akka/docs/io/HTTPServer.scala
|
||||||
|
:include: constants
|
||||||
|
|
||||||
|
And case classes to hold the resulting request:
|
||||||
|
|
||||||
|
.. includecode:: code/akka/docs/io/HTTPServer.scala
|
||||||
|
:include: request-class
|
||||||
|
|
||||||
|
Now for our first ``Iteratee``. There are 3 main sections of a HTTP request: the request line, the headers, and an optional body. The main request ``Iteratee`` handles each section separately:
|
||||||
|
|
||||||
|
.. includecode:: code/akka/docs/io/HTTPServer.scala
|
||||||
|
:include: read-request
|
||||||
|
|
||||||
|
In the above code ``readRequest`` takes the results of 3 different ``Iteratees`` (``readRequestLine``, ``readHeaders``, ``readBody``) and combines them into a single ``Request`` object. ``readRequestLine`` actually returns a tuple, so we extract it's individual components. ``readBody`` depends on values contained within the header section, so we must pass those to the method.
|
||||||
|
|
||||||
|
The request line has 3 parts to it: the HTTP method, the requested URI, and the HTTP version. The parts are separated by a single space, and the entire request line ends with a ``CRLF``.
|
||||||
|
|
||||||
|
.. includecode:: code/akka/docs/io/HTTPServer.scala
|
||||||
|
:include: read-request-line
|
||||||
|
|
||||||
|
Reading the request method is simple as it is a single string ending in a space. The simple ``Iteratee`` that performs this is ``IO.takeUntil(delimiter: ByteString): Iteratee[ByteString]``. It keeps consuming input until the specified delimiter is found. Reading the HTTP version is also a simple string that ends with a ``CRLF``.
|
||||||
|
|
||||||
|
The ``ascii`` method is a helper that takes a ``ByteString`` and parses it as a ``US-ASCII`` ``String``.
|
||||||
|
|
||||||
|
Reading the request URI is a bit more complicated because we want to parse the individual components of the URI instead of just returning a simple string:
|
||||||
|
|
||||||
|
.. includecode:: code/akka/docs/io/HTTPServer.scala
|
||||||
|
:include: read-request-uri
|
||||||
|
|
||||||
|
For this example we are only interested in handling absolute paths. To detect if we the URI is an absolute path we use ``IO.peek(length: Int): Iteratee[ByteString]``, which returns a ``ByteString`` of the request length but doesn't actually consume the input. We peek at the next bit of input and see if it matches our ``PATH`` constant (defined above as ``ByteString("/")``). If it doesn't match we throw an error, but for a more robust solution we would want to handle other valid URIs.
|
||||||
|
|
||||||
|
Reading the URI path will be our most complex ``Iteratee``. It involves a recursive method that reads in each path segment of the URI:
|
||||||
|
|
||||||
|
.. includecode:: code/akka/docs/io/HTTPServer.scala
|
||||||
|
:include: read-path
|
||||||
|
|
||||||
|
The ``step`` method is a recursive method that takes a ``List`` of the accumulated path segments. It first checks if the remaining input starts with the ``PATH`` constant, and if it does, it drops that input, and returns the ``readUriPart`` ``Iteratee`` which has it's result added to the path segment accumulator and the ``step`` method is run again.
|
||||||
|
|
||||||
|
If after reading in a path segment the next input does not start with a path, we reverse the accumulated segments and return it (dropping the last segment if it is blank).
|
||||||
|
|
||||||
|
.. includecode:: code/akka/docs/io/HTTPServer.scala
|
||||||
|
:include: read-query
|
||||||
|
|
||||||
|
.. includecode:: code/akka/docs/io/HTTPServer.scala
|
||||||
|
:include: read-uri-part
|
||||||
|
|
||||||
|
.. includecode:: code/akka/docs/io/HTTPServer.scala
|
||||||
|
:include: read-headers
|
||||||
|
|
||||||
|
.. includecode:: code/akka/docs/io/HTTPServer.scala
|
||||||
|
:include: read-body
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue