package com.limegroup.gnutella.bootstrap;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.io.*;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.net.InetAddress;
import java.beans.PropertyChangeSupport;
import java.beans.XMLDecoder;
import java.beans.XMLEncoder;
import ants.p2p.filesharing.WarriorAnt;
import ants.p2p.query.ServerInfo;
import ants.p2p.utils.addresses.InetAddressEngine;
import com.limegroup.gnutella.http.*;
/**
* A list of GWebCache servers. Provides methods to fetch address addresses
* from these servers, find the addresses of more such servers, and update the
* addresses of these and other servers.<p>
*
* Information on the GWebCache protocol can be found at
* http://zero-g.net/gwebcache/specs.html
*/
public class BootstrapServerManager {
PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this);
public static final File bootstrapServers = new File(WarriorAnt.workingPath + "gwebcacheservers.dat");
private static final Log LOG =
LogFactory.getLog(BootstrapServerManager.class);
/**
* Constant instance of the boostrap server.
*/
private static final BootstrapServerManager INSTANCE =
new BootstrapServerManager();
// Constants used as return values for fetchEndpointsAsync
/**
* GWebCache use is turned off.
*/
public static final int CACHE_OFF = 0;
/**
* A fetch was scheduled.
*/
public static final int FETCH_SCHEDULED = 1;
/**
* The fetch wasn't scheduled because one is in progress.
*/
public static final int FETCH_IN_PROGRESS = 2;
/**
* Too many endpoints were already fetch, the fetch wasn't scheduled.
*/
public static final int FETCHED_TOO_MANY = 3;
/**
* All caches were already contacted atleast once.
*/
public static final int NO_CACHES_LEFT = 4;
/**
* The maximum amount of responses to accept before we tell
* the user that we've already hit a lot of things.
*/
private static final int MAX_RESPONSES = 20;
/**
* The maximum amount of gWebCaches to hit before we tell
* the user that we've already hit a lot of things.
*/
private static final int MAX_CACHES = 2;
/** The minimum number of endpoints/urls to fetch at a time. */
private static final int ENDPOINTS_TO_ADD=50;
/** The maximum number of bootstrap servers to retain in memory. */
private static final int MAX_BOOTSTRAP_SERVERS=1000;
/** The maximum number of hosts to try per request. Prevents us from
* consuming all hosts if disconnected. Non-final for testing. */
public static int MAX_HOSTS_PER_REQUEST=5;
/** The amount of time in milliseconds between update requests.
* Public and non-final for testing purposes. */
public static int UPDATE_DELAY_MSEC=60*60*1000;
/**
* The bounded-size list of GWebCache servers, each as a BootstrapServer.
* Order doesn't matter; hosts are chosen randomly from this. Eventually
* this may be prioritized by some metric.
* LOCKING: this
* INVARIANT: _servers.size()<MAX_BOOTSTRAP_SERVERS
*/
private final List /* of BootstrapServer */ SERVERS=new ArrayList();
/** The last bootstrap server we successfully connected to, or null if none.
* Used for sending updates. _lastConnectable will generally be in
* SERVERS, though this is not strictly required because of SERVERS'
* random replacement strategy. _lastConnectable should be nulled if we
* later unsuccessfully try to reconnect to it. */
private BootstrapServer _lastConnectable;
/** Source of randomness for picking servers.
* TODO: this is thread-safe, right? */
private Random _rand=new Random();
/** True if a thread is currently executing a hostfile request.
* LOCKING: this (don't want multiple fetches) */
private volatile boolean _hostFetchInProgress=false;
/**
* The index of the last server we connected to in the list
* of servers.
*/
private volatile int _lastIndex = 0;
/**
* The total amount of endpoints we've added to HostCatcher so far.
*/
private volatile int _responsesAdded = 0;
/**
* Accessor for the <tt>BootstrapServerManager</tt> instance.
*
* @return the <tt>BootstrapServerManager</tt> instance
*/
public static BootstrapServerManager instance() {
return INSTANCE;
}
/**
* Creates a new <tt>BootstrapServerManager</tt>. Protected for testing.
*/
protected BootstrapServerManager() {
if(bootstrapServers.exists()){
try{
XMLDecoder ois = new XMLDecoder(new FileInputStream(
bootstrapServers));
Object servers = ois.readObject();
if(servers != null){
SERVERS.clear();
SERVERS.addAll((List) servers);
}
ois.close();
}catch(Exception e){}
}
this.propertyChangeSupport.addPropertyChangeListener(InetAddressEngine.getInstance());
}
private void storeServers(){
try {
XMLEncoder oos = new XMLEncoder(new FileOutputStream(
bootstrapServers));
oos.writeObject(SERVERS);
oos.close();
}
catch (Exception e) {}
}
/**
* Adds server to this.
*/
public synchronized void addBootstrapServer(BootstrapServer server) {
if(server == null)
throw new NullPointerException("null bootstrap server not allowed");
if (!SERVERS.contains(server)){
SERVERS.add(server);
if (SERVERS.size() > MAX_BOOTSTRAP_SERVERS) {
removeServer( (BootstrapServer) SERVERS.get(0));
}
this.storeServers();
}
}
/**
* Notification that all bootstrap servers have been added.
*/
public synchronized void bootstrapServersAdded() {
addDefaultsIfNeeded();
Collections.shuffle(SERVERS);
}
/**
* Resets information related to the caches & endpoints we've fetched.
*/
public synchronized void resetData() {
_lastIndex = 0;
_responsesAdded = 0;
Collections.shuffle(SERVERS);
}
/**
* Determines whether or not an endpoint fetch is in progress.
*/
public boolean isEndpointFetchInProgress() {
return _hostFetchInProgress;
}
/**
* Returns an iterator of the bootstrap servers in this, each as a
* BootstrapServer, in any order. To prevent ConcurrentModification
* problems, the caller should hold this' lock while using the
* iterator.
* @return an Iterator of BootstrapServer.
*/
public synchronized Iterator /*of BootstrapServer*/ getBootstrapServers() {
return SERVERS.iterator();
}
/**
* Asynchronously fetches other bootstrap URLs and stores them in this.
* Stops after getting "enough" endpoints or exhausting all caches. Uses
* the "urlfile=1" message.
*/
public synchronized void fetchBootstrapServersAsync() {
/*if(!ConnectionSettings.USE_GWEBCACHE.getValue()) return;*/
addDefaultsIfNeeded();
requestAsync(new UrlfileRequest(), "GWebCache urlfile");
}
/**
* Asynchronously fetches host addresses from bootstrap servers and stores
* them in the HostCatcher. Stops after getting "enough" endpoints or
* exhausting all caches. Does nothing if another endpoint request is in
* progress. Uses the "hostfile=1" message.
*/
public synchronized int fetchEndpointsAsync() {
/*if(!ConnectionSettings.USE_GWEBCACHE.getValue())
return CACHE_OFF;*/
addDefaultsIfNeeded();
if (! _hostFetchInProgress) {
if(_responsesAdded >= MAX_RESPONSES && _lastIndex >= MAX_CACHES)
return FETCHED_TOO_MANY;
if(_lastIndex >= size())
return NO_CACHES_LEFT;
_hostFetchInProgress=true; //unset in HostfileRequest.done()
requestAsync(new HostfileRequest(), "GWebCache hostfile");
return FETCH_SCHEDULED;
}
return FETCH_IN_PROGRESS;
}
/**
* Asynchronously fetches host addresses from bootstrap servers and stores
* them in the HostCatcher. Stops after getting "enough" endpoints or
* exhausting all caches. Does nothing if another endpoint request is in
* progress. Uses the "hostfile=1" message.
*/
public synchronized int fetchEndpointsAsyncV2() {
/*if(!ConnectionSettings.USE_GWEBCACHE.getValue())
return CACHE_OFF;*/
addDefaultsIfNeeded();
if (! _hostFetchInProgress) {
if(_responsesAdded >= MAX_RESPONSES && _lastIndex >= MAX_CACHES)
return FETCHED_TOO_MANY;
if(_lastIndex >= size())
return NO_CACHES_LEFT;
_hostFetchInProgress=true; //unset in HostfileRequest.done()
requestAsync(new V2HostsRequest(), "GWebCache hostfile v2");
return FETCH_SCHEDULED;
}
return FETCH_IN_PROGRESS;
}
/**
* Asynchronously sends an update message to a cache. May do nothing if
* nothing to update. Uses the "url" and "ip" messages.
*
* @param myIP my listening address and port
* @throws <tt>NullPointerException</tt> if the ip param is <tt>null</tt>
*/
public synchronized void sendUpdatesAsync(ServerInfo myIP) {
if(myIP == null)
throw new NullPointerException("cannot accept null update IP");
addDefaultsIfNeeded();
//For now we only send updates if the "ip=" parameter is null,
//regardless of whether we have a url.
try {
if (ants.p2p.gui.ConnectionAntPanel.isInternetPublicAddress(myIP.getAddress()))
requestAsync(new UpdateRequest(myIP), "GWebCache update");
} catch(Exception ignored) {}
}
/**
* Asynchronously sends an update message to a cache. May do nothing if
* nothing to update. Uses the "url" and "ip" messages.
*
* @param myIP my listening address and port
* @throws <tt>NullPointerException</tt> if the ip param is <tt>null</tt>
*/
public synchronized void sendUpdatesAsyncV2(ServerInfo myIP) {
if(myIP == null)
throw new NullPointerException("cannot accept null update IP");
addDefaultsIfNeeded();
//For now we only send updates if the "ip=" parameter is null,
//regardless of whether we have a url.
try {
if (ants.p2p.gui.ConnectionAntPanel.isInternetPublicAddress(myIP.getAddress()))
requestAsync(new V2UpdateRequest(myIP), "GWebCache update v2");
} catch(Exception ignored) {}
}
/**
* Adds default bootstrap servers to this if this needs more entries.
*/
private void addDefaultsIfNeeded() {
if (SERVERS.size()>0)
return;
DefaultBootstrapServers.addDefaults(this);
Collections.shuffle(SERVERS);
}
/////////////////////////// Request Types ////////////////////////////////
private abstract class GWebCacheRequest {
/** Returns the parameters for the given request, minus the "?" and any
* leading or trailing "&". These will be appended after common
* parameters (e.g, "client"). */
protected abstract String parameters();
/** Called when if were unable to connect to the URL, got a non-standard
* HTTP response code, or got an ERROR method. Default value: remove
* it from the list. */
protected void handleError(BootstrapServer server) {
if(LOG.isWarnEnabled())
LOG.warn("Error on server: " + server);
//For now, we just remove the host.
//Eventually we put it on probation.
synchronized (BootstrapServerManager.this) {
removeServer(server);
if (_lastConnectable==server)
_lastConnectable=null;
}
}
/** Called when we got a line of data. Implementation may wish
* to call handleError if the data is in a bad format.
* @return false if there was an error processing, true otherwise.
*/
protected abstract boolean handleResponseData(BootstrapServer server,
String line);
/** Should we go on to another host? */
protected abstract boolean needsMoreData();
/** The next server to contact */
protected abstract BootstrapServer nextServer();
/** Called when this is done. Default: does nothing. */
protected void done() { }
}
private final class HostfileRequest extends GWebCacheRequest {
private int responses=0;
protected String parameters() {
return "hostfile=1";
}
protected boolean handleResponseData(BootstrapServer server,
String line) {
try {
//Only accept numeric addresses. (An earlier version of this
//did not do strict checking, possibly resulting in HTML in the
//gnutella.net file!)
ServerInfo si = null;
final int DEFAULT = 443;
int j = line.indexOf(":");
if (j < 0) {
si = new ServerInfo("",line, new Integer(DEFAULT),"");
}
else if (j == 0) {
throw new IllegalArgumentException();
}
else if (j == (line.length() - 1)) {
si = new ServerInfo("",line.substring(0, j), new Integer(DEFAULT),"");
}
else {
String hostname = line.substring(0, j);
int port = DEFAULT;
try {
port = Integer.parseInt(line.substring(j + 1));
}
catch (NumberFormatException e) {
throw new IllegalArgumentException();
}
if (port < 0 || port > 65535) {
throw new IllegalArgumentException("invalid port");
}
si = new ServerInfo("",hostname, new Integer(port),"");
}
ArrayList al = new ArrayList();
al.add(si);
BootstrapServerManager.instance().propertyChangeSupport.firePropertyChange("inetAddressQueryCompleted", null, al);
responses++;
_responsesAdded++;
} catch (IllegalArgumentException bad) {
//One strike and you're out; skip servers that send bad data.
handleError(server);
return false;
}
return true;
}
protected boolean needsMoreData() {
return responses<ENDPOINTS_TO_ADD;
}
protected void done() {
_hostFetchInProgress=false;
}
/**
* Fetches the next server in line.
*/
protected BootstrapServer nextServer() {
BootstrapServer e = null;
synchronized (this) {
if(_lastIndex >= SERVERS.size()) {
if(LOG.isWarnEnabled())
LOG.warn("Used up all servers, last: " + _lastIndex);
} else {
e = (BootstrapServer)SERVERS.get(_lastIndex);
_lastIndex++;
}
}
return e;
}
public String toString() {
return "hostfile request";
}
}
private final class V2UpdateRequest extends GWebCacheRequest {
private boolean gotResponse=false;
private ServerInfo myIP;
/** @param ip my ip address, or null if this can't accept incoming
* connections. */
protected V2UpdateRequest(ServerInfo myIP) {
this.myIP=myIP;
}
protected String parameters() {
//The url of good server. There's a small chance that we send a
//host its own address. TODO: the encoding method we use is
//deprecated because it doesn't take care of character conversion
//properly. What to do?
String urlPart = null;
if (_lastConnectable != null)
urlPart = "url=" +
URLEncoder.encode(_lastConnectable.getURLString());
//My ip address as a parameter.
String ipPart = null;
if (myIP != null)
ipPart = "ip="+myIP.getAddress().getHostAddress()+":"+myIP.getPort();
//Some of these case are disallowed by sendUpdatesAsync, but we
//handle all of them here.
if (urlPart==null && ipPart==null)
return "";
else if (urlPart != null && ipPart == null)
return "update=1&net="+ants.p2p.Ant.getNetName()+"&"+urlPart;
else if (urlPart==null && ipPart!=null)
return "update=1&net="+ants.p2p.Ant.getNetName()+"&"+ipPart;
else {
return "update=1&net="+ants.p2p.Ant.getNetName()+"&"+ipPart+"&"+urlPart;
}
}
protected boolean handleResponseData(BootstrapServer server,
String line) {
if (BootstrapServer.startsWithIgnoreCase(line, "I|update|OK"))
gotResponse=true;
return true;
}
protected boolean needsMoreData() {
return !gotResponse;
}
protected BootstrapServer nextServer() {
if(SERVERS.size() == 0)
return null;
else
return (BootstrapServer)SERVERS.get(randomServer());
}
public String toString() {
return "update request";
}
}
private final class V2HostsRequest extends GWebCacheRequest {
private int responses=0;
protected String parameters() {
return "get=1&net="+ants.p2p.Ant.getNetName();
}
protected boolean handleResponseData(BootstrapServer server,
String completeLine) {
try {
//Only accept numeric addresses. (An earlier version of this
//did not do strict checking, possibly resulting in HTML in the
//gnutella.net file!)
String[] splittedLine = completeLine.split("[|]");
if(splittedLine[0].equalsIgnoreCase("H")){
String line = splittedLine[1];
ServerInfo si = null;
final int DEFAULT = 443;
int j = line.indexOf(":");
if (j < 0) {
si = new ServerInfo("", line, new Integer(DEFAULT), "");
}
else if (j == 0) {
throw new IllegalArgumentException();
}
else if (j == (line.length() - 1)) {
si = new ServerInfo("", line.substring(0, j), new Integer(DEFAULT),
"");
}
else {
String hostname = line.substring(0, j);
int port = DEFAULT;
try {
port = Integer.parseInt(line.substring(j + 1));
}
catch (NumberFormatException e) {
throw new IllegalArgumentException();
}
if (port < 0 || port > 65535) {
throw new IllegalArgumentException("invalid port");
}
si = new ServerInfo("", hostname, new Integer(port), "");
}
ArrayList al = new ArrayList();
al.add(si);
BootstrapServerManager.instance().propertyChangeSupport.
firePropertyChange("inetAddressQueryCompleted", null, al);
responses++;
_responsesAdded++;
}
else if(splittedLine[0].equalsIgnoreCase("U")){
/*try {
String line = splittedLine[1];
BootstrapServer e = new BootstrapServer(line);
//Ensure url in this. If list is too big, remove an
//element. Eventually we may remove "worst" element.
synchronized (BootstrapServerManager.this) {
addBootstrapServer(e);
}
responses++;
if (LOG.isDebugEnabled())
LOG.debug("Added bootstrap host: " + e);
}
catch (ParseException error) {
//One strike and you're out; skip servers that send bad data.
handleError(server);
return false;
}*/
}
} catch (IllegalArgumentException bad) {
//One strike and you're out; skip servers that send bad data.
handleError(server);
return false;
}
return true;
}
protected boolean needsMoreData() {
return responses<ENDPOINTS_TO_ADD;
}
protected void done() {
_hostFetchInProgress=false;
}
/**
* Fetches the next server in line.
*/
protected BootstrapServer nextServer() {
BootstrapServer e = null;
synchronized (this) {
if(_lastIndex >= SERVERS.size()) {
if(LOG.isWarnEnabled())
LOG.warn("Used up all servers, last: " + _lastIndex);
} else {
e = (BootstrapServer)SERVERS.get(_lastIndex);
_lastIndex++;
}
}
return e;
}
public String toString() {
return "hostfile request";
}
}
private final class UrlfileRequest extends GWebCacheRequest {
private int responses=0;
protected String parameters() {
return "urlfile=1";
}
protected boolean handleResponseData(BootstrapServer server,
String line) {
try {
BootstrapServer e=new BootstrapServer(line);
//Ensure url in this. If list is too big, remove an
//element. Eventually we may remove "worst" element.
synchronized (BootstrapServerManager.this) {
addBootstrapServer(e);
}
responses++;
if(LOG.isDebugEnabled())
LOG.debug("Added bootstrap host: " + e);
} catch (ParseException error) {
//One strike and you're out; skip servers that send bad data.
handleError(server);
return false;
}
return true;
}
protected boolean needsMoreData() {
return responses<ENDPOINTS_TO_ADD;
}
protected BootstrapServer nextServer() {
if(SERVERS.size() == 0)
return null;
else
return (BootstrapServer)SERVERS.get(randomServer());
}
public String toString() {
return "urlfile request";
}
}
private final class UpdateRequest extends GWebCacheRequest {
private boolean gotResponse=false;
private ServerInfo myIP;
/** @param ip my ip address, or null if this can't accept incoming
* connections. */
protected UpdateRequest(ServerInfo myIP) {
this.myIP=myIP;
}
protected String parameters() {
//The url of good server. There's a small chance that we send a
//host its own address. TODO: the encoding method we use is
//deprecated because it doesn't take care of character conversion
//properly. What to do?
String urlPart = null;
if (_lastConnectable != null)
urlPart = "url=" +
URLEncoder.encode(_lastConnectable.getURLString());
//My ip address as a parameter.
String ipPart = null;
if (myIP != null)
ipPart = "ip="+myIP.getAddress().getHostAddress()+":"+myIP.getPort();
//Some of these case are disallowed by sendUpdatesAsync, but we
//handle all of them here.
if (urlPart==null && ipPart==null)
return "";
else if (urlPart != null && ipPart == null)
return urlPart;
else if (urlPart==null && ipPart!=null)
return ipPart;
else {
return ipPart+"&"+urlPart;
}
}
protected boolean handleResponseData(BootstrapServer server,
String line) {
if (BootstrapServer.startsWithIgnoreCase(line, "OK"))
gotResponse=true;
return true;
}
protected boolean needsMoreData() {
return !gotResponse;
}
protected BootstrapServer nextServer() {
if(SERVERS.size() == 0)
return null;
else
return (BootstrapServer)SERVERS.get(randomServer());
}
public String toString() {
return "update request";
}
}
///////////////////////// Generic Request Functions //////////////////////
/** @param threadName a name for the thread created, for debugging */
private void requestAsync(final GWebCacheRequest request,
String threadName) {
if(request == null) {
throw new NullPointerException("asynchronous request to null cache");
}
Thread runner=new Thread() {
public void run() {
try {
requestBlocking(request);
} catch (Throwable e) {
//Internal error! Display to GUI for debugging.
e.printStackTrace();
} finally {
request.done();
}
}
};
runner.setName(threadName);
runner.setDaemon(true);
runner.start();
}
private void requestBlocking(GWebCacheRequest request) {
if(request == null) {
throw new NullPointerException("blocking request to null cache");
}
ArrayList alreadyContacted = new ArrayList();
for (int i=0; request.needsMoreData() && i<MAX_HOSTS_PER_REQUEST; i++) {
BootstrapServer e = request.nextServer();
if(e == null)
break;
else if(!alreadyContacted.contains(e)){
requestFromOneHost(request, e);
alreadyContacted.add(e);
}
}
}
private void requestFromOneHost(GWebCacheRequest request,
BootstrapServer server) {
if(request == null) {
throw new NullPointerException("null cache in request to one host");
}
if(server == null) {
throw new NullPointerException("null server in request to one host");
}
if(LOG.isTraceEnabled())
LOG.trace("requesting: " + request + " from " + server);
BufferedReader in = null;
String urlString = server.getURLString();
String connectTo = urlString
+"?client="+ants.p2p.Ant.getApplicationName()
+"&version="+URLEncoder.encode(ants.p2p.Ant.getVersion())
+"&"+request.parameters();
// add the guid if it's our cache, so we can see if we're hammering
// from a single client, or if it's a bunch of clients behind a NAT
HttpMethod get;
try {
get = new GetMethod(connectTo);
} catch(IllegalArgumentException iae) {
LOG.warn("Invalid server", iae);
// invalid uri? begone.
request.handleError(server);
return;
}
get.addRequestHeader("Cache-Control", "no-cache");
get.addRequestHeader("User-Agent", "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:0.9.3) Gecko/20010801");//sun.net.www.protocol.http.HttpURLConnection.userAgent);
get.addRequestHeader(HTTPHeaderName.CONNECTION.httpStringValue(), "close");
get.setFollowRedirects(false);
HttpClient client = HttpClientManager.getNewClient(30*1000, 10*1000);
if(ants.p2p.Ant.proxied){
String tunnelHost = System.getProperty("https.proxyHost");
int tunnelPort = Integer.getInteger("https.proxyPort").intValue();
client.getHostConfiguration().setProxy(tunnelHost, tunnelPort);
}
try {
HttpClientManager.executeMethodRedirecting(client, get);
InputStream is = get.getResponseBodyAsStream();
if(is == null) {
if(LOG.isWarnEnabled()) {
LOG.warn("Invalid server: "+server);
}
// invalid uri? begone.
request.handleError(server);
return;
}
in = new BufferedReader(new InputStreamReader(is));
if(get.getStatusCode() < 200 || get.getStatusCode() >= 300) {
if(LOG.isWarnEnabled())
LOG.warn("Invalid status code: " + get.getStatusCode());
throw new IOException("no 2XX ok.");
}
//For each line of data (excludes HTTP headers)...
boolean firstLine = true;
boolean errors = false;
while (true) {
String line = in.readLine();
if (line == null)
break;
// if(LOG.isTraceEnabled())
// LOG.trace("<< " + line);
if (firstLine && BootstrapServer.startsWithIgnoreCase(line,"ERROR")){
request.handleError(server);
errors = true;
} else {
boolean retVal = request.handleResponseData(server, line);
if (!errors) errors = !retVal;
}
firstLine = false;
}
//If no errors, record the address AFTER sending requests so we
//don't send a host its own url in update requests.
if (!errors)
_lastConnectable = server;
} catch (IOException ioe) {
LOG.warn("Exception while handling server", ioe);
request.handleError(server);
} finally {
// release the connection.
if (get != null) {
get.releaseConnection();
get.abort();
}
}
}
/** Returns the number of servers in this. */
protected synchronized int size() {
return SERVERS.size();
}
/** Returns an random valid index of SERVERS. Protected so we can override
* in test cases. PRECONDITION: SERVERS.size>0. */
protected int randomServer() {
return _rand.nextInt(SERVERS.size());
}
/**
* Removes the server.
*/
protected synchronized void removeServer(BootstrapServer server) {
SERVERS.remove(server);
_lastIndex = Math.max(0, _lastIndex - 1);
}
}