package com.limegroup.gnutella;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.limegroup.gnutella.connection.CompositeQueue;
import com.limegroup.gnutella.connection.ConnectionStats;
import com.limegroup.gnutella.connection.DeflaterWriter;
import com.limegroup.gnutella.connection.GnetConnectObserver;
import com.limegroup.gnutella.connection.InflaterReader;
import com.limegroup.gnutella.connection.MessageQueue;
import com.limegroup.gnutella.connection.MessageReader;
import com.limegroup.gnutella.connection.MessageReceiver;
import com.limegroup.gnutella.connection.MessageWriter;
import com.limegroup.gnutella.connection.OutputRunner;
import com.limegroup.gnutella.connection.SentMessageHandler;
import com.limegroup.gnutella.filters.SpamFilter;
import com.limegroup.gnutella.handshaking.AsyncIncomingHandshaker;
import com.limegroup.gnutella.handshaking.AsyncOutgoingHandshaker;
import com.limegroup.gnutella.handshaking.BadHandshakeException;
import com.limegroup.gnutella.handshaking.HandshakeObserver;
import com.limegroup.gnutella.handshaking.HandshakeResponder;
import com.limegroup.gnutella.handshaking.Handshaker;
import com.limegroup.gnutella.handshaking.LeafHandshakeResponder;
import com.limegroup.gnutella.handshaking.LeafHeaders;
import com.limegroup.gnutella.handshaking.NoGnutellaOkException;
import com.limegroup.gnutella.handshaking.UltrapeerHandshakeResponder;
import com.limegroup.gnutella.handshaking.UltrapeerHeaders;
import com.limegroup.gnutella.io.ChannelWriter;
import com.limegroup.gnutella.io.ConnectObserver;
import com.limegroup.gnutella.io.DelayedBufferWriter;
import com.limegroup.gnutella.io.NBThrottle;
import com.limegroup.gnutella.io.NIOMultiplexor;
import com.limegroup.gnutella.io.Throttle;
import com.limegroup.gnutella.io.ThrottleWriter;
import com.limegroup.gnutella.messages.BadPacketException;
import com.limegroup.gnutella.messages.Message;
import com.limegroup.gnutella.messages.PingReply;
import com.limegroup.gnutella.messages.PushRequest;
import com.limegroup.gnutella.messages.QueryReply;
import com.limegroup.gnutella.messages.QueryRequest;
import com.limegroup.gnutella.messages.vendor.CapabilitiesVM;
import com.limegroup.gnutella.messages.vendor.HopsFlowVendorMessage;
import com.limegroup.gnutella.messages.vendor.MessagesSupportedVendorMessage;
import com.limegroup.gnutella.messages.vendor.PushProxyAcknowledgement;
import com.limegroup.gnutella.messages.vendor.PushProxyRequest;
import com.limegroup.gnutella.messages.vendor.QueryStatusResponse;
import com.limegroup.gnutella.messages.vendor.SimppRequestVM;
import com.limegroup.gnutella.messages.vendor.TCPConnectBackVendorMessage;
import com.limegroup.gnutella.messages.vendor.UDPConnectBackVendorMessage;
import com.limegroup.gnutella.messages.vendor.UpdateRequest;
import com.limegroup.gnutella.messages.vendor.VendorMessage;
import com.limegroup.gnutella.routing.PatchTableMessage;
import com.limegroup.gnutella.routing.QueryRouteTable;
import com.limegroup.gnutella.routing.ResetTableMessage;
import com.limegroup.gnutella.search.SearchResultHandler;
import com.limegroup.gnutella.settings.ConnectionSettings;
import com.limegroup.gnutella.simpp.SimppManager;
import com.limegroup.gnutella.statistics.OutOfBandThroughputStat;
import com.limegroup.gnutella.statistics.ReceivedMessageStatHandler;
import com.limegroup.gnutella.updates.UpdateManager;
import com.limegroup.gnutella.util.BandwidthThrottle;
import com.limegroup.gnutella.util.DataUtils;
import com.limegroup.gnutella.util.ThreadFactory;
import com.limegroup.gnutella.util.ThrottledOutputStream;
import com.limegroup.gnutella.version.UpdateHandler;
/**
* A Connection managed by a ConnectionManager. Includes a loopForMessages
* method that runs forever (or until an IOException occurs), receiving and
* replying to Gnutella messages. ManagedConnection is only instantiated
* through a ConnectionManager.<p>
*
* ManagedConnection provides a sophisticated message buffering mechanism. When
* you call send(Message), the message is not actually delivered to the socket;
* instead it buffered in an application-level buffer. Periodically, a thread
* reads messages from the buffer, writes them to the network, and flushes the
* socket buffers. This means that there is no need to manually call flush().
* Furthermore, ManagedConnection provides a simple form of flow control. If
* messages are queued faster than they can be written to the network, they are
* dropped in the following order: PingRequest, PingReply, QueryRequest,
* QueryReply, and PushRequest. See the implementation notes below for more
* details.<p>
*
* All ManagedConnection's have two underlying spam filters: a personal filter
* (controls what I see) and a route filter (also controls what I pass along to
* others). See SpamFilter for a description. These filters are configured by
* the properties in the SettingsManager, but you can change them with
* setPersonalFilter and setRouteFilter.<p>
*
* ManagedConnection maintain a large number of statistics, such as the current
* bandwidth for upstream & downstream. ManagedConnection doesn't quite fit the
* BandwidthTracker interface, unfortunately. On the query-routing3-branch and
* pong-caching CVS branches, these statistics have been bundled into a single
* object, reducing the complexity of ManagedConnection.<p>
*
* ManagedConnection also takes care of various VendorMessage handling, in
* particular Hops Flow, UDP ConnectBack, and TCP ConnectBack. See
* handleVendorMessage().<p>
*
* This class implements ReplyHandler to route pongs and query replies that
* originated from it.<p>
*/
public class ManagedConnection extends Connection
implements ReplyHandler, MessageReceiver, SentMessageHandler {
private static final Log LOG = LogFactory.getLog(ManagedConnection.class);
/**
* The time to wait between route table updates for leaves,
* in milliseconds.
*/
private long LEAF_QUERY_ROUTE_UPDATE_TIME = 1000*60*5; //5 minutes
/**
* The time to wait between route table updates for Ultrapeers,
* in milliseconds.
*/
private long ULTRAPEER_QUERY_ROUTE_UPDATE_TIME = 1000*60; //1 minute
/** The timeout to use when connecting, in milliseconds. This is NOT used
* for bootstrap servers. */
private static final int CONNECT_TIMEOUT = 6000; //6 seconds
/** The total amount of upstream messaging bandwidth for ALL connections
* in BYTES (not bits) per second. */
private static final int TOTAL_OUTGOING_MESSAGING_BANDWIDTH=8000;
/** The maximum number of times ManagedConnection instances should send UDP
* ConnectBack requests.
*/
private static final int MAX_UDP_CONNECT_BACK_ATTEMPTS = 15;
/** The maximum number of times ManagedConnection instances should send TCP
* ConnectBack requests.
*/
private static final int MAX_TCP_CONNECT_BACK_ATTEMPTS = 10;
/** Handle to the <tt>ConnectionManager</tt>.
*/
private ConnectionManager _manager;
/** Filter for filtering out messages that are considered spam.
*/
private volatile SpamFilter _routeFilter = SpamFilter.newRouteFilter();
private volatile SpamFilter _personalFilter =
SpamFilter.newPersonalFilter();
/*
* IMPLEMENTATION NOTE: this class uses the SACHRIFC algorithm described at
* http://www.limewire.com/developer/sachrifc.txt. The basic idea is to use
* one queue for each message type. Messages are removed from the queue in
* a biased round-robin fashion. This prioritizes some messages types while
* preventing any one message type from dominating traffic. Query replies
* are further prioritized by "GUID volume", i.e., the number of bytes
* already routed for that GUID. Other messages are sorted by time and
* removed in a LIFO [sic] policy. This, coupled with timeouts, reduces
* latency.
*/
/** A lock for QRP activity on this connection */
private final Object QRP_LOCK=new Object();
/** Non-blocking throttle for outgoing messages. */
private final static Throttle _nbThrottle = new NBThrottle(true,
TOTAL_OUTGOING_MESSAGING_BANDWIDTH,
ConnectionSettings.NUM_CONNECTIONS.getValue(),
CompositeQueue.QUEUE_TIME);
/** Blocking throttle for outgoing messages. */
private final static BandwidthThrottle _throttle=
new BandwidthThrottle(TOTAL_OUTGOING_MESSAGING_BANDWIDTH);
/** The OutputRunner */
private OutputRunner _outputRunner;
/** Keeps track of sent/received [dropped] & bandwidth. */
private final ConnectionStats _connectionStats = new ConnectionStats();
/**
* The minimum time a leaf needs to be in "busy mode" before we will consider him "truly
* busy" for the purposes of QRT updates.
*/
private static long MIN_BUSY_LEAF_TIME = 1000 * 20; // 20 seconds
/** The next time I should send a query route table to this connection.
*/
private long _nextQRPForwardTime;
/**
* The bandwidth trackers for the up/downstream.
* These are not synchronized and not guaranteed to be 100% accurate.
*/
private BandwidthTrackerImpl _upBandwidthTracker=
new BandwidthTrackerImpl();
private BandwidthTrackerImpl _downBandwidthTracker=
new BandwidthTrackerImpl();
/** True iff this should not be policed by the ConnectionWatchdog, e.g.,
* because this is a connection to a Clip2 reflector. */
private boolean _isKillable=true;
/** Use this if a HopsFlowVM instructs us to stop sending queries below
* this certain hops value....
*/
private volatile int hopsFlowMax = -1;
/**
* This member contains the time beyond which, if this host is still busy (hops flow==0),
* that we should consider him as "truly idle" and should then remove his contributions
* last-hop QRTs. A value of -1 means that either the leaf isn't busy, or he is busy,
* and his busy-ness was already noticed by the MessageRouter, so we shouldn't 're-notice'
* him on the next QRT update iteration.
*/
private volatile long _busyTime = -1;
/**
* whether this connection is a push proxy for somebody
*/
private volatile boolean _pushProxy;
/** The class wide static counter for the number of udp connect back
* request sent.
*/
private static int _numUDPConnectBackRequests = 0;
/** The class wide static counter for the number of tcp connect back
* request sent.
*/
private static int _numTCPConnectBackRequests = 0;
/**
* Variable for the <tt>QueryRouteTable</tt> received for this
* connection.
*/
private QueryRouteTable _lastQRPTableReceived;
/**
* Variable for the <tt>QueryRouteTable</tt> sent for this
* connection.
*/
private QueryRouteTable _lastQRPTableSent;
/**
* Holds the mappings of GUIDs that are being proxied.
* We want to construct this lazily....
* GUID.TimedGUID -> GUID
* OOB Proxy GUID - > Original GUID
*/
private Map _guidMap = null;
/**
* The max lifetime of the GUID (10 minutes).
*/
private static long TIMED_GUID_LIFETIME = 10 * 60 * 1000;
/**
* Whether or not this was a supernode <-> client connection when message
* looping started.
*/
private boolean supernodeClientAtLooping = false;
/**
* The last clientGUID a Hops=0 QueryReply had.
*/
private byte[] clientGUID = DataUtils.EMPTY_GUID;
/** Whether or not the HandshakeResponder should use locale preferencing during handshaking. */
private boolean _useLocalPreference;
/**
* Creates a new outgoing connection to the specified host on the
* specified port.
*
* @param host the address of the host we're connecting to
* @param port the port the host is listening on
*/
public ManagedConnection(String host, int port) {
super(host, port);
_manager = RouterService.getConnectionManager();
}
/**
* Creates an incoming connection.
* ManagedConnections should only be constructed within ConnectionManager.
* @requires the word "GNUTELLA " and nothing else has just been read
* from socket
* @effects wraps a connection around socket and does the rest of the
* Gnutella handshake.
*/
ManagedConnection(Socket socket) {
super(socket);
_manager = RouterService.getConnectionManager();
}
/**
* Stub for calling initialize(null);
*/
public void initialize() throws IOException, NoGnutellaOkException, BadHandshakeException {
initialize(null);
}
/**
* Attempts to initialize the connection. If observer is non-null and this wasn't
* created with a pre-existing Socket this will return immediately. Otherwise,
* this will block while connecting or initializing the handshake.
* return immediately,
*
* @param observer
* @throws IOException
* @throws NoGnutellaOkException
* @throws BadHandshakeException
*/
public void initialize(GnetConnectObserver observer) throws IOException, NoGnutellaOkException, BadHandshakeException {
Properties requestHeaders;
HandshakeResponder responder;
if(isOutgoing()) {
String host = getAddress();
if(RouterService.isSupernode()) {
requestHeaders = new UltrapeerHeaders(host);
responder = new UltrapeerHandshakeResponder(host);
} else {
requestHeaders = new LeafHeaders(host);
responder = new LeafHandshakeResponder(host);
}
} else {
String host = getSocket().getInetAddress().getHostAddress();
requestHeaders = null;
if(RouterService.isSupernode()) {
responder = new UltrapeerHandshakeResponder(host);
} else {
responder = new LeafHandshakeResponder(host);
}
}
// Establish the socket (if needed), handshake.
super.initialize(requestHeaders, responder, CONNECT_TIMEOUT, observer);
// Nothing else should be done here. All post-init-sequences
// should be triggered from finishInitialize, which will be called
// when the socket is connected (if it connects).
}
/** Constructs a Connector that will do an asynchronous handshake. */
protected ConnectObserver createAsyncConnectObserver(Properties requestHeaders,
HandshakeResponder responder, GnetConnectObserver observer) {
return new AsyncHandshakeConnecter(requestHeaders, responder, observer);
}
/**
* Completes the initialization process.
*/
protected void preHandshakeInitialize(Properties requestHeaders, HandshakeResponder responder, GnetConnectObserver observer)
throws IOException, NoGnutellaOkException, BadHandshakeException {
responder.setLocalePreferencing(_useLocalPreference);
super.preHandshakeInitialize(requestHeaders, responder, observer);
}
/**
* Performs the handshake.
*
* If there is a GnetConnectObserver (it is non-null) & this connection supports
* asynchronous messaging, then this method will return immediately and the observer
* will be notified when handshaking completes (either succesfully or unsuccesfully).
*
* Otherwise, this will block until handshaking completes.
*/
protected void performHandshake(Properties requestHeaders, HandshakeResponder responder, GnetConnectObserver observer)
throws IOException, BadHandshakeException, NoGnutellaOkException {
if(observer == null || !isAsynchronous()) {
if(!isOutgoing() && observer != null)
throw new IllegalStateException("cannot support incoming blocking w/ observer");
super.performHandshake(requestHeaders, responder, observer);
} else {
Handshaker shaker = createAsyncHandshaker(requestHeaders, responder, observer);
try {
shaker.shake();
} catch (IOException iox) {
ErrorService.error(iox); // impossible.
}
}
}
/** Creates the asynchronous handshaker. */
protected Handshaker createAsyncHandshaker(Properties requestHeaders,
HandshakeResponder responder,
GnetConnectObserver observer) {
HandshakeWatcher shakeObserver = new HandshakeWatcher(observer);
Handshaker shaker;
if(isOutgoing())
shaker = new AsyncOutgoingHandshaker(requestHeaders, responder, _socket, shakeObserver);
else
shaker = new AsyncIncomingHandshaker(responder, _socket, shakeObserver);
shakeObserver.setHandshaker(shaker);
return shaker;
}
/**
* Starts out OutputRunners & notifies UpdateManager that this
* connection may have an update on it.
*/
protected void postHandshakeInitialize(Handshaker shaker) {
super.postHandshakeInitialize(shaker);
// Start our OutputRunner.
startOutput();
// See if this connection had an old-style update msg.
UpdateManager.instance().checkAndUpdate(this);
}
/**
* Resets the query route table for this connection. The new table will be of the size specified in <tt>rtm</tt>
* and will contain no data. If there is no <tt>QueryRouteTable</tt> yet created for this connection, this method
* will create one.
*
* @param rtm
* the <tt>ResetTableMessage</tt>
*/
public void resetQueryRouteTable(ResetTableMessage rtm) {
if (_lastQRPTableReceived == null) {
_lastQRPTableReceived =
new QueryRouteTable(rtm.getTableSize(), rtm.getInfinity());
} else {
_lastQRPTableReceived.reset(rtm);
}
}
/**
* Patches the <tt>QueryRouteTable</tt> for this connection.
*
* @param ptm the patch with the data to update
*/
public void patchQueryRouteTable(PatchTableMessage ptm) {
// we should always get a reset before a patch, but
// allocate a table in case we don't
if(_lastQRPTableReceived == null) {
_lastQRPTableReceived = new QueryRouteTable();
}
try {
_lastQRPTableReceived.patch(ptm);
} catch(BadPacketException e) {
// not sure what to do here!!
}
}
/**
* Set's a leaf's busy timer to now, if bSet is true, else clears the flag
*
* @param bSet Whether to SET or CLEAR the busy timer for this host
*/
public void setBusy( boolean bSet ){
if( bSet ){
if( _busyTime==-1 )
_busyTime=System.currentTimeMillis();
}
else
_busyTime=-1;
}
/**
*
* @return the current Hops Flow limit value for this connection, or -1 if we haven't
* yet received a HF message
*/
public byte getHopsFlowMax() {
return (byte)hopsFlowMax;
}
/** Returns true iff this connection is a shielded leaf connection, and has
* signalled that he is currently busy (full on upload slots). If so, we will
* not include his QRT table in last hop QRT tables we send out (if we are an
* Ultrapeer)
* @return true iff this connection is a busy leaf (don't include his QRT table)
*/
public boolean isBusyLeaf(){
boolean busy=isSupernodeClientConnection() && (getHopsFlowMax()==0);
return busy;
}
/**
* Determine whether or not the leaf has been busy long enough to remove his QRT tables
* from the combined last-hop QRTs, and should trigger an earlier update
*
* @return true iff this leaf is busy and should trigger an update to the last-hop QRTs
*/
public boolean isBusyEnoughToTriggerQRTRemoval(){
if( _busyTime == -1 )
return false;
if( System.currentTimeMillis() > (_busyTime+MIN_BUSY_LEAF_TIME) )
return true;
return false;
}
/**
* Determines whether or not the specified <tt>QueryRequest</tt>
* instance should be sent to the connection. The method takes a couple
* factors into account, such as QRP tables, type of query, etc.
*
* @param query the <tt>QueryRequest</tt> to check against
* the data
* @return <tt>true</tt> if the <tt>QueryRequest</tt> should be sent to
* this connection, otherwise <tt>false</tt>
*/
public boolean shouldForwardQuery(QueryRequest query) {
// special what is queries have version numbers attached to them - make
// sure that the remote host can answer the query....
if (query.isFeatureQuery()) {
if (isSupernodeClientConnection())
return (getRemoteHostFeatureQuerySelector() >=
query.getFeatureSelector());
else if (isSupernodeSupernodeConnection())
return getRemoteHostSupportsFeatureQueries();
else
return false;
}
return hitsQueryRouteTable(query);
}
/**
* Determines whether or not this query hits the QRT.
*/
protected boolean hitsQueryRouteTable(QueryRequest query) {
if(_lastQRPTableReceived == null) return false;
return _lastQRPTableReceived.contains(query);
}
/**
* Accessor for the <tt>QueryRouteTable</tt> received along this
* connection. Can be <tt>null</tt> if no query routing table has been
* received yet.
*
* @return the last <tt>QueryRouteTable</tt> received along this
* connection
*/
public QueryRouteTable getQueryRouteTableReceived() {
return _lastQRPTableReceived;
}
/**
* Accessor for the last QueryRouteTable's percent full.
*/
public double getQueryRouteTablePercentFull() {
return _lastQRPTableReceived == null ?
0 : _lastQRPTableReceived.getPercentFull();
}
/**
* Accessor for the last QueryRouteTable's size.
*/
public int getQueryRouteTableSize() {
return _lastQRPTableReceived == null ?
0 : _lastQRPTableReceived.getSize();
}
/**
* Accessor for the last QueryRouteTable's Empty Units.
*/
public int getQueryRouteTableEmptyUnits() {
return _lastQRPTableReceived == null ?
-1 : _lastQRPTableReceived.getEmptyUnits();
}
/**
* Accessor for the last QueryRouteTable's Units In Use.
*/
public int getQueryRouteTableUnitsInUse() {
return _lastQRPTableReceived == null ?
-1 : _lastQRPTableReceived.getUnitsInUse();
}
/**
* Creates a deflated output stream.
*
* If the connection supports asynchronous messaging, this does nothing,
* because we already installed an asynchronous writer that doesn't
* use streams.
*/
protected OutputStream createDeflatedOutputStream(OutputStream out) {
if(isAsynchronous())
return out;
else
return super.createDeflatedOutputStream(out);
}
/**
* Creates the deflated input stream.
*
* If the connection supports asynchronous messaging, this does nothing,
* because we're going to install a reader when we start looping for
* messages. Note, however, that if we use the 'receive' calls
* instead of loopForMessages, an UncompressingInputStream is going to
* be set up automatically.
*/
protected InputStream createInflatedInputStream(InputStream in) {
if(isAsynchronous())
return in;
else
return super.createInflatedInputStream(in);
}
/**
* Throttles the super's OutputStream. This works quite well with
* compressed streams, because the chaining mechanism writes the
* compressed bytes, ensuring that we do not attempt to request
* more data (and thus sleep while throttling) than we will actually write.
*/
protected OutputStream getOutputStream() throws IOException {
return new ThrottledOutputStream(super.getOutputStream(), _throttle);
}
/**
* Override of receive to do ConnectionManager stats and to properly shut
* down the connection on IOException
*/
public Message receive() throws IOException, BadPacketException {
Message m = null;
try {
m = super.receive();
} catch(IOException e) {
if( _manager != null )
_manager.remove(this);
throw e;
}
// record received message in stats
_connectionStats.addReceived();
return m;
}
/**
* Override of receive to do MessageRouter stats and to properly shut
* down the connection on IOException
*/
public Message receive(int timeout)
throws IOException, BadPacketException, InterruptedIOException {
Message m = null;
try {
m = super.receive(timeout);
} catch(InterruptedIOException ioe) {
//we read nothing in this timeframe,
//do not remove, just rethrow.
throw ioe;
} catch(IOException e) {
if( _manager != null )
_manager.remove(this);
throw e;
}
// record received message in stats
_connectionStats.addReceived();
return m;
}
////////////////////// Sending, Outgoing Flow Control //////////////////////
/**
* Starts an OutputRunner. If the Connection supports asynchronous writing,
* this does not use an extra thread. Otherwise, a thread is started up
* to write.
*/
private void startOutput() {
MessageQueue queue;
// Taking this change out until we can safely handle attacks and overflow
// TODO: make a cheaper Queue that still prevents flooding of ultrapeer
// and ensures that clogged leaf doesn't drop QRP messages.
//if(isSupernodeSupernodeConnection())
queue = new CompositeQueue();
//else
//queue = new BasicQueue();
if(isAsynchronous()) {
MessageWriter messager = new MessageWriter(_connectionStats, queue, this);
_outputRunner = messager;
ChannelWriter writer = messager;
if(isWriteDeflated()) {
DeflaterWriter deflater = new DeflaterWriter(_deflater);
messager.setWriteChannel(deflater);
writer = deflater;
}
DelayedBufferWriter delayer = new DelayedBufferWriter(1400);
writer.setWriteChannel(delayer);
writer = delayer;
writer.setWriteChannel(new ThrottleWriter(_nbThrottle));
((NIOMultiplexor)_socket).setWriteObserver(messager);
} else {
_outputRunner = new BlockingRunner(queue);
}
}
/**
* Sends a message. This overrides does extra buffering so that Messages
* are dropped if the socket gets backed up. Will remove any extended
* payloads if the receiving connection does not support GGGEP. Also
* updates MessageRouter stats.<p>
*
* This method IS thread safe. Multiple threads can be in a send call
* at the same time for a given connection.
*
* @requires this is fully constructed
* @modifies the network underlying this
*/
public void send(Message m) {
if (! supportsGGEP())
m=m.stripExtendedPayload();
// if Hops Flow is in effect, and this is a QueryRequest, and the
// hoppage is too biggage, discardage time...
int smh = hopsFlowMax;
if (smh > -1 && (m instanceof QueryRequest) && m.getHops() >= smh)
return;
_outputRunner.send(m);
}
/**
* This is a specialized send method for queries that we originate,
* either from ourselves directly, or on behalf of one of our leaves
* when we're an Ultrapeer. These queries have a special sending
* queue of their own and are treated with a higher priority.
*
* @param query the <tt>QueryRequest</tt> to send
*/
public void originateQuery(QueryRequest query) {
query.originate();
send(query);
}
/**
* Does nothing. Since this automatically takes care of flushing output
* buffers, there is nothing to do. Note that flush() does NOT block for
* TCP buffers to be emptied.
*/
public void flush() throws IOException {
}
public void close() {
if(_outputRunner != null)
_outputRunner.shutdown();
super.close();
// release pointer to our _guidMap so it can be gc()'ed
if (_guidMap != null)
GuidMapExpirer.removeMap(_guidMap);
}
//////////////////////////////////////////////////////////////////////////
/**
* Handles core Gnutella request/reply protocol.
* If asynchronous messaging is supported, this immediately
* returns and messages are processed asynchronously via processMessage
* calls. Otherwise, if reading blocks, this will run until the connection
* is closed.
*
* @requires this is initialized
* @modifies the network underlying this, manager
* @effects receives request and sends appropriate replies.
*
* @throws IOException passed on from the receive call; failures to forward
* or route messages are silently swallowed, allowing the message
* loop to continue.
*/
void loopForMessages() throws IOException {
supernodeClientAtLooping = isSupernodeClientConnection();
if(!isAsynchronous()) {
Thread.currentThread().setName("MessageLoopingThread");
while (true) {
Message m=null;
try {
m = receive();
if (m==null)
continue;
handleMessageInternal(m);
} catch (BadPacketException ignored) {}
}
} else {
_socket.setSoTimeout(0); // no timeout for reading.
MessageReader reader = new MessageReader(ManagedConnection.this);
if(isReadDeflated())
reader.setReadChannel(new InflaterReader(_inflater));
((NIOMultiplexor)_socket).setReadObserver(reader);
}
}
/**
* Notification that messaging has closed.
*/
public void messagingClosed() {
if( _manager != null )
_manager.remove(this);
}
/**
* Notification that a message is available to be processed (via asynch-processing).
*/
public void processReadMessage(Message m) throws IOException {
updateReadStatistics(m);
_connectionStats.addReceived();
handleMessageInternal(m);
}
/**
* Notification that a message has been sent. Updates stats.
*/
public void processSentMessage(Message m) {
updateWriteStatistics(m);
}
/**
* Handles a message without updating appropriate statistics.
*/
private void handleMessageInternal(Message m) {
// Run through the route spam filter and drop accordingly.
if (isSpam(m)) {
ReceivedMessageStatHandler.TCP_FILTERED_MESSAGES.addMessage(m);
_connectionStats.addReceivedDropped();
} else {
if(m instanceof QueryReply && m.getHops() == 0)
clientGUID = ((QueryReply)m).getClientGUID();
//special handling for proxying.
if(supernodeClientAtLooping) {
if(m instanceof QueryRequest)
m = tryToProxy((QueryRequest) m);
else if (m instanceof QueryStatusResponse)
m = morphToStopQuery((QueryStatusResponse) m);
}
MessageDispatcher.instance().dispatchTCP(m, this);
}
}
/**
* Returns the network that the MessageReceiver uses -- Message.N_TCP.
*/
public int getNetwork() {
return Message.N_TCP;
}
private QueryRequest tryToProxy(QueryRequest query) {
// we must have the following qualifications:
// 1) Leaf must be sending SuperNode a query (checked in loopForMessages)
// 2) Leaf must support Leaf Guidance
// 3) Query must not be OOB.
// 3.5) The query originator should not disallow proxying.
// 4) We must be able to OOB and have great success rate.
if (remoteHostSupportsLeafGuidance() < 1) return query;
if (query.desiresOutOfBandReplies()) return query;
if (query.doNotProxy()) return query;
if (!RouterService.isOOBCapable() ||
!OutOfBandThroughputStat.isSuccessRateGreat() ||
!OutOfBandThroughputStat.isOOBEffectiveForProxy()) return query;
// everything is a go - we need to do the following:
// 1) mutate the GUID of the query - you should maintain every param of
// the query except the new GUID and the OOB minspeed flag
// 2) set up mappings between the old guid and the new guid.
// after that, everything is set. all you need to do is map the guids
// of the replies back to the original guid. also, see if a you get a
// QueryStatusResponse message and morph it...
// THIS IS SOME MAJOR HOKERY-POKERY!!!
// 1) mutate the GUID of the query
byte[] origGUID = query.getGUID();
byte[] oobGUID = new byte[origGUID.length];
System.arraycopy(origGUID, 0, oobGUID, 0, origGUID.length);
GUID.addressEncodeGuid(oobGUID, RouterService.getAddress(),
RouterService.getPort());
query = QueryRequest.createProxyQuery(query, oobGUID);
// 2) set up mappings between the guids
if (_guidMap == null) {
_guidMap = new Hashtable();
GuidMapExpirer.addMapToExpire(_guidMap);
}
GUID.TimedGUID tGuid = new GUID.TimedGUID(new GUID(oobGUID),
TIMED_GUID_LIFETIME);
_guidMap.put(tGuid, new GUID(origGUID));
OutOfBandThroughputStat.OOB_QUERIES_SENT.incrementStat();
return query;
}
private QueryStatusResponse morphToStopQuery(QueryStatusResponse resp) {
// if the _guidMap is null, we aren't proxying anything....
if (_guidMap == null) return resp;
// if we are proxying this query, we should modify the GUID so as
// to shut off the correct query
final GUID origGUID = resp.getQueryGUID();
GUID oobGUID = null;
synchronized (_guidMap) {
Iterator entrySetIter = _guidMap.entrySet().iterator();
while (entrySetIter.hasNext()) {
Map.Entry entry = (Map.Entry) entrySetIter.next();
if (origGUID.equals(entry.getValue())) {
oobGUID = ((GUID.TimedGUID)entry.getKey()).getGUID();
break;
}
}
}
// if we had a match, then just construct a new one....
if (oobGUID != null)
return new QueryStatusResponse(oobGUID, resp.getNumResults());
else return resp;
}
/**
* Utility method for checking whether or not this message is considered
* spam.
*
* @param m the <tt>Message</tt> to check
* @return <tt>true</tt> if this is considered spam, otherwise
* <tt>false</tt>
*/
public boolean isSpam(Message m) {
return !_routeFilter.allow(m);
}
//
// Begin Message dropping and filtering calls
//
/**
* A callback for the ConnectionManager to inform this connection that a
* message was dropped. This happens when a reply received from this
* connection has no routing path.
*/
public void countDroppedMessage() {
_connectionStats.addReceivedDropped();
}
/**
* A callback for Message Handler implementations to check to see if a
* message is considered to be undesirable by the message's receiving
* connection.
* Messages ignored for this reason are not considered to be dropped, so
* no statistics are incremented here.
*
* @return true if the message is spam, false if it's okay
*/
public boolean isPersonalSpam(Message m) {
return !_personalFilter.allow(m);
}
/**
* @modifies this
* @effects sets the underlying routing filter. Note that
* most filters are not thread-safe, so they should not be shared
* among multiple connections.
*/
public void setRouteFilter(SpamFilter filter) {
_routeFilter = filter;
}
/**
* @modifies this
* @effects sets the underlying personal filter. Note that
* most filters are not thread-safe, so they should not be shared
* among multiple connections.
*/
public void setPersonalFilter(SpamFilter filter) {
_personalFilter = filter;
}
/**
* This method is called when a reply is received for a PingRequest
* originating on this Connection. So, just send it back.
* If modifying this method, note that receivingConnection may
* by null.
*/
public void handlePingReply(PingReply pingReply,
ReplyHandler receivingConnection) {
send(pingReply);
}
/**
* This method is called when a reply is received for a QueryRequest
* originating on this Connection. So, send it back.
* If modifying this method, note that receivingConnection may
* by null.
*/
public void handleQueryReply(QueryReply queryReply,
ReplyHandler receivingConnection) {
if (_guidMap != null) {
// ---------------------
// If we are proxying for a query, map back the guid of the reply
GUID.TimedGUID tGuid = new GUID.TimedGUID(new GUID(queryReply.getGUID()),
TIMED_GUID_LIFETIME);
GUID origGUID = (GUID) _guidMap.get(tGuid);
if (origGUID != null) {
byte prevHops = queryReply.getHops();
queryReply = new QueryReply(origGUID.bytes(), queryReply);
queryReply.setTTL((byte)2); // we ttl 1 more than necessary
queryReply.setHops(prevHops);
}
// ---------------------
}
send(queryReply);
}
/**
* Gets the clientGUID of the remote host of the connection.
*/
public byte[] getClientGUID() {
return clientGUID;
}
/**
* This method is called when a PushRequest is received for a QueryReply
* originating on this Connection. So, just send it back.
* If modifying this method, note that receivingConnection may
* by null.
*/
public void handlePushRequest(PushRequest pushRequest,
ReplyHandler receivingConnection) {
send(pushRequest);
}
protected void handleVendorMessage(VendorMessage vm) {
// let Connection do as needed....
super.handleVendorMessage(vm);
// now i can process
if (vm instanceof HopsFlowVendorMessage) {
// update the softMaxHops value so it can take effect....
HopsFlowVendorMessage hops = (HopsFlowVendorMessage) vm;
if( isSupernodeClientConnection() )
// If the connection is to a leaf, and it is busy (HF == 0)
// then set the global busy leaf flag appropriately
setBusy( hops.getHopValue()==0 );
hopsFlowMax = hops.getHopValue();
}
else if (vm instanceof PushProxyAcknowledgement) {
// this connection can serve as a PushProxy, so note this....
PushProxyAcknowledgement ack = (PushProxyAcknowledgement) vm;
if (Arrays.equals(ack.getGUID(),
RouterService.getMessageRouter()._clientGUID)) {
_pushProxy = true;
}
// else mistake on the server side - the guid should be my client
// guid - not really necessary but whatever
}
else if(vm instanceof CapabilitiesVM) {
//we need to see if there is a new simpp version out there.
CapabilitiesVM capVM = (CapabilitiesVM)vm;
if(capVM.supportsSIMPP() > SimppManager.instance().getVersion()) {
//request the simpp message
SimppRequestVM simppReq = new SimppRequestVM();
send(simppReq);
}
// see if there's a new update message.
int latestId = UpdateHandler.instance().getLatestId();
int currentId = capVM.supportsUpdate();
if(currentId > latestId)
send(new UpdateRequest());
else if(currentId == latestId)
UpdateHandler.instance().handleUpdateAvailable(this, currentId);
}
else if (vm instanceof MessagesSupportedVendorMessage) {
// If this is a ClientSupernodeConnection and the host supports
// leaf guidance (because we have to tell them when to stop)
// then see if there are any old queries that we can re-originate
// on this connection.
if(isClientSupernodeConnection() &&
(remoteHostSupportsLeafGuidance() >= 0)) {
SearchResultHandler srh =
RouterService.getSearchResultHandler();
List queries = srh.getQueriesToReSend();
for(Iterator i = queries.iterator(); i.hasNext(); )
send((Message)i.next());
}
// see if you need a PushProxy - the remoteHostSupportsPushProxy
// test incorporates my leaf status in it.....
if (remoteHostSupportsPushProxy() > -1) {
// get the client GUID and send off a PushProxyRequest
GUID clientGUID =
new GUID(RouterService.getMessageRouter()._clientGUID);
PushProxyRequest req = new PushProxyRequest(clientGUID);
send(req);
}
// do i need to send any ConnectBack messages????
if (!UDPService.instance().canReceiveUnsolicited() &&
(_numUDPConnectBackRequests < MAX_UDP_CONNECT_BACK_ATTEMPTS) &&
(remoteHostSupportsUDPRedirect() > -1)) {
GUID connectBackGUID = RouterService.getUDPConnectBackGUID();
Message udp = new UDPConnectBackVendorMessage(RouterService.getPort(),
connectBackGUID);
send(udp);
_numUDPConnectBackRequests++;
}
if (!RouterService.acceptedIncomingConnection() &&
(_numTCPConnectBackRequests < MAX_TCP_CONNECT_BACK_ATTEMPTS) &&
(remoteHostSupportsTCPRedirect() > -1)) {
Message tcp = new TCPConnectBackVendorMessage(RouterService.getPort());
send(tcp);
_numTCPConnectBackRequests++;
}
}
}
//
// End reply forwarding calls
//
//
// Begin statistics accessors
//
/** Returns the number of messages sent on this connection */
public int getNumMessagesSent() {
return _connectionStats.getSent();
}
/** Returns the number of messages received on this connection */
public int getNumMessagesReceived() {
return _connectionStats.getReceived();
}
/** Returns the number of messages I dropped while trying to send
* on this connection. This happens when the remote host cannot
* keep up with me. */
public int getNumSentMessagesDropped() {
return _connectionStats.getSentDropped();
}
/**
* The number of messages received on this connection either filtered out
* or dropped because we didn't know how to route them.
*/
public long getNumReceivedMessagesDropped() {
return _connectionStats.getReceivedDropped();
}
/**
* @modifies this
* @effects Returns the percentage of messages sent on this
* since the last call to getPercentReceivedDropped that were
* dropped by this end of the connection.
*/
public float getPercentReceivedDropped() {
return _connectionStats.getPercentReceivedDropped();
}
/**
* @modifies this
* @effects Returns the percentage of messages sent on this
* since the last call to getPercentSentDropped that were
* dropped by this end of the connection. This value may be
* greater than 100%, e.g., if only one message is sent but
* four are dropped during a given time period.
*/
public float getPercentSentDropped() {
return _connectionStats.getPercentSentDropped();
}
/**
* Takes a snapshot of the upstream and downstream bandwidth since the last
* call to measureBandwidth.
* @see BandwidthTracker#measureBandwidth
*/
public void measureBandwidth() {
_upBandwidthTracker.measureBandwidth(
ByteOrder.long2int(getBytesSent()));
_downBandwidthTracker.measureBandwidth(
ByteOrder.long2int(getBytesReceived()));
}
/**
* Returns the upstream bandwidth between the last two calls to
* measureBandwidth.
* @see BandwidthTracker#measureBandwidth
*/
public float getMeasuredUpstreamBandwidth() {
float retValue = 0; //initialize to default
try {
retValue = _upBandwidthTracker.getMeasuredBandwidth();
} catch(InsufficientDataException ide) {
return 0;
}
return retValue;
}
/**
* Returns the downstream bandwidth between the last two calls to
* measureBandwidth.
* @see BandwidthTracker#measureBandwidth
*/
public float getMeasuredDownstreamBandwidth() {
float retValue = 0;
try {
retValue = _downBandwidthTracker.getMeasuredBandwidth();
} catch (InsufficientDataException ide) {
return 0;
}
return retValue;
}
//
// End statistics accessors
//
/** Returns the system time that we should next forward a query route table
* along this connection. Only valid if isClientSupernodeConnection() is
* true. */
public long getNextQRPForwardTime() {
return _nextQRPForwardTime;
}
/**
* Increments the next time we should forward query route tables for
* this connection. This depends on whether or not this is a connection
* to a leaf or to an Ultrapeer.
*
* @param curTime the current time in milliseconds, used to calculate
* the next update time
*/
public void incrementNextQRPForwardTime(long curTime) {
if(isLeafConnection()) {
_nextQRPForwardTime = curTime + LEAF_QUERY_ROUTE_UPDATE_TIME;
} else {
// otherwise, it's an Ultrapeer
_nextQRPForwardTime = curTime + ULTRAPEER_QUERY_ROUTE_UPDATE_TIME;
}
}
/**
* Returns true if this should not be policed by the ConnectionWatchdog,
* e.g., because this is a connection to a Clip2 reflector. Default value:
* true.
*/
public boolean isKillable() {
return _isKillable;
}
/**
* Accessor for the query route table associated with this. This is
* guaranteed to be non-null, but it may not yet contain any data.
*
* @return the <tt>QueryRouteTable</tt> instance containing
* query route table data sent along this connection, or <tt>null</tt>
* if no data has yet been sent
*/
public QueryRouteTable getQueryRouteTableSent() {
return _lastQRPTableSent;
}
/**
* Mutator for the last query route table that was sent along this
* connection.
*
* @param qrt the last query route table that was sent along this
* connection
*/
public void setQueryRouteTableSent(QueryRouteTable qrt) {
_lastQRPTableSent = qrt;
}
public boolean isPushProxy() {
return _pushProxy;
}
public Object getQRPLock() {
return QRP_LOCK;
}
/**
* set preferencing for the responder
* (The preference of the Responder is used when creating the response
* (in Connection.java: conclude..))
*/
public void setLocalePreferencing(boolean b) {
_useLocalPreference = b;
}
public void reply(Message m){
send(m);
}
/** Repeatedly sends all the queued data using a thread. */
private class BlockingRunner implements Runnable, OutputRunner {
private final Object LOCK = new Object();
private final MessageQueue queue;
private boolean shutdown = false;
public BlockingRunner(MessageQueue queue) {
this.queue = queue;
ThreadFactory.startThread(this, "OutputRunner");
}
public void send(Message m) {
synchronized (LOCK) {
_connectionStats.addSent();
queue.add(m);
int dropped = queue.resetDropped();
_connectionStats.addSentDropped(dropped);
LOCK.notify();
}
}
public void shutdown() {
synchronized(LOCK) {
shutdown = true;
LOCK.notify();
}
}
/** While the connection is not closed, sends all data delay. */
public void run() {
//For non-IOExceptions, Throwable is caught to notify ErrorService.
try {
while (true) {
waitForQueued();
sendQueued();
}
} catch (IOException e) {
if(_manager != null)
_manager.remove(ManagedConnection.this);
} catch(Throwable t) {
if(_manager != null)
_manager.remove(ManagedConnection.this);
ErrorService.error(t);
}
}
/**
* Wait until the queue is (probably) non-empty or closed.
* @exception IOException this was closed while waiting
*/
private final void waitForQueued() throws IOException {
// Lock outside of the loop so that the MessageQueue is synchronized.
synchronized (LOCK) {
while (!shutdown && isOpen() && queue.isEmpty()) {
try {
LOCK.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
if (! isOpen() || shutdown)
throw CONNECTION_CLOSED;
}
/** Send several queued message of each type. */
private final void sendQueued() throws IOException {
// Send as many messages as we can, until we run out.
while(true) {
Message m = null;
synchronized(LOCK) {
m = queue.removeNext();
int dropped = queue.resetDropped();
_connectionStats.addSentDropped(dropped);
}
if(m == null)
break;
//Note that if the ougoing stream is compressed
//(isWriteDeflated()), this call may not actually
//do anything. This is because the Deflater waits
//until an optimal time to start deflating, buffering
//up incoming data until that time is reached, or the
//data is explicitly flushed.
ManagedConnection.super.send(m);
}
//Note that if the outgoing stream is compressed
//(isWriteDeflated()), then this call may block while the
//Deflater deflates the data.
ManagedConnection.super.flush();
}
}
/** Class-wide expiration mechanism for all ManagedConnections.
* Only expires on-demand.
*/
private static class GuidMapExpirer implements Runnable {
private static List toExpire = new LinkedList();
private static boolean scheduled = false;
public GuidMapExpirer() {};
public static synchronized void addMapToExpire(Map expiree) {
// schedule it on demand
if (!scheduled) {
RouterService.schedule(new GuidMapExpirer(), 0,
TIMED_GUID_LIFETIME);
scheduled = true;
}
toExpire.add(expiree);
}
public static synchronized void removeMap(Map expiree) {
toExpire.remove(expiree);
}
public void run() {
synchronized (GuidMapExpirer.class) {
// iterator through all the maps....
Iterator iter = toExpire.iterator();
while (iter.hasNext()) {
Map currMap = (Map) iter.next();
synchronized (currMap) {
Iterator keyIter = currMap.keySet().iterator();
// and expire as many entries as possible....
while (keyIter.hasNext())
if (((GUID.TimedGUID) keyIter.next()).shouldExpire())
keyIter.remove();
}
}
}
}
}
/**
* A ConnectObserver that continues the handshaking process in the same thread,
* expecting that performHandshake(...) callback to the observer.
*/
private class AsyncHandshakeConnecter implements ConnectObserver {
private Properties requestHeaders;
private HandshakeResponder responder;
private GnetConnectObserver observer;
AsyncHandshakeConnecter(Properties requestHeaders, HandshakeResponder responder, GnetConnectObserver observer) {
this.requestHeaders = requestHeaders;
this.responder = responder;
this.observer = observer;
}
public void handleConnect(Socket socket) throws IOException {
preHandshakeInitialize(requestHeaders, responder, observer);
}
public void shutdown() {
observer.shutdown();
}
//ignored.
public void handleIOException(IOException iox) {}
}
/**
* A HandshakeObserver that notifies the GnetConnectObserver when handshaking finishes.
*/
private class HandshakeWatcher implements HandshakeObserver {
private Handshaker shaker;
private GnetConnectObserver observer;
HandshakeWatcher(GnetConnectObserver observer) {
this.observer = observer;
}
void setHandshaker(Handshaker shaker) {
this.shaker = shaker;
}
public void shutdown() {
setHeaders(shaker);
close();
observer.shutdown();
}
public void handleHandshakeFinished(Handshaker shaker) {
postHandshakeInitialize(shaker);
observer.handleConnect();
}
public void handleBadHandshake() {
setHeaders(shaker);
close();
observer.handleBadHandshake();
}
public void handleNoGnutellaOk(int code, String msg) {
setHeaders(shaker);
close();
observer.handleNoGnutellaOk(code, msg);
}
}
}