The plane’s Telnet server

Một phần của tài liệu Artima akka concurrency (Trang 369 - 377)

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.

Một phần của tài liệu Artima akka concurrency (Trang 369 - 377)

Tải bản đầy đủ (PDF)

(515 trang)