@Test
 public void fourPeers() throws Exception {
   InboundMessageQueuer[] channels = {
     connectPeer(1), connectPeer(2), connectPeer(3), connectPeer(4)
   };
   Transaction tx = new Transaction(params);
   TransactionBroadcast broadcast = new TransactionBroadcast(peerGroup, tx);
   ListenableFuture<Transaction> future = broadcast.broadcast();
   assertFalse(future.isDone());
   // We expect two peers to receive a tx message, and at least one of the others must announce for
   // the future to
   // complete successfully.
   Message[] messages = {
     (Message) outbound(channels[0]),
     (Message) outbound(channels[1]),
     (Message) outbound(channels[2]),
     (Message) outbound(channels[3])
   };
   // 0 and 3 are randomly selected to receive the broadcast.
   assertEquals(tx, messages[0]);
   assertEquals(tx, messages[3]);
   assertNull(messages[1]);
   assertNull(messages[2]);
   Threading.waitForUserCode();
   assertFalse(future.isDone());
   inbound(channels[1], InventoryMessage.with(tx));
   pingAndWait(channels[1]);
   Threading.waitForUserCode();
   assertTrue(future.isDone());
 }
Пример #2
0
/**
 * A simple NIO MessageWriteTarget which handles all the business logic of a connection
 * (reading+writing bytes). Used only by the NioClient and NioServer classes
 */
class ConnectionHandler implements MessageWriteTarget {
  private static final org.slf4j.Logger log = LoggerFactory.getLogger(ConnectionHandler.class);

  private static final int BUFFER_SIZE_LOWER_BOUND = 4096;
  private static final int BUFFER_SIZE_UPPER_BOUND = 65536;

  private static final int OUTBOUND_BUFFER_BYTE_COUNT =
      Message.MAX_SIZE + 24; // 24 byte message header

  // We lock when touching local flags and when writing data, but NEVER when calling any methods
  // which leave this
  // class into non-Java classes.
  private final ReentrantLock lock = Threading.lock("nioConnectionHandler");

  @GuardedBy("lock")
  private final ByteBuffer readBuff;

  @GuardedBy("lock")
  private final SocketChannel channel;

  @GuardedBy("lock")
  private final SelectionKey key;

  @GuardedBy("lock")
  StreamParser parser;

  @GuardedBy("lock")
  private boolean closeCalled = false;

  @GuardedBy("lock")
  private long bytesToWriteRemaining = 0;

  @GuardedBy("lock")
  private final LinkedList<ByteBuffer> bytesToWrite = new LinkedList<ByteBuffer>();

  private Set<ConnectionHandler> connectedHandlers;

  public ConnectionHandler(StreamParserFactory parserFactory, SelectionKey key) throws IOException {
    this(
        parserFactory.getNewParser(
            ((SocketChannel) key.channel()).socket().getInetAddress(),
            ((SocketChannel) key.channel()).socket().getPort()),
        key);
    if (parser == null) throw new IOException("Parser factory.getNewParser returned null");
  }

  private ConnectionHandler(@Nullable StreamParser parser, SelectionKey key) {
    this.key = key;
    this.channel = checkNotNull(((SocketChannel) key.channel()));
    if (parser == null) {
      readBuff = null;
      closeConnection();
      return;
    }
    this.parser = parser;
    readBuff =
        ByteBuffer.allocateDirect(
            Math.min(
                Math.max(parser.getMaxMessageSize(), BUFFER_SIZE_LOWER_BOUND),
                BUFFER_SIZE_UPPER_BOUND));
    parser.setWriteTarget(this); // May callback into us (eg closeConnection() now)
    connectedHandlers = null;
  }

  public ConnectionHandler(
      StreamParser parser, SelectionKey key, Set<ConnectionHandler> connectedHandlers) {
    this(checkNotNull(parser), key);

    // closeConnection() may have already happened, in which case we shouldn't add ourselves to the
    // connectedHandlers set
    lock.lock();
    boolean alreadyClosed = false;
    try {
      alreadyClosed = closeCalled;
      this.connectedHandlers = connectedHandlers;
    } finally {
      lock.unlock();
    }
    if (!alreadyClosed) checkState(connectedHandlers.add(this));
  }

