/**
* Copyright 2009 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.waveprotocol.wave.examples.fedone.rpc;
import com.google.common.collect.Maps;
import com.google.protobuf.CodedInputStream;
import com.google.protobuf.CodedOutputStream;
import com.google.protobuf.JsonFormat;
import com.google.protobuf.Message;
import com.google.protobuf.MessageLite;
import com.google.protobuf.UnknownFieldSet;
import org.waveprotocol.wave.examples.fedone.util.Log;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.ByteChannel;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Create a two-way channel for protocol buffer exchange. Enhances this exchange
* with metadata in the form of sequence numbers.
*
*
*/
public class SequencedProtoChannel extends MessageExpectingChannel {
private static final Log LOG = Log.get(SequencedProtoChannel.class);
private final CodedOutputStream outputStream;
private final ByteChannel channel;
private final ExecutorService threadPool;
private final Runnable asyncRead;
private boolean isReading = false;
/**
* Internal helper method to remove and return the specified number of bytes
* from the beginning of the specified ByteBuffer.
*
* @param buffer the ByteBuffer instance to remove data from
* @param size the number of bytes requested
* @return an array of length exactly equal to size, the bytes at the start of
* the buffer which have now been removed - or null if the buffer
* contained less than this number of bytes
*/
private static byte[] popFromBuffer(ByteBuffer buffer, int size) {
if (buffer.position() < size) {
return null;
} else {
byte[] result = new byte[size];
buffer.flip();
buffer.get(result);
buffer.compact();
return result;
}
}
/**
* Instantiate a new SequencedProtoChannel. Requires the backing SocketChannel
* as well as the ProtoCallback to be notified on incoming messages.
*
* @param channel the backing ByteChannel, which must be blocking
* @param callback the callback for incoming known and unknown messages
* @param threadPool the service used to create threads
*/
public SequencedProtoChannel(final ByteChannel channel, final ProtoCallback callback,
ExecutorService threadPool) {
this.channel = channel;
this.threadPool = threadPool;
this.asyncRead = new Runnable() {
@Override
public void run() {
final int bufferSize = 8192 * 4;
ByteBuffer inputBuffer = ByteBuffer.allocate(bufferSize);
int requiredSize = -1;
try {
// we don't have enough data - read from buffer
while (-1 != channel.read(inputBuffer)) {
// While there's still data available, try to process it.
while (inputBuffer.position() > 0) {
// Grab our requiredSize if we still need it, popping a 32-bit int.
if (requiredSize == -1) {
byte[] buffer = popFromBuffer(inputBuffer, CodedOutputStream.LITTLE_ENDIAN_32_SIZE);
if (buffer != null) {
requiredSize = CodedInputStream.newInstance(buffer).readRawLittleEndian32();
} else {
// not enough data - fall out
break;
}
}
// Try to grab the whole payload.
if (requiredSize > bufferSize) {
throw new IllegalStateException(String.format("Payload (%d bytes) too large for" +
" buffer (%d bytes)", requiredSize, bufferSize));
} else if (requiredSize > -1) {
byte[] buffer = popFromBuffer(inputBuffer, requiredSize);
if (buffer != null) {
CodedInputStream inputStream = CodedInputStream.newInstance(buffer);
long incomingSequenceNo = inputStream.readInt64();
String messageType = inputStream.readString();
Message prototype = getMessagePrototype(messageType);
if (prototype == null) {
LOG.info("Received misunderstood message (??? " + messageType + " ???, seq "
+ incomingSequenceNo + ") from: " + channel);
// We have to emulate some of the semantics of reading a
// whole message here, including reading its encoded length.
final int length = inputStream.readRawVarint32();
final int oldLimit = inputStream.pushLimit(length);
UnknownFieldSet unknownFieldSet = UnknownFieldSet.parseFrom(inputStream);
inputStream.popLimit(oldLimit);
callback.unknown(incomingSequenceNo, messageType, unknownFieldSet);
} else {
// TODO: change to LOG.debug
LOG.fine("Received message (" + messageType + ", seq "
+ incomingSequenceNo + ") from: " + channel);
Message.Builder builder = prototype.newBuilderForType();
inputStream.readMessage(builder, null);
callback.message(incomingSequenceNo, builder.build());
}
// Reset the required size for the next invocation of this loop.
requiredSize = -1;
} else {
// not enough data - fall out
break;
}
}
}
}
} catch (IOException e) {
// TODO: error case.
e.printStackTrace();
}
}
};
outputStream = CodedOutputStream.newInstance(new OutputStream() {
@Override
public void write(int b) throws IOException {
channel.write(ByteBuffer.wrap(new byte[] {(byte) b}));
}
@Override
public void write(byte[] buf) throws IOException {
channel.write(ByteBuffer.wrap(buf));
}
@Override
public void write(byte[] buf, int off, int len) throws IOException {
channel.write(ByteBuffer.wrap(buf, off, len));
}
});
}
/**
* Create a new SequencedProtoChannel with a default thread executor. See
* {@link #SequencedProtoChannel(ByteChannel, ProtoCallback, ExecutorService)}.
*/
public SequencedProtoChannel(ByteChannel channel, ProtoCallback callback) {
this(channel, callback, Executors.newSingleThreadExecutor());
}
/**
* Kick off this class's asynchronous read method. Must be called to receive
* any messages on the callback.
*/
@Override
public void startAsyncRead() {
if (isReading) {
throw new IllegalStateException("This protoChannel is already reading asynchronously.");
}
threadPool.execute(asyncRead);
isReading = true;
}
/**
* Send the given message across the connection along with the sequence number.
*
* @param sequenceNo
* @param message
*/
public void sendMessage(long sequenceNo, Message message) {
internalSendMessage(sequenceNo, message, message.getDescriptorForType().getFullName());
}
private void internalSendMessage(long sequenceNo, MessageLite message, String messageType) {
int messageSize = message.getSerializedSize();
int size = CodedOutputStream.computeInt64SizeNoTag(sequenceNo)
+ CodedOutputStream.computeStringSizeNoTag(messageType)
+ CodedOutputStream.computeMessageSizeNoTag(message);
// TODO: change to LOG.debug
LOG.fine("Sending message (" + messageType + ", seq " + sequenceNo + ") to: " + channel);
// Only one message should be written at at time.
synchronized (outputStream) {
try {
// TODO: turn this into a data structure which can read/write itself
outputStream.writeRawLittleEndian32(size); // i.e., not including itself
outputStream.writeInt64NoTag(sequenceNo);
outputStream.writeStringNoTag(messageType);
outputStream.writeMessageNoTag(message);
outputStream.flush();
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
}
}