|
|
@ -0,0 +1,38 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2013 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package akka.io
|
||||||
|
|
||||||
|
import akka.testkit.{ TestProbe, AkkaSpec }
|
||||||
|
import Tcp._
|
||||||
|
import TestUtils._
|
||||||
|
|
||||||
|
class CapacityLimitSpec extends AkkaSpec("akka.loglevel = ERROR\nakka.io.tcp.max-channels = 4")
|
||||||
|
with TcpIntegrationSpecSupport {
|
||||||
|
|
||||||
|
"The TCP transport implementation" should {
|
||||||
|
|
||||||
|
"reply with CommandFailed to a Bind or Connect command if max-channels capacity has been reached" in new TestSetup {
|
||||||
|
establishNewClientConnection()
|
||||||
|
|
||||||
|
// we now have three channels registered: a listener, a server connection and a client connection
|
||||||
|
// so register one more channel
|
||||||
|
val commander = TestProbe()
|
||||||
|
commander.send(IO(Tcp), Bind(bindHandler.ref, temporaryServerAddress()))
|
||||||
|
commander.expectMsg(Bound)
|
||||||
|
|
||||||
|
// we are now at the configured max-channel capacity of 4
|
||||||
|
|
||||||
|
val bindToFail = Bind(bindHandler.ref, temporaryServerAddress())
|
||||||
|
commander.send(IO(Tcp), bindToFail)
|
||||||
|
commander.expectMsgType[CommandFailed].cmd must be theSameInstanceAs (bindToFail)
|
||||||
|
|
||||||
|
val connectToFail = Connect(endpoint)
|
||||||
|
commander.send(IO(Tcp), connectToFail)
|
||||||
|
commander.expectMsgType[CommandFailed].cmd must be theSameInstanceAs (connectToFail)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
724
akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala
Normal file
|
|
@ -0,0 +1,724 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2012 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package akka.io
|
||||||
|
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.{ Socket, ConnectException, InetSocketAddress, SocketException }
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.channels.{ SelectionKey, Selector, ServerSocketChannel, SocketChannel }
|
||||||
|
import java.nio.channels.spi.SelectorProvider
|
||||||
|
import scala.annotation.tailrec
|
||||||
|
import scala.collection.immutable
|
||||||
|
import scala.concurrent.duration._
|
||||||
|
import scala.util.control.NonFatal
|
||||||
|
import org.scalatest.matchers._
|
||||||
|
import akka.io.Tcp._
|
||||||
|
import akka.io.SelectionHandler._
|
||||||
|
import TestUtils._
|
||||||
|
import akka.actor.{ ActorRef, PoisonPill, Terminated }
|
||||||
|
import akka.testkit.{ AkkaSpec, EventFilter, TestActorRef, TestProbe }
|
||||||
|
import akka.util.{ Helpers, ByteString }
|
||||||
|
import akka.actor.DeathPactException
|
||||||
|
import java.nio.channels.SelectionKey._
|
||||||
|
import akka.io.Inet.SocketOption
|
||||||
|
|
||||||
|
class TcpConnectionSpec extends AkkaSpec("akka.io.tcp.register-timeout = 500ms") {
|
||||||
|
val serverAddress = temporaryServerAddress()
|
||||||
|
|
||||||
|
// Helper to avoid Windows localization specific differences
|
||||||
|
def ignoreIfWindows(): Unit = {
|
||||||
|
if (Helpers.isWindows) {
|
||||||
|
info("Detected Windows: ignoring check")
|
||||||
|
pending
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"An outgoing connection" must {
|
||||||
|
// common behavior
|
||||||
|
|
||||||
|
"set socket options before connecting" in withLocalServer() { localServer ⇒
|
||||||
|
val userHandler = TestProbe()
|
||||||
|
val selector = TestProbe()
|
||||||
|
val connectionActor =
|
||||||
|
createConnectionActor(options = Vector(Inet.SO.ReuseAddress(true)))(selector.ref, userHandler.ref)
|
||||||
|
val clientChannel = connectionActor.underlyingActor.channel
|
||||||
|
clientChannel.socket.getReuseAddress must be(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
"set socket options after connecting" ignore withLocalServer() { localServer ⇒
|
||||||
|
// Workaround for systems where SO_KEEPALIVE is true by default
|
||||||
|
val userHandler = TestProbe()
|
||||||
|
val selector = TestProbe()
|
||||||
|
val connectionActor =
|
||||||
|
createConnectionActor(options = Vector(SO.KeepAlive(false)))(selector.ref, userHandler.ref)
|
||||||
|
val clientChannel = connectionActor.underlyingActor.channel
|
||||||
|
clientChannel.socket.getKeepAlive must be(true) // only set after connection is established
|
||||||
|
EventFilter.warning(pattern = "registration timeout", occurrences = 1) intercept {
|
||||||
|
selector.send(connectionActor, ChannelConnectable)
|
||||||
|
clientChannel.socket.getKeepAlive must be(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"send incoming data to the connection handler" in withEstablishedConnection() { setup ⇒
|
||||||
|
import setup._
|
||||||
|
serverSideChannel.write(ByteBuffer.wrap("testdata".getBytes("ASCII")))
|
||||||
|
|
||||||
|
expectReceivedString("testdata")
|
||||||
|
|
||||||
|
// have two packets in flight before the selector notices
|
||||||
|
serverSideChannel.write(ByteBuffer.wrap("testdata2".getBytes("ASCII")))
|
||||||
|
serverSideChannel.write(ByteBuffer.wrap("testdata3".getBytes("ASCII")))
|
||||||
|
|
||||||
|
expectReceivedString("testdata2testdata3")
|
||||||
|
}
|
||||||
|
|
||||||
|
"bundle incoming Received messages as long as more data is available" in withEstablishedConnection(
|
||||||
|
clientSocketOptions = List(Inet.SO.ReceiveBufferSize(1000000)) // to make sure enough data gets through
|
||||||
|
) { setup ⇒
|
||||||
|
import setup._
|
||||||
|
|
||||||
|
val DataSize = 1000000
|
||||||
|
val bigData = new Array[Byte](DataSize)
|
||||||
|
val buffer = ByteBuffer.wrap(bigData)
|
||||||
|
|
||||||
|
serverSideChannel.socket.setSendBufferSize(150000)
|
||||||
|
val wrote = serverSideChannel.write(buffer)
|
||||||
|
wrote must be > 140000
|
||||||
|
|
||||||
|
expectNoMsg(1000.millis) // data should have been transferred fully by now
|
||||||
|
|
||||||
|
selector.send(connectionActor, ChannelReadable)
|
||||||
|
|
||||||
|
// 140000 is more than the direct buffer size
|
||||||
|
connectionHandler.expectMsgType[Received].data.length must be > 140000
|
||||||
|
}
|
||||||
|
|
||||||
|
"receive data directly when the connection is established" in withUnacceptedConnection() { unregisteredSetup ⇒
|
||||||
|
import unregisteredSetup._
|
||||||
|
|
||||||
|
localServer.configureBlocking(true)
|
||||||
|
val serverSideChannel = localServer.accept()
|
||||||
|
serverSideChannel must not be (null)
|
||||||
|
serverSideChannel.write(ByteBuffer.wrap("immediatedata".getBytes("ASCII")))
|
||||||
|
serverSideChannel.configureBlocking(false)
|
||||||
|
|
||||||
|
selector.send(connectionActor, ChannelConnectable)
|
||||||
|
userHandler.expectMsg(Connected(serverAddress, clientSideChannel.socket.getLocalSocketAddress.asInstanceOf[InetSocketAddress]))
|
||||||
|
|
||||||
|
// we unrealistically register the selector here so that we can observe
|
||||||
|
// the ordering between Received and ReadInterest
|
||||||
|
userHandler.send(connectionActor, Register(selector.ref))
|
||||||
|
selector.expectMsgType[Received].data.decodeString("ASCII") must be("immediatedata")
|
||||||
|
selector.expectMsg(ReadInterest)
|
||||||
|
}
|
||||||
|
|
||||||
|
"write data to network (and acknowledge)" in withEstablishedConnection() { setup ⇒
|
||||||
|
import setup._
|
||||||
|
|
||||||
|
object Ack
|
||||||
|
val writer = TestProbe()
|
||||||
|
|
||||||
|
// directly acknowledge an empty write
|
||||||
|
writer.send(connectionActor, Write(ByteString.empty, Ack))
|
||||||
|
writer.expectMsg(Ack)
|
||||||
|
|
||||||
|
// reply to write commander with Ack
|
||||||
|
val ackedWrite = Write(ByteString("testdata"), Ack)
|
||||||
|
val buffer = ByteBuffer.allocate(100)
|
||||||
|
serverSideChannel.read(buffer) must be(0)
|
||||||
|
writer.send(connectionActor, ackedWrite)
|
||||||
|
writer.expectMsg(Ack)
|
||||||
|
serverSideChannel.read(buffer) must be(8)
|
||||||
|
buffer.flip()
|
||||||
|
|
||||||
|
// not reply to write commander for writes without Ack
|
||||||
|
val unackedWrite = Write(ByteString("morestuff!"))
|
||||||
|
buffer.clear()
|
||||||
|
serverSideChannel.read(buffer) must be(0)
|
||||||
|
writer.send(connectionActor, unackedWrite)
|
||||||
|
writer.expectNoMsg(500.millis)
|
||||||
|
serverSideChannel.read(buffer) must be(10)
|
||||||
|
buffer.flip()
|
||||||
|
ByteString(buffer).take(10).decodeString("ASCII") must be("morestuff!")
|
||||||
|
}
|
||||||
|
"write data after not acknowledged data" in withEstablishedConnection() { setup ⇒
|
||||||
|
import setup._
|
||||||
|
|
||||||
|
object Ack
|
||||||
|
val writer = TestProbe()
|
||||||
|
writer.send(connectionActor, Write(ByteString(42.toByte)))
|
||||||
|
writer.expectNoMsg(500.millis)
|
||||||
|
|
||||||
|
writer.send(connectionActor, Write(ByteString.empty, Ack))
|
||||||
|
writer.expectMsg(Ack)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Disabled on Windows: http://support.microsoft.com/kb/214397
|
||||||
|
*
|
||||||
|
* "To optimize performance at the application layer, Winsock copies data buffers from application send calls
|
||||||
|
* to a Winsock kernel buffer. Then, the stack uses its own heuristics (such as Nagle algorithm) to determine
|
||||||
|
* when to actually put the packet on the wire. You can change the amount of Winsock kernel buffer allocated to
|
||||||
|
* the socket using the SO_SNDBUF option (it is 8K by default). If necessary, Winsock can buffer significantly more
|
||||||
|
* than the SO_SNDBUF buffer size. In most cases, the send completion in the application only indicates the data
|
||||||
|
* buffer in an application send call is copied to the Winsock kernel buffer and does not indicate that the data
|
||||||
|
* has hit the network medium. The only exception is when you disable the Winsock buffering by setting
|
||||||
|
* SO_SNDBUF to 0."
|
||||||
|
*/
|
||||||
|
"stop writing in cases of backpressure and resume afterwards" in
|
||||||
|
withEstablishedConnection(clientSocketOptions = List(SO.ReceiveBufferSize(1000000))) { setup ⇒
|
||||||
|
info("Currently ignored as SO_SNDBUF is usually a lower bound on the send buffer so the test fails as no real " +
|
||||||
|
"backpressure present.")
|
||||||
|
pending
|
||||||
|
ignoreIfWindows()
|
||||||
|
import setup._
|
||||||
|
object Ack1
|
||||||
|
object Ack2
|
||||||
|
|
||||||
|
clientSideChannel.socket.setSendBufferSize(1024)
|
||||||
|
|
||||||
|
awaitCond(clientSideChannel.socket.getSendBufferSize == 1024)
|
||||||
|
|
||||||
|
val writer = TestProbe()
|
||||||
|
|
||||||
|
// producing backpressure by sending much more than currently fits into
|
||||||
|
// our send buffer
|
||||||
|
val firstWrite = writeCmd(Ack1)
|
||||||
|
|
||||||
|
// try to write the buffer but since the SO_SNDBUF is too small
|
||||||
|
// it will have to keep the rest of the piece and send it
|
||||||
|
// when possible
|
||||||
|
writer.send(connectionActor, firstWrite)
|
||||||
|
selector.expectMsg(WriteInterest)
|
||||||
|
|
||||||
|
// send another write which should fail immediately
|
||||||
|
// because we don't store more than one piece in flight
|
||||||
|
val secondWrite = writeCmd(Ack2)
|
||||||
|
writer.send(connectionActor, secondWrite)
|
||||||
|
writer.expectMsg(CommandFailed(secondWrite))
|
||||||
|
|
||||||
|
// reject even empty writes
|
||||||
|
writer.send(connectionActor, Write.Empty)
|
||||||
|
writer.expectMsg(CommandFailed(Write.Empty))
|
||||||
|
|
||||||
|
// there will be immediately more space in the send buffer because
|
||||||
|
// some data will have been sent by now, so we assume we can write
|
||||||
|
// again, but still it can't write everything
|
||||||
|
selector.send(connectionActor, ChannelWritable)
|
||||||
|
|
||||||
|
// both buffers should now be filled so no more writing
|
||||||
|
// is possible
|
||||||
|
pullFromServerSide(TestSize)
|
||||||
|
writer.expectMsg(Ack1)
|
||||||
|
}
|
||||||
|
|
||||||
|
"respect StopReading and ResumeReading" in withEstablishedConnection() { setup ⇒
|
||||||
|
import setup._
|
||||||
|
connectionHandler.send(connectionActor, StopReading)
|
||||||
|
|
||||||
|
// the selector interprets StopReading to deregister interest
|
||||||
|
// for reading
|
||||||
|
selector.expectMsg(DisableReadInterest)
|
||||||
|
connectionHandler.send(connectionActor, ResumeReading)
|
||||||
|
selector.expectMsg(ReadInterest)
|
||||||
|
}
|
||||||
|
|
||||||
|
"close the connection and reply with `Closed` upon reception of a `Close` command" in withEstablishedConnection(setSmallRcvBuffer) { setup ⇒
|
||||||
|
import setup._
|
||||||
|
|
||||||
|
// we should test here that a pending write command is properly finished first
|
||||||
|
object Ack
|
||||||
|
// set an artificially small send buffer size so that the write is queued
|
||||||
|
// inside the connection actor
|
||||||
|
clientSideChannel.socket.setSendBufferSize(1024)
|
||||||
|
|
||||||
|
// we send a write and a close command directly afterwards
|
||||||
|
connectionHandler.send(connectionActor, writeCmd(Ack))
|
||||||
|
val closeCommander = TestProbe()
|
||||||
|
closeCommander.send(connectionActor, Close)
|
||||||
|
|
||||||
|
pullFromServerSide(TestSize)
|
||||||
|
connectionHandler.expectMsg(Ack)
|
||||||
|
connectionHandler.expectMsg(Closed)
|
||||||
|
closeCommander.expectMsg(Closed)
|
||||||
|
assertThisConnectionActorTerminated()
|
||||||
|
|
||||||
|
serverSelectionKey must be(selectedAs(SelectionKey.OP_READ, 2.seconds))
|
||||||
|
|
||||||
|
val buffer = ByteBuffer.allocate(1)
|
||||||
|
serverSideChannel.read(buffer) must be(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
"send only one `Closed` event to the handler, if the handler commanded the Close" in withEstablishedConnection() { setup ⇒
|
||||||
|
import setup._
|
||||||
|
|
||||||
|
connectionHandler.send(connectionActor, Close)
|
||||||
|
connectionHandler.expectMsg(Closed)
|
||||||
|
connectionHandler.expectNoMsg(500.millis)
|
||||||
|
}
|
||||||
|
|
||||||
|
"abort the connection and reply with `Aborted` upong reception of an `Abort` command (simplified)" in withEstablishedConnection() { setup ⇒
|
||||||
|
import setup._
|
||||||
|
|
||||||
|
connectionHandler.send(connectionActor, Abort)
|
||||||
|
connectionHandler.expectMsg(Aborted)
|
||||||
|
|
||||||
|
assertThisConnectionActorTerminated()
|
||||||
|
|
||||||
|
val buffer = ByteBuffer.allocate(1)
|
||||||
|
val thrown = evaluating { serverSideChannel.read(buffer) } must produce[IOException]
|
||||||
|
}
|
||||||
|
|
||||||
|
"abort the connection and reply with `Aborted` upong reception of an `Abort` command" in withEstablishedConnection() { setup ⇒
|
||||||
|
ignoreIfWindows()
|
||||||
|
import setup._
|
||||||
|
|
||||||
|
connectionHandler.send(connectionActor, Abort)
|
||||||
|
connectionHandler.expectMsg(Aborted)
|
||||||
|
|
||||||
|
assertThisConnectionActorTerminated()
|
||||||
|
|
||||||
|
val buffer = ByteBuffer.allocate(1)
|
||||||
|
val thrown = evaluating { serverSideChannel.read(buffer) } must produce[IOException]
|
||||||
|
thrown.getMessage must be("Connection reset by peer")
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Partly disabled on Windows: http://support.microsoft.com/kb/214397
|
||||||
|
*
|
||||||
|
* "To optimize performance at the application layer, Winsock copies data buffers from application send calls
|
||||||
|
* to a Winsock kernel buffer. Then, the stack uses its own heuristics (such as Nagle algorithm) to determine
|
||||||
|
* when to actually put the packet on the wire. You can change the amount of Winsock kernel buffer allocated to
|
||||||
|
* the socket using the SO_SNDBUF option (it is 8K by default). If necessary, Winsock can buffer significantly more
|
||||||
|
* than the SO_SNDBUF buffer size. In most cases, the send completion in the application only indicates the data
|
||||||
|
* buffer in an application send call is copied to the Winsock kernel buffer and does not indicate that the data
|
||||||
|
* has hit the network medium. The only exception is when you disable the Winsock buffering by setting
|
||||||
|
* SO_SNDBUF to 0."
|
||||||
|
*/
|
||||||
|
"close the connection and reply with `ConfirmedClosed` upong reception of an `ConfirmedClose` command (simplified)" in withEstablishedConnection(setSmallRcvBuffer) { setup ⇒
|
||||||
|
import setup._
|
||||||
|
|
||||||
|
// we should test here that a pending write command is properly finished first
|
||||||
|
object Ack
|
||||||
|
// set an artificially small send buffer size so that the write is queued
|
||||||
|
// inside the connection actor
|
||||||
|
clientSideChannel.socket.setSendBufferSize(1024)
|
||||||
|
|
||||||
|
// we send a write and a close command directly afterwards
|
||||||
|
connectionHandler.send(connectionActor, writeCmd(Ack))
|
||||||
|
connectionHandler.send(connectionActor, ConfirmedClose)
|
||||||
|
|
||||||
|
pullFromServerSide(TestSize)
|
||||||
|
connectionHandler.expectMsg(Ack)
|
||||||
|
|
||||||
|
selector.send(connectionActor, ChannelReadable)
|
||||||
|
|
||||||
|
val buffer = ByteBuffer.allocate(1)
|
||||||
|
serverSelectionKey must be(selectedAs(SelectionKey.OP_READ, 2.seconds))
|
||||||
|
serverSideChannel.read(buffer) must be(-1)
|
||||||
|
serverSideChannel.close()
|
||||||
|
|
||||||
|
selector.send(connectionActor, ChannelReadable)
|
||||||
|
connectionHandler.expectMsg(ConfirmedClosed)
|
||||||
|
|
||||||
|
assertThisConnectionActorTerminated()
|
||||||
|
}
|
||||||
|
|
||||||
|
"close the connection and reply with `ConfirmedClosed` upong reception of an `ConfirmedClose` command" in withEstablishedConnection(setSmallRcvBuffer) { setup ⇒
|
||||||
|
ignoreIfWindows()
|
||||||
|
import setup._
|
||||||
|
|
||||||
|
// we should test here that a pending write command is properly finished first
|
||||||
|
object Ack
|
||||||
|
// set an artificially small send buffer size so that the write is queued
|
||||||
|
// inside the connection actor
|
||||||
|
clientSideChannel.socket.setSendBufferSize(1024)
|
||||||
|
|
||||||
|
// we send a write and a close command directly afterwards
|
||||||
|
connectionHandler.send(connectionActor, writeCmd(Ack))
|
||||||
|
connectionHandler.send(connectionActor, ConfirmedClose)
|
||||||
|
|
||||||
|
connectionHandler.expectNoMsg(100.millis)
|
||||||
|
pullFromServerSide(TestSize)
|
||||||
|
connectionHandler.expectMsg(Ack)
|
||||||
|
|
||||||
|
selector.send(connectionActor, ChannelReadable)
|
||||||
|
connectionHandler.expectNoMsg(100.millis) // not yet
|
||||||
|
|
||||||
|
val buffer = ByteBuffer.allocate(1)
|
||||||
|
serverSelectionKey must be(selectedAs(SelectionKey.OP_READ, 2.seconds))
|
||||||
|
serverSideChannel.read(buffer) must be(-1)
|
||||||
|
serverSideChannel.close()
|
||||||
|
|
||||||
|
selector.send(connectionActor, ChannelReadable)
|
||||||
|
connectionHandler.expectMsg(ConfirmedClosed)
|
||||||
|
|
||||||
|
assertThisConnectionActorTerminated()
|
||||||
|
}
|
||||||
|
|
||||||
|
"report when peer closed the connection" in withEstablishedConnection() { setup ⇒
|
||||||
|
import setup._
|
||||||
|
|
||||||
|
serverSideChannel.close()
|
||||||
|
selector.send(connectionActor, ChannelReadable)
|
||||||
|
connectionHandler.expectMsg(PeerClosed)
|
||||||
|
|
||||||
|
assertThisConnectionActorTerminated()
|
||||||
|
}
|
||||||
|
"report when peer aborted the connection (simplified)" in withEstablishedConnection() { setup ⇒
|
||||||
|
import setup._
|
||||||
|
|
||||||
|
EventFilter[IOException](occurrences = 1) intercept {
|
||||||
|
abortClose(serverSideChannel)
|
||||||
|
selector.send(connectionActor, ChannelReadable)
|
||||||
|
val err = connectionHandler.expectMsgType[ErrorClosed]
|
||||||
|
}
|
||||||
|
// wait a while
|
||||||
|
connectionHandler.expectNoMsg(200.millis)
|
||||||
|
|
||||||
|
assertThisConnectionActorTerminated()
|
||||||
|
}
|
||||||
|
|
||||||
|
"report when peer aborted the connection" in withEstablishedConnection() { setup ⇒
|
||||||
|
import setup._
|
||||||
|
ignoreIfWindows()
|
||||||
|
|
||||||
|
EventFilter[IOException](occurrences = 1) intercept {
|
||||||
|
abortClose(serverSideChannel)
|
||||||
|
selector.send(connectionActor, ChannelReadable)
|
||||||
|
val err = connectionHandler.expectMsgType[ErrorClosed]
|
||||||
|
err.cause must be("Connection reset by peer")
|
||||||
|
}
|
||||||
|
// wait a while
|
||||||
|
connectionHandler.expectNoMsg(200.millis)
|
||||||
|
|
||||||
|
assertThisConnectionActorTerminated()
|
||||||
|
}
|
||||||
|
"report when peer closed the connection when trying to write" in withEstablishedConnection() { setup ⇒
|
||||||
|
import setup._
|
||||||
|
|
||||||
|
val writer = TestProbe()
|
||||||
|
|
||||||
|
abortClose(serverSideChannel)
|
||||||
|
EventFilter[IOException](occurrences = 1) intercept {
|
||||||
|
writer.send(connectionActor, Write(ByteString("testdata")))
|
||||||
|
// bother writer and handler should get the message
|
||||||
|
writer.expectMsgType[ErrorClosed]
|
||||||
|
}
|
||||||
|
connectionHandler.expectMsgType[ErrorClosed]
|
||||||
|
|
||||||
|
assertThisConnectionActorTerminated()
|
||||||
|
}
|
||||||
|
|
||||||
|
// This tets is disabled on windows, as the assumption that not calling accept on a server socket means that
|
||||||
|
// no TCP level connection has been established with the client does not hold.
|
||||||
|
"report failed connection attempt while not accepted" in withUnacceptedConnection() { setup ⇒
|
||||||
|
import setup._
|
||||||
|
ignoreIfWindows()
|
||||||
|
// close instead of accept
|
||||||
|
localServer.close()
|
||||||
|
|
||||||
|
EventFilter[SocketException](occurrences = 1) intercept {
|
||||||
|
selector.send(connectionActor, ChannelConnectable)
|
||||||
|
val err = userHandler.expectMsgType[ErrorClosed]
|
||||||
|
err.cause must be("Connection reset by peer")
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyActorTermination(connectionActor)
|
||||||
|
}
|
||||||
|
|
||||||
|
val UnboundAddress = temporaryServerAddress()
|
||||||
|
"report failed connection attempt when target is unreachable (simplified)" in
|
||||||
|
withUnacceptedConnection(connectionActorCons = createConnectionActor(serverAddress = UnboundAddress)) { setup ⇒
|
||||||
|
import setup._
|
||||||
|
|
||||||
|
val sel = SelectorProvider.provider().openSelector()
|
||||||
|
val key = clientSideChannel.register(sel, SelectionKey.OP_CONNECT | SelectionKey.OP_READ)
|
||||||
|
// This timeout should be large enough to work on Windows
|
||||||
|
sel.select(3000)
|
||||||
|
|
||||||
|
key.isConnectable must be(true)
|
||||||
|
EventFilter[ConnectException](occurrences = 1) intercept {
|
||||||
|
selector.send(connectionActor, ChannelConnectable)
|
||||||
|
val err = userHandler.expectMsgType[ErrorClosed]
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyActorTermination(connectionActor)
|
||||||
|
}
|
||||||
|
|
||||||
|
"report failed connection attempt when target is unreachable" in
|
||||||
|
withUnacceptedConnection(connectionActorCons = createConnectionActor(serverAddress = UnboundAddress)) { setup ⇒
|
||||||
|
import setup._
|
||||||
|
ignoreIfWindows()
|
||||||
|
|
||||||
|
val sel = SelectorProvider.provider().openSelector()
|
||||||
|
val key = clientSideChannel.register(sel, SelectionKey.OP_CONNECT | SelectionKey.OP_READ)
|
||||||
|
// This timeout should be large enough to work on Windows
|
||||||
|
sel.select(3000)
|
||||||
|
|
||||||
|
key.isConnectable must be(true)
|
||||||
|
EventFilter[ConnectException](occurrences = 1) intercept {
|
||||||
|
selector.send(connectionActor, ChannelConnectable)
|
||||||
|
val err = userHandler.expectMsgType[ErrorClosed]
|
||||||
|
err.cause must be("Connection refused")
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyActorTermination(connectionActor)
|
||||||
|
}
|
||||||
|
|
||||||
|
"time out when Connected isn't answered with Register" in withUnacceptedConnection() { setup ⇒
|
||||||
|
import setup._
|
||||||
|
|
||||||
|
localServer.accept()
|
||||||
|
|
||||||
|
EventFilter.warning(pattern = "registration timeout", occurrences = 1) intercept {
|
||||||
|
selector.send(connectionActor, ChannelConnectable)
|
||||||
|
userHandler.expectMsg(Connected(serverAddress, clientSideChannel.socket.getLocalSocketAddress.asInstanceOf[InetSocketAddress]))
|
||||||
|
|
||||||
|
verifyActorTermination(connectionActor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"close the connection when user handler dies while connecting" in withUnacceptedConnection() { setup ⇒
|
||||||
|
import setup._
|
||||||
|
|
||||||
|
EventFilter[DeathPactException](occurrences = 1) intercept {
|
||||||
|
userHandler.ref ! PoisonPill
|
||||||
|
|
||||||
|
verifyActorTermination(connectionActor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"close the connection when connection handler dies while connected" in withEstablishedConnection() { setup ⇒
|
||||||
|
import setup._
|
||||||
|
watch(connectionHandler.ref)
|
||||||
|
watch(connectionActor)
|
||||||
|
EventFilter[DeathPactException](occurrences = 1) intercept {
|
||||||
|
system.stop(connectionHandler.ref)
|
||||||
|
expectMsgType[Terminated].actor must be(connectionHandler.ref)
|
||||||
|
expectMsgType[Terminated].actor must be(connectionActor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def withLocalServer(setServerSocketOptions: ServerSocketChannel ⇒ Unit = _ ⇒ ())(body: ServerSocketChannel ⇒ Any): Unit = {
|
||||||
|
val localServer = ServerSocketChannel.open()
|
||||||
|
try {
|
||||||
|
setServerSocketOptions(localServer)
|
||||||
|
localServer.socket.bind(serverAddress)
|
||||||
|
localServer.configureBlocking(false)
|
||||||
|
body(localServer)
|
||||||
|
} finally localServer.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
case class UnacceptedSetup(
|
||||||
|
localServer: ServerSocketChannel,
|
||||||
|
userHandler: TestProbe,
|
||||||
|
selector: TestProbe,
|
||||||
|
connectionActor: TestActorRef[TcpOutgoingConnection],
|
||||||
|
clientSideChannel: SocketChannel)
|
||||||
|
case class RegisteredSetup(
|
||||||
|
unregisteredSetup: UnacceptedSetup,
|
||||||
|
connectionHandler: TestProbe,
|
||||||
|
serverSideChannel: SocketChannel) {
|
||||||
|
def userHandler: TestProbe = unregisteredSetup.userHandler
|
||||||
|
def selector: TestProbe = unregisteredSetup.selector
|
||||||
|
def connectionActor: TestActorRef[TcpOutgoingConnection] = unregisteredSetup.connectionActor
|
||||||
|
def clientSideChannel: SocketChannel = unregisteredSetup.clientSideChannel
|
||||||
|
|
||||||
|
val nioSelector = SelectorProvider.provider().openSelector()
|
||||||
|
|
||||||
|
val clientSelectionKey = registerChannel(clientSideChannel, "client")
|
||||||
|
val serverSelectionKey = registerChannel(serverSideChannel, "server")
|
||||||
|
|
||||||
|
def registerChannel(channel: SocketChannel, name: String): SelectionKey = {
|
||||||
|
val res = channel.register(nioSelector, 0)
|
||||||
|
res.attach(name)
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
def checkFor(key: SelectionKey, interest: Int, millis: Int = 100): Boolean =
|
||||||
|
if (key.isValid) {
|
||||||
|
key.interestOps(interest)
|
||||||
|
nioSelector.selectedKeys().clear()
|
||||||
|
val ret = nioSelector.select(millis)
|
||||||
|
key.interestOps(0)
|
||||||
|
|
||||||
|
ret > 0 && nioSelector.selectedKeys().contains(key) && key.isValid &&
|
||||||
|
(key.readyOps() & interest) != 0
|
||||||
|
} else false
|
||||||
|
|
||||||
|
def openSelectorFor(channel: SocketChannel, interests: Int): (Selector, SelectionKey) = {
|
||||||
|
val sel = SelectorProvider.provider().openSelector()
|
||||||
|
val key = channel.register(sel, interests)
|
||||||
|
(sel, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
val buffer = ByteBuffer.allocate(TestSize)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to simultaneously act on client and server side to read from the server
|
||||||
|
* all pending data from the client.
|
||||||
|
*/
|
||||||
|
@tailrec final def pullFromServerSide(remaining: Int, remainingTries: Int = 1000): Unit =
|
||||||
|
if (remainingTries <= 0)
|
||||||
|
throw new AssertionError("Pulling took too many loops")
|
||||||
|
else if (remaining > 0) {
|
||||||
|
if (selector.msgAvailable) {
|
||||||
|
selector.expectMsg(WriteInterest)
|
||||||
|
clientSelectionKey.interestOps(SelectionKey.OP_WRITE)
|
||||||
|
}
|
||||||
|
|
||||||
|
serverSelectionKey.interestOps(SelectionKey.OP_READ)
|
||||||
|
nioSelector.select(10)
|
||||||
|
if (nioSelector.selectedKeys().contains(clientSelectionKey)) {
|
||||||
|
clientSelectionKey.interestOps(0)
|
||||||
|
selector.send(connectionActor, ChannelWritable)
|
||||||
|
}
|
||||||
|
|
||||||
|
val read =
|
||||||
|
if (nioSelector.selectedKeys().contains(serverSelectionKey)) tryReading()
|
||||||
|
else 0
|
||||||
|
|
||||||
|
nioSelector.selectedKeys().clear()
|
||||||
|
|
||||||
|
pullFromServerSide(remaining - read, remainingTries - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def tryReading(): Int = {
|
||||||
|
buffer.clear()
|
||||||
|
val read = serverSideChannel.read(buffer)
|
||||||
|
|
||||||
|
if (read == 0)
|
||||||
|
throw new IllegalStateException("Made no progress")
|
||||||
|
else if (read == -1)
|
||||||
|
throw new IllegalStateException("Connection was closed unexpectedly with remaining bytes " + remaining)
|
||||||
|
else read
|
||||||
|
}
|
||||||
|
|
||||||
|
@tailrec final def expectReceivedString(data: String): Unit = {
|
||||||
|
data.length must be > 0
|
||||||
|
|
||||||
|
selector.send(connectionActor, ChannelReadable)
|
||||||
|
|
||||||
|
val gotReceived = connectionHandler.expectMsgType[Received]
|
||||||
|
val receivedString = gotReceived.data.decodeString("ASCII")
|
||||||
|
data.startsWith(receivedString) must be(true)
|
||||||
|
if (receivedString.length < data.length)
|
||||||
|
expectReceivedString(data.drop(receivedString.length))
|
||||||
|
}
|
||||||
|
|
||||||
|
def assertThisConnectionActorTerminated(): Unit = {
|
||||||
|
verifyActorTermination(connectionActor)
|
||||||
|
clientSideChannel must not be ('open)
|
||||||
|
}
|
||||||
|
|
||||||
|
def selectedAs(interest: Int, duration: Duration): BeMatcher[SelectionKey] =
|
||||||
|
new BeMatcher[SelectionKey] {
|
||||||
|
def apply(key: SelectionKey) =
|
||||||
|
MatchResult(
|
||||||
|
checkFor(key, interest, duration.toMillis.toInt),
|
||||||
|
"%s key was not selected for %s after %s" format (key.attachment(), interestsDesc(interest), duration),
|
||||||
|
"%s key was selected for %s after %s" format (key.attachment(), interestsDesc(interest), duration))
|
||||||
|
}
|
||||||
|
|
||||||
|
val interestsNames = Seq(
|
||||||
|
SelectionKey.OP_ACCEPT -> "accepting",
|
||||||
|
SelectionKey.OP_CONNECT -> "connecting",
|
||||||
|
SelectionKey.OP_READ -> "reading",
|
||||||
|
SelectionKey.OP_WRITE -> "writing")
|
||||||
|
def interestsDesc(interests: Int): String =
|
||||||
|
interestsNames.filter(i ⇒ (i._1 & interests) != 0).map(_._2).mkString(", ")
|
||||||
|
}
|
||||||
|
def withUnacceptedConnection(
|
||||||
|
setServerSocketOptions: ServerSocketChannel ⇒ Unit = _ ⇒ (),
|
||||||
|
connectionActorCons: (ActorRef, ActorRef) ⇒ TestActorRef[TcpOutgoingConnection] = createConnectionActor())(body: UnacceptedSetup ⇒ Any): Unit =
|
||||||
|
|
||||||
|
withLocalServer(setServerSocketOptions) { localServer ⇒
|
||||||
|
val userHandler = TestProbe()
|
||||||
|
val selector = TestProbe()
|
||||||
|
val connectionActor = connectionActorCons(selector.ref, userHandler.ref)
|
||||||
|
val clientSideChannel = connectionActor.underlyingActor.channel
|
||||||
|
|
||||||
|
selector.expectMsg(RegisterChannel(clientSideChannel, OP_CONNECT))
|
||||||
|
|
||||||
|
body {
|
||||||
|
UnacceptedSetup(
|
||||||
|
localServer,
|
||||||
|
userHandler,
|
||||||
|
selector,
|
||||||
|
connectionActor,
|
||||||
|
clientSideChannel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
def withEstablishedConnection(
|
||||||
|
setServerSocketOptions: ServerSocketChannel ⇒ Unit = _ ⇒ (),
|
||||||
|
clientSocketOptions: immutable.Seq[SocketOption] = Nil)(body: RegisteredSetup ⇒ Any): Unit = withUnacceptedConnection(setServerSocketOptions, createConnectionActor(options = clientSocketOptions)) { unregisteredSetup ⇒
|
||||||
|
import unregisteredSetup._
|
||||||
|
|
||||||
|
localServer.configureBlocking(true)
|
||||||
|
val serverSideChannel = localServer.accept()
|
||||||
|
serverSideChannel.configureBlocking(false)
|
||||||
|
|
||||||
|
serverSideChannel must not be (null)
|
||||||
|
selector.send(connectionActor, ChannelConnectable)
|
||||||
|
userHandler.expectMsg(Connected(serverAddress, clientSideChannel.socket.getLocalSocketAddress.asInstanceOf[InetSocketAddress]))
|
||||||
|
|
||||||
|
val connectionHandler = TestProbe()
|
||||||
|
userHandler.send(connectionActor, Register(connectionHandler.ref))
|
||||||
|
selector.expectMsg(ReadInterest)
|
||||||
|
|
||||||
|
body {
|
||||||
|
RegisteredSetup(
|
||||||
|
unregisteredSetup,
|
||||||
|
connectionHandler,
|
||||||
|
serverSideChannel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val TestSize = 10000
|
||||||
|
|
||||||
|
def writeCmd(ack: AnyRef) =
|
||||||
|
Write(ByteString(Array.fill[Byte](TestSize)(0)), ack)
|
||||||
|
|
||||||
|
def setSmallRcvBuffer(channel: ServerSocketChannel): Unit =
|
||||||
|
channel.socket.setReceiveBufferSize(1024)
|
||||||
|
|
||||||
|
def createConnectionActor(
|
||||||
|
serverAddress: InetSocketAddress = serverAddress,
|
||||||
|
localAddress: Option[InetSocketAddress] = None,
|
||||||
|
options: immutable.Seq[SocketOption] = Nil)(
|
||||||
|
_selector: ActorRef,
|
||||||
|
commander: ActorRef): TestActorRef[TcpOutgoingConnection] = {
|
||||||
|
|
||||||
|
val ref = TestActorRef(
|
||||||
|
new TcpOutgoingConnection(Tcp(system), commander, Connect(serverAddress, localAddress, options)) {
|
||||||
|
override def postRestart(reason: Throwable) {
|
||||||
|
// ensure we never restart
|
||||||
|
context.stop(self)
|
||||||
|
}
|
||||||
|
override def selector = _selector
|
||||||
|
})
|
||||||
|
|
||||||
|
ref ! ChannelRegistered
|
||||||
|
ref
|
||||||
|
}
|
||||||
|
|
||||||
|
def abortClose(channel: SocketChannel): Unit = {
|
||||||
|
try channel.socket.setSoLinger(true, 0) // causes the following close() to send TCP RST
|
||||||
|
catch {
|
||||||
|
case NonFatal(e) ⇒
|
||||||
|
// setSoLinger can fail due to http://bugs.sun.com/view_bug.do?bug_id=6799574
|
||||||
|
// (also affected: OS/X Java 1.6.0_37)
|
||||||
|
log.debug("setSoLinger(true, 0) failed with {}", e)
|
||||||
|
}
|
||||||
|
channel.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
def abort(channel: SocketChannel) {
|
||||||
|
channel.socket.setSoLinger(true, 0)
|
||||||
|
channel.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2013 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package akka.io
|
||||||
|
|
||||||
|
import akka.testkit.AkkaSpec
|
||||||
|
import akka.util.ByteString
|
||||||
|
import Tcp._
|
||||||
|
import TestUtils._
|
||||||
|
import akka.testkit.EventFilter
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
class TcpIntegrationSpec extends AkkaSpec("akka.loglevel = INFO") with TcpIntegrationSpecSupport {
|
||||||
|
|
||||||
|
"The TCP transport implementation" should {
|
||||||
|
|
||||||
|
"properly bind a test server" in new TestSetup
|
||||||
|
|
||||||
|
"allow connecting to and disconnecting from the test server" in new TestSetup {
|
||||||
|
val (clientHandler, clientConnection, serverHandler, serverConnection) = establishNewClientConnection()
|
||||||
|
clientHandler.send(clientConnection, Close)
|
||||||
|
clientHandler.expectMsg(Closed)
|
||||||
|
serverHandler.expectMsg(PeerClosed)
|
||||||
|
verifyActorTermination(clientConnection)
|
||||||
|
verifyActorTermination(serverConnection)
|
||||||
|
}
|
||||||
|
|
||||||
|
"properly handle connection abort from one side" in new TestSetup {
|
||||||
|
val (clientHandler, clientConnection, serverHandler, serverConnection) = establishNewClientConnection()
|
||||||
|
EventFilter[IOException](occurrences = 1) intercept {
|
||||||
|
clientHandler.send(clientConnection, Abort)
|
||||||
|
}
|
||||||
|
clientHandler.expectMsg(Aborted)
|
||||||
|
serverHandler.expectMsgType[ErrorClosed]
|
||||||
|
verifyActorTermination(clientConnection)
|
||||||
|
verifyActorTermination(serverConnection)
|
||||||
|
}
|
||||||
|
|
||||||
|
"properly complete one client/server request/response cycle" in new TestSetup {
|
||||||
|
val (clientHandler, clientConnection, serverHandler, serverConnection) = establishNewClientConnection()
|
||||||
|
|
||||||
|
clientHandler.send(clientConnection, Write(ByteString("Captain on the bridge!"), 'Aye))
|
||||||
|
clientHandler.expectMsg('Aye)
|
||||||
|
serverHandler.expectMsgType[Received].data.decodeString("ASCII") must be("Captain on the bridge!")
|
||||||
|
|
||||||
|
serverHandler.send(serverConnection, Write(ByteString("For the king!"), 'Yes))
|
||||||
|
serverHandler.expectMsg('Yes)
|
||||||
|
clientHandler.expectMsgType[Received].data.decodeString("ASCII") must be("For the king!")
|
||||||
|
|
||||||
|
serverHandler.send(serverConnection, Close)
|
||||||
|
serverHandler.expectMsg(Closed)
|
||||||
|
clientHandler.expectMsg(PeerClosed)
|
||||||
|
|
||||||
|
verifyActorTermination(clientConnection)
|
||||||
|
verifyActorTermination(serverConnection)
|
||||||
|
}
|
||||||
|
|
||||||
|
"support waiting for writes with backpressure" in new TestSetup {
|
||||||
|
val (clientHandler, clientConnection, serverHandler, serverConnection) = establishNewClientConnection()
|
||||||
|
|
||||||
|
serverHandler.send(serverConnection, Write(ByteString(Array.fill[Byte](100000)(0)), 'Ack))
|
||||||
|
serverHandler.expectMsg('Ack)
|
||||||
|
|
||||||
|
expectReceivedData(clientHandler, 100000)
|
||||||
|
|
||||||
|
override def bindOptions = List(SO.SendBufferSize(1024))
|
||||||
|
override def connectOptions = List(SO.ReceiveBufferSize(1024))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2013 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package akka.io
|
||||||
|
|
||||||
|
import scala.annotation.tailrec
|
||||||
|
import akka.testkit.{ AkkaSpec, TestProbe }
|
||||||
|
import akka.actor.ActorRef
|
||||||
|
import scala.collection.immutable
|
||||||
|
import akka.io.Inet.SocketOption
|
||||||
|
import Tcp._
|
||||||
|
import TestUtils._
|
||||||
|
|
||||||
|
trait TcpIntegrationSpecSupport { _: AkkaSpec ⇒
|
||||||
|
|
||||||
|
class TestSetup {
|
||||||
|
val bindHandler = TestProbe()
|
||||||
|
val endpoint = temporaryServerAddress()
|
||||||
|
|
||||||
|
bindServer()
|
||||||
|
|
||||||
|
def bindServer(): Unit = {
|
||||||
|
val bindCommander = TestProbe()
|
||||||
|
bindCommander.send(IO(Tcp), Bind(bindHandler.ref, endpoint, options = bindOptions))
|
||||||
|
bindCommander.expectMsg(Bound)
|
||||||
|
}
|
||||||
|
|
||||||
|
def establishNewClientConnection(): (TestProbe, ActorRef, TestProbe, ActorRef) = {
|
||||||
|
val connectCommander = TestProbe()
|
||||||
|
connectCommander.send(IO(Tcp), Connect(endpoint, options = connectOptions))
|
||||||
|
val Connected(`endpoint`, localAddress) = connectCommander.expectMsgType[Connected]
|
||||||
|
val clientHandler = TestProbe()
|
||||||
|
connectCommander.sender ! Register(clientHandler.ref)
|
||||||
|
|
||||||
|
val Connected(`localAddress`, `endpoint`) = bindHandler.expectMsgType[Connected]
|
||||||
|
val serverHandler = TestProbe()
|
||||||
|
bindHandler.sender ! Register(serverHandler.ref)
|
||||||
|
|
||||||
|
(clientHandler, connectCommander.sender, serverHandler, bindHandler.sender)
|
||||||
|
}
|
||||||
|
|
||||||
|
@tailrec final def expectReceivedData(handler: TestProbe, remaining: Int): Unit =
|
||||||
|
if (remaining > 0) {
|
||||||
|
val recv = handler.expectMsgType[Received]
|
||||||
|
expectReceivedData(handler, remaining - recv.data.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** allow overriding socket options for server side channel */
|
||||||
|
def bindOptions: immutable.Traversable[SocketOption] = Nil
|
||||||
|
|
||||||
|
/** allow overriding socket options for client side channel */
|
||||||
|
def connectOptions: immutable.Traversable[SocketOption] = Nil
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
120
akka-actor-tests/src/test/scala/akka/io/TcpListenerSpec.scala
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2013 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package akka.io
|
||||||
|
|
||||||
|
import java.net.Socket
|
||||||
|
import scala.concurrent.duration._
|
||||||
|
import akka.actor.{ Terminated, SupervisorStrategy, Actor, Props }
|
||||||
|
import akka.testkit.{ TestProbe, TestActorRef, AkkaSpec }
|
||||||
|
import Tcp._
|
||||||
|
import akka.testkit.EventFilter
|
||||||
|
import akka.io.SelectionHandler._
|
||||||
|
import java.nio.channels.SelectionKey._
|
||||||
|
import akka.io.TcpListener.{ RegisterIncoming, FailedRegisterIncoming }
|
||||||
|
|
||||||
|
class TcpListenerSpec extends AkkaSpec("akka.io.tcp.batch-accept-limit = 2") {
|
||||||
|
|
||||||
|
"A TcpListener" must {
|
||||||
|
|
||||||
|
"register its ServerSocketChannel with its selector" in new TestSetup
|
||||||
|
|
||||||
|
"let the Bind commander know when binding is completed" in new TestSetup {
|
||||||
|
listener ! ChannelRegistered
|
||||||
|
bindCommander.expectMsg(Bound)
|
||||||
|
}
|
||||||
|
|
||||||
|
"accept acceptable connections and register them with its parent" in new TestSetup {
|
||||||
|
bindListener()
|
||||||
|
|
||||||
|
attemptConnectionToEndpoint()
|
||||||
|
attemptConnectionToEndpoint()
|
||||||
|
attemptConnectionToEndpoint()
|
||||||
|
|
||||||
|
def expectWorkerForCommand: Unit = {
|
||||||
|
selectorRouter.expectMsgPF() {
|
||||||
|
case WorkerForCommand(RegisterIncoming(chan), commander, _) ⇒
|
||||||
|
chan.isOpen must be(true)
|
||||||
|
commander must be === listener
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// since the batch-accept-limit is 2 we must only receive 2 accepted connections
|
||||||
|
listener ! ChannelAcceptable
|
||||||
|
|
||||||
|
parent.expectMsg(AcceptInterest)
|
||||||
|
expectWorkerForCommand
|
||||||
|
expectWorkerForCommand
|
||||||
|
selectorRouter.expectNoMsg(100.millis)
|
||||||
|
|
||||||
|
// and pick up the last remaining connection on the next ChannelAcceptable
|
||||||
|
listener ! ChannelAcceptable
|
||||||
|
expectWorkerForCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
"react to Unbind commands by replying with Unbound and stopping itself" in new TestSetup {
|
||||||
|
bindListener()
|
||||||
|
|
||||||
|
val unbindCommander = TestProbe()
|
||||||
|
unbindCommander.send(listener, Unbind)
|
||||||
|
|
||||||
|
unbindCommander.expectMsg(Unbound)
|
||||||
|
parent.expectMsgType[Terminated].actor must be(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
"drop an incoming connection if it cannot be registered with a selector" in new TestSetup {
|
||||||
|
bindListener()
|
||||||
|
|
||||||
|
attemptConnectionToEndpoint()
|
||||||
|
|
||||||
|
listener ! ChannelAcceptable
|
||||||
|
val channel = selectorRouter.expectMsgPF() {
|
||||||
|
case WorkerForCommand(RegisterIncoming(chan), commander, _) ⇒
|
||||||
|
chan.isOpen must be(true)
|
||||||
|
commander must be === listener
|
||||||
|
chan
|
||||||
|
}
|
||||||
|
|
||||||
|
EventFilter.warning(pattern = "selector capacity limit", occurrences = 1) intercept {
|
||||||
|
listener ! FailedRegisterIncoming(channel)
|
||||||
|
awaitCond(!channel.isOpen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val counter = Iterator.from(0)
|
||||||
|
|
||||||
|
class TestSetup { setup ⇒
|
||||||
|
val handler = TestProbe()
|
||||||
|
val handlerRef = handler.ref
|
||||||
|
val bindCommander = TestProbe()
|
||||||
|
val parent = TestProbe()
|
||||||
|
val selectorRouter = TestProbe()
|
||||||
|
val endpoint = TestUtils.temporaryServerAddress()
|
||||||
|
private val parentRef = TestActorRef(new ListenerParent)
|
||||||
|
|
||||||
|
parent.expectMsgType[RegisterChannel]
|
||||||
|
|
||||||
|
def bindListener() {
|
||||||
|
listener ! ChannelRegistered
|
||||||
|
bindCommander.expectMsg(Bound)
|
||||||
|
}
|
||||||
|
|
||||||
|
def attemptConnectionToEndpoint(): Unit = new Socket(endpoint.getHostName, endpoint.getPort)
|
||||||
|
|
||||||
|
def listener = parentRef.underlyingActor.listener
|
||||||
|
|
||||||
|
private class ListenerParent extends Actor {
|
||||||
|
val listener = context.actorOf(
|
||||||
|
props = Props(new TcpListener(selectorRouter.ref, Tcp(system), bindCommander.ref, Bind(handler.ref, endpoint, 100, Nil))),
|
||||||
|
name = "test-listener-" + counter.next())
|
||||||
|
parent.watch(listener)
|
||||||
|
def receive: Receive = {
|
||||||
|
case msg ⇒ parent.ref forward msg
|
||||||
|
}
|
||||||
|
override def supervisorStrategy = SupervisorStrategy.stoppingStrategy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
29
akka-actor-tests/src/test/scala/akka/io/TestUtils.scala
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2013 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package akka.io
|
||||||
|
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.nio.channels.ServerSocketChannel
|
||||||
|
import akka.actor.{ Terminated, ActorSystem, ActorRef }
|
||||||
|
import akka.testkit.TestProbe
|
||||||
|
|
||||||
|
object TestUtils {
|
||||||
|
|
||||||
|
def temporaryServerAddress(address: String = "127.0.0.1"): InetSocketAddress = {
|
||||||
|
val serverSocket = ServerSocketChannel.open()
|
||||||
|
try {
|
||||||
|
serverSocket.socket.bind(new InetSocketAddress(address, 0))
|
||||||
|
val port = serverSocket.socket.getLocalPort
|
||||||
|
new InetSocketAddress(address, port)
|
||||||
|
} finally serverSocket.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
def verifyActorTermination(actor: ActorRef)(implicit system: ActorSystem): Unit = {
|
||||||
|
val watcher = TestProbe()
|
||||||
|
watcher.watch(actor)
|
||||||
|
assert(watcher.expectMsgType[Terminated].actor == actor)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2013 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
package akka.io
|
||||||
|
|
||||||
|
import akka.testkit.{ TestProbe, ImplicitSender, AkkaSpec }
|
||||||
|
import TestUtils._
|
||||||
|
import akka.util.ByteString
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import akka.actor.ActorRef
|
||||||
|
|
||||||
|
class UdpConnIntegrationSpec extends AkkaSpec("akka.loglevel = INFO") with ImplicitSender {
|
||||||
|
|
||||||
|
def bindUdp(handler: ActorRef): (InetSocketAddress, ActorRef) = {
|
||||||
|
val address = temporaryServerAddress()
|
||||||
|
val commander = TestProbe()
|
||||||
|
commander.send(IO(UdpFF), UdpFF.Bind(handler, address))
|
||||||
|
commander.expectMsg(UdpFF.Bound)
|
||||||
|
(address, commander.sender)
|
||||||
|
}
|
||||||
|
|
||||||
|
def connectUdp(localAddress: Option[InetSocketAddress], remoteAddress: InetSocketAddress, handler: ActorRef): ActorRef = {
|
||||||
|
val commander = TestProbe()
|
||||||
|
commander.send(IO(UdpConn), UdpConn.Connect(handler, localAddress, remoteAddress, Nil))
|
||||||
|
commander.expectMsg(UdpConn.Connected)
|
||||||
|
commander.sender
|
||||||
|
}
|
||||||
|
|
||||||
|
"The UDP connection oriented implementation" must {
|
||||||
|
|
||||||
|
"be able to send and receive without binding" in {
|
||||||
|
val (serverAddress, server) = bindUdp(testActor)
|
||||||
|
val data1 = ByteString("To infinity and beyond!")
|
||||||
|
val data2 = ByteString("All your datagram belong to us")
|
||||||
|
connectUdp(localAddress = None, serverAddress, testActor) ! UdpConn.Send(data1)
|
||||||
|
|
||||||
|
val clientAddress = expectMsgPF() {
|
||||||
|
case UdpFF.Received(d, a) ⇒
|
||||||
|
d must be === data1
|
||||||
|
a
|
||||||
|
}
|
||||||
|
|
||||||
|
server ! UdpFF.Send(data2, clientAddress)
|
||||||
|
|
||||||
|
// FIXME: Currently this line fails
|
||||||
|
expectMsgPF() {
|
||||||
|
case UdpConn.Received(d) ⇒ d must be === data2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"be able to send and receive with binding" in {
|
||||||
|
val clientAddress = temporaryServerAddress()
|
||||||
|
val (serverAddress, server) = bindUdp(testActor)
|
||||||
|
val data1 = ByteString("To infinity and beyond!")
|
||||||
|
val data2 = ByteString("All your datagram belong to us")
|
||||||
|
connectUdp(Some(clientAddress), serverAddress, testActor) ! UdpConn.Send(data1)
|
||||||
|
|
||||||
|
expectMsgPF() {
|
||||||
|
case UdpFF.Received(d, a) ⇒
|
||||||
|
d must be === data1
|
||||||
|
a must be === clientAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
server ! UdpFF.Send(data2, clientAddress)
|
||||||
|
|
||||||
|
// FIXME: Currently this line fails
|
||||||
|
expectMsgPF() {
|
||||||
|
case UdpConn.Received(d) ⇒ d must be === data2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2013 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
package akka.io
|
||||||
|
|
||||||
|
import akka.testkit.{ TestProbe, ImplicitSender, AkkaSpec }
|
||||||
|
import akka.io.UdpFF._
|
||||||
|
import TestUtils._
|
||||||
|
import akka.util.ByteString
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import akka.actor.ActorRef
|
||||||
|
|
||||||
|
class UdpFFIntegrationSpec extends AkkaSpec("akka.loglevel = INFO") with ImplicitSender {
|
||||||
|
|
||||||
|
def bindUdp(handler: ActorRef): (InetSocketAddress, ActorRef) = {
|
||||||
|
val address = temporaryServerAddress()
|
||||||
|
val commander = TestProbe()
|
||||||
|
commander.send(IO(UdpFF), Bind(handler, address))
|
||||||
|
commander.expectMsg(Bound)
|
||||||
|
(address, commander.sender)
|
||||||
|
}
|
||||||
|
|
||||||
|
val simpleSender: ActorRef = {
|
||||||
|
val commander = TestProbe()
|
||||||
|
commander.send(IO(UdpFF), SimpleSender)
|
||||||
|
commander.expectMsg(SimpleSendReady)
|
||||||
|
commander.sender
|
||||||
|
}
|
||||||
|
|
||||||
|
"The UDP Fire-and-Forget implementation" must {
|
||||||
|
|
||||||
|
"be able to send without binding" in {
|
||||||
|
val (serverAddress, server) = bindUdp(testActor)
|
||||||
|
val data = ByteString("To infinity and beyond!")
|
||||||
|
simpleSender ! Send(data, serverAddress)
|
||||||
|
|
||||||
|
expectMsgType[Received].data must be === data
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
"be able to send with binding" in {
|
||||||
|
val (serverAddress, server) = bindUdp(testActor)
|
||||||
|
val (clientAddress, client) = bindUdp(testActor)
|
||||||
|
val data = ByteString("Fly little packet!")
|
||||||
|
|
||||||
|
client ! Send(data, serverAddress)
|
||||||
|
|
||||||
|
expectMsgPF() {
|
||||||
|
case Received(d, a) ⇒
|
||||||
|
d must be === data
|
||||||
|
a must be === clientAddress
|
||||||
|
}
|
||||||
|
server ! Send(data, clientAddress)
|
||||||
|
expectMsgPF() {
|
||||||
|
case Received(d, a) ⇒
|
||||||
|
d must be === data
|
||||||
|
a must be === serverAddress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -402,6 +402,213 @@ akka {
|
||||||
}
|
}
|
||||||
|
|
||||||
io {
|
io {
|
||||||
|
|
||||||
|
# By default the select loops run on dedicated threads, hence using a
|
||||||
|
# PinnedDispatcher
|
||||||
|
pinned-dispatcher {
|
||||||
|
type = "PinnedDispatcher"
|
||||||
|
executor = "thread-pool-executor"
|
||||||
|
thread-pool-executor.allow-core-pool-timeout = off
|
||||||
|
}
|
||||||
|
|
||||||
|
tcp {
|
||||||
|
|
||||||
|
# The number of selectors to stripe the served channels over; each of
|
||||||
|
# these will use one select loop on the selector-dispatcher.
|
||||||
|
nr-of-selectors = 1
|
||||||
|
|
||||||
|
# Maximum number of open channels supported by this TCP module; there is
|
||||||
|
# no intrinsic general limit, this setting is meant to enable DoS
|
||||||
|
# protection by limiting the number of concurrently connected clients.
|
||||||
|
# Also note that this is a "soft" limit; in certain cases the implementation
|
||||||
|
# will accept a few connections more or a few less than the number configured
|
||||||
|
# here. Must be an integer > 0 or "unlimited".
|
||||||
|
max-channels = 256000
|
||||||
|
|
||||||
|
# The select loop can be used in two modes:
|
||||||
|
# - setting "infinite" will select without a timeout, hogging a thread
|
||||||
|
# - setting a positive timeout will do a bounded select call,
|
||||||
|
# enabling sharing of a single thread between multiple selectors
|
||||||
|
# (in this case you will have to use a different configuration for the
|
||||||
|
# selector-dispatcher, e.g. using "type=Dispatcher" with size 1)
|
||||||
|
# - setting it to zero means polling, i.e. calling selectNow()
|
||||||
|
select-timeout = infinite
|
||||||
|
|
||||||
|
# When trying to assign a new connection to a selector and the chosen
|
||||||
|
# selector is at full capacity, retry selector choosing and assignment
|
||||||
|
# this many times before giving up
|
||||||
|
selector-association-retries = 10
|
||||||
|
|
||||||
|
# The maximum number of connection that are accepted in one go,
|
||||||
|
# higher numbers decrease latency, lower numbers increase fairness on
|
||||||
|
# the worker-dispatcher
|
||||||
|
batch-accept-limit = 10
|
||||||
|
|
||||||
|
# The number of bytes per direct buffer in the pool used to read or write
|
||||||
|
# network data from the kernel.
|
||||||
|
direct-buffer-size = 128 KiB
|
||||||
|
|
||||||
|
# The maximal number of direct buffers kept in the direct buffer pool for
|
||||||
|
# reuse.
|
||||||
|
direct-buffer-pool-limit = 1000
|
||||||
|
|
||||||
|
# The duration a connection actor waits for a `Register` message from
|
||||||
|
# its commander before aborting the connection.
|
||||||
|
register-timeout = 5s
|
||||||
|
|
||||||
|
# The maximum number of bytes delivered by a `Received` message. Before
|
||||||
|
# more data is read from the network the connection actor will try to
|
||||||
|
# do other work.
|
||||||
|
max-received-message-size = unlimited
|
||||||
|
|
||||||
|
# Enable fine grained logging of what goes on inside the implementation.
|
||||||
|
# Be aware that this may log more than once per message sent to the actors
|
||||||
|
# of the tcp implementation.
|
||||||
|
trace-logging = off
|
||||||
|
|
||||||
|
# Fully qualified config path which holds the dispatcher configuration
|
||||||
|
# to be used for running the select() calls in the selectors
|
||||||
|
selector-dispatcher = "akka.io.pinned-dispatcher"
|
||||||
|
|
||||||
|
# Fully qualified config path which holds the dispatcher configuration
|
||||||
|
# for the read/write worker actors
|
||||||
|
worker-dispatcher = "akka.actor.default-dispatcher"
|
||||||
|
|
||||||
|
# Fully qualified config path which holds the dispatcher configuration
|
||||||
|
# for the selector management actors
|
||||||
|
management-dispatcher = "akka.actor.default-dispatcher"
|
||||||
|
}
|
||||||
|
|
||||||
|
udp-fire-and-forget {
|
||||||
|
|
||||||
|
# The number of selectors to stripe the served channels over; each of
|
||||||
|
# these will use one select loop on the selector-dispatcher.
|
||||||
|
nr-of-selectors = 1
|
||||||
|
|
||||||
|
# Maximum number of open channels supported by this UDP module Generally
|
||||||
|
# UDP does not require a large number of channels, therefore it is
|
||||||
|
# recommended to keep this setting low.
|
||||||
|
max-channels = 4096
|
||||||
|
|
||||||
|
# The select loop can be used in two modes:
|
||||||
|
# - setting "infinite" will select without a timeout, hogging a thread
|
||||||
|
# - setting a positive timeout will do a bounded select call,
|
||||||
|
# enabling sharing of a single thread between multiple selectors
|
||||||
|
# (in this case you will have to use a different configuration for the
|
||||||
|
# selector-dispatcher, e.g. using "type=Dispatcher" with size 1)
|
||||||
|
# - setting it to zero means polling, i.e. calling selectNow()
|
||||||
|
select-timeout = infinite
|
||||||
|
|
||||||
|
# When trying to assign a new connection to a selector and the chosen
|
||||||
|
# selector is at full capacity, retry selector choosing and assignment
|
||||||
|
# this many times before giving up
|
||||||
|
selector-association-retries = 10
|
||||||
|
|
||||||
|
# The maximum number of datagrams that are read in one go,
|
||||||
|
# higher numbers decrease latency, lower numbers increase fairness on
|
||||||
|
# the worker-dispatcher
|
||||||
|
receive-throughput = 3
|
||||||
|
|
||||||
|
# The number of bytes per direct buffer in the pool used to read or write
|
||||||
|
# network data from the kernel.
|
||||||
|
direct-buffer-size = 128 KiB
|
||||||
|
|
||||||
|
# The maximal number of direct buffers kept in the direct buffer pool for
|
||||||
|
# reuse.
|
||||||
|
direct-buffer-pool-limit = 1000
|
||||||
|
|
||||||
|
# The maximum number of bytes delivered by a `Received` message. Before
|
||||||
|
# more data is read from the network the connection actor will try to
|
||||||
|
# do other work.
|
||||||
|
received-message-size-limit = unlimited
|
||||||
|
|
||||||
|
# Enable fine grained logging of what goes on inside the implementation.
|
||||||
|
# Be aware that this may log more than once per message sent to the actors
|
||||||
|
# of the tcp implementation.
|
||||||
|
trace-logging = off
|
||||||
|
|
||||||
|
# Fully qualified config path which holds the dispatcher configuration
|
||||||
|
# to be used for running the select() calls in the selectors
|
||||||
|
selector-dispatcher = "akka.io.pinned-dispatcher"
|
||||||
|
|
||||||
|
# Fully qualified config path which holds the dispatcher configuration
|
||||||
|
# for the read/write worker actors
|
||||||
|
worker-dispatcher = "akka.actor.default-dispatcher"
|
||||||
|
|
||||||
|
# Fully qualified config path which holds the dispatcher configuration
|
||||||
|
# for the selector management actors
|
||||||
|
management-dispatcher = "akka.actor.default-dispatcher"
|
||||||
|
}
|
||||||
|
|
||||||
|
udp-connection {
|
||||||
|
|
||||||
|
# The number of selectors to stripe the served channels over; each of
|
||||||
|
# these will use one select loop on the selector-dispatcher.
|
||||||
|
nr-of-selectors = 1
|
||||||
|
|
||||||
|
# Maximum number of open channels supported by this UDP module Generally
|
||||||
|
# UDP does not require a large number of channels, therefore it is
|
||||||
|
# recommended to keep this setting low.
|
||||||
|
max-channels = 4096
|
||||||
|
|
||||||
|
# The select loop can be used in two modes:
|
||||||
|
# - setting "infinite" will select without a timeout, hogging a thread
|
||||||
|
# - setting a positive timeout will do a bounded select call,
|
||||||
|
# enabling sharing of a single thread between multiple selectors
|
||||||
|
# (in this case you will have to use a different configuration for the
|
||||||
|
# selector-dispatcher, e.g. using "type=Dispatcher" with size 1)
|
||||||
|
# - setting it to zero means polling, i.e. calling selectNow()
|
||||||
|
select-timeout = infinite
|
||||||
|
|
||||||
|
# When trying to assign a new connection to a selector and the chosen
|
||||||
|
# selector is at full capacity, retry selector choosing and assignment
|
||||||
|
# this many times before giving up
|
||||||
|
selector-association-retries = 10
|
||||||
|
|
||||||
|
# The maximum number of datagrams that are read in one go,
|
||||||
|
# higher numbers decrease latency, lower numbers increase fairness on
|
||||||
|
# the worker-dispatcher
|
||||||
|
receive-throughput = 3
|
||||||
|
|
||||||
|
# The number of bytes per direct buffer in the pool used to read or write
|
||||||
|
# network data from the kernel.
|
||||||
|
direct-buffer-size = 128 KiB
|
||||||
|
|
||||||
|
# The maximal number of direct buffers kept in the direct buffer pool for
|
||||||
|
# reuse.
|
||||||
|
direct-buffer-pool-limit = 1000
|
||||||
|
|
||||||
|
# The maximum number of bytes delivered by a `Received` message. Before
|
||||||
|
# more data is read from the network the connection actor will try to
|
||||||
|
# do other work.
|
||||||
|
received-message-size-limit = unlimited
|
||||||
|
|
||||||
|
# Enable fine grained logging of what goes on inside the implementation.
|
||||||
|
# Be aware that this may log more than once per message sent to the actors
|
||||||
|
# of the tcp implementation.
|
||||||
|
trace-logging = off
|
||||||
|
|
||||||
|
# Fully qualified config path which holds the dispatcher configuration
|
||||||
|
# to be used for running the select() calls in the selectors
|
||||||
|
selector-dispatcher = "akka.io.pinned-dispatcher"
|
||||||
|
|
||||||
|
# Fully qualified config path which holds the dispatcher configuration
|
||||||
|
# for the read/write worker actors
|
||||||
|
worker-dispatcher = "akka.actor.default-dispatcher"
|
||||||
|
|
||||||
|
# Fully qualified config path which holds the dispatcher configuration
|
||||||
|
# for the selector management actors
|
||||||
|
management-dispatcher = "akka.actor.default-dispatcher"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# IMPORTANT NOTICE:
|
||||||
|
#
|
||||||
|
# The following settings belong to the deprecated akka.actor.IO
|
||||||
|
# implementation and will be removed once that is removed. They are not
|
||||||
|
# taken into account by the akka.io.* implementation, which is configured
|
||||||
|
# above!
|
||||||
|
|
||||||
# In bytes, the size of the shared read buffer. In the span 0b..2GiB.
|
# In bytes, the size of the shared read buffer. In the span 0b..2GiB.
|
||||||
#
|
#
|
||||||
read-buffer-size = 8KiB
|
read-buffer-size = 8KiB
|
||||||
|
|
@ -413,4 +620,6 @@ akka {
|
||||||
# 0 or negative means that the platform default will be used.
|
# 0 or negative means that the platform default will be used.
|
||||||
default-backlog = 1000
|
default-backlog = 1000
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ import akka.actor.IO.Chunk
|
||||||
* This is still in an experimental state and is subject to change until it
|
* This is still in an experimental state and is subject to change until it
|
||||||
* has received more real world testing.
|
* has received more real world testing.
|
||||||
*/
|
*/
|
||||||
|
@deprecated("use the new implementation in package akka.io instead", "2.2")
|
||||||
object IO {
|
object IO {
|
||||||
|
|
||||||
final class DivergentIterateeException extends IllegalStateException("Iteratees should not return a continuation when receiving EOF")
|
final class DivergentIterateeException extends IllegalStateException("Iteratees should not return a continuation when receiving EOF")
|
||||||
|
|
@ -833,6 +834,7 @@ final class IOManager private (system: ExtendedActorSystem) extends Extension {
|
||||||
* @param option Seq of [[akka.actor.IO.ServerSocketOptions]] to setup on socket
|
* @param option Seq of [[akka.actor.IO.ServerSocketOptions]] to setup on socket
|
||||||
* @return a [[akka.actor.IO.ServerHandle]] to uniquely identify the created socket
|
* @return a [[akka.actor.IO.ServerHandle]] to uniquely identify the created socket
|
||||||
*/
|
*/
|
||||||
|
@deprecated("use the new implementation in package akka.io instead", "2.2")
|
||||||
def listen(address: SocketAddress, options: immutable.Seq[IO.ServerSocketOption])(implicit owner: ActorRef): IO.ServerHandle = {
|
def listen(address: SocketAddress, options: immutable.Seq[IO.ServerSocketOption])(implicit owner: ActorRef): IO.ServerHandle = {
|
||||||
val server = IO.ServerHandle(owner, actor)
|
val server = IO.ServerHandle(owner, actor)
|
||||||
actor ! IO.Listen(server, address, options)
|
actor ! IO.Listen(server, address, options)
|
||||||
|
|
@ -848,6 +850,7 @@ final class IOManager private (system: ExtendedActorSystem) extends Extension {
|
||||||
* @param owner the ActorRef that will receive messages from the IOManagerActor
|
* @param owner the ActorRef that will receive messages from the IOManagerActor
|
||||||
* @return a [[akka.actor.IO.ServerHandle]] to uniquely identify the created socket
|
* @return a [[akka.actor.IO.ServerHandle]] to uniquely identify the created socket
|
||||||
*/
|
*/
|
||||||
|
@deprecated("use the new implementation in package akka.io instead", "2.2")
|
||||||
def listen(address: SocketAddress)(implicit owner: ActorRef): IO.ServerHandle = listen(address, Nil)
|
def listen(address: SocketAddress)(implicit owner: ActorRef): IO.ServerHandle = listen(address, Nil)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -861,6 +864,7 @@ final class IOManager private (system: ExtendedActorSystem) extends Extension {
|
||||||
* @param owner the ActorRef that will receive messages from the IOManagerActor
|
* @param owner the ActorRef that will receive messages from the IOManagerActor
|
||||||
* @return a [[akka.actor.IO.ServerHandle]] to uniquely identify the created socket
|
* @return a [[akka.actor.IO.ServerHandle]] to uniquely identify the created socket
|
||||||
*/
|
*/
|
||||||
|
@deprecated("use the new implementation in package akka.io instead", "2.2")
|
||||||
def listen(host: String, port: Int, options: immutable.Seq[IO.ServerSocketOption] = Nil)(implicit owner: ActorRef): IO.ServerHandle =
|
def listen(host: String, port: Int, options: immutable.Seq[IO.ServerSocketOption] = Nil)(implicit owner: ActorRef): IO.ServerHandle =
|
||||||
listen(new InetSocketAddress(host, port), options)(owner)
|
listen(new InetSocketAddress(host, port), options)(owner)
|
||||||
|
|
||||||
|
|
@ -874,6 +878,7 @@ final class IOManager private (system: ExtendedActorSystem) extends Extension {
|
||||||
* @param owner the ActorRef that will receive messages from the IOManagerActor
|
* @param owner the ActorRef that will receive messages from the IOManagerActor
|
||||||
* @return a [[akka.actor.IO.SocketHandle]] to uniquely identify the created socket
|
* @return a [[akka.actor.IO.SocketHandle]] to uniquely identify the created socket
|
||||||
*/
|
*/
|
||||||
|
@deprecated("use the new implementation in package akka.io instead", "2.2")
|
||||||
def connect(address: SocketAddress, options: immutable.Seq[IO.SocketOption] = Nil)(implicit owner: ActorRef): IO.SocketHandle = {
|
def connect(address: SocketAddress, options: immutable.Seq[IO.SocketOption] = Nil)(implicit owner: ActorRef): IO.SocketHandle = {
|
||||||
val socket = IO.SocketHandle(owner, actor)
|
val socket = IO.SocketHandle(owner, actor)
|
||||||
actor ! IO.Connect(socket, address, options)
|
actor ! IO.Connect(socket, address, options)
|
||||||
|
|
@ -891,6 +896,7 @@ final class IOManager private (system: ExtendedActorSystem) extends Extension {
|
||||||
* @param owner the ActorRef that will receive messages from the IOManagerActor
|
* @param owner the ActorRef that will receive messages from the IOManagerActor
|
||||||
* @return a [[akka.actor.IO.SocketHandle]] to uniquely identify the created socket
|
* @return a [[akka.actor.IO.SocketHandle]] to uniquely identify the created socket
|
||||||
*/
|
*/
|
||||||
|
@deprecated("use the new implementation in package akka.io instead", "2.2")
|
||||||
def connect(host: String, port: Int)(implicit owner: ActorRef): IO.SocketHandle =
|
def connect(host: String, port: Int)(implicit owner: ActorRef): IO.SocketHandle =
|
||||||
connect(new InetSocketAddress(host, port))(owner)
|
connect(new InetSocketAddress(host, port))(owner)
|
||||||
|
|
||||||
|
|
|
||||||
65
akka-actor/src/main/scala/akka/io/DirectByteBufferPool.scala
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2013 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package akka.io
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import annotation.tailrec
|
||||||
|
|
||||||
|
trait BufferPool {
|
||||||
|
def acquire(): ByteBuffer
|
||||||
|
def release(buf: ByteBuffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A buffer pool which keeps a free list of direct buffers of a specified default
|
||||||
|
* size in a simple fixed size stack.
|
||||||
|
*
|
||||||
|
* If the stack is full a buffer offered back is not kept but will be let for
|
||||||
|
* being freed by normal garbage collection.
|
||||||
|
*/
|
||||||
|
private[akka] class DirectByteBufferPool(defaultBufferSize: Int, maxPoolEntries: Int) extends BufferPool {
|
||||||
|
private[this] val locked = new AtomicBoolean(false)
|
||||||
|
private[this] val pool: Array[ByteBuffer] = new Array[ByteBuffer](maxPoolEntries)
|
||||||
|
private[this] var buffersInPool: Int = 0
|
||||||
|
|
||||||
|
def acquire(): ByteBuffer =
|
||||||
|
takeBufferFromPool()
|
||||||
|
|
||||||
|
def release(buf: ByteBuffer): Unit =
|
||||||
|
offerBufferToPool(buf)
|
||||||
|
|
||||||
|
private def allocate(size: Int): ByteBuffer =
|
||||||
|
ByteBuffer.allocateDirect(size)
|
||||||
|
|
||||||
|
@tailrec
|
||||||
|
private final def takeBufferFromPool(): ByteBuffer =
|
||||||
|
if (locked.compareAndSet(false, true)) {
|
||||||
|
val buffer =
|
||||||
|
try if (buffersInPool > 0) {
|
||||||
|
buffersInPool -= 1
|
||||||
|
pool(buffersInPool)
|
||||||
|
} else null
|
||||||
|
finally locked.set(false)
|
||||||
|
|
||||||
|
// allocate new and clear outside the lock
|
||||||
|
if (buffer == null)
|
||||||
|
allocate(defaultBufferSize)
|
||||||
|
else {
|
||||||
|
buffer.clear()
|
||||||
|
buffer
|
||||||
|
}
|
||||||
|
} else takeBufferFromPool() // spin while locked
|
||||||
|
|
||||||
|
@tailrec
|
||||||
|
private final def offerBufferToPool(buf: ByteBuffer): Unit =
|
||||||
|
if (locked.compareAndSet(false, true))
|
||||||
|
try if (buffersInPool < maxPoolEntries) {
|
||||||
|
pool(buffersInPool) = buf
|
||||||
|
buffersInPool += 1
|
||||||
|
} // else let the buffer be gc'd
|
||||||
|
finally locked.set(false)
|
||||||
|
else offerBufferToPool(buf) // spin while locked
|
||||||
|
}
|
||||||
41
akka-actor/src/main/scala/akka/io/IO.scala
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2013 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package akka.io
|
||||||
|
|
||||||
|
import akka.actor._
|
||||||
|
import akka.routing.RandomRouter
|
||||||
|
import akka.io.SelectionHandler.WorkerForCommand
|
||||||
|
|
||||||
|
object IO {
|
||||||
|
|
||||||
|
trait Extension extends akka.actor.Extension {
|
||||||
|
def manager: ActorRef
|
||||||
|
}
|
||||||
|
|
||||||
|
def apply[T <: Extension](key: ExtensionKey[T])(implicit system: ActorSystem): ActorRef = key(system).manager
|
||||||
|
|
||||||
|
trait HasFailureMessage {
|
||||||
|
def failureMessage: Any
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class SelectorBasedManager(selectorSettings: SelectionHandlerSettings, nrOfSelectors: Int) extends Actor {
|
||||||
|
|
||||||
|
val selectorPool = context.actorOf(
|
||||||
|
props = Props(new SelectionHandler(self, selectorSettings)).withRouter(RandomRouter(nrOfSelectors)),
|
||||||
|
name = "selectors")
|
||||||
|
|
||||||
|
private def createWorkerMessage(pf: PartialFunction[HasFailureMessage, Props]): PartialFunction[HasFailureMessage, WorkerForCommand] = {
|
||||||
|
case cmd ⇒
|
||||||
|
val props = pf(cmd)
|
||||||
|
val commander = sender
|
||||||
|
WorkerForCommand(cmd, commander, props)
|
||||||
|
}
|
||||||
|
|
||||||
|
def workerForCommandHandler(pf: PartialFunction[Any, Props]): Receive = {
|
||||||
|
case cmd: HasFailureMessage if pf.isDefinedAt(cmd) ⇒ selectorPool ! createWorkerMessage(pf)(cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
89
akka-actor/src/main/scala/akka/io/Inet.scala
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2013 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
package akka.io
|
||||||
|
|
||||||
|
import java.net.{ DatagramSocket, Socket, ServerSocket }
|
||||||
|
|
||||||
|
object Inet {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SocketOption is a package of data (from the user) and associated
|
||||||
|
* behavior (how to apply that to a socket).
|
||||||
|
*/
|
||||||
|
trait SocketOption {
|
||||||
|
|
||||||
|
def beforeDatagramBind(ds: DatagramSocket): Unit = ()
|
||||||
|
|
||||||
|
def beforeServerSocketBind(ss: ServerSocket): Unit = ()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action to be taken for this option before calling connect()
|
||||||
|
*/
|
||||||
|
def beforeConnect(s: Socket): Unit = ()
|
||||||
|
/**
|
||||||
|
* Action to be taken for this option after connect returned (i.e. on
|
||||||
|
* the slave socket for servers).
|
||||||
|
*/
|
||||||
|
def afterConnect(s: Socket): Unit = ()
|
||||||
|
}
|
||||||
|
|
||||||
|
object SO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [[akka.io.Tcp.SocketOption]] to set the SO_RCVBUF option
|
||||||
|
*
|
||||||
|
* For more information see [[java.net.Socket.setReceiveBufferSize]]
|
||||||
|
*/
|
||||||
|
case class ReceiveBufferSize(size: Int) extends SocketOption {
|
||||||
|
require(size > 0, "ReceiveBufferSize must be > 0")
|
||||||
|
override def beforeServerSocketBind(s: ServerSocket): Unit = s.setReceiveBufferSize(size)
|
||||||
|
override def beforeDatagramBind(s: DatagramSocket): Unit = s.setReceiveBufferSize(size)
|
||||||
|
override def beforeConnect(s: Socket): Unit = s.setReceiveBufferSize(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// server socket options
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [[akka.io.Tcp.SocketOption]] to enable or disable SO_REUSEADDR
|
||||||
|
*
|
||||||
|
* For more information see [[java.net.Socket.setReuseAddress]]
|
||||||
|
*/
|
||||||
|
case class ReuseAddress(on: Boolean) extends SocketOption {
|
||||||
|
override def beforeServerSocketBind(s: ServerSocket): Unit = s.setReuseAddress(on)
|
||||||
|
override def beforeDatagramBind(s: DatagramSocket): Unit = s.setReuseAddress(on)
|
||||||
|
override def beforeConnect(s: Socket): Unit = s.setReuseAddress(on)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [[akka.io.Tcp.SocketOption]] to set the SO_SNDBUF option.
|
||||||
|
*
|
||||||
|
* For more information see [[java.net.Socket.setSendBufferSize]]
|
||||||
|
*/
|
||||||
|
case class SendBufferSize(size: Int) extends SocketOption {
|
||||||
|
require(size > 0, "SendBufferSize must be > 0")
|
||||||
|
override def afterConnect(s: Socket): Unit = s.setSendBufferSize(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [[akka.io.Tcp.SocketOption]] to set the traffic class or
|
||||||
|
* type-of-service octet in the IP header for packets sent from this
|
||||||
|
* socket.
|
||||||
|
*
|
||||||
|
* For more information see [[java.net.Socket.setTrafficClass]]
|
||||||
|
*/
|
||||||
|
case class TrafficClass(tc: Int) extends SocketOption {
|
||||||
|
require(0 <= tc && tc <= 255, "TrafficClass needs to be in the interval [0, 255]")
|
||||||
|
override def afterConnect(s: Socket): Unit = s.setTrafficClass(tc)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
trait SoForwarders {
|
||||||
|
val ReceiveBufferSize = SO.ReceiveBufferSize
|
||||||
|
val ReuseAddress = SO.ReuseAddress
|
||||||
|
val SendBufferSize = SO.SendBufferSize
|
||||||
|
val TrafficClass = SO.TrafficClass
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
225
akka-actor/src/main/scala/akka/io/SelectionHandler.scala
Normal file
|
|
@ -0,0 +1,225 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2012 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package akka.io
|
||||||
|
|
||||||
|
import java.lang.Runnable
|
||||||
|
import java.nio.channels.spi.SelectorProvider
|
||||||
|
import java.nio.channels.{ SelectableChannel, SelectionKey }
|
||||||
|
import java.nio.channels.SelectionKey._
|
||||||
|
import scala.util.control.NonFatal
|
||||||
|
import scala.collection.immutable
|
||||||
|
import scala.concurrent.duration._
|
||||||
|
import akka.actor._
|
||||||
|
import com.typesafe.config.Config
|
||||||
|
import akka.actor.Terminated
|
||||||
|
import akka.io.IO.HasFailureMessage
|
||||||
|
|
||||||
|
abstract class SelectionHandlerSettings(config: Config) {
|
||||||
|
import config._
|
||||||
|
|
||||||
|
val MaxChannels = getString("max-channels") match {
|
||||||
|
case "unlimited" ⇒ -1
|
||||||
|
case _ ⇒ getInt("max-channels")
|
||||||
|
}
|
||||||
|
val SelectTimeout = getString("select-timeout") match {
|
||||||
|
case "infinite" ⇒ Duration.Inf
|
||||||
|
case x ⇒ Duration(x)
|
||||||
|
}
|
||||||
|
val SelectorAssociationRetries = getInt("selector-association-retries")
|
||||||
|
|
||||||
|
val SelectorDispatcher = getString("selector-dispatcher")
|
||||||
|
val WorkerDispatcher = getString("worker-dispatcher")
|
||||||
|
val TraceLogging = getBoolean("trace-logging")
|
||||||
|
|
||||||
|
require(MaxChannels == -1 || MaxChannels > 0, "max-channels must be > 0 or 'unlimited'")
|
||||||
|
require(SelectTimeout >= Duration.Zero, "select-timeout must not be negative")
|
||||||
|
require(SelectorAssociationRetries >= 0, "selector-association-retries must be >= 0")
|
||||||
|
|
||||||
|
def MaxChannelsPerSelector: Int
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private[io] object SelectionHandler {
|
||||||
|
|
||||||
|
case class WorkerForCommand(apiCommand: HasFailureMessage, commander: ActorRef, childProps: Props)
|
||||||
|
|
||||||
|
case class RegisterChannel(channel: SelectableChannel, initialOps: Int)
|
||||||
|
case object ChannelRegistered
|
||||||
|
case class Retry(command: WorkerForCommand, retriesLeft: Int) { require(retriesLeft >= 0) }
|
||||||
|
|
||||||
|
case object ChannelConnectable
|
||||||
|
case object ChannelAcceptable
|
||||||
|
case object ChannelReadable
|
||||||
|
case object ChannelWritable
|
||||||
|
case object AcceptInterest
|
||||||
|
case object ReadInterest
|
||||||
|
case object DisableReadInterest
|
||||||
|
case object WriteInterest
|
||||||
|
}
|
||||||
|
|
||||||
|
private[io] class SelectionHandler(manager: ActorRef, settings: SelectionHandlerSettings) extends Actor with ActorLogging {
|
||||||
|
import SelectionHandler._
|
||||||
|
import settings._
|
||||||
|
|
||||||
|
@volatile var childrenKeys = immutable.HashMap.empty[String, SelectionKey]
|
||||||
|
val sequenceNumber = Iterator.from(0)
|
||||||
|
val selectorManagementDispatcher = context.system.dispatchers.lookup(SelectorDispatcher)
|
||||||
|
val selector = SelectorProvider.provider.openSelector
|
||||||
|
val OP_READ_AND_WRITE = OP_READ | OP_WRITE // compile-time constant
|
||||||
|
|
||||||
|
def receive: Receive = {
|
||||||
|
case WriteInterest ⇒ execute(enableInterest(OP_WRITE, sender))
|
||||||
|
case ReadInterest ⇒ execute(enableInterest(OP_READ, sender))
|
||||||
|
case AcceptInterest ⇒ execute(enableInterest(OP_ACCEPT, sender))
|
||||||
|
|
||||||
|
case DisableReadInterest ⇒ execute(disableInterest(OP_READ, sender))
|
||||||
|
|
||||||
|
case cmd: WorkerForCommand ⇒
|
||||||
|
withCapacityProtection(cmd, SelectorAssociationRetries) { spawnChild(cmd.childProps) }
|
||||||
|
|
||||||
|
case RegisterChannel(channel, initialOps) ⇒
|
||||||
|
execute(registerChannel(channel, sender, initialOps))
|
||||||
|
|
||||||
|
case Retry(WorkerForCommand(cmd, commander, _), 0) ⇒
|
||||||
|
commander ! cmd.failureMessage
|
||||||
|
|
||||||
|
case Retry(cmd, retriesLeft) ⇒
|
||||||
|
withCapacityProtection(cmd, retriesLeft) { spawnChild(cmd.childProps) }
|
||||||
|
|
||||||
|
case Terminated(child) ⇒
|
||||||
|
execute(unregister(child))
|
||||||
|
}
|
||||||
|
|
||||||
|
override def postStop() {
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
val iterator = selector.keys.iterator
|
||||||
|
while (iterator.hasNext) {
|
||||||
|
val key = iterator.next()
|
||||||
|
try key.channel.close()
|
||||||
|
catch {
|
||||||
|
case NonFatal(e) ⇒ log.error(e, "Error closing channel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally selector.close()
|
||||||
|
} catch {
|
||||||
|
case NonFatal(e) ⇒ log.error(e, "Error closing selector")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we can never recover from failures of a connection or listener child
|
||||||
|
override def supervisorStrategy = SupervisorStrategy.stoppingStrategy
|
||||||
|
|
||||||
|
def withCapacityProtection(cmd: WorkerForCommand, retriesLeft: Int)(body: ⇒ Unit): Unit = {
|
||||||
|
log.debug("Executing [{}]", cmd)
|
||||||
|
if (MaxChannelsPerSelector == -1 || childrenKeys.size < MaxChannelsPerSelector) {
|
||||||
|
body
|
||||||
|
} else {
|
||||||
|
log.warning("Rejecting [{}] with [{}] retries left, retrying...", cmd, retriesLeft)
|
||||||
|
context.parent forward Retry(cmd, retriesLeft - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def spawnChild(props: Props): ActorRef =
|
||||||
|
context.watch {
|
||||||
|
context.actorOf(
|
||||||
|
props = props.withDispatcher(WorkerDispatcher),
|
||||||
|
name = sequenceNumber.next().toString)
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////// Management Tasks scheduled via the selectorManagementDispatcher /////////////
|
||||||
|
|
||||||
|
def execute(task: Task): Unit = {
|
||||||
|
selectorManagementDispatcher.execute(task)
|
||||||
|
selector.wakeup()
|
||||||
|
}
|
||||||
|
|
||||||
|
def updateKeyMap(child: ActorRef, key: SelectionKey): Unit =
|
||||||
|
childrenKeys = childrenKeys.updated(child.path.name, key)
|
||||||
|
|
||||||
|
def registerChannel(channel: SelectableChannel, channelActor: ActorRef, initialOps: Int): Task =
|
||||||
|
new Task {
|
||||||
|
def tryRun() {
|
||||||
|
updateKeyMap(channelActor, channel.register(selector, initialOps, channelActor))
|
||||||
|
channelActor ! ChannelRegistered
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: evaluate whether we could run the following two tasks directly on the TcpSelector actor itself rather than
|
||||||
|
// on the selector-management-dispatcher. The trade-off would be using a ConcurrentHashMap
|
||||||
|
// rather than an unsynchronized one, but since switching interest ops is so frequent
|
||||||
|
// the change might be beneficial, provided the underlying implementation really is thread-safe
|
||||||
|
// and behaves consistently on all platforms.
|
||||||
|
def enableInterest(op: Int, connection: ActorRef) =
|
||||||
|
new Task {
|
||||||
|
def tryRun() {
|
||||||
|
val key = childrenKeys(connection.path.name)
|
||||||
|
key.interestOps(key.interestOps | op)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def disableInterest(op: Int, connection: ActorRef) =
|
||||||
|
new Task {
|
||||||
|
def tryRun() {
|
||||||
|
val key = childrenKeys(connection.path.name)
|
||||||
|
key.interestOps(key.interestOps & ~op)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def unregister(child: ActorRef) =
|
||||||
|
new Task {
|
||||||
|
def tryRun() {
|
||||||
|
childrenKeys = childrenKeys - child.path.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val select = new Task {
|
||||||
|
val doSelect: () ⇒ Int =
|
||||||
|
SelectTimeout match {
|
||||||
|
case Duration.Zero ⇒ () ⇒ selector.selectNow()
|
||||||
|
case Duration.Inf ⇒ () ⇒ selector.select()
|
||||||
|
case x ⇒ val millis = x.toMillis; () ⇒ selector.select(millis)
|
||||||
|
}
|
||||||
|
def tryRun() {
|
||||||
|
if (doSelect() > 0) {
|
||||||
|
val keys = selector.selectedKeys
|
||||||
|
val iterator = keys.iterator()
|
||||||
|
while (iterator.hasNext) {
|
||||||
|
val key = iterator.next
|
||||||
|
if (key.isValid) {
|
||||||
|
// Cache because the performance implications of calling this on different platforms are not clear
|
||||||
|
val readyOps = key.readyOps()
|
||||||
|
key.interestOps(key.interestOps & ~readyOps) // prevent immediate reselection by always clearing
|
||||||
|
val connection = key.attachment.asInstanceOf[ActorRef]
|
||||||
|
readyOps match {
|
||||||
|
case OP_READ ⇒ connection ! ChannelReadable
|
||||||
|
case OP_WRITE ⇒ connection ! ChannelWritable
|
||||||
|
case OP_READ_AND_WRITE ⇒ connection ! ChannelWritable; connection ! ChannelReadable
|
||||||
|
case x if (x & OP_ACCEPT) > 0 ⇒ connection ! ChannelAcceptable
|
||||||
|
case x if (x & OP_CONNECT) > 0 ⇒ connection ! ChannelConnectable
|
||||||
|
case x ⇒ log.warning("Invalid readyOps: [{}]", x)
|
||||||
|
}
|
||||||
|
} else log.warning("Invalid selection key: [{}]", key)
|
||||||
|
}
|
||||||
|
keys.clear() // we need to remove the selected keys from the set, otherwise they remain selected
|
||||||
|
}
|
||||||
|
selectorManagementDispatcher.execute(this) // re-schedules select behind all currently queued tasks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectorManagementDispatcher.execute(select) // start selection "loop"
|
||||||
|
|
||||||
|
// FIXME: Add possibility to signal failure of task to someone
|
||||||
|
abstract class Task extends Runnable {
|
||||||
|
def tryRun()
|
||||||
|
def run() {
|
||||||
|
try tryRun()
|
||||||
|
catch {
|
||||||
|
case _: java.nio.channels.ClosedSelectorException ⇒ // ok, expected during shutdown
|
||||||
|
case NonFatal(e) ⇒ log.error(e, "Error during selector management task: [{}]", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
160
akka-actor/src/main/scala/akka/io/Tcp.scala
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2013 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package akka.io
|
||||||
|
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.net.Socket
|
||||||
|
import akka.io.Inet._
|
||||||
|
import com.typesafe.config.Config
|
||||||
|
import scala.concurrent.duration._
|
||||||
|
import scala.collection.immutable
|
||||||
|
import akka.util.ByteString
|
||||||
|
import akka.actor._
|
||||||
|
|
||||||
|
object Tcp extends ExtensionKey[TcpExt] {
|
||||||
|
|
||||||
|
// Java API
|
||||||
|
override def get(system: ActorSystem): TcpExt = super.get(system)
|
||||||
|
|
||||||
|
// shared socket options
|
||||||
|
object SO extends Inet.SoForwarders {
|
||||||
|
|
||||||
|
// general socket options
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [[akka.io.Inet.SocketOption]] to enable or disable SO_KEEPALIVE
|
||||||
|
*
|
||||||
|
* For more information see [[java.net.Socket.setKeepAlive]]
|
||||||
|
*/
|
||||||
|
case class KeepAlive(on: Boolean) extends SocketOption {
|
||||||
|
override def afterConnect(s: Socket): Unit = s.setKeepAlive(on)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [[akka.io.Inet.SocketOption]] to enable or disable OOBINLINE (receipt
|
||||||
|
* of TCP urgent data) By default, this option is disabled and TCP urgent
|
||||||
|
* data is silently discarded.
|
||||||
|
*
|
||||||
|
* For more information see [[java.net.Socket.setOOBInline]]
|
||||||
|
*/
|
||||||
|
case class OOBInline(on: Boolean) extends SocketOption {
|
||||||
|
override def afterConnect(s: Socket): Unit = s.setOOBInline(on)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SO_LINGER is handled by the Close code
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [[akka.io.Inet.SocketOption]] to enable or disable TCP_NODELAY
|
||||||
|
* (disable or enable Nagle's algorithm)
|
||||||
|
*
|
||||||
|
* For more information see [[java.net.Socket.setTcpNoDelay]]
|
||||||
|
*/
|
||||||
|
case class TcpNoDelay(on: Boolean) extends SocketOption {
|
||||||
|
override def afterConnect(s: Socket): Unit = s.setTcpNoDelay(on)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// COMMANDS
|
||||||
|
trait Command extends IO.HasFailureMessage {
|
||||||
|
def failureMessage = CommandFailed(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
case class Connect(remoteAddress: InetSocketAddress,
|
||||||
|
localAddress: Option[InetSocketAddress] = None,
|
||||||
|
options: immutable.Traversable[SocketOption] = Nil) extends Command
|
||||||
|
case class Bind(handler: ActorRef,
|
||||||
|
endpoint: InetSocketAddress,
|
||||||
|
backlog: Int = 100,
|
||||||
|
options: immutable.Traversable[SocketOption] = Nil) extends Command
|
||||||
|
case class Register(handler: ActorRef) extends Command
|
||||||
|
case object Unbind extends Command
|
||||||
|
|
||||||
|
sealed trait CloseCommand extends Command
|
||||||
|
case object Close extends CloseCommand
|
||||||
|
case object ConfirmedClose extends CloseCommand
|
||||||
|
case object Abort extends CloseCommand
|
||||||
|
|
||||||
|
case object NoAck
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write data to the TCP connection. If no ack is needed use the special
|
||||||
|
* `NoAck` object.
|
||||||
|
*/
|
||||||
|
case class Write(data: ByteString, ack: Any) extends Command {
|
||||||
|
require(ack != null, "ack must be non-null. Use NoAck if you don't want acks.")
|
||||||
|
|
||||||
|
def wantsAck: Boolean = ack != NoAck
|
||||||
|
}
|
||||||
|
object Write {
|
||||||
|
val Empty: Write = Write(ByteString.empty, NoAck)
|
||||||
|
def apply(data: ByteString): Write =
|
||||||
|
if (data.isEmpty) Empty else Write(data, NoAck)
|
||||||
|
}
|
||||||
|
|
||||||
|
case object StopReading extends Command
|
||||||
|
case object ResumeReading extends Command
|
||||||
|
|
||||||
|
/// EVENTS
|
||||||
|
trait Event
|
||||||
|
|
||||||
|
case class Received(data: ByteString) extends Event
|
||||||
|
case class Connected(remoteAddress: InetSocketAddress, localAddress: InetSocketAddress) extends Event
|
||||||
|
case class CommandFailed(cmd: Command) extends Event
|
||||||
|
case object Bound extends Event
|
||||||
|
case object Unbound extends Event
|
||||||
|
|
||||||
|
sealed trait ConnectionClosed extends Event
|
||||||
|
case object Closed extends ConnectionClosed
|
||||||
|
case object Aborted extends ConnectionClosed
|
||||||
|
case object ConfirmedClosed extends ConnectionClosed
|
||||||
|
case object PeerClosed extends ConnectionClosed
|
||||||
|
case class ErrorClosed(cause: String) extends ConnectionClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
class TcpExt(system: ExtendedActorSystem) extends IO.Extension {
|
||||||
|
|
||||||
|
val Settings = new Settings(system.settings.config.getConfig("akka.io.tcp"))
|
||||||
|
class Settings private[TcpExt] (_config: Config) extends SelectionHandlerSettings(_config) {
|
||||||
|
import _config._
|
||||||
|
|
||||||
|
val NrOfSelectors = getInt("nr-of-selectors")
|
||||||
|
|
||||||
|
val BatchAcceptLimit = getInt("batch-accept-limit")
|
||||||
|
val DirectBufferSize = getIntBytes("direct-buffer-size")
|
||||||
|
val MaxDirectBufferPoolSize = getInt("direct-buffer-pool-limit")
|
||||||
|
val RegisterTimeout = getString("register-timeout") match {
|
||||||
|
case "infinite" ⇒ Duration.Undefined
|
||||||
|
case x ⇒ Duration(x)
|
||||||
|
}
|
||||||
|
val ReceivedMessageSizeLimit = getString("max-received-message-size") match {
|
||||||
|
case "unlimited" ⇒ Int.MaxValue
|
||||||
|
case x ⇒ getIntBytes("received-message-size-limit")
|
||||||
|
}
|
||||||
|
val ManagementDispatcher = getString("management-dispatcher")
|
||||||
|
|
||||||
|
require(NrOfSelectors > 0, "nr-of-selectors must be > 0")
|
||||||
|
require(MaxChannels == -1 || MaxChannels > 0, "max-channels must be > 0 or 'unlimited'")
|
||||||
|
require(SelectTimeout >= Duration.Zero, "select-timeout must not be negative")
|
||||||
|
require(SelectorAssociationRetries >= 0, "selector-association-retries must be >= 0")
|
||||||
|
require(BatchAcceptLimit > 0, "batch-accept-limit must be > 0")
|
||||||
|
|
||||||
|
val MaxChannelsPerSelector = if (MaxChannels == -1) -1 else math.max(MaxChannels / NrOfSelectors, 1)
|
||||||
|
|
||||||
|
private[this] def getIntBytes(path: String): Int = {
|
||||||
|
val size = getBytes(path)
|
||||||
|
require(size < Int.MaxValue, s"$path must be < 2 GiB")
|
||||||
|
size.toInt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val manager: ActorRef = {
|
||||||
|
system.asInstanceOf[ActorSystemImpl].systemActorOf(
|
||||||
|
props = Props(new TcpManager(this)).withDispatcher(Settings.ManagementDispatcher),
|
||||||
|
name = "IO-TCP")
|
||||||
|
}
|
||||||
|
|
||||||
|
val bufferPool: BufferPool = new DirectByteBufferPool(Settings.DirectBufferSize, Settings.MaxDirectBufferPoolSize)
|
||||||
|
}
|
||||||
315
akka-actor/src/main/scala/akka/io/TcpConnection.scala
Normal file
|
|
@ -0,0 +1,315 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2013 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package akka.io
|
||||||
|
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.io.IOException
|
||||||
|
import java.nio.channels.SocketChannel
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import scala.annotation.tailrec
|
||||||
|
import scala.collection.immutable
|
||||||
|
import scala.util.control.NonFatal
|
||||||
|
import scala.concurrent.duration._
|
||||||
|
import akka.actor._
|
||||||
|
import akka.util.ByteString
|
||||||
|
import akka.io.Inet.SocketOption
|
||||||
|
import akka.io.Tcp._
|
||||||
|
import akka.io.SelectionHandler._
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for TcpIncomingConnection and TcpOutgoingConnection.
|
||||||
|
*/
|
||||||
|
private[io] abstract class TcpConnection(val channel: SocketChannel,
|
||||||
|
val tcp: TcpExt) extends Actor with ActorLogging {
|
||||||
|
import tcp.Settings._
|
||||||
|
import tcp.bufferPool
|
||||||
|
import TcpConnection._
|
||||||
|
var pendingWrite: PendingWrite = null
|
||||||
|
|
||||||
|
// Needed to send the ConnectionClosed message in the postStop handler.
|
||||||
|
var closedMessage: CloseInformation = null
|
||||||
|
|
||||||
|
def writePending = pendingWrite ne null
|
||||||
|
|
||||||
|
def selector = context.parent
|
||||||
|
|
||||||
|
// STATES
|
||||||
|
|
||||||
|
/** connection established, waiting for registration from user handler */
|
||||||
|
def waitingForRegistration(commander: ActorRef): Receive = {
|
||||||
|
case Register(handler) ⇒
|
||||||
|
if (TraceLogging) log.debug("[{}] registered as connection handler", handler)
|
||||||
|
doRead(handler, None) // immediately try reading
|
||||||
|
|
||||||
|
context.setReceiveTimeout(Duration.Undefined)
|
||||||
|
context.watch(handler) // sign death pact
|
||||||
|
|
||||||
|
context.become(connected(handler))
|
||||||
|
|
||||||
|
case cmd: CloseCommand ⇒
|
||||||
|
handleClose(commander, Some(sender), closeResponse(cmd))
|
||||||
|
|
||||||
|
case ReceiveTimeout ⇒
|
||||||
|
// after sending `Register` user should watch this actor to make sure
|
||||||
|
// it didn't die because of the timeout
|
||||||
|
log.warning("Configured registration timeout of [{}] expired, stopping", RegisterTimeout)
|
||||||
|
context.stop(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** normal connected state */
|
||||||
|
def connected(handler: ActorRef): Receive = {
|
||||||
|
case StopReading ⇒ selector ! DisableReadInterest
|
||||||
|
case ResumeReading ⇒ selector ! ReadInterest
|
||||||
|
case ChannelReadable ⇒ doRead(handler, None)
|
||||||
|
|
||||||
|
case write: Write if writePending ⇒
|
||||||
|
if (TraceLogging) log.debug("Dropping write because queue is full")
|
||||||
|
sender ! CommandFailed(write)
|
||||||
|
|
||||||
|
case write: Write if write.data.isEmpty ⇒
|
||||||
|
if (write.wantsAck)
|
||||||
|
sender ! write.ack
|
||||||
|
|
||||||
|
case write: Write ⇒
|
||||||
|
pendingWrite = createWrite(write)
|
||||||
|
doWrite(handler)
|
||||||
|
|
||||||
|
case ChannelWritable ⇒ if (writePending) doWrite(handler)
|
||||||
|
|
||||||
|
case cmd: CloseCommand ⇒ handleClose(handler, Some(sender), closeResponse(cmd))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** connection is closing but a write has to be finished first */
|
||||||
|
def closingWithPendingWrite(handler: ActorRef, closeCommander: Option[ActorRef], closedEvent: ConnectionClosed): Receive = {
|
||||||
|
case StopReading ⇒ selector ! DisableReadInterest
|
||||||
|
case ResumeReading ⇒ selector ! ReadInterest
|
||||||
|
case ChannelReadable ⇒ doRead(handler, closeCommander)
|
||||||
|
|
||||||
|
case ChannelWritable ⇒
|
||||||
|
doWrite(handler)
|
||||||
|
if (!writePending) // writing is now finished
|
||||||
|
handleClose(handler, closeCommander, closedEvent)
|
||||||
|
|
||||||
|
case Abort ⇒ handleClose(handler, Some(sender), Aborted)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** connection is closed on our side and we're waiting from confirmation from the other side */
|
||||||
|
def closing(handler: ActorRef, closeCommander: Option[ActorRef]): Receive = {
|
||||||
|
case StopReading ⇒ selector ! DisableReadInterest
|
||||||
|
case ResumeReading ⇒ selector ! ReadInterest
|
||||||
|
case ChannelReadable ⇒ doRead(handler, closeCommander)
|
||||||
|
case Abort ⇒ handleClose(handler, Some(sender), Aborted)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AUXILIARIES and IMPLEMENTATION
|
||||||
|
|
||||||
|
/** used in subclasses to start the common machinery above once a channel is connected */
|
||||||
|
def completeConnect(commander: ActorRef, options: immutable.Traversable[SocketOption]): Unit = {
|
||||||
|
options.foreach(_.afterConnect(channel.socket))
|
||||||
|
|
||||||
|
commander ! Connected(
|
||||||
|
channel.socket.getRemoteSocketAddress.asInstanceOf[InetSocketAddress],
|
||||||
|
channel.socket.getLocalSocketAddress.asInstanceOf[InetSocketAddress])
|
||||||
|
|
||||||
|
context.setReceiveTimeout(RegisterTimeout)
|
||||||
|
context.become(waitingForRegistration(commander))
|
||||||
|
}
|
||||||
|
|
||||||
|
def doRead(handler: ActorRef, closeCommander: Option[ActorRef]): Unit = {
|
||||||
|
@tailrec def innerRead(buffer: ByteBuffer, receivedData: ByteString, remainingLimit: Int): ReadResult =
|
||||||
|
if (remainingLimit > 0) {
|
||||||
|
// never read more than the configured limit
|
||||||
|
buffer.clear()
|
||||||
|
val maxBufferSpace = math.min(DirectBufferSize, remainingLimit)
|
||||||
|
buffer.limit(maxBufferSpace)
|
||||||
|
val readBytes = channel.read(buffer)
|
||||||
|
buffer.flip()
|
||||||
|
|
||||||
|
val totalData = receivedData ++ ByteString(buffer)
|
||||||
|
|
||||||
|
readBytes match {
|
||||||
|
case `maxBufferSpace` ⇒ innerRead(buffer, totalData, remainingLimit - maxBufferSpace)
|
||||||
|
case x if totalData.length > 0 ⇒ GotCompleteData(totalData)
|
||||||
|
case 0 ⇒ NoData
|
||||||
|
case -1 ⇒ EndOfStream
|
||||||
|
case _ ⇒
|
||||||
|
throw new IllegalStateException("Unexpected value returned from read: " + readBytes)
|
||||||
|
}
|
||||||
|
} else MoreDataWaiting(receivedData)
|
||||||
|
|
||||||
|
val buffer = bufferPool.acquire()
|
||||||
|
try innerRead(buffer, ByteString.empty, ReceivedMessageSizeLimit) match {
|
||||||
|
case NoData ⇒
|
||||||
|
if (TraceLogging) log.debug("Read nothing.")
|
||||||
|
selector ! ReadInterest
|
||||||
|
case GotCompleteData(data) ⇒
|
||||||
|
if (TraceLogging) log.debug("Read [{}] bytes.", data.length)
|
||||||
|
|
||||||
|
handler ! Received(data)
|
||||||
|
selector ! ReadInterest
|
||||||
|
case MoreDataWaiting(data) ⇒
|
||||||
|
if (TraceLogging) log.debug("Read [{}] bytes. More data waiting.", data.length)
|
||||||
|
|
||||||
|
handler ! Received(data)
|
||||||
|
self ! ChannelReadable
|
||||||
|
case EndOfStream ⇒
|
||||||
|
if (TraceLogging) log.debug("Read returned end-of-stream")
|
||||||
|
doCloseConnection(handler, closeCommander, closeReason)
|
||||||
|
} catch {
|
||||||
|
case e: IOException ⇒ handleError(handler, e)
|
||||||
|
} finally bufferPool.release(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
final def doWrite(handler: ActorRef): Unit = {
|
||||||
|
@tailrec def innerWrite(): Unit = {
|
||||||
|
val toWrite = pendingWrite.buffer.remaining()
|
||||||
|
require(toWrite != 0)
|
||||||
|
val writtenBytes = channel.write(pendingWrite.buffer)
|
||||||
|
if (TraceLogging) log.debug("Wrote [{}] bytes to channel", writtenBytes)
|
||||||
|
|
||||||
|
pendingWrite = pendingWrite.consume(writtenBytes)
|
||||||
|
|
||||||
|
if (pendingWrite.hasData)
|
||||||
|
if (writtenBytes == toWrite) innerWrite() // wrote complete buffer, try again now
|
||||||
|
else selector ! WriteInterest // try again later
|
||||||
|
else { // everything written
|
||||||
|
if (pendingWrite.wantsAck)
|
||||||
|
pendingWrite.commander ! pendingWrite.ack
|
||||||
|
|
||||||
|
val buffer = pendingWrite.buffer
|
||||||
|
pendingWrite = null
|
||||||
|
|
||||||
|
bufferPool.release(buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try innerWrite()
|
||||||
|
catch { case e: IOException ⇒ handleError(handler, e) }
|
||||||
|
}
|
||||||
|
|
||||||
|
def closeReason =
|
||||||
|
if (channel.socket.isOutputShutdown) ConfirmedClosed
|
||||||
|
else PeerClosed
|
||||||
|
|
||||||
|
def handleClose(handler: ActorRef, closeCommander: Option[ActorRef], closedEvent: ConnectionClosed): Unit =
|
||||||
|
if (closedEvent == Aborted) { // close instantly
|
||||||
|
if (TraceLogging) log.debug("Got Abort command. RESETing connection.")
|
||||||
|
doCloseConnection(handler, closeCommander, closedEvent)
|
||||||
|
|
||||||
|
} else if (writePending) { // finish writing first
|
||||||
|
if (TraceLogging) log.debug("Got Close command but write is still pending.")
|
||||||
|
context.become(closingWithPendingWrite(handler, closeCommander, closedEvent))
|
||||||
|
|
||||||
|
} else if (closedEvent == ConfirmedClosed) { // shutdown output and wait for confirmation
|
||||||
|
if (TraceLogging) log.debug("Got ConfirmedClose command, sending FIN.")
|
||||||
|
channel.socket.shutdownOutput()
|
||||||
|
context.become(closing(handler, closeCommander))
|
||||||
|
|
||||||
|
} else { // close now
|
||||||
|
if (TraceLogging) log.debug("Got Close command, closing connection.")
|
||||||
|
doCloseConnection(handler, closeCommander, closedEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
def doCloseConnection(handler: ActorRef, closeCommander: Option[ActorRef], closedEvent: ConnectionClosed): Unit = {
|
||||||
|
if (closedEvent == Aborted) abort()
|
||||||
|
else channel.close()
|
||||||
|
|
||||||
|
closedMessage = CloseInformation(Set(handler) ++ closeCommander, closedEvent)
|
||||||
|
|
||||||
|
context.stop(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
def closeResponse(closeCommand: CloseCommand): ConnectionClosed =
|
||||||
|
closeCommand match {
|
||||||
|
case Close ⇒ Closed
|
||||||
|
case Abort ⇒ Aborted
|
||||||
|
case ConfirmedClose ⇒ ConfirmedClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
def handleError(handler: ActorRef, exception: IOException): Unit = {
|
||||||
|
closedMessage = CloseInformation(Set(handler), ErrorClosed(extractMsg(exception)))
|
||||||
|
|
||||||
|
throw exception
|
||||||
|
}
|
||||||
|
@tailrec private[this] def extractMsg(t: Throwable): String =
|
||||||
|
if (t == null) "unknown"
|
||||||
|
else {
|
||||||
|
t.getMessage match {
|
||||||
|
case null | "" ⇒ extractMsg(t.getCause)
|
||||||
|
case msg ⇒ msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def abort(): Unit = {
|
||||||
|
try channel.socket.setSoLinger(true, 0) // causes the following close() to send TCP RST
|
||||||
|
catch {
|
||||||
|
case NonFatal(e) ⇒
|
||||||
|
// setSoLinger can fail due to http://bugs.sun.com/view_bug.do?bug_id=6799574
|
||||||
|
// (also affected: OS/X Java 1.6.0_37)
|
||||||
|
if (TraceLogging) log.debug("setSoLinger(true, 0) failed with [{}]", e)
|
||||||
|
}
|
||||||
|
channel.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
override def postStop(): Unit = {
|
||||||
|
if (channel.isOpen)
|
||||||
|
abort()
|
||||||
|
|
||||||
|
if (writePending)
|
||||||
|
bufferPool.release(pendingWrite.buffer)
|
||||||
|
|
||||||
|
if (closedMessage != null) {
|
||||||
|
val interestedInClose =
|
||||||
|
if (writePending) closedMessage.notificationsTo + pendingWrite.commander
|
||||||
|
else closedMessage.notificationsTo
|
||||||
|
|
||||||
|
interestedInClose.foreach(_ ! closedMessage.closedEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def postRestart(reason: Throwable): Unit =
|
||||||
|
throw new IllegalStateException("Restarting not supported for connection actors.")
|
||||||
|
|
||||||
|
private[TcpConnection] case class PendingWrite(
|
||||||
|
commander: ActorRef,
|
||||||
|
ack: Any,
|
||||||
|
remainingData: ByteString,
|
||||||
|
buffer: ByteBuffer) {
|
||||||
|
|
||||||
|
def consume(writtenBytes: Int): PendingWrite =
|
||||||
|
if (buffer.remaining() == 0) {
|
||||||
|
buffer.clear()
|
||||||
|
val copied = remainingData.copyToBuffer(buffer)
|
||||||
|
buffer.flip()
|
||||||
|
copy(remainingData = remainingData.drop(copied))
|
||||||
|
} else this
|
||||||
|
|
||||||
|
def hasData = buffer.remaining() > 0 || remainingData.size > 0
|
||||||
|
def wantsAck = ack != NoAck
|
||||||
|
}
|
||||||
|
def createWrite(write: Write): PendingWrite = {
|
||||||
|
val buffer = bufferPool.acquire()
|
||||||
|
val copied = write.data.copyToBuffer(buffer)
|
||||||
|
buffer.flip()
|
||||||
|
|
||||||
|
PendingWrite(sender, write.ack, write.data.drop(copied), buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private[io] object TcpConnection {
|
||||||
|
sealed trait ReadResult
|
||||||
|
object NoData extends ReadResult
|
||||||
|
object EndOfStream extends ReadResult
|
||||||
|
case class GotCompleteData(data: ByteString) extends ReadResult
|
||||||
|
case class MoreDataWaiting(data: ByteString) extends ReadResult
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to transport information to the postStop method to notify
|
||||||
|
* interested party about a connection close.
|
||||||
|
*/
|
||||||
|
case class CloseInformation(
|
||||||
|
notificationsTo: Set[ActorRef],
|
||||||
|
closedEvent: ConnectionClosed)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2013 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package akka.io
|
||||||
|
|
||||||
|
import java.nio.channels.SocketChannel
|
||||||
|
import scala.collection.immutable
|
||||||
|
import akka.actor.ActorRef
|
||||||
|
import akka.io.Inet.SocketOption
|
||||||
|
import akka.io.SelectionHandler.{ ChannelRegistered, RegisterChannel }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An actor handling the connection state machine for an incoming, already connected
|
||||||
|
* SocketChannel.
|
||||||
|
*/
|
||||||
|
private[io] class TcpIncomingConnection(_channel: SocketChannel,
|
||||||
|
_tcp: TcpExt,
|
||||||
|
handler: ActorRef,
|
||||||
|
options: immutable.Traversable[SocketOption])
|
||||||
|
extends TcpConnection(_channel, _tcp) {
|
||||||
|
|
||||||
|
context.watch(handler) // sign death pact
|
||||||
|
|
||||||
|
context.parent ! RegisterChannel(channel, 0)
|
||||||
|
|
||||||
|
def receive = {
|
||||||
|
case ChannelRegistered ⇒ completeConnect(handler, options)
|
||||||
|
}
|
||||||
|
}
|
||||||
107
akka-actor/src/main/scala/akka/io/TcpListener.scala
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2012 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package akka.io
|
||||||
|
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.nio.channels.{ SocketChannel, SelectionKey, ServerSocketChannel }
|
||||||
|
import scala.annotation.tailrec
|
||||||
|
import scala.collection.immutable
|
||||||
|
import scala.util.control.NonFatal
|
||||||
|
import akka.actor.{ Props, ActorLogging, ActorRef, Actor }
|
||||||
|
import akka.io.SelectionHandler._
|
||||||
|
import akka.io.Inet.SocketOption
|
||||||
|
import akka.io.Tcp._
|
||||||
|
import akka.io.IO.HasFailureMessage
|
||||||
|
|
||||||
|
private[io] object TcpListener {
|
||||||
|
|
||||||
|
case class RegisterIncoming(channel: SocketChannel) extends HasFailureMessage {
|
||||||
|
def failureMessage = FailedRegisterIncoming(channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
case class FailedRegisterIncoming(channel: SocketChannel)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private[io] class TcpListener(val selectorRouter: ActorRef,
|
||||||
|
val tcp: TcpExt,
|
||||||
|
val bindCommander: ActorRef,
|
||||||
|
val bind: Bind) extends Actor with ActorLogging {
|
||||||
|
|
||||||
|
def selector: ActorRef = context.parent
|
||||||
|
import TcpListener._
|
||||||
|
import tcp.Settings._
|
||||||
|
import bind._
|
||||||
|
|
||||||
|
context.watch(handler) // sign death pact
|
||||||
|
val channel = {
|
||||||
|
val serverSocketChannel = ServerSocketChannel.open
|
||||||
|
serverSocketChannel.configureBlocking(false)
|
||||||
|
val socket = serverSocketChannel.socket
|
||||||
|
options.foreach(_.beforeServerSocketBind(socket))
|
||||||
|
try socket.bind(endpoint, backlog)
|
||||||
|
catch {
|
||||||
|
case NonFatal(e) ⇒
|
||||||
|
bindCommander ! CommandFailed(bind)
|
||||||
|
log.error(e, "Bind failed for TCP channel on endpoint [{}]", endpoint)
|
||||||
|
context.stop(self)
|
||||||
|
}
|
||||||
|
serverSocketChannel
|
||||||
|
}
|
||||||
|
context.parent ! RegisterChannel(channel, SelectionKey.OP_ACCEPT)
|
||||||
|
log.debug("Successfully bound to {}", endpoint)
|
||||||
|
|
||||||
|
def receive: Receive = {
|
||||||
|
case ChannelRegistered ⇒
|
||||||
|
bindCommander ! Bound
|
||||||
|
context.become(bound)
|
||||||
|
}
|
||||||
|
|
||||||
|
def bound: Receive = {
|
||||||
|
case ChannelAcceptable ⇒
|
||||||
|
acceptAllPending(BatchAcceptLimit)
|
||||||
|
|
||||||
|
case FailedRegisterIncoming(socketChannel) ⇒
|
||||||
|
log.warning("Could not register incoming connection since selector capacity limit is reached, closing connection")
|
||||||
|
try socketChannel.close()
|
||||||
|
catch {
|
||||||
|
case NonFatal(e) ⇒ log.error(e, "Error closing channel")
|
||||||
|
}
|
||||||
|
|
||||||
|
case Unbind ⇒
|
||||||
|
log.debug("Unbinding endpoint {}", endpoint)
|
||||||
|
channel.close()
|
||||||
|
sender ! Unbound
|
||||||
|
log.debug("Unbound endpoint {}, stopping listener", endpoint)
|
||||||
|
context.stop(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
@tailrec final def acceptAllPending(limit: Int): Unit =
|
||||||
|
if (limit > 0) {
|
||||||
|
val socketChannel =
|
||||||
|
try channel.accept()
|
||||||
|
catch {
|
||||||
|
case NonFatal(e) ⇒ log.error(e, "Accept error: could not accept new connection due to {}", e); null
|
||||||
|
}
|
||||||
|
if (socketChannel != null) {
|
||||||
|
log.debug("New connection accepted")
|
||||||
|
socketChannel.configureBlocking(false)
|
||||||
|
selectorRouter ! WorkerForCommand(RegisterIncoming(socketChannel), self, Props(new TcpIncomingConnection(socketChannel, tcp, handler, options)))
|
||||||
|
acceptAllPending(limit - 1)
|
||||||
|
}
|
||||||
|
} else context.parent ! AcceptInterest
|
||||||
|
|
||||||
|
override def postStop() {
|
||||||
|
try {
|
||||||
|
if (channel.isOpen) {
|
||||||
|
log.debug("Closing serverSocketChannel after being stopped")
|
||||||
|
channel.close()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
case NonFatal(e) ⇒ log.error(e, "Error closing ServerSocketChannel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
57
akka-actor/src/main/scala/akka/io/TcpManager.scala
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2013 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package akka.io
|
||||||
|
|
||||||
|
import Tcp._
|
||||||
|
import akka.actor.{ ActorLogging, Props }
|
||||||
|
import akka.io.IO.SelectorBasedManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TcpManager is a facade for accepting commands ([[akka.io.Tcp.Command]]) to open client or server TCP connections.
|
||||||
|
*
|
||||||
|
* TcpManager is obtainable by calling {{{ IO(Tcp) }}} (see [[akka.io.IO]] and [[akka.io.Tcp]])
|
||||||
|
*
|
||||||
|
* == Bind ==
|
||||||
|
*
|
||||||
|
* To bind and listen to a local address, a [[akka.io.Tcp.Bind]] command must be sent to this actor. If the binding
|
||||||
|
* was successful, the sender of the [[akka.io.Tcp.Bind]] will be notified with a [[akka.io.Tcp.Bound]]
|
||||||
|
* message. The sender of the [[akka.io.Tcp.Bound]] message is the Listener actor (an internal actor responsible for
|
||||||
|
* listening to server events). To unbind the port an [[akka.io.Tcp.Unbind]] message must be sent to the Listener actor.
|
||||||
|
*
|
||||||
|
* If the bind request is rejected because the Tcp system is not able to register more channels (see the nr-of-selectors
|
||||||
|
* and max-channels configuration options in the akka.io.tcp section of the configuration) the sender will be notified
|
||||||
|
* with a [[akka.io.Tcp.CommandFailed]] message. This message contains the original command for reference.
|
||||||
|
*
|
||||||
|
* When an inbound TCP connection is established, the handler will be notified by a [[akka.io.Tcp.Connected]] message.
|
||||||
|
* The sender of this message is the Connection actor (an internal actor representing the TCP connection). At this point
|
||||||
|
* the procedure is the same as for outbound connections (see section below).
|
||||||
|
*
|
||||||
|
* == Connect ==
|
||||||
|
*
|
||||||
|
* To initiate a connection to a remote server, a [[akka.io.Tcp.Connect]] message must be sent to this actor. If the
|
||||||
|
* connection succeeds, the sender will be notified with a [[akka.io.Tcp.Connected]] message. The sender of the
|
||||||
|
* [[akka.io.Tcp.Connected]] message is the Connection actor (an internal actor representing the TCP connection). Before
|
||||||
|
* starting to use the connection, a handler must be registered to the Connection actor by sending a [[akka.io.Tcp.Register]]
|
||||||
|
* command message. After a handler has been registered, all incoming data will be sent to the handler in the form of
|
||||||
|
* [[akka.io.Tcp.Received]] messages. To write data to the connection, a [[akka.io.Tcp.Write]] message must be sent
|
||||||
|
* to the Connection actor.
|
||||||
|
*
|
||||||
|
* If the connect request is rejected because the Tcp system is not able to register more channels (see the nr-of-selectors
|
||||||
|
* and max-channels configuration options in the akka.io.tcp section of the configuration) the sender will be notified
|
||||||
|
* with a [[akka.io.Tcp.CommandFailed]] message. This message contains the original command for reference.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private[io] class TcpManager(tcp: TcpExt) extends SelectorBasedManager(tcp.Settings, tcp.Settings.NrOfSelectors) with ActorLogging {
|
||||||
|
|
||||||
|
def receive = workerForCommandHandler {
|
||||||
|
case c: Connect ⇒
|
||||||
|
val commander = sender
|
||||||
|
Props(new TcpOutgoingConnection(tcp, commander, c))
|
||||||
|
case b: Bind ⇒
|
||||||
|
val commander = sender
|
||||||
|
Props(new TcpListener(selectorPool, tcp, commander, b))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2013 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
|
||||||
|
package akka.io
|
||||||
|
|
||||||
|
import akka.actor.ActorRef
|
||||||
|
import akka.io.Inet.SocketOption
|
||||||
|
import akka.io.SelectionHandler._
|
||||||
|
import akka.io.Tcp._
|
||||||
|
import java.io.IOException
|
||||||
|
import java.nio.channels.{ SelectionKey, SocketChannel }
|
||||||
|
import scala.collection.immutable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An actor handling the connection state machine for an outgoing connection
|
||||||
|
* to be established.
|
||||||
|
*/
|
||||||
|
private[io] class TcpOutgoingConnection(_tcp: TcpExt,
|
||||||
|
commander: ActorRef,
|
||||||
|
connect: Connect)
|
||||||
|
extends TcpConnection(TcpOutgoingConnection.newSocketChannel(), _tcp) {
|
||||||
|
|
||||||
|
import connect._
|
||||||
|
|
||||||
|
context.watch(commander) // sign death pact
|
||||||
|
|
||||||
|
localAddress.foreach(channel.socket.bind)
|
||||||
|
options.foreach(_.beforeConnect(channel.socket))
|
||||||
|
selector ! RegisterChannel(channel, SelectionKey.OP_CONNECT)
|
||||||
|
|
||||||
|
def receive: Receive = {
|
||||||
|
case ChannelRegistered ⇒
|
||||||
|
log.debug("Attempting connection to [{}]", remoteAddress)
|
||||||
|
if (channel.connect(remoteAddress))
|
||||||
|
completeConnect(commander, options)
|
||||||
|
else {
|
||||||
|
context.become(connecting(commander, options))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def connecting(commander: ActorRef, options: immutable.Traversable[SocketOption]): Receive = {
|
||||||
|
case ChannelConnectable ⇒
|
||||||
|
try {
|
||||||
|
val connected = channel.finishConnect()
|
||||||
|
assert(connected, "Connectable channel failed to connect")
|
||||||
|
log.debug("Connection established")
|
||||||
|
completeConnect(commander, options)
|
||||||
|
} catch {
|
||||||
|
case e: IOException ⇒ handleError(commander, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
object TcpOutgoingConnection {
|
||||||
|
private def newSocketChannel() = {
|
||||||
|
val channel = SocketChannel.open()
|
||||||
|
channel.configureBlocking(false)
|
||||||
|
channel
|
||||||
|
}
|
||||||
|
}
|
||||||
48
akka-actor/src/main/scala/akka/io/Udp.scala
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2013 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
package akka.io
|
||||||
|
|
||||||
|
import java.net.DatagramSocket
|
||||||
|
import akka.io.Inet.SocketOption
|
||||||
|
import com.typesafe.config.Config
|
||||||
|
import akka.actor.{ Props, ActorSystemImpl }
|
||||||
|
|
||||||
|
object Udp {
|
||||||
|
|
||||||
|
object SO extends Inet.SoForwarders {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [[akka.io.Inet.SocketOption]] to set the SO_BROADCAST option
|
||||||
|
*
|
||||||
|
* For more information see [[java.net.DatagramSocket#setBroadcast]]
|
||||||
|
*/
|
||||||
|
case class Broadcast(on: Boolean) extends SocketOption {
|
||||||
|
override def beforeDatagramBind(s: DatagramSocket): Unit = s.setBroadcast(on)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private[io] class UdpSettings(_config: Config) extends SelectionHandlerSettings(_config) {
|
||||||
|
import _config._
|
||||||
|
|
||||||
|
val NrOfSelectors = getInt("nr-of-selectors")
|
||||||
|
val DirectBufferSize = getIntBytes("direct-buffer-size")
|
||||||
|
val MaxDirectBufferPoolSize = getInt("direct-buffer-pool-limit")
|
||||||
|
val BatchReceiveLimit = getInt("receive-throughput")
|
||||||
|
|
||||||
|
val ManagementDispatcher = getString("management-dispatcher")
|
||||||
|
|
||||||
|
// FIXME: Use new requiring
|
||||||
|
require(NrOfSelectors > 0, "nr-of-selectors must be > 0")
|
||||||
|
|
||||||
|
override val MaxChannelsPerSelector = if (MaxChannels == -1) -1 else math.max(MaxChannels / NrOfSelectors, 1)
|
||||||
|
|
||||||
|
private[this] def getIntBytes(path: String): Int = {
|
||||||
|
val size = getBytes(path)
|
||||||
|
require(size < Int.MaxValue, s"$path must be < 2 GiB")
|
||||||
|
size.toInt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
64
akka-actor/src/main/scala/akka/io/UdpConn.scala
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2013 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
package akka.io
|
||||||
|
|
||||||
|
import akka.actor._
|
||||||
|
import akka.io.Inet.SocketOption
|
||||||
|
import akka.io.Udp.UdpSettings
|
||||||
|
import akka.util.ByteString
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import scala.collection.immutable
|
||||||
|
|
||||||
|
object UdpConn extends ExtensionKey[UdpConnExt] {
|
||||||
|
// Java API
|
||||||
|
override def get(system: ActorSystem): UdpConnExt = super.get(system)
|
||||||
|
|
||||||
|
trait Command extends IO.HasFailureMessage {
|
||||||
|
def failureMessage = CommandFailed(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
case object NoAck
|
||||||
|
case class Send(payload: ByteString, ack: Any) extends Command {
|
||||||
|
require(ack != null, "ack must be non-null. Use NoAck if you don't want acks.")
|
||||||
|
|
||||||
|
def wantsAck: Boolean = ack != NoAck
|
||||||
|
}
|
||||||
|
object Send {
|
||||||
|
def apply(data: ByteString): Send = Send(data, NoAck)
|
||||||
|
}
|
||||||
|
|
||||||
|
case class Connect(handler: ActorRef,
|
||||||
|
localAddress: Option[InetSocketAddress],
|
||||||
|
remoteAddress: InetSocketAddress,
|
||||||
|
options: immutable.Traversable[SocketOption] = Nil) extends Command
|
||||||
|
|
||||||
|
case object StopReading extends Command
|
||||||
|
case object ResumeReading extends Command
|
||||||
|
|
||||||
|
trait Event
|
||||||
|
|
||||||
|
case class Received(data: ByteString) extends Event
|
||||||
|
case class CommandFailed(cmd: Command) extends Event
|
||||||
|
case object Connected extends Event
|
||||||
|
case object Disconnected extends Event
|
||||||
|
|
||||||
|
case object Close extends Command
|
||||||
|
|
||||||
|
case class SendFailed(cause: Throwable) extends Event
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class UdpConnExt(system: ExtendedActorSystem) extends IO.Extension {
|
||||||
|
|
||||||
|
val settings: UdpSettings = new UdpSettings(system.settings.config.getConfig("akka.io.udp-fire-and-forget"))
|
||||||
|
|
||||||
|
val manager: ActorRef = {
|
||||||
|
system.asInstanceOf[ActorSystemImpl].systemActorOf(
|
||||||
|
props = Props(new UdpConnManager(this)),
|
||||||
|
name = "IO-UDP-CONN")
|
||||||
|
}
|
||||||
|
|
||||||
|
val bufferPool: BufferPool = new DirectByteBufferPool(settings.DirectBufferSize, settings.MaxDirectBufferPoolSize)
|
||||||
|
|
||||||
|
}
|
||||||
18
akka-actor/src/main/scala/akka/io/UdpConnManager.scala
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2013 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
package akka.io
|
||||||
|
|
||||||
|
import akka.actor.Props
|
||||||
|
import akka.io.IO.SelectorBasedManager
|
||||||
|
import akka.io.UdpConn.Connect
|
||||||
|
|
||||||
|
class UdpConnManager(udpConn: UdpConnExt) extends SelectorBasedManager(udpConn.settings, udpConn.settings.NrOfSelectors) {
|
||||||
|
|
||||||
|
def receive = workerForCommandHandler {
|
||||||
|
case c: Connect ⇒
|
||||||
|
val commander = sender
|
||||||
|
Props(new UdpConnection(udpConn, commander, c))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
132
akka-actor/src/main/scala/akka/io/UdpConnection.scala
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2013 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
package akka.io
|
||||||
|
|
||||||
|
import akka.actor.{ Actor, ActorLogging, ActorRef }
|
||||||
|
import akka.io.SelectionHandler._
|
||||||
|
import akka.io.UdpConn._
|
||||||
|
import akka.util.ByteString
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.channels.DatagramChannel
|
||||||
|
import java.nio.channels.SelectionKey._
|
||||||
|
import scala.annotation.tailrec
|
||||||
|
import scala.util.control.NonFatal
|
||||||
|
|
||||||
|
private[io] class UdpConnection(val udpConn: UdpConnExt,
|
||||||
|
val commander: ActorRef,
|
||||||
|
val connect: Connect) extends Actor with ActorLogging {
|
||||||
|
|
||||||
|
def selector: ActorRef = context.parent
|
||||||
|
|
||||||
|
import connect._
|
||||||
|
import udpConn._
|
||||||
|
import udpConn.settings._
|
||||||
|
|
||||||
|
var pendingSend: (Send, ActorRef) = null
|
||||||
|
def writePending = pendingSend ne null
|
||||||
|
|
||||||
|
context.watch(handler) // sign death pact
|
||||||
|
val channel = {
|
||||||
|
val datagramChannel = DatagramChannel.open
|
||||||
|
datagramChannel.configureBlocking(false)
|
||||||
|
val socket = datagramChannel.socket
|
||||||
|
options.foreach(_.beforeDatagramBind(socket))
|
||||||
|
try {
|
||||||
|
localAddress foreach socket.bind
|
||||||
|
datagramChannel.connect(remoteAddress)
|
||||||
|
} catch {
|
||||||
|
case NonFatal(e) ⇒
|
||||||
|
log.error(e, "Failure while connecting UDP channel to remote address [{}] local address [{}]",
|
||||||
|
remoteAddress, localAddress.map { _.toString }.getOrElse("undefined"))
|
||||||
|
commander ! CommandFailed(connect)
|
||||||
|
context.stop(self)
|
||||||
|
}
|
||||||
|
datagramChannel
|
||||||
|
}
|
||||||
|
selector ! RegisterChannel(channel, OP_READ)
|
||||||
|
log.debug("Successfully connected to [{}]", remoteAddress)
|
||||||
|
|
||||||
|
def receive = {
|
||||||
|
case ChannelRegistered ⇒
|
||||||
|
commander ! Connected
|
||||||
|
context.become(connected, discardOld = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
def connected: Receive = {
|
||||||
|
case StopReading ⇒ selector ! DisableReadInterest
|
||||||
|
case ResumeReading ⇒ selector ! ReadInterest
|
||||||
|
case ChannelReadable ⇒ doRead(handler)
|
||||||
|
|
||||||
|
case Close ⇒
|
||||||
|
log.debug("Closing UDP connection to [{}]", remoteAddress)
|
||||||
|
channel.close()
|
||||||
|
sender ! Disconnected
|
||||||
|
log.debug("Connection closed to [{}], stopping listener", remoteAddress)
|
||||||
|
context.stop(self)
|
||||||
|
|
||||||
|
case send: Send if writePending ⇒
|
||||||
|
if (TraceLogging) log.debug("Dropping write because queue is full")
|
||||||
|
sender ! CommandFailed(send)
|
||||||
|
|
||||||
|
case send: Send if send.payload.isEmpty ⇒
|
||||||
|
if (send.wantsAck)
|
||||||
|
sender ! send.ack
|
||||||
|
|
||||||
|
case send: Send ⇒
|
||||||
|
pendingSend = (send, sender)
|
||||||
|
selector ! WriteInterest
|
||||||
|
|
||||||
|
case ChannelWritable ⇒ doWrite()
|
||||||
|
}
|
||||||
|
|
||||||
|
def doRead(handler: ActorRef): Unit = {
|
||||||
|
@tailrec def innerRead(readsLeft: Int, buffer: ByteBuffer): Unit = {
|
||||||
|
buffer.clear()
|
||||||
|
buffer.limit(DirectBufferSize)
|
||||||
|
|
||||||
|
if (channel.read(buffer) > 0) {
|
||||||
|
buffer.flip()
|
||||||
|
handler ! Received(ByteString(buffer))
|
||||||
|
innerRead(readsLeft - 1, buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val buffer = bufferPool.acquire()
|
||||||
|
try innerRead(BatchReceiveLimit, buffer) finally {
|
||||||
|
selector ! ReadInterest
|
||||||
|
bufferPool.release(buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final def doWrite(): Unit = {
|
||||||
|
|
||||||
|
val buffer = udpConn.bufferPool.acquire()
|
||||||
|
try {
|
||||||
|
val (send, commander) = pendingSend
|
||||||
|
buffer.clear()
|
||||||
|
send.payload.copyToBuffer(buffer)
|
||||||
|
buffer.flip()
|
||||||
|
val writtenBytes = channel.write(buffer)
|
||||||
|
if (TraceLogging) log.debug("Wrote [{}] bytes to channel", writtenBytes)
|
||||||
|
|
||||||
|
// Datagram channel either sends the whole message, or nothing
|
||||||
|
if (writtenBytes == 0) commander ! CommandFailed(send)
|
||||||
|
else if (send.wantsAck) commander ! send.ack
|
||||||
|
} finally {
|
||||||
|
udpConn.bufferPool.release(buffer)
|
||||||
|
pendingSend = null
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override def postStop() {
|
||||||
|
if (channel.isOpen) {
|
||||||
|
log.debug("Closing DatagramChannel after being stopped")
|
||||||
|
try channel.close()
|
||||||
|
catch {
|
||||||
|
case NonFatal(e) ⇒ log.error(e, "Error closing DatagramChannel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
66
akka-actor/src/main/scala/akka/io/UdpFF.scala
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2013 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
package akka.io
|
||||||
|
|
||||||
|
import akka.actor._
|
||||||
|
import akka.io.Inet.SocketOption
|
||||||
|
import akka.io.Udp.UdpSettings
|
||||||
|
import akka.util.ByteString
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import scala.collection.immutable
|
||||||
|
|
||||||
|
object UdpFF extends ExtensionKey[UdpFFExt] {
|
||||||
|
|
||||||
|
// Java API
|
||||||
|
override def get(system: ActorSystem): UdpFFExt = super.get(system)
|
||||||
|
|
||||||
|
trait Command extends IO.HasFailureMessage {
|
||||||
|
def failureMessage = CommandFailed(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
case object NoAck
|
||||||
|
case class Send(payload: ByteString, target: InetSocketAddress, ack: Any) extends Command {
|
||||||
|
require(ack != null, "ack must be non-null. Use NoAck if you don't want acks.")
|
||||||
|
|
||||||
|
def wantsAck: Boolean = ack != NoAck
|
||||||
|
}
|
||||||
|
object Send {
|
||||||
|
def apply(data: ByteString, target: InetSocketAddress): Send = Send(data, target, NoAck)
|
||||||
|
}
|
||||||
|
|
||||||
|
case class Bind(handler: ActorRef,
|
||||||
|
endpoint: InetSocketAddress,
|
||||||
|
options: immutable.Traversable[SocketOption] = Nil) extends Command
|
||||||
|
case object Unbind extends Command
|
||||||
|
|
||||||
|
case class SimpleSender(options: immutable.Traversable[SocketOption] = Nil) extends Command
|
||||||
|
object SimpleSender extends SimpleSender(Nil)
|
||||||
|
|
||||||
|
case object StopReading extends Command
|
||||||
|
case object ResumeReading extends Command
|
||||||
|
|
||||||
|
trait Event
|
||||||
|
|
||||||
|
case class Received(data: ByteString, sender: InetSocketAddress) extends Event
|
||||||
|
case class CommandFailed(cmd: Command) extends Event
|
||||||
|
case object Bound extends Event
|
||||||
|
case object SimpleSendReady extends Event
|
||||||
|
case object Unbound extends Event
|
||||||
|
|
||||||
|
case class SendFailed(cause: Throwable) extends Event
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class UdpFFExt(system: ExtendedActorSystem) extends IO.Extension {
|
||||||
|
|
||||||
|
val settings: UdpSettings = new UdpSettings(system.settings.config.getConfig("akka.io.udp-fire-and-forget"))
|
||||||
|
|
||||||
|
val manager: ActorRef = {
|
||||||
|
system.asInstanceOf[ActorSystemImpl].systemActorOf(
|
||||||
|
props = Props(new UdpFFManager(this)),
|
||||||
|
name = "IO-UDP-FF")
|
||||||
|
}
|
||||||
|
|
||||||
|
val bufferPool: BufferPool = new DirectByteBufferPool(settings.DirectBufferSize, settings.MaxDirectBufferPoolSize)
|
||||||
|
}
|
||||||
96
akka-actor/src/main/scala/akka/io/UdpFFListener.scala
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2013 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
package akka.io
|
||||||
|
|
||||||
|
import akka.actor.{ ActorLogging, Actor, ActorRef }
|
||||||
|
import akka.io.SelectionHandler._
|
||||||
|
import akka.io.UdpFF._
|
||||||
|
import akka.util.ByteString
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.channels.DatagramChannel
|
||||||
|
import java.nio.channels.SelectionKey._
|
||||||
|
import scala.annotation.tailrec
|
||||||
|
import scala.util.control.NonFatal
|
||||||
|
|
||||||
|
private[io] class UdpFFListener(val udpFF: UdpFFExt,
|
||||||
|
val bindCommander: ActorRef,
|
||||||
|
val bind: Bind)
|
||||||
|
extends Actor with ActorLogging with WithUdpFFSend {
|
||||||
|
|
||||||
|
import bind._
|
||||||
|
import udpFF.bufferPool
|
||||||
|
import udpFF.settings._
|
||||||
|
|
||||||
|
def selector: ActorRef = context.parent
|
||||||
|
|
||||||
|
context.watch(handler) // sign death pact
|
||||||
|
val channel = {
|
||||||
|
val datagramChannel = DatagramChannel.open
|
||||||
|
datagramChannel.configureBlocking(false)
|
||||||
|
val socket = datagramChannel.socket
|
||||||
|
options.foreach(_.beforeDatagramBind(socket))
|
||||||
|
try socket.bind(endpoint)
|
||||||
|
catch {
|
||||||
|
case NonFatal(e) ⇒
|
||||||
|
bindCommander ! CommandFailed(bind)
|
||||||
|
log.error(e, "Failed to bind UDP channel to endpoint [{}]", endpoint)
|
||||||
|
context.stop(self)
|
||||||
|
}
|
||||||
|
datagramChannel
|
||||||
|
}
|
||||||
|
context.parent ! RegisterChannel(channel, OP_READ)
|
||||||
|
log.debug("Successfully bound to [{}]", endpoint)
|
||||||
|
|
||||||
|
def receive: Receive = {
|
||||||
|
case ChannelRegistered ⇒
|
||||||
|
bindCommander ! Bound
|
||||||
|
context.become(readHandlers orElse sendHandlers, discardOld = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
def readHandlers: Receive = {
|
||||||
|
case StopReading ⇒ selector ! DisableReadInterest
|
||||||
|
case ResumeReading ⇒ selector ! ReadInterest
|
||||||
|
case ChannelReadable ⇒ doReceive(handler)
|
||||||
|
|
||||||
|
case Unbind ⇒
|
||||||
|
log.debug("Unbinding endpoint [{}]", endpoint)
|
||||||
|
try {
|
||||||
|
channel.close()
|
||||||
|
sender ! Unbound
|
||||||
|
log.debug("Unbound endpoint [{}], stopping listener", endpoint)
|
||||||
|
} finally context.stop(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
def doReceive(handler: ActorRef): Unit = {
|
||||||
|
@tailrec def innerReceive(readsLeft: Int, buffer: ByteBuffer) {
|
||||||
|
buffer.clear()
|
||||||
|
buffer.limit(DirectBufferSize)
|
||||||
|
|
||||||
|
channel.receive(buffer) match {
|
||||||
|
case sender: InetSocketAddress ⇒
|
||||||
|
buffer.flip()
|
||||||
|
handler ! Received(ByteString(buffer), sender)
|
||||||
|
if (readsLeft > 0) innerReceive(readsLeft - 1, buffer)
|
||||||
|
case null ⇒ // null means no data was available
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val buffer = bufferPool.acquire()
|
||||||
|
try innerReceive(BatchReceiveLimit, buffer) finally {
|
||||||
|
bufferPool.release(buffer)
|
||||||
|
selector ! ReadInterest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def postStop() {
|
||||||
|
if (channel.isOpen) {
|
||||||
|
log.debug("Closing DatagramChannel after being stopped")
|
||||||
|
try channel.close()
|
||||||
|
catch {
|
||||||
|
case NonFatal(e) ⇒ log.error(e, "Error closing DatagramChannel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
56
akka-actor/src/main/scala/akka/io/UdpFFManager.scala
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2013 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
package akka.io
|
||||||
|
|
||||||
|
import akka.actor.Props
|
||||||
|
import akka.io.IO.SelectorBasedManager
|
||||||
|
import akka.io.UdpFF._
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UdpFFManager is a facade for simple fire-and-forget style UDP operations
|
||||||
|
*
|
||||||
|
* UdpFFManager is obtainable by calling {{{ IO(UdpFF) }}} (see [[akka.io.IO]] and [[akka.io.UdpFF]])
|
||||||
|
*
|
||||||
|
* *Warning!* UdpFF uses [[java.nio.channels.DatagramChannel#send]] to deliver datagrams, and as a consequence if a
|
||||||
|
* security manager has been installed then for each datagram it will verify if the target address and port number are
|
||||||
|
* permitted. If this performance overhead is undesirable use the connection style Udp extension.
|
||||||
|
*
|
||||||
|
* == Bind and send ==
|
||||||
|
*
|
||||||
|
* To bind and listen to a local address, a [[akka.io.UdpFF..Bind]] command must be sent to this actor. If the binding
|
||||||
|
* was successful, the sender of the [[akka.io.UdpFF.Bind]] will be notified with a [[akka.io.UdpFF.Bound]]
|
||||||
|
* message. The sender of the [[akka.io.UdpFF.Bound]] message is the Listener actor (an internal actor responsible for
|
||||||
|
* listening to server events). To unbind the port an [[akka.io.Tcp.Unbind]] message must be sent to the Listener actor.
|
||||||
|
*
|
||||||
|
* If the bind request is rejected because the Udp system is not able to register more channels (see the nr-of-selectors
|
||||||
|
* and max-channels configuration options in the akka.io.udpFF section of the configuration) the sender will be notified
|
||||||
|
* with a [[akka.io.UdpFF.CommandFailed]] message. This message contains the original command for reference.
|
||||||
|
*
|
||||||
|
* The handler provided in the [[akka.io.UdpFF.Bind]] message will receive inbound datagrams to the bound port
|
||||||
|
* wrapped in [[akka.io.UdpFF.Received]] messages which contain the payload of the datagram and the sender address.
|
||||||
|
*
|
||||||
|
* UDP datagrams can be sent by sending [[akka.io.UdpFF.Send]] messages to the Listener actor. The sender port of the
|
||||||
|
* outbound datagram will be the port to which the Listener is bound.
|
||||||
|
*
|
||||||
|
* == Simple send ==
|
||||||
|
*
|
||||||
|
* UdpFF provides a simple method of sending UDP datagrams if no reply is expected. To acquire the Sender actor
|
||||||
|
* a SimpleSend message has to be sent to the manager. The sender of the command will be notified by a SimpleSendReady
|
||||||
|
* message that the service is available. UDP datagrams can be sent by sending [[akka.io.UdpFF.Send]] messages to the
|
||||||
|
* sender of SimpleSendReady. All the datagrams will contain an ephemeral local port as sender and answers will be
|
||||||
|
* discarded.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private[io] class UdpFFManager(udpFF: UdpFFExt) extends SelectorBasedManager(udpFF.settings, udpFF.settings.NrOfSelectors) {
|
||||||
|
|
||||||
|
def receive = workerForCommandHandler {
|
||||||
|
case b: Bind ⇒
|
||||||
|
val commander = sender
|
||||||
|
Props(new UdpFFListener(udpFF, commander, b))
|
||||||
|
case SimpleSender(options) ⇒
|
||||||
|
val commander = sender
|
||||||
|
Props(new UdpFFSender(udpFF, options, commander))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
51
akka-actor/src/main/scala/akka/io/UdpFFSender.scala
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2013 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
package akka.io
|
||||||
|
|
||||||
|
import akka.actor._
|
||||||
|
import java.nio.channels.DatagramChannel
|
||||||
|
import akka.io.UdpFF._
|
||||||
|
import akka.io.SelectionHandler.{ ChannelRegistered, RegisterChannel }
|
||||||
|
import scala.collection.immutable
|
||||||
|
import akka.io.Inet.SocketOption
|
||||||
|
import scala.util.control.NonFatal
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for TcpIncomingConnection and TcpOutgoingConnection.
|
||||||
|
*/
|
||||||
|
private[io] class UdpFFSender(val udpFF: UdpFFExt, options: immutable.Traversable[SocketOption], val commander: ActorRef)
|
||||||
|
extends Actor with ActorLogging with WithUdpFFSend {
|
||||||
|
|
||||||
|
def selector: ActorRef = context.parent
|
||||||
|
|
||||||
|
val channel = {
|
||||||
|
val datagramChannel = DatagramChannel.open
|
||||||
|
datagramChannel.configureBlocking(false)
|
||||||
|
val socket = datagramChannel.socket
|
||||||
|
|
||||||
|
options foreach { _.beforeDatagramBind(socket) }
|
||||||
|
|
||||||
|
datagramChannel
|
||||||
|
}
|
||||||
|
selector ! RegisterChannel(channel, 0)
|
||||||
|
|
||||||
|
def receive: Receive = {
|
||||||
|
case ChannelRegistered ⇒
|
||||||
|
context.become(sendHandlers, discardOld = true)
|
||||||
|
commander ! SimpleSendReady
|
||||||
|
}
|
||||||
|
|
||||||
|
override def postStop(): Unit = if (channel.isOpen) {
|
||||||
|
log.debug("Closing DatagramChannel after being stopped")
|
||||||
|
try channel.close()
|
||||||
|
catch {
|
||||||
|
case NonFatal(e) ⇒ log.error(e, "Error closing DatagramChannel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def postRestart(reason: Throwable): Unit =
|
||||||
|
throw new IllegalStateException("Restarting not supported for connection actors.")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
75
akka-actor/src/main/scala/akka/io/WithUdpFFSend.scala
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2009-2013 Typesafe Inc. <http://www.typesafe.com>
|
||||||
|
*/
|
||||||
|
package akka.io
|
||||||
|
|
||||||
|
import akka.actor.{ ActorRef, ActorLogging, Actor }
|
||||||
|
import akka.io.UdpFF.{ CommandFailed, Send }
|
||||||
|
import akka.io.SelectionHandler._
|
||||||
|
import java.nio.channels.DatagramChannel
|
||||||
|
|
||||||
|
private[io] trait WithUdpFFSend {
|
||||||
|
me: Actor with ActorLogging ⇒
|
||||||
|
|
||||||
|
var pendingSend: Send = null
|
||||||
|
var pendingCommander: ActorRef = null
|
||||||
|
// If send fails first, we allow a second go after selected writable, but no more. This flag signals that
|
||||||
|
// pending send was already tried once.
|
||||||
|
var retriedSend = false
|
||||||
|
def hasWritePending = pendingSend ne null
|
||||||
|
|
||||||
|
def selector: ActorRef
|
||||||
|
def channel: DatagramChannel
|
||||||
|
def udpFF: UdpFFExt
|
||||||
|
val settings = udpFF.settings
|
||||||
|
|
||||||
|
import settings._
|
||||||
|
|
||||||
|
def sendHandlers: Receive = {
|
||||||
|
|
||||||
|
case send: Send if hasWritePending ⇒
|
||||||
|
if (TraceLogging) log.debug("Dropping write because queue is full")
|
||||||
|
sender ! CommandFailed(send)
|
||||||
|
|
||||||
|
case send: Send if send.payload.isEmpty ⇒
|
||||||
|
if (send.wantsAck)
|
||||||
|
sender ! send.ack
|
||||||
|
|
||||||
|
case send: Send ⇒
|
||||||
|
pendingSend = send
|
||||||
|
pendingCommander = sender
|
||||||
|
doSend()
|
||||||
|
|
||||||
|
case ChannelWritable ⇒ if (hasWritePending) doSend()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
final def doSend(): Unit = {
|
||||||
|
|
||||||
|
val buffer = udpFF.bufferPool.acquire()
|
||||||
|
try {
|
||||||
|
buffer.clear()
|
||||||
|
pendingSend.payload.copyToBuffer(buffer)
|
||||||
|
buffer.flip()
|
||||||
|
val writtenBytes = channel.send(buffer, pendingSend.target)
|
||||||
|
if (TraceLogging) log.debug("Wrote [{}] bytes to channel", writtenBytes)
|
||||||
|
|
||||||
|
// Datagram channel either sends the whole message, or nothing
|
||||||
|
if (writtenBytes == 0) {
|
||||||
|
if (retriedSend) {
|
||||||
|
pendingCommander ! CommandFailed(pendingSend)
|
||||||
|
retriedSend = false
|
||||||
|
pendingSend = null
|
||||||
|
pendingCommander = null
|
||||||
|
} else {
|
||||||
|
selector ! WriteInterest
|
||||||
|
retriedSend = true
|
||||||
|
}
|
||||||
|
} else if (pendingSend.wantsAck) pendingCommander ! pendingSend.ack
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
udpFF.bufferPool.release(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ Information for Developers
|
||||||
|
|
||||||
building-akka
|
building-akka
|
||||||
multi-jvm-testing
|
multi-jvm-testing
|
||||||
|
io-layer
|
||||||
developer-guidelines
|
developer-guidelines
|
||||||
documentation
|
documentation
|
||||||
team
|
team
|
||||||
|
|
|
||||||
116
akka-docs/rst/dev/io-layer.rst
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
.. _io-layer:
|
||||||
|
|
||||||
|
################
|
||||||
|
I/O Layer Design
|
||||||
|
################
|
||||||
|
|
||||||
|
The ``akka.io`` package has been developed in collaboration between the Akka
|
||||||
|
and `spray.io`_ teams. Its design incorporates the experiences with the
|
||||||
|
``spray-io`` module along with improvements that were jointly developed for
|
||||||
|
more general consumption as an actor-based service.
|
||||||
|
|
||||||
|
Requirements
|
||||||
|
============
|
||||||
|
|
||||||
|
In order to form a general and extensible IO layer basis for a wide range of
|
||||||
|
applications, with Akka remoting and spray HTTP being the initial ones, the
|
||||||
|
following requirements were established as key drivers for the design:
|
||||||
|
|
||||||
|
* scalability to millions of concurrent connections
|
||||||
|
|
||||||
|
* lowest possible latency in getting data from an input channel into the
|
||||||
|
target actor’s mailbox
|
||||||
|
|
||||||
|
* maximal throughput
|
||||||
|
|
||||||
|
* optional back-pressure in both directions (i.e. throttling local senders as
|
||||||
|
well as allowing local readers to throttle remote senders, where allowed by
|
||||||
|
the protocol)
|
||||||
|
|
||||||
|
* a purely actor-based API with immutable data representation
|
||||||
|
|
||||||
|
* extensibility for integrating new transports by way of a very lean SPI; the
|
||||||
|
goal is to not force I/O mechanisms into a lowest common denominator but
|
||||||
|
instead allow completely protocol-specific user-level APIs.
|
||||||
|
|
||||||
|
Basic Architecture
|
||||||
|
==================
|
||||||
|
|
||||||
|
Each transport implementation will be made available as a separate Akka
|
||||||
|
extension, offering an :class:`ActorRef` representing the initial point of
|
||||||
|
contact for client code. This "manager" accepts requests for establishing a
|
||||||
|
communications channel (e.g. connect or listen on a TCP socket). Each
|
||||||
|
communications channel is represented by one dedicated actor, which is exposed
|
||||||
|
to client code for all interaction with this channel over its entire lifetime.
|
||||||
|
|
||||||
|
The central element of the implementation is the transport-specific “selector”
|
||||||
|
actor; in the case of TCP this would wrap a :class:`java.nio.channels.Selector`.
|
||||||
|
The channel actors register their interest in readability or writability of
|
||||||
|
their channel by sending corresponding messages to their assigned selector
|
||||||
|
actor. However, the actual channel reading and writing is performed by the
|
||||||
|
channel actors themselves, which frees the selector actors from time-consuming
|
||||||
|
tasks and thereby ensures low latency. The selector actor's only responsibility
|
||||||
|
is the management of the underlying selector's key set and the actual select
|
||||||
|
operation, which is the only operation to typically block.
|
||||||
|
|
||||||
|
The assignment of channels to selectors is performed by the manager actor and
|
||||||
|
remains unchanged for the entire lifetime of a channel. Thereby the management
|
||||||
|
actor "stripes" new channels across one or more selector actors based on some
|
||||||
|
implementation-specific distribution logic. This logic may be delegated (in
|
||||||
|
part) to the selectors actors, which could, for example, choose to reject the
|
||||||
|
assignment of a new channel when they consider themselves to be at capacity.
|
||||||
|
|
||||||
|
The manager actor creates (and therefore supervises) the selector actors, which
|
||||||
|
in turn create and supervise their channel actors. The actor hierarchy of one
|
||||||
|
single transport implementation therefore consists of three distinct actor
|
||||||
|
levels, with the management actor at the top-, the channel actors at the leaf-
|
||||||
|
and the selector actors at the mid-level.
|
||||||
|
|
||||||
|
Back-pressure for output is enabled by allowing the user to specify within its
|
||||||
|
:class:`Write` messages whether it wants to receive an acknowledgement for
|
||||||
|
enqueuing that write to the O/S kernel. Back-pressure for input is enabled by
|
||||||
|
sending the channel actor a message which temporarily disables read interest
|
||||||
|
for the channel until reading is re-enabled with a corresponding resume command.
|
||||||
|
In the case of transports with flow control—like TCP—the act of not
|
||||||
|
consuming data at the receiving end (thereby causing them to remain in the
|
||||||
|
kernels read buffers) is propagated back to the sender, linking these two
|
||||||
|
mechanisms across the network.
|
||||||
|
|
||||||
|
Design Benefits
|
||||||
|
===============
|
||||||
|
|
||||||
|
Staying within the actor model for the whole implementation allows us to remove
|
||||||
|
the need for explicit thread handling logic, and it also means that there are
|
||||||
|
no locks involved (besides those which are part of the underlying transport
|
||||||
|
library). Writing only actor code results in a cleaner implementation,
|
||||||
|
while Akka’s efficient actor messaging does not impose a high tax for this
|
||||||
|
benefit. In fact the event-based nature of I/O maps so well to the actor model
|
||||||
|
that we expect clear performance and especially scalability benefits over
|
||||||
|
traditional solutions with explicit thread management and synchronization.
|
||||||
|
|
||||||
|
Another benefit of supervision hierarchies is that clean-up of resources comes
|
||||||
|
naturally: shutting down a selector actor will automatically clean up all
|
||||||
|
channel actors, allowing proper closing of the channels and sending the
|
||||||
|
appropriate messages to user-level client actors. DeathWatch allows the channel
|
||||||
|
actors to notice the demise of their user-level handler actors and terminate in
|
||||||
|
an orderly fashion in that case as well; this naturally reduces the chances of
|
||||||
|
leaking open channels.
|
||||||
|
|
||||||
|
The choice of using :class:`ActorRef` for exposing all functionality entails
|
||||||
|
that these references can be distributed or delegated freely and in general
|
||||||
|
handled as the user sees fit, including the use of remoting and life-cycle
|
||||||
|
monitoring (just to name two).
|
||||||
|
|
||||||
|
How to go about Adding a New Transport
|
||||||
|
======================================
|
||||||
|
|
||||||
|
The best start is to study the TCP reference implementation to get a good grip
|
||||||
|
on the basic working principle and then design an implementation, which is
|
||||||
|
similar in spirit, but adapted to the new protocol in question. There are vast
|
||||||
|
differences between I/O mechanisms (e.g. compare file I/O to a message broker)
|
||||||
|
and the goal of this I/O layer is explicitly **not** to shoehorn all of them
|
||||||
|
into a uniform API, which is why only the basic architecture ideas are
|
||||||
|
documented here.
|
||||||
|
|
||||||
|
.. _spray.io: http://spray.io
|
||||||
|
|
||||||
7831
akka-docs/rst/images/tcp-message-protocol.graffle
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
3
akka-docs/rst/images/tcp-message-protocol_binding.svg
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
3
akka-docs/rst/images/tcp-message-protocol_closing.svg
Normal file
|
After Width: | Height: | Size: 5 KiB |
|
After Width: | Height: | Size: 9 KiB |
|
After Width: | Height: | Size: 9 KiB |
|
|
@ -0,0 +1,3 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="25 1593 490 155" width="490pt" height="155pt"><metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:date>2013-01-23 14:28Z</dc:date><!-- Produced by OmniGraffle Professional 5.3.6 --></metadata><defs><font-face font-family="Helvetica" font-size="10" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="522.94922" cap-height="717.28516" ascent="770.01953" descent="-229.98047" font-weight="500"><font-face-src><font-face-name name="Helvetica"/></font-face-src></font-face><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="NegativeControls_Marker" viewBox="-1 -5 2 10" markerWidth="2" markerHeight="10" color="black"><g><line x1="0" y1="-4" x2="0" y2="4" fill="none" stroke="currentColor" stroke-width="1"/></g></marker><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledArrow_Marker" viewBox="-1 -4 10 8" markerWidth="10" markerHeight="8" color="black"><g><path d="M 8 0 L 0 -3 L 0 3 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/></g></marker><font-face font-family="Helvetica" font-size="8" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="522.94922" cap-height="717.28516" ascent="770.01953" descent="-229.98047" font-weight="500"><font-face-src><font-face-name name="Helvetica"/></font-face-src></font-face><font-face font-family="Helvetica" font-size="10" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="532.22656" cap-height="719.72656" ascent="770.01953" descent="-229.98047" font-weight="bold"><font-face-src><font-face-name name="Helvetica-Bold"/></font-face-src></font-face></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Canvas 1</title><g><title>Layer 1</title><rect x="36" y="1620" width="468" height="18" fill="#ccc"/><rect x="36" y="1620" width="468" height="18" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(41 1623)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="176.46582" y="10" textLength="105.06836">Connection Established</tspan></text><text transform="translate(90 1650)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="18.490234" y="10" textLength="35.019531">Handler</tspan></text><text transform="translate(360 1650)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="20.923828" y="10" textLength="6.1083984">T</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="25.923828" y="10" textLength="61.152344">cpConnection</tspan></text><line x1="126" y1="1665" x2="126" y2="1719" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><line x1="414" y1="1665" x2="414" y2="1698.5" marker-end="url(#NegativeControls_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><line x1="414" y1="1683" x2="135.89999" y2="1683" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(135 1673.5)" fill="black"><tspan font-family="Helvetica" font-size="8" font-weight="500" x="4.4414062" y="8" textLength="50.246094">Closed / Confi</tspan><tspan font-family="Helvetica" font-size="8" font-weight="500" x="54.6875" y="8" textLength="49.796875">rmedClosed / </tspan><tspan font-family="Helvetica" font-size="8" font-weight="500" x="104.046875" y="8" textLength="152.51172">Aborted / PeerClosed / ErrorClosed(cause)</tspan></text><rect x="36" y="1719" width="468" height="18" fill="#ccc"/><rect x="36" y="1719" width="468" height="18" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(41 1722)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="168.68506" y="10" textLength="120.62988">No Connection Established</tspan></text><text transform="translate(36 1605)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="0" y="10" textLength="183.35449">Noticing that a Connection was closed</tspan></text></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 4.4 KiB |
3
akka-docs/rst/images/tcp-message-protocol_receiving.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="25 972 490 173" width="490pt" height="173pt"><metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:date>2013-01-23 14:28Z</dc:date><!-- Produced by OmniGraffle Professional 5.3.6 --></metadata><defs><font-face font-family="Helvetica" font-size="10" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="522.94922" cap-height="717.28516" ascent="770.01953" descent="-229.98047" font-weight="500"><font-face-src><font-face-name name="Helvetica"/></font-face-src></font-face><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledArrow_Marker" viewBox="-1 -4 10 8" markerWidth="10" markerHeight="8" color="black"><g><path d="M 8 0 L 0 -3 L 0 3 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/></g></marker><font-face font-family="Helvetica" font-size="8" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="522.94922" cap-height="717.28516" ascent="770.01953" descent="-229.98047" font-weight="500"><font-face-src><font-face-name name="Helvetica"/></font-face-src></font-face><font-face font-family="Helvetica" font-size="10" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="532.22656" cap-height="719.72656" ascent="770.01953" descent="-229.98047" font-weight="bold"><font-face-src><font-face-name name="Helvetica-Bold"/></font-face-src></font-face></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Canvas 1</title><g><title>Layer 1</title><rect x="36" y="999" width="468" height="18" fill="#ccc"/><rect x="36" y="999" width="468" height="18" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(41 1002)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="176.46582" y="10" textLength="105.06836">Connection Established</tspan></text><text transform="translate(234 1029)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="9.876953" y="10" textLength="6.1083984">T</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="14.876953" y="10" textLength="47.246094">cpSelector</tspan></text><text transform="translate(90 1029)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="18.490234" y="10" textLength="35.019531">Handler</tspan></text><text transform="translate(360 1029)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="20.923828" y="10" textLength="6.1083984">T</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="25.923828" y="10" textLength="61.152344">cpConnection</tspan></text><line x1="126" y1="1044" x2="126" y2="1116" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><line x1="270" y1="1044" x2="270" y2="1116" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><line x1="414" y1="1044" x2="414" y2="1116" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><line x1="270" y1="1062" x2="404.09998" y2="1062" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(333 1052.5)" fill="black"><tspan font-family="Helvetica" font-size="8" font-weight="500" x="3.9746094" y="8" textLength="64.05078">ChannelReadable</tspan></text><line x1="414" y1="1098" x2="279.90002" y2="1098" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(279 1088.5)" fill="black"><tspan font-family="Helvetica" font-size="8" font-weight="500" x="4.0976562" y="8" textLength="45.804688">ReadInterest</tspan></text><path d="M 414 1080 L 275.5 1080 C 275.5 1074.5 264.5 1074.5 264.5 1080 L 135.89999 1080" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(135 1070.5)" fill="black"><tspan font-family="Helvetica" font-size="8" font-weight="500" x="4.375" y="8" textLength="54.25">Received(data)</tspan></text><rect x="36" y="1116" width="468" height="18" fill="#ccc"/><rect x="36" y="1116" width="468" height="18" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(41 1119)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="176.46582" y="10" textLength="105.06836">Connection Established</tspan></text><text transform="translate(36 984)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="0" y="10" textLength="162.80762">Receiving Data from a Connection</tspan></text></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 4.9 KiB |
3
akka-docs/rst/images/tcp-message-protocol_unbinding.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="25 243 490 173" width="490pt" height="173pt"><metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:date>2013-01-22 22:05Z</dc:date><!-- Produced by OmniGraffle Professional 5.3.6 --></metadata><defs><font-face font-family="Helvetica" font-size="10" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="522.94922" cap-height="717.28516" ascent="770.01953" descent="-229.98047" font-weight="500"><font-face-src><font-face-name name="Helvetica"/></font-face-src></font-face><font-face font-family="Helvetica" font-size="8" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="522.94922" cap-height="717.28516" ascent="770.01953" descent="-229.98047" font-weight="500"><font-face-src><font-face-name name="Helvetica"/></font-face-src></font-face><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledArrow_Marker" viewBox="-1 -4 10 8" markerWidth="10" markerHeight="8" color="black"><g><path d="M 8 0 L 0 -3 L 0 3 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/></g></marker><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="NegativeControls_Marker" viewBox="-1 -5 2 10" markerWidth="2" markerHeight="10" color="black"><g><line x1="0" y1="-4" x2="0" y2="4" fill="none" stroke="currentColor" stroke-width="1"/></g></marker><font-face font-family="Helvetica" font-size="10" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="532.22656" cap-height="719.72656" ascent="770.01953" descent="-229.98047" font-weight="bold"><font-face-src><font-face-name name="Helvetica-Bold"/></font-face-src></font-face></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Canvas 1</title><g><title>Layer 1</title><text transform="translate(126 300)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="12.657715" y="10" textLength="23.891602">User </tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="36.002441" y="10" textLength="23.339844">Actor</tspan></text><text transform="translate(171 341.5)" fill="black"><tspan font-family="Helvetica" font-size="8" font-weight="500" x="6.263672" y="8" textLength="32.472656">Unbound</tspan></text><line x1="162" y1="315" x2="162" y2="387" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><line x1="162" y1="333" x2="368.09998" y2="333" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(333 323.5)" fill="black"><tspan font-family="Helvetica" font-size="8" font-weight="500" x="5.3242188" y="8" textLength="25.351562">Unbind</tspan></text><line x1="378" y1="351" x2="171.90001" y2="351" marker-end="url(#FilledArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><line x1="378" y1="315" x2="378" y2="366.5" marker-end="url(#NegativeControls_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><rect x="36" y="270" width="468" height="18" fill="#ccc"/><rect x="36" y="270" width="468" height="18" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(41 273)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="198.42627" y="10" textLength="61.14746">Server Bound</tspan></text><text transform="translate(342 300)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="10.4311523" y="10" textLength="6.1083984">T</tspan><tspan font-family="Helvetica" font-size="10" font-weight="500" x="15.431152" y="10" textLength="46.137695">cpListener</tspan></text><rect x="36" y="387" width="468" height="18" fill="#ccc"/><rect x="36" y="387" width="468" height="18" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(41 390)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="500" x="192.58887" y="10" textLength="72.822266">Server Unbound</tspan></text><text transform="translate(36 255)" fill="black"><tspan font-family="Helvetica" font-size="10" font-weight="bold" x="0" y="10" textLength="91.68457">Unbinding a Server</tspan></text></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 4.5 KiB |
3
akka-docs/rst/images/tcp-message-protocol_writing.svg
Normal file
|
After Width: | Height: | Size: 7 KiB |
248
akka-docs/rst/scala/io-old.rst
Normal file
|
|
@ -0,0 +1,248 @@
|
||||||
|
.. _io-scala-old:
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
This is the documentation of the old IO implementation that is considered now deprecated. Please take a look
|
||||||
|
at new IO API: :ref:`io-scala`
|
||||||
|
|
||||||
|
IO (Scala)
|
||||||
|
==========
|
||||||
|
|
||||||
|
|
||||||
|
Introduction
|
||||||
|
------------
|
||||||
|
|
||||||
|
This documentation is in progress and some sections may be incomplete. More will be coming.
|
||||||
|
|
||||||
|
Components
|
||||||
|
----------
|
||||||
|
|
||||||
|
ByteString
|
||||||
|
^^^^^^^^^^
|
||||||
|
|
||||||
|
A primary goal of Akka's IO support is to only communicate between actors with immutable objects. When dealing with network IO on the jvm ``Array[Byte]`` and ``ByteBuffer`` are commonly used to represent collections of ``Byte``\s, but they are mutable. Scala's collection library also lacks a suitably efficient immutable collection for ``Byte``\s. Being able to safely and efficiently move ``Byte``\s around is very important for this IO support, so ``ByteString`` was developed.
|
||||||
|
|
||||||
|
``ByteString`` is a `Rope-like <http://en.wikipedia.org/wiki/Rope_(computer_science)>`_ data structure that is immutable and efficient. When 2 ``ByteString``\s are concatenated together they are both stored within the resulting ``ByteString`` instead of copying both to a new ``Array``. Operations such as ``drop`` and ``take`` return ``ByteString``\s that still reference the original ``Array``, but just change the offset and length that is visible. Great care has also been taken to make sure that the internal ``Array`` cannot be modified. Whenever a potentially unsafe ``Array`` is used to create a new ``ByteString`` a defensive copy is created. If you require a ``ByteString`` that only blocks a much memory as necessary for it's content, use the ``compact`` method to get a ``CompactByteString`` instance. If the ``ByteString`` represented only a slice of the original array, this will result in copying all bytes in that slice.
|
||||||
|
|
||||||
|
``ByteString`` inherits all methods from ``IndexedSeq``, and it also has some new ones. For more information, look up the ``akka.util.ByteString`` class and it's companion object in the ScalaDoc.
|
||||||
|
|
||||||
|
``ByteString`` also comes with it's own optimized builder and iterator classes ``ByteStringBuilder`` and ``ByteIterator`` which provides special features in addition to the standard builder / iterator methods:
|
||||||
|
|
||||||
|
Compatibility with java.io
|
||||||
|
..........................
|
||||||
|
|
||||||
|
A ``ByteStringBuilder`` can be wrapped in a `java.io.OutputStream` via the ``asOutputStream`` method. Likewise, ``ByteIterator`` can we wrapped in a ``java.io.InputStream`` via ``asInputStream``. Using these, ``akka.io`` applications can integrate legacy code based on ``java.io`` streams.
|
||||||
|
|
||||||
|
Encoding and decoding of binary data
|
||||||
|
....................................
|
||||||
|
|
||||||
|
``ByteStringBuilder`` and ``ByteIterator`` support encoding and decoding of binary data. As an example, consider a stream of binary data frames with the following format:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
frameLen: Int
|
||||||
|
n: Int
|
||||||
|
m: Int
|
||||||
|
n times {
|
||||||
|
a: Short
|
||||||
|
b: Long
|
||||||
|
}
|
||||||
|
data: m times Double
|
||||||
|
|
||||||
|
In this example, the data is to be stored in arrays of ``a``, ``b`` and ``data``.
|
||||||
|
|
||||||
|
Decoding of such frames can be efficiently implemented in the following fashion:
|
||||||
|
|
||||||
|
.. includecode:: code/docs/io/BinaryCoding.scala
|
||||||
|
:include: decoding
|
||||||
|
|
||||||
|
This implementation naturally follows the example data format. In a true Scala application, one might, of course, want use specialized immutable Short/Long/Double containers instead of mutable Arrays.
|
||||||
|
|
||||||
|
After extracting data from a ``ByteIterator``, the remaining content can also be turned back into a ``ByteString`` using the ``toSeq`` method
|
||||||
|
|
||||||
|
.. includecode:: code/docs/io/BinaryCoding.scala
|
||||||
|
:include: rest-to-seq
|
||||||
|
|
||||||
|
with no copying from bytes to rest involved. In general, conversions from ByteString to ByteIterator and vice versa are O(1) for non-chunked ByteStrings and (at worst) O(nChunks) for chunked ByteStrings.
|
||||||
|
|
||||||
|
Encoding of data also is very natural, using ``ByteStringBuilder``
|
||||||
|
|
||||||
|
.. includecode:: code/docs/io/BinaryCoding.scala
|
||||||
|
:include: encoding
|
||||||
|
|
||||||
|
|
||||||
|
The encoded data then can be sent over socket (see ``IOManager``):
|
||||||
|
|
||||||
|
.. includecode:: code/docs/io/BinaryCoding.scala
|
||||||
|
:include: sending
|
||||||
|
|
||||||
|
|
||||||
|
IO.Handle
|
||||||
|
^^^^^^^^^
|
||||||
|
|
||||||
|
``IO.Handle`` is an immutable reference to a Java NIO ``Channel``. Passing mutable ``Channel``\s between ``Actor``\s could lead to unsafe behavior, so instead subclasses of the ``IO.Handle`` trait are used. Currently there are 2 concrete subclasses: ``IO.SocketHandle`` (representing a ``SocketChannel``) and ``IO.ServerHandle`` (representing a ``ServerSocketChannel``).
|
||||||
|
|
||||||
|
IOManager
|
||||||
|
^^^^^^^^^
|
||||||
|
|
||||||
|
The ``IOManager`` takes care of the low level IO details. Each ``ActorSystem`` has it's own ``IOManager``, which can be accessed calling ``IOManager(system: ActorSystem)``. ``Actor``\s communicate with the ``IOManager`` with specific messages. The messages sent from an ``Actor`` to the ``IOManager`` are handled automatically when using certain methods and the messages sent from an ``IOManager`` are handled within an ``Actor``\'s ``receive`` method.
|
||||||
|
|
||||||
|
Connecting to a remote host:
|
||||||
|
|
||||||
|
.. code-block:: scala
|
||||||
|
|
||||||
|
val address = new InetSocketAddress("remotehost", 80)
|
||||||
|
val socket = IOManager(actorSystem).connect(address)
|
||||||
|
|
||||||
|
.. code-block:: scala
|
||||||
|
|
||||||
|
val socket = IOManager(actorSystem).connect("remotehost", 80)
|
||||||
|
|
||||||
|
Creating a server:
|
||||||
|
|
||||||
|
.. code-block:: scala
|
||||||
|
|
||||||
|
val address = new InetSocketAddress("localhost", 80)
|
||||||
|
val serverSocket = IOManager(actorSystem).listen(address)
|
||||||
|
|
||||||
|
.. code-block:: scala
|
||||||
|
|
||||||
|
val serverSocket = IOManager(actorSystem).listen("localhost", 80)
|
||||||
|
|
||||||
|
Receiving messages from the ``IOManager``:
|
||||||
|
|
||||||
|
.. code-block:: scala
|
||||||
|
|
||||||
|
def receive = {
|
||||||
|
|
||||||
|
case IO.Listening(server, address) =>
|
||||||
|
println("The server is listening on socket " + address)
|
||||||
|
|
||||||
|
case IO.Connected(socket, address) =>
|
||||||
|
println("Successfully connected to " + address)
|
||||||
|
|
||||||
|
case IO.NewClient(server) =>
|
||||||
|
println("New incoming connection on server")
|
||||||
|
val socket = server.accept()
|
||||||
|
println("Writing to new client socket")
|
||||||
|
socket.write(bytes)
|
||||||
|
println("Closing socket")
|
||||||
|
socket.close()
|
||||||
|
|
||||||
|
case IO.Read(socket, bytes) =>
|
||||||
|
println("Received incoming data from socket")
|
||||||
|
|
||||||
|
case IO.Closed(socket: IO.SocketHandle, cause) =>
|
||||||
|
println("Socket has closed, cause: " + cause)
|
||||||
|
|
||||||
|
case IO.Closed(server: IO.ServerHandle, cause) =>
|
||||||
|
println("Server socket has closed, cause: " + cause)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
IO.Iteratee
|
||||||
|
^^^^^^^^^^^
|
||||||
|
|
||||||
|
Included with Akka's IO support is a basic implementation of ``Iteratee``\s. ``Iteratee``\s are an effective way of handling a stream of data without needing to wait for all the data to arrive. This is especially useful when dealing with non blocking IO since we will usually receive data in chunks which may not include enough information to process, or it may contain much more data than we currently need.
|
||||||
|
|
||||||
|
This ``Iteratee`` implementation is much more basic than what is usually found. There is only support for ``ByteString`` input, and enumerators aren't used. The reason for this limited implementation is to reduce the amount of explicit type signatures needed and to keep things simple. It is important to note that Akka's ``Iteratee``\s are completely optional, incoming data can be handled in any way, including other ``Iteratee`` libraries.
|
||||||
|
|
||||||
|
``Iteratee``\s work by processing the data that it is given and returning either the result (with any unused input) or a continuation if more input is needed. They are monadic, so methods like ``flatMap`` can be used to pass the result of an ``Iteratee`` to another.
|
||||||
|
|
||||||
|
The basic ``Iteratee``\s included in the IO support can all be found in the ScalaDoc under ``akka.actor.IO``, and some of them are covered in the example below.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
|
||||||
|
Http Server
|
||||||
|
^^^^^^^^^^^
|
||||||
|
|
||||||
|
This example will create a simple high performance HTTP server. We begin with our imports:
|
||||||
|
|
||||||
|
.. includecode:: code/docs/io/HTTPServer.scala
|
||||||
|
:include: imports
|
||||||
|
|
||||||
|
Some commonly used constants:
|
||||||
|
|
||||||
|
.. includecode:: code/docs/io/HTTPServer.scala
|
||||||
|
:include: constants
|
||||||
|
|
||||||
|
And case classes to hold the resulting request:
|
||||||
|
|
||||||
|
.. includecode:: code/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/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/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/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.
|
||||||
|
|
||||||
|
Next we handle the path itself:
|
||||||
|
|
||||||
|
.. includecode:: code/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).
|
||||||
|
|
||||||
|
Following the path we read in the query (if it exists):
|
||||||
|
|
||||||
|
.. includecode:: code/docs/io/HTTPServer.scala
|
||||||
|
:include: read-query
|
||||||
|
|
||||||
|
It is much simpler than reading the path since we aren't doing any parsing of the query since there is no standard format of the query string.
|
||||||
|
|
||||||
|
Both the path and query used the ``readUriPart`` ``Iteratee``, which is next:
|
||||||
|
|
||||||
|
.. includecode:: code/docs/io/HTTPServer.scala
|
||||||
|
:include: read-uri-part
|
||||||
|
|
||||||
|
Here we have several ``Set``\s that contain valid characters pulled from the URI spec. The ``readUriPart`` method takes a ``Set`` of valid characters (already mapped to ``Byte``\s) and will continue to match characters until it reaches on that is not part of the ``Set``. If it is a percent encoded character then that is handled as a valid character and processing continues, or else we are done collecting this part of the URI.
|
||||||
|
|
||||||
|
Headers are next:
|
||||||
|
|
||||||
|
.. includecode:: code/docs/io/HTTPServer.scala
|
||||||
|
:include: read-headers
|
||||||
|
|
||||||
|
And if applicable, we read in the message body:
|
||||||
|
|
||||||
|
.. includecode:: code/docs/io/HTTPServer.scala
|
||||||
|
:include: read-body
|
||||||
|
|
||||||
|
Finally we get to the actual ``Actor``:
|
||||||
|
|
||||||
|
.. includecode:: code/docs/io/HTTPServer.scala
|
||||||
|
:include: actor
|
||||||
|
|
||||||
|
And it's companion object:
|
||||||
|
|
||||||
|
.. includecode:: code/docs/io/HTTPServer.scala
|
||||||
|
:include: actor-companion
|
||||||
|
|
||||||
|
And the OKResponse:
|
||||||
|
|
||||||
|
.. includecode:: code/docs/io/HTTPServer.scala
|
||||||
|
:include: ok-response
|
||||||
|
|
||||||
|
A ``main`` method to start everything up:
|
||||||
|
|
||||||
|
.. includecode:: code/docs/io/HTTPServer.scala
|
||||||
|
:include: main
|
||||||
|
|
@ -1,21 +1,75 @@
|
||||||
.. _io-scala:
|
.. _io-scala:
|
||||||
|
|
||||||
IO (Scala)
|
I/O (Scala)
|
||||||
==========
|
==========
|
||||||
|
|
||||||
|
|
||||||
Introduction
|
Introduction
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
The ``akka.io`` package has been developed in collaboration between the Akka
|
||||||
|
and `spray.io`_ teams. Its design incorporates the experiences with the
|
||||||
|
``spray-io`` module along with improvements that were jointly developed for
|
||||||
|
more general consumption as an actor-based service.
|
||||||
|
|
||||||
This documentation is in progress and some sections may be incomplete. More will be coming.
|
This documentation is in progress and some sections may be incomplete. More will be coming.
|
||||||
|
|
||||||
Components
|
.. note::
|
||||||
----------
|
The old I/O implementation has been deprecated and its documentation has been moved: :ref:`io-scala-old`
|
||||||
|
|
||||||
|
Terminology, Concepts
|
||||||
|
---------------------
|
||||||
|
The I/O API is completely actor based, meaning that all operations are implemented as message passing instead of
|
||||||
|
direct method calls. Every I/O driver (TCP, UDP) has a special actor, called *manager* that serves
|
||||||
|
as the entry point for the API. The manager is accessible through an extension, for example the following code
|
||||||
|
looks up the TCP manager and returns its ``ActorRef``:
|
||||||
|
|
||||||
|
.. code-block:: scala
|
||||||
|
|
||||||
|
val tcpManager = IO(Tcp)
|
||||||
|
|
||||||
|
For various I/O commands the manager instantiates worker actors that will expose themselves to the user of the
|
||||||
|
API by replying to the command. For example after a ``Connect`` command sent to the TCP manager the manager creates
|
||||||
|
an actor representing the TCP connection. All operations related to the given TCP connections can be invoked by sending
|
||||||
|
messages to the connection actor which announces itself by sending a ``Connected`` message.
|
||||||
|
|
||||||
|
DeathWatch and Resource Management
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Worker actors usually need a user-side counterpart actor listening for events (such events could be inbound connections,
|
||||||
|
incoming bytes or acknowledgements for writes). These worker actors *watch* their listener counterparts, therefore the
|
||||||
|
resources assigned to them are automatically released when the listener stops. This design makes the API more robust
|
||||||
|
against resource leaks.
|
||||||
|
|
||||||
|
Thanks to the completely actor based approach of the I/O API the opposite direction works as well: a user actor
|
||||||
|
responsible for handling a connection might watch the connection actor to be notified if it unexpectedly terminates.
|
||||||
|
|
||||||
|
Write models (Ack, Nack)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Basically all of the I/O devices have a maximum throughput which limits the frequency and size of writes. When an
|
||||||
|
application tries to push more data then a device can handle, the driver has to buffer all bytes that the device has
|
||||||
|
not yet been able to write. With this approach it is possible to handle short bursts of intensive writes --- but no buffer is infinite.
|
||||||
|
Therefore, the driver has to notify the writer (a user-side actor) either that no further writes are possible, or by
|
||||||
|
explicitly notifying it when the next chunk is possible to be written or buffered.
|
||||||
|
|
||||||
|
Both of these models are available in the TCP and UDP implementations of Akka I/O. Ack based flow control can be enabled
|
||||||
|
by providing an ack object in the write message (``Write`` in the case of TCP and ``Send`` for UDP) that will be used by
|
||||||
|
the worker to notify the writer about the success.
|
||||||
|
|
||||||
|
If a write (or any other command) fails, the driver notifies the commander with a special message (``CommandFailed`` in
|
||||||
|
the case of UDP and TCP). This message also serves as a means to notify the writer of a failed write. Please note, that
|
||||||
|
in a Nack based flow-control setting the writer has to buffer some of the writes as the failure notification for a
|
||||||
|
write ``W1`` might arrive after additional write commands ``W2`` ``W3`` has been sent.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
An acknowledged write does not mean acknowledged delivery or storage. The Ack/Nack
|
||||||
|
protocol described here is a means of flow control not error handling: receiving an Ack for a write signals that the
|
||||||
|
I/O driver is ready to accept a new one.
|
||||||
|
|
||||||
ByteString
|
ByteString
|
||||||
^^^^^^^^^^
|
^^^^^^^^^^
|
||||||
|
|
||||||
A primary goal of Akka's IO support is to only communicate between actors with immutable objects. When dealing with network IO on the jvm ``Array[Byte]`` and ``ByteBuffer`` are commonly used to represent collections of ``Byte``\s, but they are mutable. Scala's collection library also lacks a suitably efficient immutable collection for ``Byte``\s. Being able to safely and efficiently move ``Byte``\s around is very important for this IO support, so ``ByteString`` was developed.
|
A primary goal of Akka's IO support is to only communicate between actors with immutable objects. When dealing with network I/O on the jvm ``Array[Byte]`` and ``ByteBuffer`` are commonly used to represent collections of ``Byte``\s, but they are mutable. Scala's collection library also lacks a suitably efficient immutable collection for ``Byte``\s. Being able to safely and efficiently move ``Byte``\s around is very important for this I/O support, so ``ByteString`` was developed.
|
||||||
|
|
||||||
``ByteString`` is a `Rope-like <http://en.wikipedia.org/wiki/Rope_(computer_science)>`_ data structure that is immutable and efficient. When 2 ``ByteString``\s are concatenated together they are both stored within the resulting ``ByteString`` instead of copying both to a new ``Array``. Operations such as ``drop`` and ``take`` return ``ByteString``\s that still reference the original ``Array``, but just change the offset and length that is visible. Great care has also been taken to make sure that the internal ``Array`` cannot be modified. Whenever a potentially unsafe ``Array`` is used to create a new ``ByteString`` a defensive copy is created. If you require a ``ByteString`` that only blocks a much memory as necessary for it's content, use the ``compact`` method to get a ``CompactByteString`` instance. If the ``ByteString`` represented only a slice of the original array, this will result in copying all bytes in that slice.
|
``ByteString`` is a `Rope-like <http://en.wikipedia.org/wiki/Rope_(computer_science)>`_ data structure that is immutable and efficient. When 2 ``ByteString``\s are concatenated together they are both stored within the resulting ``ByteString`` instead of copying both to a new ``Array``. Operations such as ``drop`` and ``take`` return ``ByteString``\s that still reference the original ``Array``, but just change the offset and length that is visible. Great care has also been taken to make sure that the internal ``Array`` cannot be modified. Whenever a potentially unsafe ``Array`` is used to create a new ``ByteString`` a defensive copy is created. If you require a ``ByteString`` that only blocks a much memory as necessary for it's content, use the ``compact`` method to get a ``CompactByteString`` instance. If the ``ByteString`` represented only a slice of the original array, this will result in copying all bytes in that slice.
|
||||||
|
|
||||||
|
|
@ -57,7 +111,7 @@ After extracting data from a ``ByteIterator``, the remaining content can also be
|
||||||
|
|
||||||
.. includecode:: code/docs/io/BinaryCoding.scala
|
.. includecode:: code/docs/io/BinaryCoding.scala
|
||||||
:include: rest-to-seq
|
:include: rest-to-seq
|
||||||
|
|
||||||
with no copying from bytes to rest involved. In general, conversions from ByteString to ByteIterator and vice versa are O(1) for non-chunked ByteStrings and (at worst) O(nChunks) for chunked ByteStrings.
|
with no copying from bytes to rest involved. In general, conversions from ByteString to ByteIterator and vice versa are O(1) for non-chunked ByteStrings and (at worst) O(nChunks) for chunked ByteStrings.
|
||||||
|
|
||||||
Encoding of data also is very natural, using ``ByteStringBuilder``
|
Encoding of data also is very natural, using ``ByteStringBuilder``
|
||||||
|
|
@ -65,180 +119,263 @@ Encoding of data also is very natural, using ``ByteStringBuilder``
|
||||||
.. includecode:: code/docs/io/BinaryCoding.scala
|
.. includecode:: code/docs/io/BinaryCoding.scala
|
||||||
:include: encoding
|
:include: encoding
|
||||||
|
|
||||||
|
Using TCP
|
||||||
The encoded data then can be sent over socket (see ``IOManager``):
|
---------
|
||||||
|
|
||||||
.. includecode:: code/docs/io/BinaryCoding.scala
|
|
||||||
:include: sending
|
|
||||||
|
|
||||||
|
As with all of the Akka I/O APIs, everything starts with acquiring a reference to the appropriate manager:
|
||||||
IO.Handle
|
|
||||||
^^^^^^^^^
|
|
||||||
|
|
||||||
``IO.Handle`` is an immutable reference to a Java NIO ``Channel``. Passing mutable ``Channel``\s between ``Actor``\s could lead to unsafe behavior, so instead subclasses of the ``IO.Handle`` trait are used. Currently there are 2 concrete subclasses: ``IO.SocketHandle`` (representing a ``SocketChannel``) and ``IO.ServerHandle`` (representing a ``ServerSocketChannel``).
|
|
||||||
|
|
||||||
IOManager
|
|
||||||
^^^^^^^^^
|
|
||||||
|
|
||||||
The ``IOManager`` takes care of the low level IO details. Each ``ActorSystem`` has it's own ``IOManager``, which can be accessed calling ``IOManager(system: ActorSystem)``. ``Actor``\s communicate with the ``IOManager`` with specific messages. The messages sent from an ``Actor`` to the ``IOManager`` are handled automatically when using certain methods and the messages sent from an ``IOManager`` are handled within an ``Actor``\'s ``receive`` method.
|
|
||||||
|
|
||||||
Connecting to a remote host:
|
|
||||||
|
|
||||||
.. code-block:: scala
|
.. code-block:: scala
|
||||||
|
|
||||||
val address = new InetSocketAddress("remotehost", 80)
|
import akka.io.IO
|
||||||
val socket = IOManager(actorSystem).connect(address)
|
import akka.io.Tcp
|
||||||
|
val tcpManager = IO(Tcp)
|
||||||
|
|
||||||
|
This is an actor that handles the underlying low level I/O resources (Selectors, channels) and instantiates workers for
|
||||||
|
specific tasks, like listening to incoming connections.
|
||||||
|
|
||||||
|
Connecting
|
||||||
|
^^^^^^^^^^
|
||||||
|
|
||||||
|
The first step of connecting to a remote address is sending a ``Connect`` message to the TCP manager:
|
||||||
|
|
||||||
.. code-block:: scala
|
.. code-block:: scala
|
||||||
|
|
||||||
val socket = IOManager(actorSystem).connect("remotehost", 80)
|
import akka.io.Tcp._
|
||||||
|
IO(Tcp) ! Connect(remoteSocketAddress)
|
||||||
|
// It is also possible to set various socket options or specify a local address:
|
||||||
|
IO(Tcp) ! Connect(remoteSocketAddress, Some(localSocketAddress), List(SO.KeepAlive(true)))
|
||||||
|
|
||||||
Creating a server:
|
After issuing the Connect command the TCP manager spawns a worker actor that will handle commands related to the
|
||||||
|
connection. This worker actor will reveal itself by replying with a ``Connected`` message to the actor who sent the
|
||||||
|
``Connect`` command.
|
||||||
|
|
||||||
.. code-block:: scala
|
.. code-block:: scala
|
||||||
|
|
||||||
val address = new InetSocketAddress("localhost", 80)
|
case Connected(remoteAddress, localAddress) =>
|
||||||
val serverSocket = IOManager(actorSystem).listen(address)
|
connectionActor = sender
|
||||||
|
|
||||||
|
At this point, there is still no listener associated with the connection. To finish the connection setup a ``Register``
|
||||||
|
has to be sent to the connection actor with the listener ``ActorRef`` as a parameter.
|
||||||
|
|
||||||
.. code-block:: scala
|
.. code-block:: scala
|
||||||
|
|
||||||
val serverSocket = IOManager(actorSystem).listen("localhost", 80)
|
connectionActor ! Register(listener)
|
||||||
|
|
||||||
Receiving messages from the ``IOManager``:
|
After registration, the listener actor provided in the ``listener`` parameter will be watched by the connection actor.
|
||||||
|
If the listener stops, the connection is closed, and all resources allocated for the connection released. During the
|
||||||
|
lifetime the listener may receive various event notifications:
|
||||||
|
|
||||||
.. code-block:: scala
|
.. code-block:: scala
|
||||||
|
|
||||||
def receive = {
|
case Received(dataByteString) => // handle incoming chunk of data
|
||||||
|
case CommandFailed(cmd) => // handle failure of command: cmd
|
||||||
|
case _: ConnectionClosed => // handle closed connections
|
||||||
|
|
||||||
case IO.Listening(server, address) =>
|
The last line handles all connection close events in the same way. It is possible to listen for more fine-grained
|
||||||
println("The server is listening on socket " + address)
|
connection events, see the appropriate section below.
|
||||||
|
|
||||||
case IO.Connected(socket, address) =>
|
|
||||||
println("Successfully connected to " + address)
|
|
||||||
|
|
||||||
case IO.NewClient(server) =>
|
Accepting connections
|
||||||
println("New incoming connection on server")
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
val socket = server.accept()
|
|
||||||
println("Writing to new client socket")
|
|
||||||
socket.write(bytes)
|
|
||||||
println("Closing socket")
|
|
||||||
socket.close()
|
|
||||||
|
|
||||||
case IO.Read(socket, bytes) =>
|
To create a TCP server and listen for inbound connection, a ``Bind`` command has to be sent to the TCP manager:
|
||||||
println("Received incoming data from socket")
|
|
||||||
|
|
||||||
case IO.Closed(socket: IO.SocketHandle, cause) =>
|
.. code-block:: scala
|
||||||
println("Socket has closed, cause: " + cause)
|
|
||||||
|
|
||||||
case IO.Closed(server: IO.ServerHandle, cause) =>
|
import akka.io.IO
|
||||||
println("Server socket has closed, cause: " + cause)
|
import akka.io.Tcp
|
||||||
|
IO(Tcp) ! Bind(handler, localAddress)
|
||||||
|
|
||||||
}
|
The actor sending the ``Bind`` message will receive a ``Bound`` message signalling that the server is ready to accept
|
||||||
|
incoming connections. Accepting connections is very similar to the last two steps of opening outbound connections: when
|
||||||
|
an incoming connection is established, the actor provided in ``handler`` will receive a ``Connected`` message whose
|
||||||
|
sender is the connection actor:
|
||||||
|
|
||||||
IO.Iteratee
|
.. code-block:: scala
|
||||||
^^^^^^^^^^^
|
|
||||||
|
|
||||||
Included with Akka's IO support is a basic implementation of ``Iteratee``\s. ``Iteratee``\s are an effective way of handling a stream of data without needing to wait for all the data to arrive. This is especially useful when dealing with non blocking IO since we will usually receive data in chunks which may not include enough information to process, or it may contain much more data than we currently need.
|
case Connected(remoteAddress, localAddress) =>
|
||||||
|
connectionActor = sender
|
||||||
|
|
||||||
This ``Iteratee`` implementation is much more basic than what is usually found. There is only support for ``ByteString`` input, and enumerators aren't used. The reason for this limited implementation is to reduce the amount of explicit type signatures needed and to keep things simple. It is important to note that Akka's ``Iteratee``\s are completely optional, incoming data can be handled in any way, including other ``Iteratee`` libraries.
|
At this point, there is still no listener associated with the connection. To finish the connection setup a ``Register``
|
||||||
|
has to be sent to the connection actor with the listener ``ActorRef`` as a parameter.
|
||||||
|
|
||||||
``Iteratee``\s work by processing the data that it is given and returning either the result (with any unused input) or a continuation if more input is needed. They are monadic, so methods like ``flatMap`` can be used to pass the result of an ``Iteratee`` to another.
|
.. code-block:: scala
|
||||||
|
|
||||||
The basic ``Iteratee``\s included in the IO support can all be found in the ScalaDoc under ``akka.actor.IO``, and some of them are covered in the example below.
|
connectionActor ! Register(listener)
|
||||||
|
|
||||||
Examples
|
After registration, the listener actor provided in the ``listener`` parameter will be watched by the connection actor.
|
||||||
--------
|
If the listener stops, the connection is closed, and all resources allocated for the connection released. During the
|
||||||
|
lifetime the listener will receive various event notifications in the same way as we has seen in the outbound
|
||||||
|
connection case.
|
||||||
|
|
||||||
Http Server
|
Closing connections
|
||||||
^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
This example will create a simple high performance HTTP server. We begin with our imports:
|
A connection can be closed by sending one of the commands ``Close``, ``ConfirmedClose`` or ``Abort`` to the connection
|
||||||
|
actor.
|
||||||
|
|
||||||
.. includecode:: code/docs/io/HTTPServer.scala
|
``Close`` will close the connection by sending a ``FIN`` message, but without waiting for confirmation from
|
||||||
:include: imports
|
the remote endpoint. Pending writes will be flushed. If the close is successful, the listener will be notified with
|
||||||
|
``Closed``
|
||||||
|
|
||||||
Some commonly used constants:
|
``ConfirmedClose`` will close the sending direction of the connection by sending a ``FIN`` message, but receives
|
||||||
|
will continue until the remote endpoint closes the connection, too. Pending writes will be flushed. If the close is
|
||||||
|
successful, the listener will be notified with ``ConfirmedClosed``
|
||||||
|
|
||||||
.. includecode:: code/docs/io/HTTPServer.scala
|
``Abort`` will immediately terminate the connection by sending a ``RST`` message to the remote endpoint. Pending
|
||||||
:include: constants
|
writes will be not flushed. If the close is successful, the listener will be notified with ``Aborted``
|
||||||
|
|
||||||
And case classes to hold the resulting request:
|
``PeerClosed`` will be sent to the listener if the connection has been closed by the remote endpoint.
|
||||||
|
|
||||||
.. includecode:: code/docs/io/HTTPServer.scala
|
``ErrorClosed`` will be sent to the listener whenever an error happened that forced the connection to be closed.
|
||||||
: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:
|
All close notifications are subclasses of ``ConnectionClosed`` so listeners who do not need fine-grained close events
|
||||||
|
may handle all close events in the same way.
|
||||||
|
|
||||||
.. includecode:: code/docs/io/HTTPServer.scala
|
Throttling Reads and Writes
|
||||||
: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.
|
*This section is not yet ready. More coming soon*
|
||||||
|
|
||||||
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``.
|
Using UDP
|
||||||
|
---------
|
||||||
|
|
||||||
.. includecode:: code/docs/io/HTTPServer.scala
|
UDP support comes in two flavors: connectionless, and connection based:
|
||||||
: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``.
|
.. code-block:: scala
|
||||||
|
|
||||||
The ``ascii`` method is a helper that takes a ``ByteString`` and parses it as a ``US-ASCII`` ``String``.
|
import akka.io.IO
|
||||||
|
import akka.io.UdpFF
|
||||||
|
val connectionLessUdp = IO(UdpFF)
|
||||||
|
// ... or ...
|
||||||
|
import akka.io.UdpConn
|
||||||
|
val connectionBasedUdp = IO(UdpConn)
|
||||||
|
|
||||||
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:
|
UDP servers can be only implemented by the connectionless API, but clients can use both.
|
||||||
|
|
||||||
.. includecode:: code/docs/io/HTTPServer.scala
|
Connectionless UDP
|
||||||
: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.
|
Simple Send
|
||||||
|
............
|
||||||
|
|
||||||
Next we handle the path itself:
|
To simply send a UDP datagram without listening to an answer one needs to send the ``SimpleSender`` command to the
|
||||||
|
manager:
|
||||||
|
|
||||||
.. includecode:: code/docs/io/HTTPServer.scala
|
.. code-block:: 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.
|
IO(UdpFF) ! SimpleSender
|
||||||
|
// or with socket options:
|
||||||
|
import akka.io.Udp._
|
||||||
|
IO(UdpFF) ! SimpleSender(List(SO.Broadcast(true)))
|
||||||
|
|
||||||
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).
|
The manager will create a worker for sending, and the worker will reply with a ``SimpleSendReady`` message:
|
||||||
|
|
||||||
Following the path we read in the query (if it exists):
|
.. code-block:: scala
|
||||||
|
|
||||||
.. includecode:: code/docs/io/HTTPServer.scala
|
case SimpleSendReady =>
|
||||||
:include: read-query
|
simpleSender = sender
|
||||||
|
|
||||||
It is much simpler than reading the path since we aren't doing any parsing of the query since there is no standard format of the query string.
|
After saving the sender of the ``SimpleSendReady`` message it is possible to send out UDP datagrams with a simple
|
||||||
|
message send:
|
||||||
|
|
||||||
Both the path and query used the ``readUriPart`` ``Iteratee``, which is next:
|
.. code-block:: scala
|
||||||
|
|
||||||
.. includecode:: code/docs/io/HTTPServer.scala
|
simpleSender ! Send(data, serverAddress)
|
||||||
:include: read-uri-part
|
|
||||||
|
|
||||||
Here we have several ``Set``\s that contain valid characters pulled from the URI spec. The ``readUriPart`` method takes a ``Set`` of valid characters (already mapped to ``Byte``\s) and will continue to match characters until it reaches on that is not part of the ``Set``. If it is a percent encoded character then that is handled as a valid character and processing continues, or else we are done collecting this part of the URI.
|
|
||||||
|
|
||||||
Headers are next:
|
Bind (and Send)
|
||||||
|
...............
|
||||||
|
|
||||||
.. includecode:: code/docs/io/HTTPServer.scala
|
To listen for UDP datagrams arriving on a given port, the ``Bind`` command has to be sent to the connectionless UDP
|
||||||
:include: read-headers
|
manager
|
||||||
|
|
||||||
And if applicable, we read in the message body:
|
.. code-block:: scala
|
||||||
|
|
||||||
.. includecode:: code/docs/io/HTTPServer.scala
|
IO(UdpFF) ! Bind(handler, localAddress)
|
||||||
:include: read-body
|
|
||||||
|
|
||||||
Finally we get to the actual ``Actor``:
|
After the bind succeeds, the sender of the ``Bind`` command will be notified with a ``Bound`` message. The sender of
|
||||||
|
this message is the worker for the UDP channel bound to the local address.
|
||||||
|
|
||||||
.. includecode:: code/docs/io/HTTPServer.scala
|
.. code-block:: scala
|
||||||
:include: actor
|
|
||||||
|
|
||||||
And it's companion object:
|
case Bound =>
|
||||||
|
udpWorker = sender // Save the worker ref for later use
|
||||||
|
|
||||||
.. includecode:: code/docs/io/HTTPServer.scala
|
The actor passed in the ``handler`` parameter will receive inbound UDP datagrams sent to the bound address:
|
||||||
:include: actor-companion
|
|
||||||
|
|
||||||
And the OKResponse:
|
.. code-block:: scala
|
||||||
|
|
||||||
.. includecode:: code/docs/io/HTTPServer.scala
|
case Received(dataByteString, remoteAddress) => // Do something with the data
|
||||||
:include: ok-response
|
|
||||||
|
|
||||||
A ``main`` method to start everything up:
|
The ``Received`` message contains the payload of the datagram and the address of the sender.
|
||||||
|
|
||||||
.. includecode:: code/docs/io/HTTPServer.scala
|
It is also possible to send UDP datagrams using the ``ActorRef`` of the worker saved in ``udpWorker``:
|
||||||
:include: main
|
|
||||||
|
.. code-block:: scala
|
||||||
|
|
||||||
|
udpWorker ! Send(data, serverAddress)
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
The difference between using a bound UDP worker to send instead of a simple-send worker is that in the former case
|
||||||
|
the sender field of the UDP datagram will be the bound local address, while in the latter it will be an undetermined
|
||||||
|
ephemeral port.
|
||||||
|
|
||||||
|
Connection based UDP
|
||||||
|
^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
The service provided by the connection based UDP API is similar to the bind-and-send service we have seen earlier, but
|
||||||
|
the main difference is that a connection is only able to send to the remoteAddress it was connected to, and will
|
||||||
|
receive datagrams only from that address.
|
||||||
|
|
||||||
|
Connecting is similar to what we have seen in the previous section:
|
||||||
|
|
||||||
|
.. code-block:: scala
|
||||||
|
|
||||||
|
IO(UdpConn) ! Connect(handler, remoteAddress)
|
||||||
|
// or, with more options:
|
||||||
|
IO(UdpConn) ! Connect(handler, Some(localAddress), remoteAddress, List(SO.Broadcast(true)))
|
||||||
|
|
||||||
|
After the connect succeeds, the sender of the ``Connect`` command will be notified with a ``Connected`` message. The sender of
|
||||||
|
this message is the worker for the UDP connection.
|
||||||
|
|
||||||
|
.. code-block:: scala
|
||||||
|
|
||||||
|
case Connected =>
|
||||||
|
udpConnectionActor = sender // Save the worker ref for later use
|
||||||
|
|
||||||
|
The actor passed in the ``handler`` parameter will receive inbound UDP datagrams sent to the bound address:
|
||||||
|
|
||||||
|
.. code-block:: scala
|
||||||
|
|
||||||
|
case Received(dataByteString) => // Do something with the data
|
||||||
|
|
||||||
|
The ``Received`` message contains the payload of the datagram but unlike in the connectionless case, no sender address
|
||||||
|
will be provided, as an UDP connection only receives messages from the endpoint it has been connected to.
|
||||||
|
|
||||||
|
It is also possible to send UDP datagrams using the ``ActorRef`` of the worker saved in ``udpWorker``:
|
||||||
|
|
||||||
|
.. code-block:: scala
|
||||||
|
|
||||||
|
udpConnectionActor ! Send(data)
|
||||||
|
|
||||||
|
Again, the send does not contain a remote address, as it is always the endpoint we have been connected to.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
There is a small performance benefit in using connection based UDP API over the connectionless one.
|
||||||
|
If there is a SecurityManager enabled on the system, every connectionless message send has to go through a security
|
||||||
|
check, while in the case of connection-based UDP the security check is cached after connect, thus writes does
|
||||||
|
not suffer an additional performance penalty.
|
||||||
|
|
||||||
|
Throttling Reads and Writes
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
*This section is not yet ready. More coming soon*
|
||||||
|
|
||||||
|
|
||||||
|
Architecture in-depth
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
For further details on the design and internal architecture see :ref:`io-layer`.
|
||||||
|
|
||||||
|
.. _spray.io: http://spray.io
|
||||||