Basic support for datagram messaging. Hope I haven't fucked

anything up!


git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@5090 542714f4-19e9-0310-aa3c-eee0fc999fb1
This commit is contained in:
Andrzej Kapolka
2008-05-13 21:12:39 +00:00
parent 02654017d0
commit 9bafa5e7c8
20 changed files with 1397 additions and 109 deletions
@@ -0,0 +1,97 @@
//
// $Id$
package com.threerings.io;
import java.io.IOException;
import java.io.InputStream;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.InvalidMarkException;
/**
* Reads input from a {@link ByteBuffer}.
*/
public class ByteBufferInputStream extends InputStream
{
/**
* Creates a new input stream to read from the specified buffer.
*/
public ByteBufferInputStream (ByteBuffer buffer)
{
_buffer = buffer;
}
/**
* Returns a reference to the underlying buffer.
*/
public ByteBuffer getBuffer ()
{
return _buffer;
}
@Override // documentation inherited
public int read ()
{
try {
return (_buffer.get() & 0xFF);
} catch (BufferUnderflowException e) {
return -1;
}
}
@Override // documentation inherited
public int read (byte[] b, int offset, int length)
throws IOException
{
length = Math.min(length, _buffer.remaining());
if (length <= 0) {
return -1;
}
_buffer.get(b, offset, length);
return length;
}
@Override // documentation inherited
public long skip (long n)
throws IOException
{
n = Math.min(n, _buffer.remaining());
_buffer.position((int)(_buffer.position() + n));
return n;
}
@Override // documentation inherited
public int available ()
{
return _buffer.remaining();
}
@Override // documentation inherited
public boolean markSupported ()
{
return true;
}
@Override // documentation inherited
public void mark (int readLimit)
{
_buffer.mark();
}
@Override // documentation inherited
public void reset ()
throws IOException
{
try {
_buffer.reset();
} catch (InvalidMarkException e) {
throw new IOException("No mark set.");
}
}
/** The buffer from which we read. */
protected ByteBuffer _buffer;
}
@@ -0,0 +1,102 @@
//
// $Id$
package com.threerings.io;
import java.io.OutputStream;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
/**
* Stores output in an {@link ByteBuffer} that grows automatically to accommodate the data.
*/
public class ByteBufferOutputStream extends OutputStream
{
/**
* Creates a new byte buffer output stream.
*/
public ByteBufferOutputStream ()
{
_buffer = ByteBuffer.allocate(INITIAL_BUFFER_SIZE);
}
/**
* Returns a reference to the underlying buffer.
*/
public ByteBuffer getBuffer ()
{
return _buffer;
}
/**
* Flips and returns the buffer. The returned buffer will have a position of zero and a limit
* equal to the number of bytes written. Call {@link #reset} to reset the buffer before
* writing again.
*/
public ByteBuffer flip ()
{
_buffer.flip();
return _buffer;
}
/**
* Resets our internal buffer.
*/
public void reset ()
{
_buffer.clear();
}
@Override // documentation inherited
public void write (int b)
{
try {
_buffer.put((byte)b);
} catch (BufferOverflowException boe) {
expand(1);
_buffer.put((byte)b);
}
}
@Override // documentation inherited
public void write (byte[] b, int off, int len)
{
// sanity check the arguments
if ((off < 0) || (off > b.length) || (len < 0) ||
((off + len) > b.length) || ((off + len) < 0)) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return;
}
try {
_buffer.put(b, off, len);
} catch (BufferOverflowException boe) {
expand(len);
_buffer.put(b, off, len);
}
}
/**
* Expands our buffer to accomodate the specified capacity.
*/
protected final void expand (int needed)
{
int ocapacity = _buffer.capacity();
int ncapacity = _buffer.position() + needed;
if (ncapacity > ocapacity) {
// increase the buffer size in large increments
ncapacity = Math.max(ocapacity << 1, ncapacity);
ByteBuffer newbuf = ByteBuffer.allocate(ncapacity);
newbuf.put((ByteBuffer)_buffer.flip());
_buffer = newbuf;
}
}
/** The buffer in which we store our frame data. */
protected ByteBuffer _buffer;
/** The default initial size of the internal buffer. */
protected static final int INITIAL_BUFFER_SIZE = 32;
}
@@ -40,69 +40,23 @@ import java.nio.ByteBuffer;
* to its internal buffer. It is intended to only be accessed from a
* single thread.
*/
public class FramingOutputStream extends OutputStream
public class FramingOutputStream extends ByteBufferOutputStream
{
public FramingOutputStream ()
{
_buffer = ByteBuffer.allocate(INITIAL_BUFFER_SIZE);
_buffer.put(HEADER_PAD);
}
/**
* Writes the specified byte to this framing output stream.
*
* @param b the byte to be written.
*/
public void write (int b)
@Override // documentation inherited
public ByteBuffer flip ()
{
try {
_buffer.put((byte)b);
} catch (BufferOverflowException boe) {
expand(1);
_buffer.put((byte)b);
}
throw new UnsupportedOperationException("Use frameAndReturnBuffer() instead.");
}
/**
* Writes <code>len</code> bytes from the specified byte array
* starting at offset <code>off</code> to this framing output stream.
*
* @param b the data.
* @param off the start offset in the data.
* @param len the number of bytes to write.
*/
public void write (byte[] b, int off, int len)
@Override // documentation inherited
public void reset ()
{
// sanity check the arguments
if ((off < 0) || (off > b.length) || (len < 0) ||
((off + len) > b.length) || ((off + len) < 0)) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return;
}
try {
_buffer.put(b, off, len);
} catch (BufferOverflowException boe) {
expand(len);
_buffer.put(b, off, len);
}
}
/**
* Expands our buffer to accomodate the specified capacity.
*/
protected final void expand (int needed)
{
int ocapacity = _buffer.capacity();
int ncapacity = _buffer.position() + needed;
if (ncapacity > ocapacity) {
// increase the buffer size in large increments
ncapacity = Math.max(ocapacity << 1, ncapacity);
ByteBuffer newbuf = ByteBuffer.allocate(ncapacity);
newbuf.put((ByteBuffer)_buffer.flip());
_buffer = newbuf;
}
throw new UnsupportedOperationException("Use resetFrame() instead.");
}
/**
@@ -136,12 +90,6 @@ public class FramingOutputStream extends OutputStream
_buffer.put(HEADER_PAD);
}
/** The buffer in which we store our frame data. */
protected ByteBuffer _buffer;
/** The default initial size of the internal buffer. */
protected static final int INITIAL_BUFFER_SIZE = 32;
/** We pad the beginning of our buffer so that we can write the frame
* length when the time comes. */
protected static final byte[] HEADER_PAD = new byte[4];
@@ -114,22 +114,8 @@ public class ObjectInputStream extends DataInputStream
}
}
// create a class mapping record, and cache it
Class sclass = Class.forName(cname, true, _loader);
Streamer streamer = Streamer.getStreamer(sclass);
if (STREAM_DEBUG) {
log.info(hashCode() + ": New class '" + cname + "' [code=" + code + "].");
}
// sanity check
if (streamer == null) {
String errmsg = "Aiya! Unable to create streamer for newly seen class " +
"[code=" + code + ", class=" + cname + "]";
throw new RuntimeException(errmsg);
}
cmap = new ClassMapping(code, sclass, streamer);
_classmap.add(code, cmap);
// create the class mapping
cmap = mapClass(code, cname);
} else {
cmap = _classmap.get(code);
@@ -162,6 +148,41 @@ public class ObjectInputStream extends DataInputStream
}
}
/**
* Creates, adds, and returns the class mapping for the specified code and class name.
*/
protected ClassMapping mapClass (short code, String cname)
throws IOException, ClassNotFoundException
{
// create a class mapping record, and cache it
ClassMapping cmap = createClassMapping(code, cname);
_classmap.add(code, cmap);
return cmap;
}
/**
* Creates and returns a class mapping for the specified code and class name.
*/
protected ClassMapping createClassMapping (short code, String cname)
throws IOException, ClassNotFoundException
{
// resolve the class and streamer
Class sclass = Class.forName(cname, true, _loader);
Streamer streamer = Streamer.getStreamer(sclass);
if (STREAM_DEBUG) {
log.info(hashCode() + ": New class '" + cname + "' [code=" + code + "].");
}
// sanity check
if (streamer == null) {
String errmsg = "Aiya! Unable to create streamer for newly seen class " +
"[code=" + code + ", class=" + cname + "]";
throw new RuntimeException(errmsg);
}
return new ClassMapping(code, sclass, streamer);
}
/**
* Reads an object from the input stream that was previously written with {@link
* ObjectOutputStream#writeBareObject}.
@@ -89,32 +89,66 @@ public class ObjectOutputStream extends DataOutputStream
log.info(hashCode() + ": Creating class mapping [code=" + _nextCode +
", class=" + sclass.getName() + "].");
}
cmap = new ClassMapping(_nextCode++, sclass, streamer);
cmap = createClassMapping(_nextCode++, sclass, streamer);
_classmap.put(sclass, cmap);
// make sure we didn't blow past our maximum class count
if (_nextCode <= 0) {
throw new RuntimeException("Too many unique classes written to ObjectOutputStream");
}
// writing a negative class code indicates that the class name will follow
writeShort(-cmap.code);
String cname = sclass.getName();
if (_translations != null) {
String tname = _translations.get(cname);
if (tname != null) {
cname = tname;
}
}
writeUTF(cname);
writeNewClassMapping(cmap);
} else {
writeShort(cmap.code);
writeExistingClassMapping(cmap);
}
writeBareObject(object, cmap.streamer, true);
}
/**
* Creates and returns a new class mapping.
*/
protected ClassMapping createClassMapping (short code, Class sclass, Streamer streamer)
{
return new ClassMapping(code, sclass, streamer);
}
/**
* Writes a new class mapping to the stream.
*/
protected void writeNewClassMapping (ClassMapping cmap)
throws IOException
{
// writing a negative class code indicates that the class name will follow
writeClassMapping(-cmap.code, cmap.sclass);
}
/**
* Writes an existing class mapping to the stream.
*/
protected void writeExistingClassMapping (ClassMapping cmap)
throws IOException
{
writeShort(cmap.code);
}
/**
* Writes out the mapping for a class.
*/
protected void writeClassMapping (int code, Class sclass)
throws IOException
{
writeShort(code);
String cname = sclass.getName();
if (_translations != null) {
String tname = _translations.get(cname);
if (tname != null) {
cname = tname;
}
}
writeUTF(cname);
}
/**
* Writes a {@link Streamable} instance or one of the support object types <em>without
* associated class metadata</em> to the output stream. The caller is responsible for knowing
@@ -0,0 +1,46 @@
//
// $Id$
package com.threerings.io;
import java.io.IOException;
import java.io.InputStream;
/**
* The counterpart of {@link UnreliableObjectOutputStream}.
*/
public class UnreliableObjectInputStream extends ObjectInputStream
{
/**
* Constructs an object input stream which will read its data from the supplied source stream.
*/
public UnreliableObjectInputStream (InputStream source)
{
super(source);
}
@Override // documentation inherited
protected ClassMapping mapClass (short code, String cname)
throws IOException, ClassNotFoundException
{
// see if we already have a mapping
ClassMapping cmap = (code < _classmap.size()) ? _classmap.get(code) : null;
if (cmap != null) {
// sanity check
if (!cmap.sclass.getName().equals(cname)) {
throw new RuntimeException(
"Received mapping for class that conflicts with existing mapping " +
"[code=" + code + ", oclass=" + cmap.sclass.getName() + ", nclass=" +
cname + "]");
}
return cmap;
}
// insert null entries for missing mappings
cmap = createClassMapping(code, cname);
for (int ii = 0, nn = (code + 1) - _classmap.size(); ii < nn; ii++) {
_classmap.add(null);
}
_classmap.set(code, cmap);
return cmap;
}
}
@@ -0,0 +1,112 @@
//
// $Id$
package com.threerings.io;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
/**
* Extends {@link ObjectOutputStream} for use in unreliable channels, where we must transmit class
* mappings with every object until we are explicitly notified that the receiver has cached the
* mappings.
*/
public class UnreliableObjectOutputStream extends ObjectOutputStream
{
/**
* Constructs an object output stream which will write its data to the supplied target stream.
*/
public UnreliableObjectOutputStream (OutputStream target)
{
super(target);
}
/**
* Sets the reference to the set that will hold the classes for which mappings have been
* written.
*/
public void setMappedClasses (Set<Class> mappedClasses)
{
_mappedClasses = mappedClasses;
}
/**
* Returns a reference to the set of classes for which mappings have been written.
*/
public Set<Class> getMappedClasses ()
{
return _mappedClasses;
}
/**
* Notes that the receiver has received the mappings for a group of classes, and thus that from
* now on, only the class codes need be sent.
*/
public void noteMappingsReceived (Collection<Class> sclasses)
{
// sanity check
if (_classmap == null) {
throw new RuntimeException("Missing class map");
}
// make each class's code positive to signify that we no longer need to send metadata
for (Class sclass : sclasses) {
ClassMapping cmap = _classmap.get(sclass);
if (cmap == null) {
throw new RuntimeException("No class mapping for " + sclass.getName());
}
cmap.code = (short)Math.abs(cmap.code);
}
}
@Override // documentation inherited
protected ClassMapping createClassMapping (short code, Class sclass, Streamer streamer)
{
// the negative class code indicates that we must rewrite the metadata for the first
// instance in each go-round; when we are notified that the other side has cached the
// mapping, we can simply write the (positive) code
return new ClassMapping((short)(-code), sclass, streamer);
}
@Override // documentation inherited
protected void writeNewClassMapping (ClassMapping cmap)
throws IOException
{
writeClassMapping(cmap.code, cmap.sclass);
}
@Override // documentation inherited
protected void writeExistingClassMapping (ClassMapping cmap)
throws IOException
{
// if the other side has received the mapping, we need only send the reference
if (cmap.code > 0) {
writeShort(cmap.code);
// likewise if we've written the class once this go-round
} else if (_mappedClasses.contains(cmap.sclass)) {
writeShort(-cmap.code);
// otherwise, we must retransmit the mapping
} else {
writeClassMapping(cmap.code, cmap.sclass);
}
}
@Override // documentation inherited
protected void writeClassMapping (int code, Class sclass)
throws IOException
{
super.writeClassMapping(code, sclass);
// note that we've written the mapping
_mappedClasses.add(sclass);
}
/** The set of classes for which we have written mappings. */
protected Set<Class> _mappedClasses = new HashSet<Class>();
}