  @GuardedBy("lock")
  private void setWriteOps() {
    // Make sure we are registered to get updated when writing is available again
    key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
    // Refresh the selector to make sure it gets the new interestOps
    key.selector().wakeup();
  }

  // Tries to write any outstanding write bytes, runs in any thread (possibly unlocked)
  private void tryWriteBytes() throws IOException {
    lock.lock();
    try {
      // Iterate through the outbound ByteBuff queue, pushing as much as possible into the OS'
      // network buffer.
      Iterator<ByteBuffer> bytesIterator = bytesToWrite.iterator();
      while (bytesIterator.hasNext()) {
        ByteBuffer buff = bytesIterator.next();
        bytesToWriteRemaining -= channel.write(buff);
        if (!buff.hasRemaining()) bytesIterator.remove();
        else {
          setWriteOps();
          break;
        }
      }
      // If we are done writing, clear the OP_WRITE interestOps
      if (bytesToWrite.isEmpty()) key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);
      // Don't bother waking up the selector here, since we're just removing an op, not adding
    } finally {
      lock.unlock();
    }
  }

  @Override
  public void writeBytes(byte[] message) throws IOException {
    lock.lock();
    try {
      // Network buffers are not unlimited (and are often smaller than some messages we may wish to
      // send), and
      // thus we have to buffer outbound messages sometimes. To do this, we use a queue of
      // ByteBuffers and just
      // append to it when we want to send a message. We then let tryWriteBytes() either send the
      // message or
      // register our SelectionKey to wakeup when we have free outbound buffer space available.

      if (bytesToWriteRemaining + message.length > OUTBOUND_BUFFER_BYTE_COUNT)
        throw new IOException("Outbound buffer overflowed");
      // Just dump the message onto the write buffer and call tryWriteBytes
      // TODO: Kill the needless message duplication when the write completes right away
      bytesToWrite.offer(ByteBuffer.wrap(Arrays.copyOf(message, message.length)));
      bytesToWriteRemaining += message.length;
      setWriteOps();
    } catch (IOException e) {
      lock.unlock();
      log.error("Error writing message to connection, closing connection", e);
      closeConnection();
      throw e;
    } catch (CancelledKeyException e) {
      lock.unlock();
      log.error("Error writing message to connection, closing connection", e);
      closeConnection();
      throw new IOException(e);
    }
    lock.unlock();
  }

  @Override
  // May NOT be called with lock held
  public void closeConnection() {
    try {
      channel.close();
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
    connectionClosed();
  }

  private void connectionClosed() {
    boolean callClosed = false;
    lock.lock();
    try {
      callClosed = !closeCalled;
      closeCalled = true;
    } finally {
      lock.unlock();
    }
    if (callClosed) {
      checkState(connectedHandlers == null || connectedHandlers.remove(this));
      parser.connectionClosed();
    }
  }

  // Handle a SelectionKey which was selected
  // Runs unlocked as the caller is single-threaded (or if not, should enforce that handleKey is
  // only called
  // atomically for a given ConnectionHandler)
  public static void handleKey(SelectionKey key) {
    ConnectionHandler handler = ((ConnectionHandler) key.attachment());
    try {
      if (handler == null) return;
      if (!key.isValid()) {
        handler.closeConnection(); // Key has been cancelled, make sure the socket gets closed
        return;
      }
      if (key.isReadable()) {
        // Do a socket read and invoke the parser's receiveBytes message
        int read = handler.channel.read(handler.readBuff);
        if (read == 0) return; // Was probably waiting on a write
        else if (read == -1) { // Socket was closed
          key.cancel();
          handler.closeConnection();
          return;
        }
        // "flip" the buffer - setting the limit to the current position and setting position to 0
        handler.readBuff.flip();
        // Use parser.receiveBytes's return value as a check that it stopped reading at the right
        // location
        int bytesConsumed = checkNotNull(handler.parser).receiveBytes(handler.readBuff);
        checkState(handler.readBuff.position() == bytesConsumed);
        // Now drop the bytes which were read by compacting readBuff (resetting limit and keeping
        // relative
        // position)
        handler.readBuff.compact();
      }
      if (key.isWritable()) handler.tryWriteBytes();
    } catch (Exception e) {
      // This can happen eg if the channel closes while the thread is about to get killed
      // (ClosedByInterruptException), or if handler.parser.receiveBytes throws something
      log.error("Error handling SelectionKey: {}", Throwables.getRootCause(e).getMessage());
      handler.closeConnection();
    }
  }
}
  @Test
  public void peerGroupWalletIntegration() throws Exception {
    // Make sure we can create spends, and that they are announced. Then do the same with offline
    // mode.

    // Set up connections and block chain.
    VersionMessage ver = new VersionMessage(params, 2);
    ver.localServices = VersionMessage.NODE_NETWORK;
    InboundMessageQueuer p1 = connectPeer(1, ver);
    InboundMessageQueuer p2 = connectPeer(2);

    // Send ourselves a bit of money.
    Block b1 = TestUtils.makeSolvedTestBlock(blockStore, address);
    inbound(p1, b1);
    pingAndWait(p1);
    assertNull(outbound(p1));
    assertEquals(Utils.toNanoCoins(50, 0), wallet.getBalance());

    // Check that the wallet informs us of changes in confidence as the transaction ripples across
    // the network.
    final Transaction[] transactions = new Transaction[1];
    wallet.addEventListener(
        new AbstractWalletEventListener() {
          @Override
          public void onTransactionConfidenceChanged(Wallet wallet, Transaction tx) {
            transactions[0] = tx;
          }
        });

    // Now create a spend, and expect the announcement on p1.
    Address dest = new ECKey().toAddress(params);
    Wallet.SendResult sendResult = wallet.sendCoins(peerGroup, dest, Utils.toNanoCoins(1, 0));
    assertNotNull(sendResult.tx);
    Threading.waitForUserCode();
    assertFalse(sendResult.broadcastComplete.isDone());
    assertEquals(transactions[0], sendResult.tx);
    assertEquals(0, transactions[0].getConfidence().numBroadcastPeers());
    transactions[0] = null;
    Transaction t1 = (Transaction) outbound(p1);
    assertNotNull(t1);
    // 49 BTC in change.
    assertEquals(Utils.toNanoCoins(49, 0), t1.getValueSentToMe(wallet));
    // The future won't complete until it's heard back from the network on p2.
    InventoryMessage inv = new InventoryMessage(params);
    inv.addTransaction(t1);
    inbound(p2, inv);
    pingAndWait(p2);
    Threading.waitForUserCode();
    assertTrue(sendResult.broadcastComplete.isDone());
    assertEquals(transactions[0], sendResult.tx);
    assertEquals(1, transactions[0].getConfidence().numBroadcastPeers());
    // Confirm it.
    Block b2 = TestUtils.createFakeBlock(blockStore, t1).block;
    inbound(p1, b2);
    pingAndWait(p1);
    assertNull(outbound(p1));

    // Do the same thing with an offline transaction.
    peerGroup.removeWallet(wallet);
    Wallet.SendRequest req = Wallet.SendRequest.to(dest, Utils.toNanoCoins(2, 0));
    req.ensureMinRequiredFee = false;
    Transaction t3 = checkNotNull(wallet.sendCoinsOffline(req));
    assertNull(outbound(p1)); // Nothing sent.
    // Add the wallet to the peer group (simulate initialization). Transactions should be announced.
    peerGroup.addWallet(wallet);
    // Transaction announced to the first peer.
    assertEquals(t3.getHash(), ((Transaction) outbound(p1)).getHash());
  }