Akka Tcp创建对等体系结构而不是客户端服务器

时间:2016-06-14 17:16:08

标签: scala sockets tcp akka

在目前的Akka文档中有一个nice example of creating a client server architecture。我正在创建一个可以发送和接收比特币协议消息的Akka actor。到目前为止,我已经能够发送消息&收到我发送的消息的回复,但我无法按照对等协议的要求接收未经请求的消息。

我尝试使用Tcp.BindTcp.Connect能够在端口18333上收听未经请求的消息,并且能够向网络上的对等方发送消息。但是,我遇到这个问题,它会说端口已被绑定(由Tcp.Connect事件),或者它将无法从该端口发送消息(由于Tcp.Bind事件)。

如何在同一端口上发送消息并接收未经请求的消息?我在这里错过了什么吗?

sealed trait Client extends Actor with BitcoinSLogger {

  /**
    * The address of the peer we are attempting to connect to
    * on the p2p network
    * @return
    */
  def remote: InetSocketAddress

  /**
    * The actor that is listening to all communications between the
    * client and its peer on the network
    * @return
    */
  def listener : ActorRef


  def actorSystem : ActorSystem
  /**
    * The manager is an actor that handles the underlying low level I/O resources (selectors, channels)
    * and instantiates workers for specific tasks, such as listening to incoming connections.
    */
  def manager : ActorRef = IO(Tcp)(actorSystem)


  /**
    * This actor signifies the node we are connected to on the p2p network
    * This is set when we received a [[Tcp.Connected]] message
    */
  private var peer : Option[ActorRef] = None

  def receive = {
    case message : Tcp.Message => message match {
      case event : Tcp.Event =>
        logger.debug("Event: " + event)
        handleEvent(event)
      case command : Tcp.Command =>
        logger.debug("Command: " + command)
        handleCommand(command)
    }
    case unknownMessage => throw new IllegalArgumentException("Unknown message for client: " + unknownMessage)
  }

  /**
    * This function is responsible for handling a [[Tcp.Event]] algebraic data type
    * @param event
    */
  private def handleEvent(event : Tcp.Event) = event match {
    case Tcp.Bound(localAddress) =>
      logger.debug("Actor is now bound to the local address: " + localAddress)
    case Tcp.CommandFailed(w: Tcp.Write) =>
      logger.debug("Client write command failed: " + Tcp.CommandFailed(w))
      logger.debug("O/S buffer was full")
      // O/S buffer was full
      //listener ! "write failed"
    case Tcp.CommandFailed(command) =>
      logger.debug("Client Command failed:" + command)
    case Tcp.Received(data) =>
      logger.debug("Received data from our peer on the network: " + BitcoinSUtil.encodeHex(data.toArray))
      //listener ! data
    case Tcp.Connected(remote, local) =>
      logger.debug("Tcp connection to: " + remote)
      logger.debug("Local: " + local)
      peer = Some(sender)
      peer.get ! Tcp.Register(listener)
      listener ! Tcp.Connected(remote,local)
    case Tcp.ConfirmedClosed =>
      logger.debug("Client received confirmed closed msg: " + Tcp.ConfirmedClosed)
      peer = None
      context stop self
  }
  /**
    * This function is responsible for handling a [[Tcp.Command]] algebraic data type
    * @param command
    */
  private def handleCommand(command : Tcp.Command) = command match {
    case Tcp.ConfirmedClose =>
      logger.debug("Client received connection closed msg: " + Tcp.ConfirmedClose)
      listener ! Tcp.ConfirmedClose
      peer.get ! Tcp.ConfirmedClose
  }

}


case class ClientImpl(remote: InetSocketAddress, network : NetworkParameters,
                      listener: ActorRef, actorSystem : ActorSystem) extends Client {

  manager ! Tcp.Bind(listener, new InetSocketAddress(network.port))
  //this eagerly connects the client with our peer on the network as soon
  //as the case class is instantiated
  manager ! Tcp.Connect(remote)

}

object Client {


