Telnet is one of those really cool apps that will survive until the end of the Internet, which will happen right around the same time you have to duck for flying pigs. We’ll use this nifty little client to attach to our plane and ask it some questions.
For simplicity’s sake, we’ll build something that will help us grab the current heading and altitude from the plane by typing in a couple of obvious commands. It’ll look something like this:
-> telnet localhost 31733 Trying ::1...
Connected to localhost.
Escape character is 'ˆ]'.
Welcome to the Airplane!
---
Valid commands are: 'heading' and 'altitude'
> heading
current heading is: 359.92 degrees
> altitude
current altitude is: 345.25 feet
> Hiya there Dave!
What?
Pretty awesome looking, no?1 We’ll implement this with a standard ac- tor; therefore, it will be as reactive as we’re already used to and interact perfectly with everything else we have.
import akka.actor.{Actor, ActorRef, IO,
IOManager, ActorLogging, Props}
import akka.util.ByteString
import scala.collection.mutable.Map
class TelnetServer(plane: ActorRef) extends Actor
with ActorLogging { import TelnetServer._
// The 'subservers' stores the map of Actors-to-clients that we // need in order to route future communications
val subservers = Map.empty[IO.Handle, ActorRef]
// Opens the server's socket and starts listening for incoming // stuff
val serverSocket =
IOManager(context.system).listen("0.0.0.0", 31733)
def receive = {
// This message is sent by IO when our server officially // starts
case IO.Listening(server, address) =>
1There’s nothing more beautiful than a solid text-mode interface and anyone who claims otherwise deserves the evil eye.
log.info("Telnet Server listeninig on port {}", address) // When a client connects (e.g. telnet) we get this
// message
case IO.NewClient(server) =>
log.info("New incoming client connection on server") // You must accept the socket, which can pass to the sub // server as well as used as a 'key' into our map to know // where future communications come from
val socket = server.accept() socket.write(ByteString(welcome)) subservers +=
(socket ->
context.actorOf(Props(new SubServer(socket, plane)))) // Every time we get a message it comes in as a ByteString on // this message
case IO.Read(socket, bytes) =>
// Convert from ByteString to ascii (helper from companion) val cmd = ascii(bytes)
// Send the message to the subserver, looked up by socket subservers(socket) ! NewMessage(cmd)
// Client closed connection, kill the sub server case IO.Closed(socket, cause) =>
context.stop(subservers(socket)) subservers -= socket
} }
Simple, no? This class is the first of two; it’s the main server that handles incoming client connections as well as future socket handling. Every single byte of incoming data comes through theTelnetServer, even those bytes that are from client connections that have already been made. Therefore, our
TelnetServermust ensure that future incoming bytes are routed to the right actor that’s servicing that client, which Figure 13.1shows.
The IOManager will send our actor (specified to the IOManager’s
listenmethod by the implicitly definedself) several messages. We’re in- terested in the ones here that areIO.Listening,IO.Read, andIO.Closed. These three simple messages will give us everything we need in order to
Telnet Client A
Telnet Server Actor
Telent Sub Server Actor A
Telent Sub Server Actor B Telnet
Client B
"hea ding"
"heading"
"current heading is…"
Connect
Spa wn
Figure 13.1ãThe telnet server routes all incoming data to the right endpoint.
manage the incoming clients and make our server do what we want.2 The business logic belongs inside the sub-server, which we define as part of the TelnetServer’s companion object (along with some helper stuff).
Let’s have a look:
object TelnetServer {
// For the upcoming ask calls
implicit val askTimeout = Timeout(1.second) // The welcome message was sent on connection val welcome =
"""|Welcome to the Airplane!
|---
|
|Valid commands are: 'heading' and 'altitude'
|
|> """.stripMargin
// Simple method to convert from ByteString messages to // the Strings we know we're going to get
2I remember when writing a Telnet Server washard, and something that made people think you were a god of some sort. Damn.
def ascii(bytes: ByteString): String = { bytes.decodeString("UTF-8").trim }
// To ease the SubServer's implementation we will send it // Strings instead of ByteStrings that it would need to decode // anyway
case class NewMessage(msg: String)
// The SubServer. We give it the socket that it can use for // giving replies back to the telnet client and the plane to // which it will ask questions to get status
class SubServer(socket: IO.SocketHandle,
plane: ActorRef) extends Actor { import HeadingIndicator._
import Altimeter._
// Helpers just to make it easier to format for the book :) def headStr(head: Float): ByteString =
ByteString(f"current heading is: $head%3.2f degrees\n\n> ") def altStr(alt: Double): ByteString =
ByteString(f"current altitude is: $alt%5.2f feet\n\n> ") def unknown(str: String): ByteString =
ByteString(f"current $str is: unknown\n\n> ")
// Ask the Plane for the CurrentHeading and send it to the // client
def handleHeading() = {
(plane ? GetCurrentHeading).mapTo[CurrentHeading]
.onComplete {
case Success(CurrentHeading(heading)) =>
socket.write(headStr(heading)) case Failure(_) =>
socket.write(unknown("heading")) }
}
// Ask the Plane for the CurrentAltitude and send it to the // client
def handleAltitude() = {
(plane ? GetCurrentAltitude).mapTo[CurrentAltitude]
.onComplete {
case Success(CurrentAltitude(altitude)) =>
socket.write(altStr(altitude)) case Failure(_) =>
socket.write(unknown("altitude")) }
}
// Receive NewMessages and deal with them def receive = {
case NewMessage(msg) =>
msg match {
case "heading" =>
handleHeading() case "altitude" =>
handleAltitude() case m =>
socket.write(ByteString("What?\n\n")) }
} } }
In terms of IO, there’s really not much to it, but you can see that we’ve created a bit of plumbing in the plane. We can now ask it for the
CurrentAltitude and theCurrentHeading, which we tie inside of a fu- ture and side-effect the result back to the socket. We don’t need to examine these changes because they’re so obvious to you now, but while a picture is probably overkill at this point, let’s look at Figure 13.2.
To get it running, we just have to hook the plane up to it. The following implementation of the avionics main method will do nicely.
import akka.actor.{Props, ActorSystem}
object Avionics {
val system = ActorSystem.create("PlaneSimulation") def main(args: Array[String]) {
val plane = system.actorOf(Props(Plane()), "Plane")
val server = system.actorOf(Props(new TelnetServer(plane)),
"Telnet")
Plane
Altimeter
Heading Indicator Sub
Server
GetCurrentAltitude
forward CurrentAltitude
GetCurrentHeading
forward Curre
ntHeading
Figure 13.2ãThe plane relays messages the appropriate actors.
} }
Testing IO
Testing our TelnetServer is pretty darn simple. Since the TestKit and
ImplicitSenderturn our test class into what is effectively an actor all by itself, we can use the IO system to test the IO system. Let’s first create a simple plane that we can inject into the TelnetServerto make it easy to test:
class PlaneForTelnet extends Actor { import HeadingIndicator._
import Altimeter._
def receive = {
case GetCurrentAltitude =>
sender ! CurrentAltitude(52500f) case GetCurrentHeading =>
sender ! CurrentHeading(233.4f) }
}
That gives us a deterministic result that we can verify when we write to the socket. Now let’s write the test that uses the socket and goes the full way around the network to verify the results.
"TelnetServer" should {
"work" in {
val p = system.actorOf(Props[PlaneForTelnet]) val s = system.actorOf(Props(new TelnetServer(p)))
// The 'test' is now implicitly the Actor that the IOManager // talks to
val socket = IOManager(system).connect("localhost", 31733) // We expect the IOManager to send us IO.Connected
expectMsgType[IO.Connected]
// Skip the welcome message expectMsgType[IO.Read]
// Verify the "heading" command socket.write(ByteString("heading")) expectMsgPF() {
case IO.Read(_, bytes) =>
TelnetServer.ascii(bytes) must include ("233.40 degrees") }
// Verify the "altitude" command socket.write(ByteString("altitude")) expectMsgPF() {
case IO.Read(_, bytes) =>
TelnetServer.ascii(bytes) must include ("52500.00 feet") }
// Close it up socket.close() }
}
That’s it. We’ve tested theTelnetServerby using IO—definitely not a “unit” test, but quite awesome! It’s so easy, why wouldn’t you write this test?
IO actor integration
That’s all we’ll cover on the IO package’s actor integration. There’s a bit more to it, but not a ton. The bottom line is that it just fits. Network pro- gramming with the IO package and actors is almost like coding anything else with actors; it’s easy. If you want to make sure you have all the nuts and bolts nicely handled, head over to the Akka reference documentation and the ScalaDoc.
I will say that most, probably, wouldn’t really write a telnet-style of server for interrogating (or even manipulating) actors inside a running appli- cation; they would go for an HTTP implementation instead. Now, while you could whip up your own HTTP server with Akka IO, you wouldn’t. There are other fantastic HTTP servers that you can easily integrate with Akka and we talk about those later in the section discussing add-ons.