在目前的Akka文档中有一个nice example of creating a client server architecture。我正在创建一个可以发送和接收比特币协议消息的Akka actor。到目前为止,我已经能够发送消息&收到我发送的消息的回复,但我无法按照对等协议的要求接收未经请求的消息。
我尝试使用Tcp.Bind
和Tcp.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没有注册它们
答案 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消息。由此单元测试提取NetworkHeader
和VersionMessage
,但完全有可能在此缓冲区中有更多消息被提取和处理,具体取决于比特币协议的细节,但它没有处理。相反,缓冲区被丢弃,测试用例等待第二个缓冲区(可能会或可能不会到达),并从此缓冲区构造另一条消息,隐藏的期望它恰好是完全字节对齐的。
在架构上我建议专门创建一个新的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
}
我认为这就是你所寻找的,但我可能误解了这个问题。