org.bitcoinj.core.PeerSocketHandler Maven / Gradle / Ivy
/*
* Copyright 2013 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.bitcoinj.core;
import org.bitcoinj.net.AbstractTimeoutHandler;
import org.bitcoinj.net.MessageWriteTarget;
import org.bitcoinj.net.StreamConnection;
import org.bitcoinj.utils.Threading;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.channels.NotYetConnectedException;
import java.util.concurrent.locks.Lock;
import static com.google.common.base.Preconditions.*;
/**
* Handles high-level message (de)serialization for peers, acting as the bridge between the
* {@link org.bitcoinj.net} classes and {@link Peer}.
*/
public abstract class PeerSocketHandler extends AbstractTimeoutHandler implements StreamConnection {
private static final Logger log = LoggerFactory.getLogger(PeerSocketHandler.class);
private final MessageSerializer serializer;
protected PeerAddress peerAddress;
// If we close() before we know our writeTarget, set this to true to call writeTarget.closeConnection() right away.
private boolean closePending = false;
// writeTarget will be thread-safe, and may call into PeerGroup, which calls us, so we should call it unlocked
@VisibleForTesting protected MessageWriteTarget writeTarget = null;
// The ByteBuffers passed to us from the writeTarget are static in size, and usually smaller than some messages we
// will receive. For SPV clients, this should be rare (ie we're mostly dealing with small transactions), but for
// messages which are larger than the read buffer, we have to keep a temporary buffer with its bytes.
private byte[] largeReadBuffer;
private int largeReadBufferPos;
private BitcoinSerializer.BitcoinPacketHeader header;
private Lock lock = Threading.lock("PeerSocketHandler");
public PeerSocketHandler(NetworkParameters params, InetSocketAddress remoteIp) {
checkNotNull(params);
serializer = params.getDefaultSerializer();
this.peerAddress = new PeerAddress(params, remoteIp);
}
public PeerSocketHandler(NetworkParameters params, PeerAddress peerAddress) {
checkNotNull(params);
serializer = params.getDefaultSerializer();
this.peerAddress = checkNotNull(peerAddress);
}
/**
* Sends the given message to the peer. Due to the asynchronousness of network programming, there is no guarantee
* the peer will have received it. Throws NotYetConnectedException if we are not yet connected to the remote peer.
* TODO: Maybe use something other than the unchecked NotYetConnectedException here
*/
public void sendMessage(Message message) throws NotYetConnectedException {
lock.lock();
try {
if (writeTarget == null)
throw new NotYetConnectedException();
} finally {
lock.unlock();
}
// TODO: Some round-tripping could be avoided here
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
serializer.serialize(message, out);
writeTarget.writeBytes(out.toByteArray());
} catch (IOException e) {
exceptionCaught(e);
}
}
/**
* Closes the connection to the peer if one exists, or immediately closes the connection as soon as it opens
*/
public void close() {
lock.lock();
try {
if (writeTarget == null) {
closePending = true;
return;
}
} finally {
lock.unlock();
}
writeTarget.closeConnection();
}
@Override
protected void timeoutOccurred() {
log.info("{}: Timed out", getAddress());
close();
}
/**
* Called every time a message is received from the network
*/
protected abstract void processMessage(Message m) throws Exception;
@Override
public int receiveBytes(ByteBuffer buff) {
checkArgument(buff.position() == 0 &&
buff.capacity() >= BitcoinSerializer.BitcoinPacketHeader.HEADER_LENGTH + 4);
try {
// Repeatedly try to deserialize messages until we hit a BufferUnderflowException
boolean firstMessage = true;
while (true) {
// If we are in the middle of reading a message, try to fill that one first, before we expect another
if (largeReadBuffer != null) {
// This can only happen in the first iteration
checkState(firstMessage);
// Read new bytes into the largeReadBuffer
int bytesToGet = Math.min(buff.remaining(), largeReadBuffer.length - largeReadBufferPos);
buff.get(largeReadBuffer, largeReadBufferPos, bytesToGet);
largeReadBufferPos += bytesToGet;
// Check the largeReadBuffer's status
if (largeReadBufferPos == largeReadBuffer.length) {
// ...processing a message if one is available
processMessage(serializer.deserializePayload(header, ByteBuffer.wrap(largeReadBuffer)));
largeReadBuffer = null;
header = null;
firstMessage = false;
} else // ...or just returning if we don't have enough bytes yet
return buff.position();
}
// Now try to deserialize any messages left in buff
Message message;
int preSerializePosition = buff.position();
try {
message = serializer.deserialize(buff);
} catch (BufferUnderflowException e) {
// If we went through the whole buffer without a full message, we need to use the largeReadBuffer
if (firstMessage && buff.limit() == buff.capacity()) {
// ...so reposition the buffer to 0 and read the next message header
buff.position(0);
try {
serializer.seekPastMagicBytes(buff);
header = serializer.deserializeHeader(buff);
// Initialize the largeReadBuffer with the next message's size and fill it with any bytes
// left in buff
largeReadBuffer = new byte[header.size];
largeReadBufferPos = buff.remaining();
buff.get(largeReadBuffer, 0, largeReadBufferPos);
} catch (BufferUnderflowException e1) {
// If we went through a whole buffer's worth of bytes without getting a header, give up
// In cases where the buff is just really small, we could create a second largeReadBuffer
// that we use to deserialize the magic+header, but that is rather complicated when the buff
// should probably be at least that big anyway (for efficiency)
throw new ProtocolException("No magic bytes+header after reading " + buff.capacity() + " bytes");
}
} else {
// Reposition the buffer to its original position, which saves us from skipping messages by
// seeking past part of the magic bytes before all of them are in the buffer
buff.position(preSerializePosition);
}
return buff.position();
}
// Process our freshly deserialized message
processMessage(message);
firstMessage = false;
}
} catch (Exception e) {
exceptionCaught(e);
return -1; // Returning -1 also throws an IllegalStateException upstream and kills the connection
}
}
/**
* Sets the {@link MessageWriteTarget} used to write messages to the peer. This should almost never be called, it is
* called automatically by {@link org.bitcoinj.net.NioClient} or
* {@link org.bitcoinj.net.NioClientManager} once the socket finishes initialization.
*/
@Override
public void setWriteTarget(MessageWriteTarget writeTarget) {
checkArgument(writeTarget != null);
lock.lock();
boolean closeNow = false;
try {
checkArgument(this.writeTarget == null);
closeNow = closePending;
this.writeTarget = writeTarget;
} finally {
lock.unlock();
}
if (closeNow)
writeTarget.closeConnection();
}
@Override
public int getMaxMessageSize() {
return Message.MAX_SIZE;
}
/**
* @return the IP address and port of peer.
*/
public PeerAddress getAddress() {
return peerAddress;
}
/** Catch any exceptions, logging them and then closing the channel. */
private void exceptionCaught(Exception e) {
PeerAddress addr = getAddress();
String s = addr == null ? "?" : addr.toString();
if (e instanceof ConnectException || e instanceof IOException) {
// Short message for network errors
log.info(s + " - " + e.getMessage());
} else {
log.warn(s + " - ", e);
Thread.UncaughtExceptionHandler handler = Threading.uncaughtExceptionHandler;
if (handler != null)
handler.uncaughtException(Thread.currentThread(), e);
}
close();
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy