All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.threerings.presents.server.net.PresentsConnectionManager Maven / Gradle / Ivy

//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/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.util.List;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
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.PrivateKey;

import com.google.common.collect.Lists;
import com.google.inject.Inject;
import com.google.inject.Singleton;

import com.samskivert.util.Invoker;
import com.samskivert.util.Lifecycle;
import com.samskivert.util.Queue;
import com.samskivert.util.ResultListener;
import com.samskivert.util.Tuple;

import com.threerings.io.ByteBufferInputStream;
import com.threerings.io.FramingOutputStream;
import com.threerings.io.ObjectOutputStream;
import com.threerings.io.UnreliableObjectInputStream;
import com.threerings.io.UnreliableObjectOutputStream;

import com.threerings.presents.annotation.AuthInvoker;
import com.threerings.presents.client.Client;
import com.threerings.presents.data.PresentsConMgrStats;
import com.threerings.presents.net.Message;
import com.threerings.presents.net.PingRequest;
import com.threerings.presents.net.PongResponse;
import com.threerings.presents.net.Transport;
import com.threerings.presents.server.Authenticator;
import com.threerings.presents.server.ChainedAuthenticator;
import com.threerings.presents.server.ClientManager;
import com.threerings.presents.server.DummyAuthenticator;
import com.threerings.presents.server.PresentsDObjectMgr;
import com.threerings.presents.server.ReportManager;
import com.threerings.presents.util.DatagramSequencer;
import com.threerings.presents.util.SecureUtil;

import com.threerings.nio.conman.Connection;
import com.threerings.nio.conman.ConnectionManager;
import com.threerings.nio.conman.NetEventHandler;

import static com.threerings.presents.Log.log;

