org.bitcoinj.net.BlockingClient 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.net;
import com.google.common.util.concurrent.*;
import org.bitcoinj.core.*;
import org.slf4j.*;
import javax.annotation.*;
import javax.net.*;
import java.io.*;
import java.net.*;
import java.nio.*;
import java.util.*;
import static com.google.common.base.Preconditions.*;
/**
* Creates a simple connection to a server using a {@link StreamConnection} to process data.
*
* Generally, using {@link NioClient} and {@link NioClientManager} should be preferred over {@link BlockingClient}
* and {@link BlockingClientManager}, unless you wish to connect over a proxy or use some other network settings that
* cannot be set using NIO.
*/
public class BlockingClient implements MessageWriteTarget {
private static final org.slf4j.Logger log = LoggerFactory.getLogger(BlockingClient.class);
private static final int BUFFER_SIZE_LOWER_BOUND = 4096;
private static final int BUFFER_SIZE_UPPER_BOUND = 65536;
private Socket socket;
private volatile boolean vCloseRequested = false;
private SettableFuture connectFuture;
/**
* Creates a new client to the given server address using the given {@link StreamConnection} to decode the data.
* The given connection MUST be unique to this object. This does not block while waiting for the connection to
* open, but will call either the {@link StreamConnection#connectionOpened()} or
* {@link StreamConnection#connectionClosed()} callback on the created network event processing thread.
*
* @param connectTimeoutMillis The connect timeout set on the connection (in milliseconds). 0 is interpreted as no
* timeout.
* @param socketFactory An object that creates {@link Socket} objects on demand, which may be customised to control
* how this client connects to the internet. If not sure, use SocketFactory.getDefault()
* @param clientSet A set which this object will add itself to after initialization, and then remove itself from
*/
public BlockingClient(final SocketAddress serverAddress, final StreamConnection connection,
final int connectTimeoutMillis, final SocketFactory socketFactory,
@Nullable final Set clientSet) throws IOException {
connectFuture = SettableFuture.create();
// Try to fit at least one message in the network buffer, but place an upper and lower limit on its size to make
// sure it doesn't get too large or have to call read too often.
connection.setWriteTarget(this);
socket = socketFactory.createSocket();
final Context context = Context.get();
Thread t = new Thread() {
@Override
public void run() {
Context.propagate(context);
if (clientSet != null)
clientSet.add(BlockingClient.this);
try {
socket.connect(serverAddress, connectTimeoutMillis);
connection.connectionOpened();
connectFuture.set(serverAddress);
InputStream stream = socket.getInputStream();
runReadLoop(stream, connection);
} catch (Exception e) {
if (!vCloseRequested) {
log.error("Error trying to open/read from connection: {}: {}", serverAddress, e.getMessage());
connectFuture.setException(e);
}
} finally {
try {
socket.close();
} catch (IOException e1) {
// At this point there isn't much we can do, and we can probably assume the channel is closed
}
if (clientSet != null)
clientSet.remove(BlockingClient.this);
connection.connectionClosed();
}
}
};
t.setName("BlockingClient network thread for " + serverAddress);
t.setDaemon(true);
t.start();
}
/**
* A blocking call that never returns, except by throwing an exception. It reads bytes from the input stream
* and feeds them to the provided {@link StreamConnection}, for example, a {@link Peer}.
*/
public static void runReadLoop(InputStream stream, StreamConnection connection) throws Exception {
ByteBuffer dbuf = ByteBuffer.allocateDirect(Math.min(Math.max(connection.getMaxMessageSize(), BUFFER_SIZE_LOWER_BOUND), BUFFER_SIZE_UPPER_BOUND));
byte[] readBuff = new byte[dbuf.capacity()];
while (true) {
// TODO Kill the message duplication here
checkState(dbuf.remaining() > 0 && dbuf.remaining() <= readBuff.length);
int read = stream.read(readBuff, 0, Math.max(1, Math.min(dbuf.remaining(), stream.available())));
if (read == -1)
return;
dbuf.put(readBuff, 0, read);
// "flip" the buffer - setting the limit to the current position and setting position to 0
dbuf.flip();
// Use connection.receiveBytes's return value as a double-check that it stopped reading at the right
// location
int bytesConsumed = connection.receiveBytes(dbuf);
checkState(dbuf.position() == bytesConsumed);
// Now drop the bytes which were read by compacting dbuf (resetting limit and keeping relative
// position)
dbuf.compact();
}
}
/**
* Closes the connection to the server, triggering the {@link StreamConnection#connectionClosed()}
* event on the network-handling thread where all callbacks occur.
*/
@Override
public void closeConnection() {
// Closes the channel, triggering an exception in the network-handling thread triggering connectionClosed()
try {
vCloseRequested = true;
socket.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public synchronized void writeBytes(byte[] message) throws IOException {
try {
OutputStream stream = socket.getOutputStream();
stream.write(message);
stream.flush();
} catch (IOException e) {
log.error("Error writing message to connection, closing connection", e);
closeConnection();
throw e;
}
}
/** Returns a future that completes once connection has occurred at the socket level or with an exception if failed to connect. */
public ListenableFuture getConnectFuture() {
return connectFuture;
}
}