package com.limegroup.gnutella; import java.io.IOException; import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.InetAddress; import java.net.Socket; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.TreeMap; import java.util.Map; import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; import java.util.List; import java.util.Set; import com.limegroup.gnutella.messages.*; import com.limegroup.gnutella.messages.vendor.*; import com.limegroup.gnutella.search.ResultCounter; import com.limegroup.gnutella.security.User; import com.limegroup.gnutella.udpconnect.UDPConnectionMessage; import com.limegroup.gnutella.udpconnect.UDPMultiplexor; import com.limegroup.gnutella.util.FixedsizeHashMap; import com.limegroup.gnutella.util.NetworkUtils; import com.limegroup.gnutella.util.NoMoreStorageException; import com.limegroup.gnutella.util.Sockets; import com.limegroup.gnutella.util.Utilities; import com.limegroup.gnutella.util.IOUtils; import com.limegroup.gnutella.util.ProcessingQueue; import com.util.LOG; /** * One of the three classes that make up the core of the backend. This * class' job is to direct the routing of messages and to count those message * as they pass through. To do so, it aggregates a ConnectionManager that * maintains a list of connections. */ public abstract class MessageRouter { /** * Handle to the <tt>ConnectionManager</tt> to access our TCP connections. */ protected static ConnectionManager _manager; /** * The GUID we attach to QueryReplies to allow PushRequests in * responses. */ protected byte[] _clientGUID; /** * Reference to the <tt>ReplyHandler</tt> for messages intended for * this node. */ private final ReplyHandler FOR_ME_REPLY_HANDLER = ForMeReplyHandler.instance(); /** * The maximum size for <tt>RouteTable</tt>s. */ private int MAX_ROUTE_TABLE_SIZE = 100; //actually 100,000 entries /** * Maps QueryRequest GUIDs to QueryReplyHandlers. Stores 5-10 minutes, * typically around 13000 entries, but never more than 100,000 entries. */ private RouteTable _queryRouteTable = new RouteTable(5*60, MAX_ROUTE_TABLE_SIZE); /** * Maps QueryReply client GUIDs to PushRequestHandlers. Stores 7-14 * minutes, typically around 3500 entries, but never more than 100,000 * entries. */ private RouteTable _pushRouteTable = new RouteTable(7*60, MAX_ROUTE_TABLE_SIZE); /** * Maps HeadPong guids to the originating pingers. Short-lived since * we expect replies from our leaves quickly. */ private RouteTable _headPongRouteTable = new RouteTable(10, MAX_ROUTE_TABLE_SIZE); /** How long to buffer up out-of-band replies. */ private static final long CLEAR_TIME = 30 * 1000; // 30 seconds /** Time between sending HopsFlow messages. */ private static final long HOPS_FLOW_INTERVAL = 15 * 1000; // 15 seconds /** The maximum number of UDP replies to buffer up. Non-final for * testing. */ static int MAX_BUFFERED_REPLIES = 250; /** * Keeps track of QueryReplies to be sent after recieving LimeAcks (sent * if the sink wants them). Cleared every CLEAR_TIME seconds. * TimedGUID->QueryResponseBundle. */ private final Map _outOfBandReplies = new Hashtable(); /** * Keeps track of what hosts we have recently tried to connect back to via * UDP. The size is limited and once the size is reached, no more connect * back attempts will be honored. */ private static final FixedsizeHashMap _udpConnectBacks = new FixedsizeHashMap(200); /** * The maximum numbers of ultrapeers to forward a UDPConnectBackRedirect * message to, per forward. */ private static final int MAX_UDP_CONNECTBACK_FORWARDS = 5; /** * Keeps track of what hosts we have recently tried to connect back to via * TCP. The size is limited and once the size is reached, no more connect * back attempts will be honored. */ private static final FixedsizeHashMap _tcpConnectBacks = new FixedsizeHashMap(200); /** * The maximum numbers of ultrapeers to forward a TCPConnectBackRedirect * message to, per forward. */ private static final int MAX_TCP_CONNECTBACK_FORWARDS = 5; /** * The processingqueue to add tcpconnectback socket connections to. */ private static final ProcessingQueue TCP_CONNECT_BACKER = new ProcessingQueue("TCPConnectBack"); /** * A handle to the thread that deals with QRP Propagation */ /** * The lifetime of OOBs guids. */ private static final long TIMED_GUID_LIFETIME = 25 * 1000; /** * Keeps track of Listeners of GUIDs. * GUID -> List of MessageListener */ private volatile Map _messageListeners = Collections.EMPTY_MAP; /** * Lock that registering & unregistering listeners can hold * while replacing the listeners map / lists. */ private final Object MESSAGE_LISTENER_LOCK = new Object(); /** * Router for UDPConnection messages. */ private final UDPMultiplexor _udpConnectionMultiplexor = UDPMultiplexor.instance(); /** * Creates a MessageRouter. Must call initialize before using. */ protected MessageRouter() { _clientGUID=RouterService.getMyGUID(); } /** * Links the MessageRouter up with the other back end pieces */ public void initialize() { _manager = RouterService.getConnectionManager(); // schedule a runner to clear unused out-of-band replies RouterService.schedule(new Expirer(), CLEAR_TIME, CLEAR_TIME); // schedule a runner to clear guys we've connected back to RouterService.schedule(new ConnectBackExpirer(), 10 * CLEAR_TIME, 10 * CLEAR_TIME); // schedule a runner to send hops-flow messages RouterService.schedule(new HopsFlowManager(), HOPS_FLOW_INTERVAL*10, HOPS_FLOW_INTERVAL); } public String getQueryRouteTableDump() { return _queryRouteTable.toString(); } /** * A callback for ConnectionManager to clear a <tt>ReplyHandler</tt> from * the routing tables when the connection is closed. */ public void removeConnection(ReplyHandler rh) { _queryRouteTable.removeReplyHandler(rh); _pushRouteTable.removeReplyHandler(rh); _headPongRouteTable.removeReplyHandler(rh); } /** * The handler for all message types. Processes a message based on the * message type. * * @param m the <tt>Message</tt> instance to route appropriately * @param receivingConnection the <tt>ManagedConnection</tt> over which * the message was received */ public void handleMessage(Message msg, ManagedConnection receivingConnection) { // Increment hops and decrease TTL. msg.hop(); if(msg instanceof PingRequest) { handlePingRequest((PingRequest)msg, receivingConnection); } else if (msg instanceof PingReply) { handlePingReply((PingReply)msg, receivingConnection); } else if (msg instanceof ByeRequest) { //ReceivedMessageStatHandler.TCP_PING_REPLIES.addMessage(msg); //TODO: add stat code //LOG.logSp("bye " + receivingConnection.getAddress()); receivingConnection.close(); //handlePingReply((PingReply)msg, receivingConnection); } else if (msg instanceof QueryReply) { // if someone sent a TCP QueryReply with the MCAST header, // that's bad, so ignore it. QueryReply qmsg = (QueryReply)msg; handleQueryReply(qmsg, receivingConnection); } else if (msg instanceof PushRequest) { handlePushRequest((PushRequest)msg, receivingConnection); } else if (msg instanceof TCPConnectBackVendorMessage) { handleTCPConnectBackRequest((TCPConnectBackVendorMessage) msg, receivingConnection); } else if (msg instanceof UDPConnectBackVendorMessage) { handleUDPConnectBackRequest((UDPConnectBackVendorMessage) msg, receivingConnection); } else if (msg instanceof TCPConnectBackRedirect) { handleTCPConnectBackRedirect((TCPConnectBackRedirect) msg, receivingConnection); } else if (msg instanceof UDPConnectBackRedirect) { handleUDPConnectBackRedirect((UDPConnectBackRedirect) msg, receivingConnection); } else if (msg instanceof HeadPing) { //TODO: add the statistics recording code handleHeadPing((HeadPing)msg, receivingConnection); } else if (msg instanceof HeadPong) { handleHeadPong((HeadPong)msg, receivingConnection); } else if (msg instanceof VendorMessage) { receivingConnection.handleVendorMessage((VendorMessage)msg); } //This may trigger propogation of query route tables. We do this AFTER //any handshake pings. Otherwise we'll think all clients are old //clients. //forwardQueryRouteTables(); notifyMessageListener(msg, receivingConnection); } /** * Notifies any message listeners of this message's guid about the message. * This holds no locks. */ private final void notifyMessageListener(Message msg, ReplyHandler handler) { List all = (List)_messageListeners.get(msg.getGUID()); if(all != null) { for(Iterator i = all.iterator(); i.hasNext(); ) { MessageListener next = (MessageListener)i.next(); next.processMessage(msg, handler); } } } /** * The handler for all message types. Processes a message based on the * message type. * * @param msg the <tt>Message</tt> received * @param addr the <tt>InetSocketAddress</tt> containing the IP and * port of the client node */ public void handleUDPMessage(Message msg, InetSocketAddress addr) { // Increment hops and decrement TTL. msg.hop(); InetAddress address = addr.getAddress(); int port = addr.getPort(); // Verify that the address and port are valid. // If they are not, we cannot send any replies to them. if(!RouterService.isIpPortValid()) return; // Send UDPConnection messages on to the connection multiplexor // for routing to the appropriate connection processor if ( msg instanceof UDPConnectionMessage ) { _udpConnectionMultiplexor.routeMessage( (UDPConnectionMessage)msg, address, port); return; } ReplyHandler handler = new UDPReplyHandler(address, port); if (msg instanceof QueryReply) { QueryReply qr = (QueryReply) msg; handleQueryReply(qr, handler); } else if(msg instanceof PingRequest) { handleUDPPingRequest((PingRequest)msg, handler, addr); } else if(msg instanceof PingReply) { handleUDPPingReply((PingReply)msg, handler, address, port); } else if(msg instanceof PushRequest) { handlePushRequest((PushRequest)msg, handler); } else if(msg instanceof ReplyNumberVendorMessage) { handleReplyNumberMessage((ReplyNumberVendorMessage) msg, addr); } else if (msg instanceof HeadPing) { //TODO: add the statistics recording code handleHeadPing((HeadPing)msg, handler); } notifyMessageListener(msg, handler); } /** * Sends an ack back to the GUESS client node. */ protected void sendAcknowledgement(InetSocketAddress addr, byte[] guid) { ConnectionManager manager = RouterService.getConnectionManager(); Endpoint host = manager.getConnectedGUESSUltrapeer(); PingReply reply; if(host != null) { try { reply = PingReply.createGUESSReply(guid, (byte)1, host); } catch(UnknownHostException e) { reply = createPingReply(guid); } } else { reply = createPingReply(guid); } // No GUESS endpoints existed and our IP/port was invalid. if( reply == null ) return; UDPService.instance().send(reply, addr.getAddress(), addr.getPort()); } /** * Creates a new <tt>PingReply</tt> from the set of cached * GUESS endpoints, or a <tt>PingReply</tt> for localhost * if no GUESS endpoints are available. */ private PingReply createPingReply(byte[] guid) { if(RouterService.isIpPortValid()) return PingReply.create(guid, (byte)1); else return null; } /** * Handles pings from the network. With the addition of pong caching, this * method will either respond with cached pongs, or it will ignore the ping * entirely if another ping has been received from this connection very * recently. If the ping is TTL=1, we will always process it, as it may * be a hearbeat ping to make sure the connection is alive and well. * * @param ping the ping to handle * @param handler the <tt>ReplyHandler</tt> instance that sent the ping */ final private void handlePingRequest(PingRequest ping, ReplyHandler handler) { // Send it along if it's a heartbeat ping or if we should allow new // pings on this connection. if(ping.isHeartbeat() || handler.allowNewPings()) { respondToPingRequest(ping, handler); } } /** * The default handler for PingRequests received in * ManagedConnection.loopForMessages(). This implementation updates stats, * does the broadcast, and generates a response. * * You can customize behavior in three ways: * 1. Override. You can assume that duplicate messages * (messages with the same GUID that arrived via different paths) have * already been filtered. If you want stats updated, you'll * have to call super.handlePingRequest. * 2. Override broadcastPingRequest. This allows you to use the default * handling framework and just customize request routing. * 3. Implement respondToPingRequest. This allows you to use the default * handling framework and just customize responses. */ protected void handleUDPPingRequest(PingRequest pingRequest, ReplyHandler handler, InetSocketAddress addr) { if (!pingRequest.isQueryKeyRequest()) respondToUDPPingRequest(pingRequest, addr, handler); } protected void handleUDPPingReply(PingReply reply, ReplyHandler handler, InetAddress address, int port) { // normal pong processing... handlePingReply(reply, handler); } /** This is called when a client on the network has results for us that we * may want. We may contact them back directly or just cache them for * use. */ protected void handleReplyNumberMessage(ReplyNumberVendorMessage reply, InetSocketAddress addr) { GUID qGUID = new GUID(reply.getGUID()); int numResults = RouterService.getSearchResultHandler().getNumResultsForQuery(qGUID); // see if we need more results for this query.... // if not, remember this location for a future, 'find more sources' // targeted GUESS query, as long as the other end said they can receive // unsolicited. if ((numResults<0) || (numResults>Const.ULTRAPEER_RESULTS)) { return; } LimeACKVendorMessage ack = new LimeACKVendorMessage(qGUID, reply.getNumResults()); UDPService.instance().send(ack, addr.getAddress(), addr.getPort()); } /** Stores (for a limited time) the resps for later out-of-band delivery - * interacts with handleLimeACKMessage * @return true if the operation failed, false if not (i.e. too busy) */ protected boolean bufferResponsesForLaterDelivery(QueryRequest query, Response[] resps) { // store responses by guid for later retrieval synchronized (_outOfBandReplies) { if (_outOfBandReplies.size() < MAX_BUFFERED_REPLIES) { GUID.TimedGUID tGUID = new GUID.TimedGUID(new GUID(query.getGUID()), TIMED_GUID_LIFETIME); _outOfBandReplies.put(tGUID, new QueryResponseBundle(query, resps)); return true; } return false; } } /** * Forwards the UDPConnectBack to neighboring peers * as a UDPConnectBackRedirect request. */ protected void handleUDPConnectBackRequest(UDPConnectBackVendorMessage udp, Connection source) { GUID guidToUse = udp.getConnectBackGUID(); int portToContact = udp.getConnectBackPort(); InetAddress sourceAddr = source.getInetAddress(); Message msg = new UDPConnectBackRedirect(guidToUse, sourceAddr, portToContact); int sentTo = 0; List peers = new ArrayList(_manager.getInitializedConnections()); Collections.shuffle(peers); for(Iterator i = peers.iterator(); i.hasNext() && sentTo < MAX_UDP_CONNECTBACK_FORWARDS;) { ManagedConnection currMC = (ManagedConnection)i.next(); if(currMC == source) continue; if (currMC.remoteHostSupportsUDPRedirect() >= 0) { currMC.send(msg); sentTo++; } } } /** * Sends a ping to the person requesting the connectback request. */ protected void handleUDPConnectBackRedirect(UDPConnectBackRedirect udp, Connection source) { // only allow other UPs to send you this message.... if (!source.isSupernodeSupernodeConnection()) return; GUID guidToUse = udp.getConnectBackGUID(); int portToContact = udp.getConnectBackPort(); InetAddress addrToContact = udp.getConnectBackAddress(); // only connect back if you aren't connected to the host - that is the // whole point of redirect after all.... Endpoint endPoint = new Endpoint(addrToContact.getAddress(), portToContact); if (_manager.isConnectedTo(endPoint.getAddress())) return; // keep track of who you tried connecting back too, don't do it too // much.... String addrString = addrToContact.getHostAddress(); Object placeHolder = _udpConnectBacks.get(addrString); if (placeHolder == null) { try { _udpConnectBacks.put(addrString, new Object()); } catch (NoMoreStorageException nomo) { return; // we've done too many connect backs, stop.... } } else { return; // we've connected back to this guy recently.... } PingRequest pr = new PingRequest(guidToUse.bytes(), (byte) 1, (byte) 0); UDPService.instance().send(pr, addrToContact, portToContact); } /** * Forwards the request to neighboring Ultrapeers as a * TCPConnectBackRedirect message. */ protected void handleTCPConnectBackRequest(TCPConnectBackVendorMessage tcp, Connection source) { final int portToContact = tcp.getConnectBackPort(); InetAddress sourceAddr = source.getInetAddress(); Message msg = new TCPConnectBackRedirect(sourceAddr, portToContact); int sentTo = 0; List peers = new ArrayList(_manager.getInitializedConnections()); Collections.shuffle(peers); for(Iterator i = peers.iterator(); i.hasNext() && sentTo < MAX_TCP_CONNECTBACK_FORWARDS;) { ManagedConnection currMC = (ManagedConnection)i.next(); if(currMC == source) continue; if (currMC.remoteHostSupportsTCPRedirect() >= 0) { currMC.send(msg); sentTo++; } } } /** * Basically, just get the correct parameters, create a Socket, and * send a "/n/n". */ protected void handleTCPConnectBackRedirect(TCPConnectBackRedirect tcp, Connection source) { // only allow other UPs to send you this message.... if (!source.isSupernodeSupernodeConnection()) return; final int portToContact = tcp.getConnectBackPort(); final String addrToContact =tcp.getConnectBackAddress().getHostAddress(); // only connect back if you aren't connected to the host - that is the // whole point of redirect after all.... Endpoint endPoint = new Endpoint(addrToContact, portToContact); if (_manager.isConnectedTo(endPoint.getAddress())) return; // keep track of who you tried connecting back too, don't do it too // much.... Object placeHolder = _tcpConnectBacks.get(addrToContact); if (placeHolder == null) { try { _tcpConnectBacks.put(addrToContact, new Object()); } catch (NoMoreStorageException nomo) { return; // we've done too many connect backs, stop.... } } else { return; // we've connected back to this guy recently.... } TCP_CONNECT_BACKER.add(new Runnable() { public void run() { Socket sock = null; try { sock = Sockets.connect(addrToContact, portToContact, 12000); OutputStream os = sock.getOutputStream(); os.write("CONNECT BACK\r\n\r\n".getBytes()); os.flush(); if(LOG.isTraceEnabled()) LOG.trace("Succesful connectback to: " + addrToContact); try { Thread.sleep(500); // let the other side get it. } catch(InterruptedException ignored) { LOG.warn("Interrupted connectback", ignored); } } catch (IOException ignored) { LOG.warn("IOX during connectback", ignored); } catch (Throwable t) { ErrorService.error(t); } finally { IOUtils.close(sock); } } }); } /** * Sends the ping request to the designated connection, * setting up the proper reply routing. */ public void sendPingRequest(PingRequest request, ManagedConnection connection) { if(request == null) { throw new NullPointerException("null ping"); } if(connection == null) { throw new NullPointerException("null connection"); } connection.send(request); } /** * Sends the query request to the designated connection, * setting up the proper reply routing. */ public void sendQueryRequest(QueryRequest request, ManagedConnection connection) { if(request == null) { throw new NullPointerException("null query"); } if(connection == null) { throw new NullPointerException("null connection"); } _queryRouteTable.routeReply(request.getGUID(), FOR_ME_REPLY_HANDLER); connection.send(request); } /** * Broadcasts the ping request to all initialized connections, * setting up the proper reply routing. */ public void broadcastPingRequest(PingRequest ping) { if(ping == null) { throw new NullPointerException("null ping"); } broadcastPingRequest(ping, FOR_ME_REPLY_HANDLER, _manager); } /** * Generates a new dynamic query. This method is used to send a new * dynamic query from this host (the user initiated this query directly, * so it's replies are intended for this node. * * @param query the <tt>QueryRequest</tt> instance that generates * queries for this dynamic query * @throws <tt>NullPointerException</tt> if the <tt>QueryHandler</tt> * argument is <tt>null</tt> */ public void sendDynamicQuery(QueryRequest query) { if(query == null) { throw new NullPointerException("null QueryHandler"); } _queryRouteTable.routeReply(query.getGUID(), FOR_ME_REPLY_HANDLER); // get the result counter so we can track the number of results originateLeafQuery(query); // always send the query to your multicast people multicastQueryRequest(QueryRequest.createMulticastQuery(query)); } /** * Broadcasts the ping request to all initialized connections that * are not the receivingConnection, setting up the routing * to the designated PingReplyHandler. This is called from the default * handlePingRequest and the default broadcastPingRequest(PingRequest) * * If different (smarter) broadcasting functionality is desired, override * as desired. If you do, note that receivingConnection may be null (for * requests originating here). */ private void broadcastPingRequest(PingRequest request, ReplyHandler receivingConnection, ConnectionManager manager) { // Note the use of initializedConnections only. // Note that we have zero allocations here. //Broadcast the ping to other connected nodes (supernodes or older //nodes), but DON'T forward any ping not originating from me //along leaf to ultrapeer connections. List list = manager.getInitializedConnections(); int size = list.size(); boolean randomlyForward = false; if(size > 3) randomlyForward = true; double percentToIgnore; for(int i=0; i<size; i++) { ManagedConnection mc = (ManagedConnection)list.get(i); if(!mc.isStable()) continue; if (receivingConnection == FOR_ME_REPLY_HANDLER || (mc != receivingConnection && !mc.isClientSupernodeConnection())) { if(mc.supportsPongCaching()) { percentToIgnore = 0.70; } else { percentToIgnore = 0.90; } if(randomlyForward && (Math.random() < percentToIgnore)) { continue; } else { mc.send(request); } } } } /** * Send the query to the multicast group. */ protected void multicastQueryRequest(QueryRequest query) { // set the TTL on outgoing udp queries to 1 query.setTTL((byte)1); // record the stat MulticastService.send(query); } /** * Originate a new query from this leaf node. * * @param qr the <tt>QueryRequest</tt> to send */ private void originateLeafQuery(QueryRequest qr) { List list = _manager.getInitializedConnections(); // only send to at most 4 Ultrapeers, as we could have more // as a result of race conditions - also, don't send what is new // requests down too many connections final int max = 3; int start = 0; int limit = Math.min(max, list.size()); final boolean wantsOOB = qr.desiresOutOfBandReplies(); for(int i=start; i<start+limit; i++) { ManagedConnection mc = (ManagedConnection)list.get(i); QueryRequest qrToSend = qr; if (wantsOOB && (mc.remoteHostSupportsLeafGuidance() < 0)) qrToSend = QueryRequest.unmarkOOBQuery(qr); sendQueryRequest(qrToSend, mc, FOR_ME_REPLY_HANDLER); } } public void resendQuery(ManagedConnection mc, QueryRequest qr) { if (qr.desiresOutOfBandReplies() && (mc.remoteHostSupportsLeafGuidance() < 0)) qr = QueryRequest.unmarkOOBQuery(qr); sendQueryRequest(qr, mc, FOR_ME_REPLY_HANDLER); } /** * Sends the passed query request, received on handler, * to the passed sendConnection, only if the handler and * the sendConnection are authenticated to a common domain * * To only send it the route table has a hit, use * sendRoutedQueryToHost. * * @param queryRequest Query Request to send * @param sendConnection The connection on which to send out the query * @param handler The connection on which we originally * received the query */ public void sendQueryRequest(QueryRequest request, ManagedConnection sendConnection, ReplyHandler handler) { if(request == null) { throw new NullPointerException("null query"); } if(sendConnection == null) { throw new NullPointerException("null send connection"); } if(handler == null) { throw new NullPointerException("null reply handler"); } //send the query over this connection only if any of the following //is true: //1. The query originated from our node //2. The connection under consideration is an unauthenticated //connection (normal gnutella connection) //3. It is an authenticated connection, and the connection on //which we received query and this connection, are both //authenticated to a common domain if((handler == FOR_ME_REPLY_HANDLER || containsDefaultUnauthenticatedDomainOnly(sendConnection.getDomains()) || Utilities.hasIntersection(handler.getDomains(), sendConnection.getDomains()))) { sendConnection.send(request); } } /** * Originates a new query request to the ManagedConnection. * * @param request The query to send. * @param mc The ManagedConnection to send the query along * @return false if the query was not sent, true if so */ public boolean originateQuery(QueryRequest query, ManagedConnection mc) { if( query == null ) throw new NullPointerException("null query"); if( mc == null ) throw new NullPointerException("null connection"); mc.originateQuery(query); return true; } /** * Checks if the passed set of domains contains only * default unauthenticated domain * @param domains Set (of String) of domains to be tested * @return true if the passed set of domains contains only * default unauthenticated domain, false otherwise */ private static boolean containsDefaultUnauthenticatedDomainOnly(Set domains) { //check if the set contains only one entry, and that entry is the //default unauthenticated domain if((domains.size() == 1) && domains.contains( User.DEFAULT_UNAUTHENTICATED_DOMAIN)) return true; else return false; } /** * Respond to the ping request. Implementations typically will either * do nothing (if they don't think a response is appropriate) or call * sendPingReply(PingReply). * This method is called from the default handlePingRequest. */ protected abstract void respondToPingRequest(PingRequest request, ReplyHandler handler); /** * Responds to a ping received over UDP -- implementations * handle this differently from pings received over TCP, as it is * assumed that the requester only wants pongs from other nodes * that also support UDP messaging. * * @param request the <tt>PingRequest</tt> to service * @param addr the <tt>InetSocketAddress</tt> containing the ping * @param handler the <tt>ReplyHandler</tt> instance from which the * ping was received and to which pongs should be sent */ protected abstract void respondToUDPPingRequest(PingRequest request, InetSocketAddress addr, ReplyHandler handler); /** * The default handler for PingRequests received in * ManagedConnection.loopForMessages(). This implementation * uses the ping route table to route a ping reply. If an appropriate route * doesn't exist, records the error statistics. On sucessful routing, * the PingReply count is incremented.<p> * * In all cases, the ping reply is recorded into the host catcher.<p> * * Override as desired, but you probably want to call super.handlePingReply * if you do. */ protected void handlePingReply(PingReply reply, ReplyHandler handler) { //update hostcatcher (even if the reply isn't for me) RouterService.getHostCatcher().add(reply); } /** * The default handler for QueryReplies received in * ManagedConnection.loopForMessages(). This implementation * uses the query route table to route a query reply. If an appropriate * route doesn't exist, records the error statistics. On sucessful routing, * the QueryReply count is incremented.<p> * * Override as desired, but you probably want to call super.handleQueryReply * if you do. This is public for testing purposes. */ public void handleQueryReply(QueryReply queryReply, ReplyHandler handler) { if(queryReply == null) { throw new NullPointerException("null query reply"); } if(handler == null) { throw new NullPointerException("null ReplyHandler"); } //For flow control reasons, we keep track of the bytes routed for this //GUID. Replies with less volume have higher priorities (i.e., lower //numbers). RouteTable.ReplyRoutePair rrp = _queryRouteTable.getReplyHandler(queryReply.getGUID(), queryReply.getTotalLength(), queryReply.getResultCount()); if(rrp != null) { queryReply.setPriority(rrp.getBytesRouted()); // Prepare a routing for a PushRequest, which works // here like a QueryReplyReply // Note the use of getClientGUID() here, not getGUID() _pushRouteTable.routeReply(queryReply.getClientGUID(), handler); //Simple flow control: don't route this message along other //connections if we've already routed too many replies for this //GUID. Note that replies destined for me all always delivered to //the GUI. ReplyHandler rh = rrp.getReplyHandler(); if(!shouldDropReply(rrp, rh, queryReply)) { rh.handleQueryReply(queryReply, handler); } else { handler.countDroppedMessage(); } } else { handler.countDroppedMessage(); } } /** * Checks if the <tt>QueryReply</tt> should be dropped for various reasons. * * Reason 1) The reply has already routed enough traffic. Based on per-TTL * hard limits for the number of bytes routed for the given reply guid. * This algorithm favors replies that don't have as far to go on the * network -- i.e., low TTL hits have more liberal limits than high TTL * hits. This ensures that hits that are closer to the query originator * -- hits for which we've already done most of the work, are not * dropped unless we've routed a really large number of bytes for that * guid. This method also checks that hard number of results that have * been sent for this GUID. If this number is greater than a specified * limit, we simply drop the reply. * * Reason 2) The reply was meant for me -- DO NOT DROP. * * Reason 3) The TTL is 0, drop. * * @param rrp the <tt>ReplyRoutePair</tt> containing data about what's * been routed for this GUID * @param ttl the time to live of the query hit * @return <tt>true if the reply should be dropped, otherwise <tt>false</tt> */ private boolean shouldDropReply(RouteTable.ReplyRoutePair rrp, ReplyHandler rh, QueryReply qr) { int ttl = qr.getTTL(); // Reason 2 -- The reply is meant for me, do not drop it. if( rh == FOR_ME_REPLY_HANDLER ) return false; // Reason 3 -- drop if TTL is 0. if( ttl == 0 ) return true; // Reason 1 ... int resultsRouted = rrp.getResultsRouted(); // drop the reply if we've already sent more than the specified number // of results for this GUID if(resultsRouted > 100) return true; int bytesRouted = rrp.getBytesRouted(); // send replies with ttl above 2 if we've routed under 50K if(ttl > 2 && bytesRouted < 50 * 1024) return false; // send replies with ttl 1 if we've routed under 1000K if(ttl == 1 && bytesRouted < 200 * 1024) return false; // send replies with ttl 2 if we've routed under 333K if(ttl == 2 && bytesRouted < 100 * 1024) return false; // if none of the above conditions holds true, drop the reply return true; } /** * The default handler for PushRequests received in * ManagedConnection.loopForMessages(). This implementation * uses the push route table to route a push request. If an appropriate * route doesn't exist, records the error statistics. On sucessful routing, * the PushRequest count is incremented. * * Override as desired, but you probably want to call * super.handlePushRequest if you do. */ protected void handlePushRequest(PushRequest request, ReplyHandler handler) { if(request == null) { throw new NullPointerException("null request"); } if(handler == null) { throw new NullPointerException("null ReplyHandler"); } // Note the use of getClientGUID() here, not getGUID() ReplyHandler replyHandler = getPushHandler(request.getClientGUID()); if(replyHandler != null) replyHandler.handlePushRequest(request, handler); else { handler.countDroppedMessage(); } } /** * Returns the appropriate handler from the _pushRouteTable. * This enforces that requests for my clientGUID will return * FOR_ME_REPLY_HANDLER, even if it's not in the table. */ protected ReplyHandler getPushHandler(byte[] guid) { ReplyHandler replyHandler = _pushRouteTable.getReplyHandler(guid); if(replyHandler != null) return replyHandler; else if(Arrays.equals(_clientGUID, guid)) return FOR_ME_REPLY_HANDLER; else return null; } /** * Uses the ping route table to send a PingReply to the appropriate * connection. Since this is used for PingReplies orginating here, no * stats are updated. */ protected void sendPingReply(PingReply pong, ReplyHandler handler) { if(pong == null) { throw new NullPointerException("null pong"); } if(handler == null) { throw new NullPointerException("null reply handler"); } handler.handlePingReply(pong, null); } /** * Uses the query route table to send a QueryReply to the appropriate * connection. Since this is used for QueryReplies orginating here, no * stats are updated. * @throws IOException if no appropriate route exists. */ protected void sendQueryReply(QueryReply queryReply) throws IOException { if(queryReply == null) { throw new NullPointerException("null reply"); } //For flow control reasons, we keep track of the bytes routed for this //GUID. Replies with less volume have higher priorities (i.e., lower //numbers). RouteTable.ReplyRoutePair rrp = _queryRouteTable.getReplyHandler(queryReply.getGUID(), queryReply.getTotalLength(), queryReply.getResultCount()); if(rrp != null) { queryReply.setPriority(rrp.getBytesRouted()); rrp.getReplyHandler().handleQueryReply(queryReply, null); } else throw new IOException("no route for reply"); } /** * Uses the push route table to send a push request to the appropriate * connection. Since this is used for PushRequests orginating here, no * stats are updated. * @throws IOException if no appropriate route exists. */ public void sendPushRequest(PushRequest push) throws IOException { if(push == null) { throw new NullPointerException("null push"); } // Note the use of getClientGUID() here, not getGUID() ReplyHandler replyHandler = getPushHandler(push.getClientGUID()); if(replyHandler != null) replyHandler.handlePushRequest(push, FOR_ME_REPLY_HANDLER); else throw new IOException("no route for push"); } /** * Sends a push request to the multicast network. No lookups are * performed in the push route table, because the message will always * be broadcast to everyone. */ protected void sendMulticastPushRequest(PushRequest push) { if(push == null) { throw new NullPointerException("null push"); } // must have a TTL of 1 Assert.that(push.getTTL() == 1, "multicast push ttl not 1"); MulticastService.send(push); } /** * Abstract method for creating query hits. Subclasses must specify * how this list is created. * * @return a <tt>List</tt> of <tt>QueryReply</tt> instances */ protected abstract List createQueryReply(byte[] guid, byte ttl, long speed, Response[] res, byte[] clientGUID, boolean busy, boolean uploaded, boolean measuredSpeed, boolean isFromMcast, boolean shouldMarkForFWTransfer); /** * Utility method for checking whether or not the given connection * is able to pass QRP messages. * * @param c the <tt>Connection</tt> to check * @return <tt>true</tt> if this is a QRP-enabled connection, * otherwise <tt>false</tt> */ private static boolean isQRPConnection(Connection c) { if(c.isSupernodeClientConnection()) return true; if(c.isUltrapeerQueryRoutingConnection()) return true; return false; } /** * Adds the specified MessageListener for messages with this GUID. * You must manually unregister the listener. * * This works by replacing the necessary maps & lists, so that * notifying doesn't have to hold any locks. */ public void registerMessageListener(byte[] guid, MessageListener ml) { ml.registered(guid); synchronized(MESSAGE_LISTENER_LOCK) { Map listeners = new TreeMap(GUID.GUID_BYTE_COMPARATOR); listeners.putAll(_messageListeners); List all = (List)listeners.get(guid); if(all == null) { all = new ArrayList(1); all.add(ml); } else { List temp = new ArrayList(all.size() + 1); temp.addAll(all); all = temp; all.add(ml); } listeners.put(guid, Collections.unmodifiableList(all)); _messageListeners = Collections.unmodifiableMap(listeners); } } /** * Unregisters this MessageListener from listening to the GUID. * * This works by replacing the necessary maps & lists so that * notifying doesn't have to hold any locks. */ public void unregisterMessageListener(byte[] guid, MessageListener ml) { boolean removed = false; synchronized(MESSAGE_LISTENER_LOCK) { List all = (List)_messageListeners.get(guid); if(all != null) { all = new ArrayList(all); if(all.remove(ml)) { removed = true; Map listeners = new TreeMap(GUID.GUID_BYTE_COMPARATOR); listeners.putAll(_messageListeners); if(all.isEmpty()) listeners.remove(guid); else listeners.put(guid, Collections.unmodifiableList(all)); _messageListeners = Collections.unmodifiableMap(listeners); } } } if(removed) ml.unregistered(guid); } /** * Replies to a head ping sent from the given ReplyHandler. */ private void handleHeadPing(HeadPing ping, ReplyHandler handler) { GUID clientGUID = ping.getClientGuid(); ReplyHandler pingee; if(clientGUID != null) pingee = getPushHandler(clientGUID.bytes()); else pingee = FOR_ME_REPLY_HANDLER; // handle ourselves. //drop the ping if no entry for the given clientGUID if (pingee == null) return; //don't bother routing if this is intended for me. // TODO: Clean up ReplyHandler interface so we aren't // afraid to use it like it's intended. // That way, we can do pingee.handleHeadPing(ping) // and not need this anti-OO instanceof check. if (pingee instanceof ForMeReplyHandler) { } else { // Otherwise, remember who sent it and forward it on. //remember where to send the pong to. //the pong will have the same GUID as the ping. // Note that this uses the messageGUID, not the clientGUID _headPongRouteTable.routeReply(ping.getGUID(), handler); //and send off the routed ping pingee.reply(ping); } } /** * Handles a pong received from the given handler. */ private void handleHeadPong(HeadPong pong, ReplyHandler handler) { ReplyHandler forwardTo = _headPongRouteTable.getReplyHandler(pong.getGUID()); // TODO: Clean up ReplyHandler interface so we're not afraid // to use it correctly. // Ideally, we'd do forwardTo.handleHeadPong(pong) // instead of this instanceof check // if this pong is for me, process it as usual (not implemented yet) if (forwardTo != null && !(forwardTo instanceof ForMeReplyHandler)) { forwardTo.reply(pong); _headPongRouteTable.removeReplyHandler(forwardTo); } } private static class QueryResponseBundle { public final QueryRequest _query; public final Response[] _responses; public QueryResponseBundle(QueryRequest query, Response[] responses) { _query = query; _responses = responses; } } /** Can be run to invalidate out-of-band ACKs that we are waiting for.... */ private class Expirer implements Runnable { public void run() { try { Set toRemove = new HashSet(); synchronized (_outOfBandReplies) { Iterator keys = _outOfBandReplies.keySet().iterator(); while (keys.hasNext()) { GUID.TimedGUID currQB = (GUID.TimedGUID) keys.next(); if ((currQB != null) && (currQB.shouldExpire())) toRemove.add(currQB); } // done iterating through _outOfBandReplies, remove the // keys now... keys = toRemove.iterator(); while (keys.hasNext()) _outOfBandReplies.remove(keys.next()); } } catch(Throwable t) { ErrorService.error(t); } } } /** This is run to clear out the registry of connect back attempts... * Made package access for easy test access. */ static class ConnectBackExpirer implements Runnable { public void run() { try { _tcpConnectBacks.clear(); _udpConnectBacks.clear(); } catch(Throwable t) { ErrorService.error(t); } } } static class HopsFlowManager implements Runnable { /* in case we don't want any queries any more */ private static final byte BUSY_HOPS_FLOW = 0; /* in case we want to reenable queries */ private static final byte FREE_HOPS_FLOW = 5; /* small optimization: send only HopsFlowVendorMessages if the busy state changed */ private static boolean _oldBusyState = false; public void run() { // busy hosts don't want to receive any queries, if this node is not // busy, we need to reset the HopsFlow value boolean isBusy = true; // state changed? don't bother the ultrapeer with information // that it already knows. we need to inform new ultrapeers, though. final List connections = _manager.getInitializedConnections(); final HopsFlowVendorMessage hops = new HopsFlowVendorMessage(isBusy ? BUSY_HOPS_FLOW : FREE_HOPS_FLOW); if (isBusy == _oldBusyState) { for (int i = 0; i < connections.size(); i++) { ManagedConnection c = (ManagedConnection)connections.get(i); // Yes, we may tell a new ultrapeer twice, but // without a buffer of some kind, we might forget // some ultrapeers. The clean solution would be // to remember the hops-flow value in the connection. if (c != null && c.getConnectionTime() + 1.25 * HOPS_FLOW_INTERVAL > System.currentTimeMillis() && c.isClientSupernodeConnection() ) c.send(hops); } } else { _oldBusyState = isBusy; for (int i = 0; i < connections.size(); i++) { ManagedConnection c = (ManagedConnection)connections.get(i); if (c != null && c.isClientSupernodeConnection()) c.send(hops); } } } } }