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:
@@ -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>();
|
||||
}
|
||||
Reference in New Issue
Block a user