  def props(remote : InetSocketAddress, network : NetworkParameters, listener : ActorRef, actorSystem : ActorSystem) : Props = {
    Props(classOf[ClientImpl], remote, network, listener, actorSystem)
  }

  def apply(remote : InetSocketAddress, network : NetworkParameters, listener : ActorRef, actorSystem : ActorSystem) : ActorRef = {
   actorSystem.actorOf(props(remote, network, listener, actorSystem))
  }

  def apply(network : NetworkParameters, listener : ActorRef, actorSystem : ActorSystem) : ActorRef = {
    //val randomSeed = ((Math.random() * 10) % network.dnsSeeds.size).toInt
    val remote = new InetSocketAddress(network.dnsSeeds(0), network.port)
    Client(remote, network, listener, actorSystem)
  }

编辑:添加使用我的演员的测试用例

  "Client" must "connect to a node on the bitcoin network, " +
    "send a version message to a peer on the network and receive a version message back, then close that connection" in {
    val probe = TestProbe()
    val client = Client(TestNet3, probe.ref, system)

    val conn : Tcp.Connected = probe.expectMsgType[Tcp.Connected]

    val versionMessage = VersionMessage(TestNet3, conn.localAddress.getAddress,conn.remoteAddress.getAddress)
    val networkMessage = NetworkMessage(TestNet3, versionMessage)
    client ! networkMessage
    val receivedMsg = probe.expectMsgType[Tcp.Received](5.seconds)

    //~~~~~~~~THIS IS WHERE THE TEST IS FAILING~~~~~~~~~~~~~~~~~~
    //the bitcoin protocol states that after exchanging version messages a verack message is sent if the version message is accepted
    //this is appearing on wireshark, but not being found by my actor
    val verackMessage = probe.expectMsgType[Tcp.Received](2.seconds)

  }

EDIT2:

Wireshark输出显示我收到这些消息,而akka没有注册它们

enter image description here

2 个答案:

答案 0 :(得分:2)

Akka的核心抽象是Actors,因此Tcp中的对等只是Actors,你可以从AND接收消息并发送消息。

在这种情况下,您可以在收到ActorRef消息后拨打sender()来获取同行的Tcp.Connected。在您的代码中,您已经在peer中保存了该引用。它应该像peer.get ! Write(data)一样简单,将任意数据发送回该对等体。

由于连接可能在任何时候中断,docs似乎正在使用actor监督来处理这个问题:

class SimpleClient(connection: ActorRef, remote: InetSocketAddress)
    extends Actor with ActorLogging {

  import Tcp._

  // sign death pact: this actor terminates when connection breaks
  context watch connection
  ...
}

更新

(这让我花了很长时间才意识到。)你遇到的问题是你没有明确地处理消息框架:即缓冲区累积和消息重建的机制。 Akka TCP只提供原始缓冲区。这些缓冲区不一定会在消息边界上中断,甚至不知道任何有关搭乘TCP的高级协议(如BitCoin)的消息。

如果运行测试用例,则侦听器会收到包含1244字节数据的Tcp.Receive消息。由此单元测试提取NetworkHeaderVersionMessage,但完全有可能在此缓冲区中有更多消息被提取和处理,具体取决于比特币协议的细节,但它没有处理。相反,缓冲区被丢弃,测试用例等待第二个缓冲区(可能会或可能不会到达),并从此缓冲区构造另一条消息,隐藏的期望它恰好是完全字节对齐的。

在架构上我建议专门创建一个新的actor来处理消息框架。该actor将接收原始位并重建已完成的消息以发送给侦听器。

答案 1 :(得分:0)

TCP套接字有一个SO_REUSEADDR,我相信你可以在这里使用

.reuseAddress(true)

在套接字对象上

here我看到一个包含此属性的socket-options数组:

socket-options {
  so-receive-buffer-size = undefined
  so-send-buffer-size = undefined
  so-reuse-address = undefined
  so-traffic-class = undefined
  tcp-keep-alive = undefined
  tcp-oob-inline = undefined
  tcp-no-delay = undefined
}

我认为这就是你所寻找的,但我可能误解了这个问题。

相关问题