Files
narya/src/java/com/threerings/presents/server/net/Connection.java
T
Charlie Groves 0d4837e71d ConnectionManager allowed a server to listen on multiple tcp and datagram ports, but it only closed
the last opened.  Close all opened listening sockets.

Also, the last opened datagram socket would be used to send all datagram messages instead of the one
datagrams were received on.  It now goes through the same channel where datagrams were last
received.



git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@6156 542714f4-19e9-0310-aa3c-eee0fc999fb1
2010-09-14 22:47:45 +00:00

480 lines
15 KiB
Java

//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2010 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.presents.server.net;
import java.io.EOFException;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import com.threerings.io.FramedInputStream;
import com.threerings.io.FramingOutputStream;
import com.threerings.io.ObjectInputStream;
import com.threerings.io.ObjectOutputStream;
import com.threerings.presents.net.Message;
import com.threerings.presents.net.PingRequest;
import com.threerings.presents.util.DatagramSequencer;
import static com.threerings.presents.Log.log;
/**
* The base connection class implements the net event handler interface and processes raw incoming
* network data into a stream of parsed {@link Message} objects. It also provides the means to send
* messages to the client and facilities for checking delinquency.
*/
public class Connection implements NetEventHandler
{
/** Used with {@link Connection#setMessageHandler}. */
public static interface MessageHandler {
/** Called when a complete message has been parsed from incoming network data. */
void handleMessage (Message message);
}
/** The key used by the NIO code to track this connection. */
public SelectionKey selkey;
/**
* Initializes a connection object with a socket and related info.
*
* @param cmgr The connection manager with which this connection is associated.
* @param channel The socket channel from which we'll be reading messages.
* @param createStamp The time at which this connection was created.
*/
public void init (ConnectionManager cmgr, SocketChannel channel, long createStamp)
throws IOException
{
_cmgr = cmgr;
_channel = channel;
_lastEvent = createStamp;
_connectionId = ++_lastConnectionId;
}
/**
* Instructs the connection to pass parsed messages on to this handler for processing. This
* should be done before the connection is turned loose to process messages.
*/
public void setMessageHandler (MessageHandler handler)
{
_handler = handler;
}
/**
* Configures this connection with a custom class loader.
*/
public void setClassLoader (ClassLoader loader)
{
_loader = loader;
if (_oin != null) {
_oin.setClassLoader(loader);
}
}
/**
* Returns the connection's unique identifier.
*/
public int getConnectionId ()
{
return _connectionId;
}
/**
* Returns the non-blocking socket object used to construct this connection.
*/
public SocketChannel getChannel ()
{
return _channel;
}
/**
* Returns the address associated with this connection or null if it has no underlying socket
* channel.
*/
public InetAddress getInetAddress ()
{
return (_channel == null) ? null : _channel.socket().getInetAddress();
}
/**
* Sets whether we should transmit datagrams.
*/
public void setTransmitDatagrams (boolean transmit)
{
_transmitDatagrams = transmit;
}
/**
* Checks whether we should transmit datagrams.
*/
public boolean getTransmitDatagrams ()
{
return _transmitDatagrams;
}
/**
* Returns the address to which datagrams should be sent or null if no datagram address has
* been established.
*/
public InetSocketAddress getDatagramAddress ()
{
return _datagramAddress;
}
/**
* Returns the channel through which datagrams should be sent or null if no datagram channel
* has been established.
*/
public DatagramChannel getDatagramChannel ()
{
return _datagramChannel;
}
/**
* Sets the secret string used to authenticate datagrams from the client.
*/
public void setDatagramSecret (String secret)
{
try {
_datagramSecret = secret.getBytes("UTF-8");
} catch (Exception e) {
_datagramSecret = new byte[0]; // shouldn't happen
}
}
/**
* Returns true if this connection is closed.
*/
public boolean isClosed ()
{
return (_channel == null);
}
/**
* Closes this connection and unregisters it from the connection manager. This should only be
* called from the conmgr thread.
*/
public void close ()
{
// we shouldn't be closed twice
if (isClosed()) {
log.warning("Attempted to re-close connection " + this + ".", new Exception());
return;
}
// unregister from the select set
_cmgr.connectionClosed(this);
// close our socket
closeSocket();
}
/**
* Queues up a request to have this connection closed by the connection manager once all
* messages in its queue have been written to its target.
*/
public void asyncClose ()
{
_cmgr.postAsyncClose(this);
}
/**
* Posts a message for delivery to this connection. The message will be delivered by the conmgr
* thread as soon as it gets to it.
*/
public void postMessage (Message msg)
{
// pass this along to the connection manager
_cmgr.postMessage(this, msg);
}
/**
* Called when an outgoing socket experiences a connect failure. The connection manager will
* have cleaned up the partial registration needed during the connect process, so we are only
* responsible for closing our socket.
*/
public void connectFailure (IOException ioe)
{
closeSocket();
}
/**
* Called when there is a failure reading or writing to this connection. We notify the
* connection manager and close ourselves down.
*/
public void networkFailure (IOException ioe)
{
// if we're already closed, then something is seriously funny
if (isClosed()) {
log.warning("Failure reported on closed connection " + this + ".", new Exception());
return;
}
// let the connection manager know we're hosed
_cmgr.connectionFailed(this, ioe);
// and close our socket
closeSocket();
}
/**
* Processes a datagram sent to this connection.
*/
public void handleDatagram (InetSocketAddress source, DatagramChannel channel,
ByteBuffer buf, long when)
{
// lazily create our various bits and bobs
if (_digest == null) {
try {
_digest = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException nsae) {
log.warning("Missing MD5 algorithm.");
return;
}
_sequencer = _cmgr.createDatagramSequencer();
}
// verify the hash
buf.position(12);
_digest.update(buf);
byte[] hash = _digest.digest(_datagramSecret);
buf.position(4);
for (int ii = 0; ii < 8; ii++) {
if (hash[ii] != buf.get()) {
log.warning("Datagram failed hash check", "id", _connectionId, "source", source);
return;
}
}
// update our target address
_datagramAddress = source;
_datagramChannel = channel;
// read the contents through the sequencer
try {
Message msg = _sequencer.readDatagram();
if (msg == null) {
return; // received out of order
}
msg.received = when;
_handler.handleMessage(msg);
} catch (ClassNotFoundException cnfe) {
log.warning("Error reading datagram", "error", cnfe);
} catch (IOException ioe) {
log.warning("Error reading datagram", "error", ioe);
}
}
// from interface NetEventHandler
public int handleEvent (long when)
{
// make a note that we received an event as of this time
_lastEvent = when;
int bytesIn = 0;
try {
// we're lazy about creating our input streams because we may be inheriting them from
// our authing connection and we don't want to unnecessarily create them in that case
if (_fin == null) {
_fin = new FramedInputStream();
_oin = new ObjectInputStream(_fin);
if (_loader != null) {
_oin.setClassLoader(_loader);
}
}
// there may be more than one frame in the buffer, so we keep reading them until we run
// out of data
while (_fin.readFrame(_channel)) {
// make a note of how many bytes are in this frame (including the frame length
// bytes which aren't reported in available())
bytesIn = _fin.available() + 4;
// parse the message and pass it on
Message msg = (Message)_oin.readObject();
msg.received = when;
// Log.info("Read message " + msg + ".");
_handler.handleMessage(msg);
}
} catch (EOFException eofe) {
// close down the socket gracefully
close();
} catch (ClassNotFoundException cnfe) {
log.warning("Error reading message from socket", "channel", _channel, "error", cnfe);
// deal with the failure
String errmsg = "Unable to decode incoming message.";
networkFailure((IOException) new IOException(errmsg).initCause(cnfe));
} catch (IOException ioe) {
// don't log a warning for the ever-popular "the client dropped the connection" failure
String msg = ioe.getMessage();
if (msg == null || msg.indexOf("reset by peer") == -1) {
log.warning("Error reading message from socket", "channel", _channel, "error", ioe);
}
// deal with the failure
networkFailure(ioe);
}
return bytesIn;
}
// from interface NetEventHandler
public boolean checkIdle (long now)
{
long idleMillis = now - _lastEvent;
if (idleMillis < PingRequest.PING_INTERVAL + LATENCY_GRACE) {
return false;
}
if (isClosed()) {
return true;
}
log.info("Disconnecting non-communicative client", "conn", this, "idle", idleMillis + "ms");
return true;
}
// from interface NetEventHandler
public void becameIdle ()
{
_cmgr.closeConnection(this);
}
@Override // from Object
public String toString ()
{
return "[id=" + (hashCode() % 1000) + ", addr=" + getInetAddress() + "]";
}
/**
* Returns the object input stream associated with this connection. This should only be used
* by the connection manager.
*/
protected ObjectInputStream getObjectInputStream ()
{
return _oin;
}
/**
* Instructs this connection to inherit its streams from the supplied connection object. This
* is called by the connection manager when the time comes to pass streams from the authing
* connection to the running connection.
*/
protected void inheritStreams (Connection other)
{
_fin = other._fin;
_oin = other._oin;
_oout = other._oout;
if (_loader != null) {
_oin.setClassLoader(_loader);
}
}
/**
* Returns the object output stream associated with this connection (creating it if
* necessary). This should only be used by the connection manager.
*/
protected ObjectOutputStream getObjectOutputStream (FramingOutputStream fout)
{
// we're lazy about creating our output stream because we may be inheriting it from our
// authing connection and we don't want to unnecessarily create it in that case
if (_oout == null) {
_oout = new ObjectOutputStream(fout);
}
return _oout;
}
/**
* Sets the object output stream used by this connection. This should only be called by the
* connection manager.
*/
protected void setObjectOutputStream (ObjectOutputStream oout)
{
_oout = oout;
}
/**
* Returns a reference to the connection's datagram sequencer. This should only be called by
* the connection manager.
*/
protected DatagramSequencer getDatagramSequencer ()
{
return _sequencer;
}
/**
* Closes the socket associated with this connection. This happens when we receive EOF, are
* requested to close down or when our connection fails.
*/
protected void closeSocket ()
{
if (_channel == null) {
return;
}
log.debug("Closing channel " + this + ".");
try {
_channel.close();
} catch (IOException ioe) {
log.warning("Error closing connection", "conn", this, "error", ioe);
}
// clear out our references to prevent repeat closings
_channel = null;
}
protected ConnectionManager _cmgr;
protected SocketChannel _channel;
protected long _lastEvent;
protected int _connectionId;
protected FramedInputStream _fin;
protected ObjectInputStream _oin;
protected ObjectOutputStream _oout;
protected InetSocketAddress _datagramAddress;
protected DatagramChannel _datagramChannel;
protected byte[] _datagramSecret;
protected boolean _transmitDatagrams;
protected MessageDigest _digest;
protected DatagramSequencer _sequencer;
protected MessageHandler _handler;
protected ClassLoader _loader;
/** The last connection id assigned. */
protected static int _lastConnectionId;
/** The number of milliseconds beyond the ping interval that we allow a client's network
* connection to be idle before we forcibly disconnect them. */
protected static final long LATENCY_GRACE = 30 * 1000L;
}