+str #17310: Basic framing support

This commit is contained in:
Endre Sándor Varga 2015-05-05 15:02:11 +02:00
parent e2eab532ab
commit 6318f3e972
9 changed files with 589 additions and 85 deletions

View file

@ -20,9 +20,10 @@ class RecipeParseLines extends RecipeSpec {
ByteString("\nHello Akka!\r\nHello Streams!"),
ByteString("\r\n\r\n")))
import RecipeParseLines._
val linesStream = rawData.transform(() => parseLines("\r\n", 100))
//#parse-lines
import akka.stream.io.Framing
val linesStream = rawData.via(Framing.lines("\r\n", maximumLineBytes = 100))
//#parse-lines
Await.result(linesStream.grouped(10).runWith(Sink.head), 3.seconds) should be(List(
"Hello World\r!",
@ -34,58 +35,3 @@ class RecipeParseLines extends RecipeSpec {
}
}
object RecipeParseLines {
import akka.stream.stage._
//#parse-lines
def parseLines(separator: String, maximumLineBytes: Int) =
new StatefulStage[ByteString, String] {
private val separatorBytes = ByteString(separator)
private val firstSeparatorByte = separatorBytes.head
private var buffer = ByteString.empty
private var nextPossibleMatch = 0
def initial = new State {
override def onPush(chunk: ByteString, ctx: Context[String]): SyncDirective = {
buffer ++= chunk
if (buffer.size > maximumLineBytes)
ctx.fail(new IllegalStateException(s"Read ${buffer.size} bytes " +
s"which is more than $maximumLineBytes without seeing a line terminator"))
else emit(doParse(Vector.empty).iterator, ctx)
}
@tailrec
private def doParse(parsedLinesSoFar: Vector[String]): Vector[String] = {
val possibleMatchPos = buffer.indexOf(firstSeparatorByte, from = nextPossibleMatch)
if (possibleMatchPos == -1) {
// No matching character, we need to accumulate more bytes into the buffer
nextPossibleMatch = buffer.size
parsedLinesSoFar
} else if (possibleMatchPos + separatorBytes.size > buffer.size) {
// We have found a possible match (we found the first character of the terminator
// sequence) but we don't have yet enough bytes. We remember the position to
// retry from next time.
nextPossibleMatch = possibleMatchPos
parsedLinesSoFar
} else {
if (buffer.slice(possibleMatchPos, possibleMatchPos + separatorBytes.size)
== separatorBytes) {
// Found a match
val parsedLine = buffer.slice(0, possibleMatchPos).utf8String
buffer = buffer.drop(possibleMatchPos + separatorBytes.size)
nextPossibleMatch -= possibleMatchPos + separatorBytes.size
doParse(parsedLinesSoFar :+ parsedLine)
} else {
nextPossibleMatch += 1
doParse(parsedLinesSoFar)
}
}
}
}
}
//#parse-lines
}

View file

@ -41,11 +41,13 @@ class StreamTcpDocSpec extends AkkaSpec {
Tcp().bind(localhost.getHostName, localhost.getPort) // TODO getHostString in Java7
//#echo-server-simple-handle
import akka.stream.io.Framing
connections runForeach { connection =>
println(s"New connection from: ${connection.remoteAddress}")
val echo = Flow[ByteString]
.transform(() => RecipeParseLines.parseLines("\n", maximumLineBytes = 256))
.via(Framing.lines("\n", maximumLineBytes = 256, allowTruncation = false))
.map(_ + "!!!\n")
.map(ByteString(_))
@ -60,7 +62,9 @@ class StreamTcpDocSpec extends AkkaSpec {
val connections = Tcp().bind(localhost.getHostName, localhost.getPort) // TODO getHostString in Java7
val serverProbe = TestProbe()
import akka.stream.io.Framing
//#welcome-banner-chat-server
connections runForeach { connection =>
val serverLogic = Flow() { implicit b =>
@ -81,7 +85,7 @@ class StreamTcpDocSpec extends AkkaSpec {
val welcome = Source.single(ByteString(welcomeMsg))
val echo = b.add(Flow[ByteString]
.transform(() => RecipeParseLines.parseLines("\n", maximumLineBytes = 256))
.via(Framing.lines("\n", maximumLineBytes = 256, allowTruncation = false))
//#welcome-banner-chat-server
.map { command serverProbe.ref ! command; command }
//#welcome-banner-chat-server
@ -101,6 +105,7 @@ class StreamTcpDocSpec extends AkkaSpec {
connection.handleWith(serverLogic)
}
import akka.stream.io.Framing
//#welcome-banner-chat-server
val input = new AtomicReference("Hello world" :: "What a lovely day" :: Nil)
@ -131,7 +136,7 @@ class StreamTcpDocSpec extends AkkaSpec {
}
val repl = Flow[ByteString]
.transform(() => RecipeParseLines.parseLines("\n", maximumLineBytes = 256))
.via(Framing.lines("\n", maximumLineBytes = 256, allowTruncation = false))
.map(text => println("Server: " + text))
.map(_ => readLine("> "))
.transform(() replParser)