@Singleton
public class PresentsConnectionManager extends ConnectionManager
    implements ReportManager.Reporter
{

    @Inject
    public PresentsConnectionManager (Lifecycle cycle, ReportManager repmgr)
        throws IOException
    {
        super(cycle, LATENCY_GRACE + PingRequest.PING_INTERVAL);
        repmgr.registerReporter(this);
        _stats = new PresentsConMgrStats();
    }

    @Override
    public synchronized PresentsConMgrStats getStats ()
    {
        ((PresentsConMgrStats)_stats).authQueueSize = _authq.size();
        return ((PresentsConMgrStats)super.getStats());
    }

    // from interface ReportManager.Reporter
    public void appendReport (StringBuilder report, long now, long sinceLast, boolean reset)
    {
        PresentsConMgrStats stats = getStats();
        long eventCount = stats.eventCount - _lastStats.eventCount;
        int connects = stats.connects - _lastStats.connects;
        int disconnects = stats.disconnects - _lastStats.disconnects;
        int closes = stats.closes - _lastStats.closes;
        long bytesIn = stats.bytesIn - _lastStats.bytesIn;
        long bytesOut = stats.bytesOut - _lastStats.bytesOut;
        long msgsIn = stats.msgsIn - _lastStats.msgsIn;
        long msgsOut = stats.msgsOut - _lastStats.msgsOut;
        if (reset) {
            _lastStats = stats;
        }

        // make sure we don't div0 if this method somehow gets called twice in
        // the same millisecond
        sinceLast = Math.max(sinceLast, 1L);

        report.append("* presents.net.ConnectionManager:\n");
        report.append("- Network connections: ");
        report.append(stats.connectionCount).append(" connections, ");
        report.append(stats.handlerCount).append(" handlers\n");
        report.append("- Network activity: ");
        report.append(eventCount).append(" events, ");
        report.append(connects).append(" connects, ");
        report.append(disconnects).append(" disconnects, ");
        report.append(closes).append(" closes\n");
        report.append("- Network input: ");
        report.append(bytesIn).append(" bytes, ");
        report.append(msgsIn).append(" msgs, ");
        report.append(msgsIn*1000/sinceLast).append(" mps, ");
        long avgIn = (msgsIn == 0) ? 0 : (bytesIn/msgsIn);
        report.append(avgIn).append(" avg size, ");
        report.append(bytesIn*1000/sinceLast).append(" bps\n");
        report.append("- Network output: ");
        report.append(bytesOut).append(" bytes, ");
        report.append(msgsOut).append(" msgs, ");
        report.append(msgsOut*1000/sinceLast).append(" mps, ");
        long avgOut = (msgsOut == 0) ? 0 : (bytesOut/msgsOut);
        report.append(avgOut).append(" avg size, ");
        report.append(bytesOut*1000/sinceLast).append(" bps\n");
    }

    /**
     * Adds an authenticator to the authentication chain. This authenticator will be offered a
     * chance to authenticate incoming connections before falling back to the main authenticator.
     */
    public void addChainedAuthenticator (ChainedAuthenticator author)
    {
        _authors.add(author);
    }

    /**
     * Sets the private key if the ciphers are supported.
     *
     * @return true if the key is set
     */
    public boolean setPrivateKey (PrivateKey key)
    {
        if (SecureUtil.ciphersSupported(key)) {
            _privateKey = key;
            return true;
        }
        return false;
    }

    /**
     * Sets the private key if the ciphers are supported.
     *
     * @return true if the key is set
     */
    public boolean setPrivateKey (String key)
    {
        return (key != null) && setPrivateKey(SecureUtil.stringToRSAPrivateKey(key));
    }

    /**
     * Returns the private key used in secure authentication.
     */
    public PrivateKey getPrivateKey ()
    {
        return _privateKey;
    }

    /**
     * Called when a datagram message is ready to be read off its channel.
     */
    protected int handleDatagram (DatagramChannel listener, long when)
    {
        InetSocketAddress source;
        _databuf.clear();
        try {
            source = (InetSocketAddress)listener.receive(_databuf);
        } catch (IOException ioe) {
            log.warning("Failure receiving datagram.", ioe);
            return 0;
        }

        // make sure we actually read a packet
        if (source == null) {
            log.info("Psych! Got READ_READY, but no datagram.");
            return 0;
        }

        // flip the buffer and record the size (which must be at least 14 to contain the connection
        // id, authentication hash, and a class reference)
        int size = _databuf.flip().remaining();
        if (size < 14) {
            log.warning("Received undersized datagram", "source", source, "size", size);
            return 0;
        }

        // the first four bytes are the connection id
        int connectionId = _databuf.getInt();
        Connection conn = _connections.get(connectionId);
        if (conn != null) {
            ((PresentsConnection)conn).handleDatagram(source, listener, _databuf, when);
        } else {
            log.debug("Received datagram for unknown connection", "id", connectionId,
                      "source", source);
        }

        return size;
    }

    /**
     * Called by a connection when it has a downstream message that needs to be delivered.
     * Note: this method is called as a result of a call to
     * {@link PresentsConnection#postMessage} which happens when forwarding an event to a client
     * and at the completion of authentication, both of which must happen only on the
     * distributed object thread.
     */
    protected void postMessage (PresentsConnection conn, Message msg)
    {
        if (!isRunning()) {
            log.warning("Posting message to inactive connection manager",
                        "msg", msg, new Exception());
        }

        // sanity check
        if (conn == null || msg == null) {
            log.warning("postMessage() bogosity", "conn", conn, "msg", msg, new Exception());
            return;
        }

        // more sanity check; messages must only be posted from the dobjmgr thread
        if (!_omgr.isDispatchThread()) {
            log.warning("Message posted on non-distributed object thread", "conn", conn,
                        "msg", msg, "thread", Thread.currentThread(), new Exception());
            // let it through though as we don't want to break things unnecessarily
        }

        try {
            // send it as a datagram if hinted and possible (pongs must be sent as part of the
            // negotation process)
            if (!msg.getTransport().isReliable() &&
                    (conn.getTransmitDatagrams() || msg instanceof PongResponse) &&
                        postDatagram(conn, msg)) {
                return;
            }

            // note the actual transport
            msg.noteActualTransport(Transport.RELIABLE_ORDERED);

            _framer.resetFrame();

            // flatten this message using the connection's output stream
            ObjectOutputStream oout = conn.getObjectOutputStream(_framer);
            oout.writeObject(msg);
            oout.flush();

            // now extract that data into a byte array
            ByteBuffer buffer = _framer.frameAndReturnBuffer();
            byte[] data = new byte[buffer.limit()];
            buffer.get(data);
            // log.info("Flattened " + msg + " into " + data.length + " bytes.");

            // and slap both on the queue
            _outq.append(Tuple.newTuple(conn, data));

        } catch (Exception e) {
            log.warning("Failure flattening message", "conn", conn, "msg", msg, e);
        }
    }

    /**
     * Helper function for {@link #postMessage}; handles posting the message as a datagram.
     *
     * @return true if the datagram was successfully posted, false if it was too big.
     */
    protected boolean postDatagram (PresentsConnection conn, Message msg)
        throws Exception
    {
        _flattener.reset();

        // flatten the message using the connection's sequencer
        DatagramSequencer sequencer = conn.getDatagramSequencer();
        sequencer.writeDatagram(msg);

        // if the message is too big, we must fall back to sending it through the stream channel
        if (_flattener.size() > Client.MAX_DATAGRAM_SIZE) {
            return false;
        }

        // note the actual transport
        msg.noteActualTransport(Transport.UNRELIABLE_UNORDERED);

        // extract as a byte array
        byte[] data = _flattener.toByteArray();

        // slap it on the queue
        _dataq.append(Tuple.newTuple(conn, data));

        return true;
    }

    /**
     * Creates a datagram sequencer for use by a {@link Connection}.
     */
    protected DatagramSequencer createDatagramSequencer ()
    {
        return new DatagramSequencer(
            new UnreliableObjectInputStream(new ByteBufferInputStream(_databuf)),
            new UnreliableObjectOutputStream(_flattener));
    }

    /**
     * Opens an outgoing connection to the supplied address. The connection will be opened in a
     * non-blocking manner and added to the connection manager's select set. Messages posted to the
     * connection prior to it being actually connected to its destination will remain in the queue.
     * If the connection fails those messages will be dropped.
     *
     * @param conn the connection to be initialized and opened. Callers may want to provide a
     * {@link Connection} derived class so that they may intercept calldown methods.
     * @param hostname the hostname of the server to which to connect.
     * @param port the port on which to connect to the server.
     *
     * @exception IOException thrown if an error occurs creating our socket. Everything else
     * happens asynchronously. If the connection attempt fails, the Connection will be notified via
     * {@link Connection#networkFailure}.
     */
    public void openOutgoingConnection (Connection conn, String hostname, int port)
        throws IOException
    {
        // create a socket channel to use for this connection, initialize it and queue it up to
        // have the non-blocking connect process started
        SocketChannel sockchan = SocketChannel.open();
        sockchan.configureBlocking(false);
        conn.init(this, sockchan, System.currentTimeMillis());
        _connectq.append(Tuple.newTuple(conn, new InetSocketAddress(hostname, port)));
    }

    /**
     * Starts the connection process for an outgoing connection. This is called as part of the
     * conmgr tick for any pending outgoing connections.
     */
    protected void startOutgoingConnection (final Connection conn, InetSocketAddress addr)
    {
        final SocketChannel sockchan = conn.getChannel();
        try {
            // register our channel with the selector (if this fails, we abandon ship immediately)
            conn.selkey = sockchan.register(_selector, SelectionKey.OP_CONNECT);

            // start our connection process (now if we fail we need to clean things up)
            NetEventHandler handler;
            if (sockchan.connect(addr)) {
                // it is possible even for a non-blocking socket to connect immediately, in which
                // case we stick the connection in as its event handler immediately
                handler = conn;

            } else {
                // otherwise we wire up a special event handler that will wait for our socket to
                // finish the connection process and then wire things up fully
                handler = new OutgoingConnectionHandler(conn);
            }
            _handlers.put(conn.selkey, handler);

        } catch (IOException ioe) {
            log.warning("Failed to initiate connection for " + sockchan + ".", ioe);
            conn.connectFailure(ioe); // nothing else to clean up
        }
    }

    @Override // from LoopingThread
    protected void iterate ()
    {
        super.iterate();

        // reap any outgoing connection handlers that failed to connect due to idleness
        OutgoingConnectionHandler handler;
        while ((handler = _outfailq.getNonBlocking()) != null) {
            handler.handleError(new IOException("Pending connection became idle."));
        }
    }

    @Override // from LoopingThread
    public boolean isRunning ()
    {
        // Prevent exiting our thread until the object manager is done.
        return super.isRunning() || _omgr.isRunning();
    }

    @Override
    protected void handleIncoming (long iterStamp)
    {
        super.handleIncoming(iterStamp);

        // start up any outgoing connections that need to be connected
        Tuple pconn;
        while ((pconn = _connectq.getNonBlocking()) != null) {
            startOutgoingConnection(pconn.left, pconn.right);
        }

        // check for connections that have completed authentication
        processAuthedConnections(iterStamp);
    }

    @Override
    protected void connectionFailed (Connection conn, IOException ioe)
    {
        super.connectionFailed(conn, ioe);

        // let the client manager know what's up
        _clmgr.connectionFailed(conn, ioe);
    }

    @Override
    protected void connectionClosed (Connection conn)
    {
        super.connectionClosed(conn);

        // let the client manager know what's up
        _clmgr.connectionClosed(conn);
    }

    /**
     * Performs the authentication process on the specified connection. This is called by {@link
     * AuthingConnection} itself once it receives its auth request.
     */
    protected void authenticateConnection (AuthingConnection conn)
    {
        Authenticator author = _author;
        for (ChainedAuthenticator cauthor : _authors) {
            if (cauthor.shouldHandleConnection(conn)) {
                author = cauthor;
                break;
            }
        }

        author.authenticateConnection(_authInvoker, conn, new ResultListener() {
            public void requestCompleted (AuthingConnection conn) {
                _authq.append(conn);
            }
            public void requestFailed (Exception cause) {
                // this never happens
            }
        });
    }

    /**
     * Starts an accepted socket down the path to authorization.
     */
    @Override
    protected void handleAcceptedSocket (SocketChannel channel)
    {
        handleAcceptedSocket(channel, new AuthingConnection());
    }

    /**
     * Converts connections that have completed the authentication process into full running
     * connections and notifies the client manager that new connections have been established.
     */
    protected void processAuthedConnections (long iterStamp)
    {
        AuthingConnection conn;
        while ((conn = _authq.getNonBlocking()) != null) {
            try {
                // construct a new running connection to handle this connections network traffic
                // from here on out
                PresentsConnection rconn = new PresentsConnection();
                rconn.init(this, conn.getChannel(), iterStamp);
                rconn.selkey = conn.selkey;

                // we need to keep using the same object input and output streams from the
                // beginning of the session because they have context that needs to be preserved
                rconn.inheritStreams(conn);

                // replace the mapping in the handlers table from the old conn with the new one
                _handlers.put(rconn.selkey, rconn);

                // add a mapping for the connection id and set the datagram secret
                _connections.put(rconn.getConnectionId(), rconn);
                rconn.setDatagramSecret(conn.getAuthRequest().getCredentials().getDatagramSecret());

                // transfer any overflow queue for that connection
                OverflowQueue oflowHandler = _oflowqs.remove(conn);
                if (oflowHandler != null) {
                    _oflowqs.put(rconn, oflowHandler);
                }

                // and let the client manager know about our new connection
                _clmgr.connectionEstablished(rconn, conn.getAuthName(), conn.getAuthRequest(),
                                             conn.getAuthResponse());

            } catch (IOException ioe) {
                log.warning("Failure upgrading authing connection to running.", ioe);
            }
        }
    }

    @Override
    protected void sendOutgoingMessages (long iterStamp)
    {
        super.sendOutgoingMessages(iterStamp);

        // send any datagrams
        Tuple tup;
        while ((tup = _dataq.getNonBlocking()) != null) {
            writeDatagram(tup.left, tup.right);
        }
    }

    /**
     * Sends a datagram to the specified connection.
     *
     * @return true if the datagram was sent, false if we failed to send for any reason.
     */
    protected boolean writeDatagram (PresentsConnection conn, byte[] data)
    {
        InetSocketAddress target = conn.getDatagramAddress();
        if (target == null) {
            log.warning("No address to send datagram", "conn", conn);
            return false;
        }

        _databuf.clear();
        _databuf.put(data).flip();
        try {
            return conn.getDatagramChannel().send(_databuf, target) > 0;
        } catch (IOException ioe) {
            log.warning("Failed to send datagram.", ioe);
            return false;
        }
    }

    protected class OutgoingConnectionHandler implements NetEventHandler
    {
        public OutgoingConnectionHandler (Connection conn)
        {
            _conn = conn;
        }

        public int handleEvent (long when)
        {
            SocketChannel sockchan = _conn.getChannel();
            try {
                if (sockchan.finishConnect()) {
                    // great, we're ready to roll, wire up the connection
                    _conn.selkey = sockchan.register(_selector, SelectionKey.OP_READ);
                    _handlers.put(_conn.selkey, _conn);
                    log.info("Outgoing connection ready", "conn", _conn);
                }
            } catch (IOException ioe) {
                handleError(ioe);
            }
            return 0;
        }

        public boolean checkIdle (long idleStamp)
        {
            return _conn.checkIdle(idleStamp);
        }

        public void becameIdle ()
        {
            // this failed connection will be cleaned up in the next iterate() tick
            _outfailq.append(this);
        }

        protected void handleError (IOException ioe)
        {
            _handlers.remove(_conn.selkey);
            _oflowqs.remove(_conn);
            _conn.connectFailure(ioe);
        }

        protected final Connection _conn;
    }

    /** Handles client authentication. The base authenticator is injected but optional services
     * like the PeerManager may replace this authenticator with one that intercepts certain types
     * of authentication and then passes normal authentications through. */
    @Inject(optional=true) protected Authenticator _author = new DummyAuthenticator();
    protected List _authors = Lists.newArrayList();
    protected PrivateKey _privateKey;

    protected Queue _authq = Queue.newQueue();
    protected Queue> _connectq = Queue.newQueue();

    /** failed (idled out) outgoing connections that need to be cleaned up */
    protected Queue _outfailq = Queue.newQueue();

    protected FramingOutputStream _framer = new FramingOutputStream();
    protected ByteArrayOutputStream _flattener = new ByteArrayOutputStream();

    // some dependencies
    @Inject @AuthInvoker protected Invoker _authInvoker;
    @Inject protected ClientManager _clmgr;
    @Inject protected PresentsDObjectMgr _omgr;

    /** A snapshot of our runtime stats as of our last report. */
    protected PresentsConMgrStats _lastStats = new PresentsConMgrStats();

    protected Queue> _dataq = Queue.newQueue();
    protected ByteBuffer _databuf = ByteBuffer.allocateDirect(Client.MAX_DATAGRAM_SIZE);
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy