/*
* 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 com.google.devcoin.protocols.niowrapper;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousCloseException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SocketChannel;
import javax.annotation.Nonnull;
import org.slf4j.LoggerFactory;
import static com.google.common.base.Preconditions.checkState;
/**
* Creates a simple connection to a server using a {@link ProtobufParser} to process data.
*/
public class ProtobufClient extends MessageWriteTarget {
private static final org.slf4j.Logger log = LoggerFactory.getLogger(ProtobufClient.class);
private static final int BUFFER_SIZE_LOWER_BOUND = 4096;
private static final int BUFFER_SIZE_UPPER_BOUND = 65536;
@Nonnull private final ByteBuffer dbuf;
@Nonnull private final SocketChannel sc;
/**
* <p>Creates a new client to the given server address using the given {@link ProtobufParser} to decode the data.
* The given parser <b>MUST</b> be unique to this object. This does not block while waiting for the connection to
* open, but will call either the {@link ProtobufParser#connectionOpen()} or {@link ProtobufParser#connectionClosed()}
* callback on the created network event processing thread.</p>
*
* @param connectTimeoutMillis The connect timeout set on the connection (in milliseconds). 0 is interpreted as no
* timeout.
*/
public ProtobufClient(final InetSocketAddress serverAddress, final ProtobufParser parser,
final int connectTimeoutMillis) throws IOException {
// 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 doesnt get too large or have to call read too often.
dbuf = ByteBuffer.allocateDirect(Math.min(Math.max(parser.maxMessageSize, BUFFER_SIZE_LOWER_BOUND), BUFFER_SIZE_UPPER_BOUND));
parser.setWriteTarget(this);
sc = SocketChannel.open();
new Thread() {
@Override
public void run() {
try {
sc.socket().connect(serverAddress, connectTimeoutMillis);
parser.connectionOpen();
while (true) {
int read = sc.read(dbuf);
if (read == 0)
continue;
else if (read == -1)
return;
// "flip" the buffer - setting the limit to the current position and setting position to 0
dbuf.flip();
// Use parser.receive's return value as a double-check that it stopped reading at the right
// location
int bytesConsumed = parser.receive(dbuf);
checkState(dbuf.position() == bytesConsumed);
// Now drop the bytes which were read by compacting dbuf (resetting limit and keeping relative
// position)
dbuf.compact();
}
} catch (AsynchronousCloseException e) {// Expected if the connection is closed
} catch (ClosedChannelException e) { // Expected if the connection is closed
} catch (Exception e) {
log.error("Error trying to open/read from connection", e);
} finally {
try {
sc.close();
} catch (IOException e1) {
// At this point there isn't much we can do, and we can probably assume the channel is closed
}
parser.connectionClosed();
}
}
}.start();
}
/**
* Closes the connection to the server, triggering the {@link ProtobufParser#connectionClosed()}
* event on the network-handling thread where all callbacks occur.
*/
public void closeConnection() {
// Closes the channel, triggering an exception in the network-handling thread triggering connectionClosed()
try {
sc.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// Writes raw bytes to the channel (used by the write method in ProtobufParser)
@Override
synchronized void writeBytes(byte[] message) {
try {
if (sc.write(ByteBuffer.wrap(message)) != message.length)
throw new IOException("Couldn't write all of message to socket");
} catch (IOException e) {
log.error("Error writing message to connection, closing connection", e);
closeConnection();
}
}
}