package org.jgroups.stack; import org.jgroups.Address; import org.jgroups.PhysicalAddress; import org.jgroups.protocols.PingData; import org.jgroups.annotations.ManagedAttribute; import org.jgroups.annotations.ManagedOperation; import org.jgroups.annotations.Property; import org.jgroups.jmx.JmxConfigurator; import org.jgroups.logging.Log; import org.jgroups.logging.LogFactory; import org.jgroups.util.*; import org.jgroups.util.UUID; import javax.management.MBeanServer; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.EOFException; import java.io.IOException; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketTimeoutException; import java.util.*; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicBoolean; /** * Router for TCP based group comunication (using layer TCP instead of UDP). Instead of the TCP * layer sending packets point-to-point to each other member, it sends the packet to the router * which - depending on the target address - multicasts or unicasts it to the group / or single member. * <p/> * This class is especially interesting for applets which cannot directly make connections (neither * UDP nor TCP) to a host different from the one they were loaded from. Therefore, an applet would * create a normal channel plus protocol stack, but the bottom layer would have to be the TCP layer * which sends all packets point-to-point (over a TCP connection) to the router, which in turn * forwards them to their end location(s) (also over TCP). A centralized router would therefore have * to be running on the host the applet was loaded from. * <p/> * An alternative for running JGroups in an applet (IP multicast is not allows in applets as of * 1.2), is to use point-to-point UDP communication via the gossip server. However, then the appplet * has to be signed which involves additional administrative effort on the part of the user. * <p/> * Note that a GossipRouter is also a good way of running JGroups in Amazon's EC2 environment which (as of summer 09) * doesn't support IP multicasting. * @author Bela Ban * @author Vladimir Blagojevic * @author Ovidiu Feodorov <ovidiuf@users.sourceforge.net> * @since 2.1.1 */ public class GossipRouter { public static final byte CONNECT=1; // CONNECT(group, addr) --> local address public static final byte DISCONNECT=2; // DISCONNECT(group, addr) public static final byte GOSSIP_GET=4; // GET(group) --> List<addr> (members) public static final byte MESSAGE=10; public static final byte SUSPECT=11; public static final byte PING=12; public static final byte CLOSE=13; public static final byte CONNECT_OK=14; public static final byte OP_FAIL=15; public static final byte DISCONNECT_OK=16; public static final int PORT=12001; @ManagedAttribute(description="server port on which the GossipRouter accepts client connections", writable=true) private int port; @ManagedAttribute(description="address to which the GossipRouter should bind", writable=true, name="bindAddress") private String bindAddressString; @ManagedAttribute(description="time (in msecs) until gossip entry expires", writable=true) private long expiryTime=0; // Maintains associations between groups and their members private final ConcurrentMap<String, ConcurrentMap<Address, ConnectionHandler>> routingTable=new ConcurrentHashMap<String, ConcurrentMap<Address, ConnectionHandler>>(); /** * Store physical address(es) associated with a logical address. Used mainly by TCPGOSSIP */ private final Map<Address, Set<PhysicalAddress>> address_mappings=new ConcurrentHashMap<Address,Set<PhysicalAddress>>(); private ServerSocket srvSock=null; private InetAddress bindAddress=null; @Property(description="Time (in ms) for setting SO_LINGER on sockets returned from accept(). 0 means do not set SO_LINGER") private long linger_timeout=2000L; @Property(description="Time (in ms) for SO_TIMEOUT on sockets returned from accept(). 0 means don't set SO_TIMEOUT") private long sock_read_timeout=0L; @Property(description="The max queue size of backlogged connections") private int backlog=1000; private final AtomicBoolean running = new AtomicBoolean(false); @ManagedAttribute(description="whether to discard message sent to self", writable=true) private boolean discard_loopbacks=false; protected List<ConnectionTearListener> connectionTearListeners=new CopyOnWriteArrayList<ConnectionTearListener>(); protected ThreadFactory default_thread_factory=new DefaultThreadFactory("gossip-handlers", true, true); protected Timer timer=null; protected final Log log=LogFactory.getLog(this.getClass()); private boolean jmx=false; private boolean registered=false; public GossipRouter() { this(PORT); } public GossipRouter(int port) { this(port, null); } public GossipRouter(int port, String bindAddressString) { this(port,bindAddressString,false,0); } public GossipRouter(int port, String bindAddressString, boolean jmx) { this(port, bindAddressString,jmx,0); } public GossipRouter(int port, String bindAddressString, boolean jmx, long expiryTime) { this.port = port; this.bindAddressString = bindAddressString; this.jmx = jmx; this.expiryTime = expiryTime; this.connectionTearListeners.add(new FailureDetectionListener()); } public void setPort(int port) { this.port=port; } public int getPort() { return port; } public void setBindAddress(String bindAddress) { bindAddressString=bindAddress; } public String getBindAddress() { return bindAddressString; } public int getBacklog() { return backlog; } public void setBacklog(int backlog) { this.backlog=backlog; } public void setExpiryTime(long expiryTime) { this.expiryTime = expiryTime; } public long getExpiryTime() { return expiryTime; } @ManagedAttribute(description="status") public boolean isStarted() { return isRunning(); } public boolean isDiscardLoopbacks() { return discard_loopbacks; } public void setDiscardLoopbacks(boolean discard_loopbacks) { this.discard_loopbacks=discard_loopbacks; } public long getLingerTimeout() { return linger_timeout; } public void setLingerTimeout(long linger_timeout) { this.linger_timeout=linger_timeout; } public long getSocketReadTimeout() { return sock_read_timeout; } public void setSocketReadTimeout(long sock_read_timeout) { this.sock_read_timeout=sock_read_timeout; } public ThreadFactory getDefaultThreadPoolThreadFactory() { return default_thread_factory; } public static String type2String(int type) { switch (type) { case CONNECT: return "CONNECT"; case DISCONNECT: return "DISCONNECT"; case GOSSIP_GET: return "GOSSIP_GET"; case MESSAGE: return "MESSAGE"; case SUSPECT: return "SUSPECT"; case PING: return "PING"; case CLOSE: return "CLOSE"; case CONNECT_OK: return "CONNECT_OK"; case DISCONNECT_OK: return "DISCONNECT_OK"; case OP_FAIL: return "OP_FAIL"; default: return "unknown (" + type + ")"; } } /** * Lifecycle operation. Called after create(). When this method is called, the managed attributes * have already been set.<br> * Brings the Router into a fully functional state. */ @ManagedOperation(description="Lifecycle operation. Called after create(). When this method is called, " + "the managed attributes have already been set. Brings the Router into a fully functional state.") public void start() throws Exception { if(running.compareAndSet(false, true)) { if(jmx && !registered) { MBeanServer server=Util.getMBeanServer(); JmxConfigurator.register(this, server, "jgroups:name=GossipRouter"); registered=true; } if(bindAddressString != null) { bindAddress=InetAddress.getByName(bindAddressString); srvSock=new ServerSocket(port, backlog, bindAddress); } else { srvSock=new ServerSocket(port, backlog); } Runtime.getRuntime().addShutdownHook(new Thread() { public void run() { GossipRouter.this.stop(); } }); // start the main server thread new Thread(new Runnable() { public void run() { mainLoop(); } }, "GossipRouter").start(); long expiryTime = getExpiryTime(); if (expiryTime > 0) { timer = new Timer(true); timer.schedule(new TimerTask() { public void run() { sweep(); } }, expiryTime, expiryTime); } } else { throw new Exception("Router already started."); } } /** * Always called before destroy(). Close connections and frees resources. */ @ManagedOperation(description="Always called before destroy(). Closes connections and frees resources") public void stop() { clear(); if(running.compareAndSet(true, false)){ Util.close(srvSock); if(log.isDebugEnabled()) log.debug("router stopped"); } } @ManagedOperation(description="Closes all connections and clears routing table (leave the server socket open)") public void clear() { if(running.get()) { for(ConcurrentMap<Address,ConnectionHandler> map: routingTable.values()) { for(ConnectionHandler ce: map.values()) ce.close(); } routingTable.clear(); } } public void destroy() { } @ManagedAttribute(description="operational status", name="running") public boolean isRunning() { return running.get(); } @ManagedOperation(description="dumps the contents of the routing table") public String dumpRoutingTable() { String label="routing"; StringBuilder sb=new StringBuilder(); if(routingTable.isEmpty()) { sb.append("empty ").append(label).append(" table"); } else { boolean first=true; for(Map.Entry<String, ConcurrentMap<Address, ConnectionHandler>> entry : routingTable.entrySet()) { String gname=entry.getKey(); if(!first) sb.append("\n"); else first=false; sb.append(gname + ": "); Map<Address,ConnectionHandler> map=entry.getValue(); if(map == null || map.isEmpty()) { sb.append("null"); } else { sb.append(Util.printListWithDelimiter(map.keySet(), ", ")); } } } return sb.toString(); } @ManagedOperation(description="dumps the contents of the routing table") public String dumpRoutingTableDetailed() { String label="routing"; StringBuilder sb=new StringBuilder(); if(routingTable.isEmpty()) { sb.append("empty ").append(label).append(" table"); } else { boolean first=true; for(Map.Entry<String, ConcurrentMap<Address, ConnectionHandler>> entry : routingTable.entrySet()) { String gname=entry.getKey(); if(!first) sb.append("\n"); else first=false; sb.append(gname + ":\n"); Map<Address,ConnectionHandler> map=entry.getValue(); if(map == null || map.isEmpty()) { sb.append("null"); } else { for(Map.Entry<Address,ConnectionHandler> en: map.entrySet()) { sb.append(en.getKey() + ": "); ConnectionHandler handler=en.getValue(); sb.append("sock=" +handler.sock).append("\n"); } } sb.append("\n"); } } return sb.toString(); } @ManagedOperation(description="dumps the mappings between logical and physical addresses") public String dumpAddresssMappings() { StringBuilder sb=new StringBuilder(); for(Map.Entry<Address,Set<PhysicalAddress>> entry: address_mappings.entrySet()) { sb.append(entry.getKey()).append(": ").append(entry.getValue()).append("\n"); } return sb.toString(); } private void mainLoop() { if(bindAddress == null) bindAddress=srvSock.getInetAddress(); printStartupInfo(); while(isRunning()) { Socket sock=null; try { sock=srvSock.accept(); if(linger_timeout > 0) { int linger=Math.max(1, (int)(linger_timeout / 1000)); sock.setSoLinger(true, linger); } if(sock_read_timeout > 0) sock.setSoTimeout((int)sock_read_timeout); if(log.isDebugEnabled()) log.debug("Accepted connection, socket is " + sock); ConnectionHandler ch=new ConnectionHandler(sock); getDefaultThreadPoolThreadFactory().newThread(ch).start(); } catch(IOException e) { //only consider this exception if GR is not shutdown if(isRunning()) { log.error("failure handling connection from " + sock, e); Util.close(sock); } } } } /** * Removes expired gossip entries (entries older than EXPIRY_TIME msec). * @since 2.2.1 */ private void sweep() { long diff, currentTime = System.currentTimeMillis(); List <ConnectionHandler> victims = new ArrayList<ConnectionHandler>(); for (Iterator<Entry<String, ConcurrentMap<Address, ConnectionHandler>>> it = routingTable.entrySet().iterator(); it.hasNext();) { Map<Address, ConnectionHandler> map = it.next().getValue(); if (map == null || map.isEmpty()) { it.remove(); continue; } for (Iterator<Entry<Address, ConnectionHandler>> it2 = map.entrySet().iterator(); it2.hasNext();) { ConnectionHandler ch = it2.next().getValue(); diff = currentTime - ch.timestamp; if (diff > expiryTime) { victims.add(ch); } } } for (ConnectionHandler v : victims) { v.close(); } } private void route(Address dest, String group, byte[] msg) { if(dest == null) { // send to all members in group if(group == null) { if(log.isErrorEnabled()) log.error("group is null"); } else { sendToAllMembersInGroup(group, msg); } } else { // send unicast ConnectionHandler handler=findAddressEntry(group, dest); if(handler == null) { if(log.isTraceEnabled()) log.trace("cannot find " + dest + " in the routing table, \nrouting table=\n" + dumpRoutingTable()); return; } if(handler.output == null) { if(log.isErrorEnabled()) log.error(dest + " is associated with a null output stream"); return; } try { sendToMember(dest, handler.output, msg); } catch(Exception e) { if(log.isErrorEnabled()) log.error("failed sending message to " + dest + ": " + e.getMessage()); removeEntry(group, dest); // will close socket } } } private void removeEntry(String group, Address addr) { // Remove from routing table ConcurrentMap<Address, ConnectionHandler> map; if(group != null) { map=routingTable.get(group); if(map != null && map.remove(addr) != null) { if(log.isTraceEnabled()) log.trace("Removed " +addr + " from group " + group); if(map.isEmpty()) { boolean removed=removeGroupIfEmpty(group); if(removed && log.isTraceEnabled()) log.trace("Removed group " + group); } } } else { for(Map.Entry<String,ConcurrentMap<Address,ConnectionHandler>> entry: routingTable.entrySet()) { map=entry.getValue(); if(map != null && map.remove(addr) != null && map.isEmpty()) { boolean removed=removeGroupIfEmpty(entry.getKey()); if(removed && log.isTraceEnabled()) log.trace("Removed " + entry.getKey() + " from group " + group); } } } address_mappings.remove(addr); UUID.remove(addr); } protected boolean removeGroupIfEmpty(String group) { if(group == null) return false; synchronized(routingTable) { ConcurrentMap<Address,ConnectionHandler> val=routingTable.get(group); if(val != null && val.isEmpty()) { routingTable.remove(group); return true; } return false; } } /** * @return null if not found */ private ConnectionHandler findAddressEntry(String group, Address addr) { if(group == null || addr == null) return null; ConcurrentMap<Address,ConnectionHandler> map=routingTable.get(group); if(map == null) return null; return map.get(addr); } private void sendToAllMembersInGroup(String group, byte[] msg) { final ConcurrentMap<Address,ConnectionHandler> map=routingTable.get(group); if(map == null || map.isEmpty()) { if(log.isWarnEnabled()) log.warn("didn't find any members for group " + group); return; } synchronized(map) { for(Map.Entry<Address,ConnectionHandler> entry: map.entrySet()) { ConnectionHandler handler=entry.getValue(); DataOutputStream dos=handler.output; if(dos != null) { try { sendToMember(null, dos, msg); } catch(Exception e) { if(log.isWarnEnabled()) log.warn("cannot send to " + entry.getKey() + ": " + e.getMessage()); } } } } } private static void sendToMember(Address dest, final DataOutputStream out, byte[] msg) throws Exception { if(out == null) return; synchronized(out) { GossipData request=new GossipData(GossipRouter.MESSAGE, null, dest, msg); request.writeTo(out); out.flush(); } } private void notifyAbnormalConnectionTear(final ConnectionHandler ch, final Exception e) { for (ConnectionTearListener l : connectionTearListeners) { l.connectionTorn(ch, e); } } public interface ConnectionTearListener { public void connectionTorn(ConnectionHandler ch, Exception e); } /* * https://jira.jboss.org/jira/browse/JGRP-902 */ class FailureDetectionListener implements ConnectionTearListener { public void connectionTorn(ConnectionHandler ch, Exception e) { Set<String> groups = ch.known_groups; for (String group : groups) { if(group == null) continue; Map<Address, ConnectionHandler> map = routingTable.get(group); if (map != null && !map.isEmpty()) { for (Iterator<Entry<Address, ConnectionHandler>> i = map.entrySet().iterator(); i.hasNext();) { ConnectionHandler entry = i.next().getValue(); DataOutputStream stream = entry.output; try { for (Address a : ch.logical_addrs) { GossipData suspect = new GossipData(GossipRouter.SUSPECT); suspect.writeTo(stream); Util.writeAddress(a, stream); stream.flush(); } } catch (Exception ioe) { // intentionally ignored } } } } } } /** * Prints startup information. */ private void printStartupInfo() { System.out.println("GossipRouter started at " + new Date()); System.out.print("Listening on port " + port); System.out.println(" bound on address " + bindAddress); System.out.print("Backlog is " + backlog); System.out.print(", linger timeout is " + linger_timeout); System.out.println(", and read timeout is " + sock_read_timeout); } /** * Handles the requests from a client (RouterStub) */ class ConnectionHandler implements Runnable { private final AtomicBoolean active = new AtomicBoolean(false); private final Socket sock; private final DataOutputStream output; private final DataInputStream input; private final List<Address> logical_addrs=new ArrayList<Address>(); Set<String> known_groups = new HashSet<String>(); private long timestamp; public ConnectionHandler(Socket sock) throws IOException { this.sock=sock; this.input=new DataInputStream(sock.getInputStream()); this.output=new DataOutputStream(sock.getOutputStream()); } void close() { if(active.compareAndSet(true, false)) { if(log.isDebugEnabled()) log.debug(this + " is being closed"); Util.close(input); Util.close(output); Util.close(sock); for(Address addr: logical_addrs) { removeEntry(null, addr); } } } public void run() { if(active.compareAndSet(false, true)) { try { if(log.isDebugEnabled()) log.debug(this + " entering receive loop"); readLoop(); } finally { close(); } } } public boolean isRunning() { return active.get(); } private void readLoop() { while(isRunning()) { GossipData request; Address addr; String group; try { request=new GossipData(); request.readFrom(input); byte command=request.getType(); addr=request.getAddress(); group=request.getGroup(); known_groups.add(group); timestamp = System.currentTimeMillis(); if(log.isTraceEnabled()) log.trace(this + " received " + request); switch(command) { case GossipRouter.CONNECT: handleConnect(request, addr, group); break; case GossipRouter.PING: // do nothing here - client doesn't expect response data break; case GossipRouter.MESSAGE: if(request.buffer == null || request.buffer.length == 0) { if(log.isWarnEnabled()) log.warn(this +" received null message"); break; } try { route(addr, request.getGroup(), request.getBuffer()); } catch(Exception e) { if(log.isErrorEnabled()) log.error(this +" failed in routing request to " + addr, e); } break; case GossipRouter.GOSSIP_GET: Set<PhysicalAddress> physical_addrs; List<PingData> mbrs=new ArrayList<PingData>(); ConcurrentMap<Address,ConnectionHandler> map=routingTable.get(group); if(map != null) { for(Address logical_addr: map.keySet()) { physical_addrs=address_mappings.get(logical_addr); PingData rsp=new PingData(logical_addr, null, true, UUID.get(logical_addr), physical_addrs != null? new ArrayList<PhysicalAddress>(physical_addrs) : null); mbrs.add(rsp); } } output.writeShort(mbrs.size()); for(PingData data: mbrs) data.writeTo(output); output.flush(); if(log.isDebugEnabled()) log.debug(this + " responded to GOSSIP_GET with " + mbrs); break; case GossipRouter.DISCONNECT: try { removeEntry(group, addr); sendData(new GossipData(DISCONNECT_OK)); if(log.isDebugEnabled()) log.debug(this + " disconnect completed"); } catch(Exception e) { sendData(new GossipData(OP_FAIL)); } break; case GossipRouter.CLOSE: close(); break; case -1: // EOF notifyAbnormalConnectionTear(this, new EOFException("Connection broken")); break; } if(log.isTraceEnabled()) log.trace(this + " processed " + request); } catch(SocketTimeoutException ste) { } catch(IOException ioex) { notifyAbnormalConnectionTear(this, ioex); break; } catch(Exception ex) { if (active.get()) { if (log.isWarnEnabled()) log.warn("Exception in ConnectionHandler thread", ex); } break; } } } private void handleConnect(GossipData request, Address addr, String group) throws Exception { try { checkExistingConnection(addr,group); String logical_name = request.getLogicalName(); if (logical_name != null && addr instanceof org.jgroups.util.UUID) org.jgroups.util.UUID.add(addr, logical_name); // group name, logical address, logical name, physical addresses (could be null) logical_addrs.add(addr); // allows us to remove the entries for this connection on socket close addGroup(group, addr, this); Set<PhysicalAddress> physical_addrs; if (request.getPhysicalAddresses() != null) { physical_addrs = address_mappings.get(addr); if (physical_addrs == null) { physical_addrs = new HashSet<PhysicalAddress>(); address_mappings.put(addr, physical_addrs); } physical_addrs.addAll(request.getPhysicalAddresses()); } sendStatus(CONNECT_OK); if(log.isDebugEnabled()) log.debug(this + " connection handshake completed, added " +addr + " to group "+ group); } catch (Exception e) { removeEntry(group, addr); sendStatus(OP_FAIL); throw new Exception("Unsuccessful connection setup handshake for " + this); } } protected void addGroup(String group, Address addr, ConnectionHandler handler) { if(group == null || handler == null) return; synchronized(routingTable) { ConcurrentMap<Address,ConnectionHandler> map=routingTable.get(group); if(map == null) { map=new ConcurrentHashMap<Address, ConnectionHandler>(); routingTable.put(group, map); } map.put(addr, this); } } private boolean checkExistingConnection(Address addr, String group) throws Exception { boolean isOldExists = false; if (address_mappings.containsKey(addr)) { ConcurrentMap<Address, ConnectionHandler> map = null; ConnectionHandler oldConnectionH = null; if (group != null) { map = routingTable.get(group); if (map != null) { oldConnectionH = map.get(addr); } } else { for (Map.Entry<String, ConcurrentMap<Address, ConnectionHandler>> entry : routingTable .entrySet()) { map = entry.getValue(); if (map != null) { oldConnectionH = map.get(addr); } } } if (oldConnectionH != null) { isOldExists = true; if (log.isDebugEnabled()) { log.debug("Found old connection[" + oldConnectionH + "] for addr[" + addr + "]. Closing old connection ..."); } oldConnectionH.close(); } else { if (log.isDebugEnabled()) { log.debug("No old connection for addr[" + addr + "] exists"); } } } return isOldExists; } private void sendStatus(byte status) { try { output.writeByte(status); output.flush(); } catch (IOException e1) { //ignored } } private void sendData(GossipData data) { try { data.writeTo(output); output.flush(); } catch (Exception e1) { //ignored } } public String toString() { StringBuilder sb=new StringBuilder(); sb.append("ConnectionHandler[peer: " + sock.getInetAddress()); if(!logical_addrs.isEmpty()) sb.append(", logical_addrs: " + Util.printListWithDelimiter(logical_addrs, ", ")); sb.append("]"); return sb.toString(); } } public static void main(String[] args) throws Exception { int port=12001; int backlog=0; long soLinger=-1; long soTimeout=-1; long expiry_time=0; GossipRouter router=null; String bind_addr=null; boolean jmx=true; for(int i=0; i < args.length; i++) { String arg=args[i]; if("-port".equals(arg)) { port=Integer.parseInt(args[++i]); continue; } if("-bindaddress".equals(arg) || "-bind_addr".equals(arg)) { bind_addr=args[++i]; continue; } if("-backlog".equals(arg)) { backlog=Integer.parseInt(args[++i]); continue; } if("-expiry".equals(arg)) { expiry_time=Long.parseLong(args[++i]); continue; } if("-jmx".equals(arg)) { jmx=Boolean.valueOf(args[++i]); continue; } // this option is not used and should be deprecated/removed in a future release if("-timeout".equals(arg)) { System.out.println(" -timeout is deprecated and will be ignored"); ++i; continue; } // this option is not used and should be deprecated/removed in a future release if("-rtimeout".equals(arg)) { System.out.println(" -rtimeout is deprecated and will be ignored"); ++i; continue; } if("-solinger".equals(arg)) { soLinger=Long.parseLong(args[++i]); continue; } if("-sotimeout".equals(arg)) { soTimeout=Long.parseLong(args[++i]); continue; } help(); return; } System.out.println("GossipRouter is starting. CTRL-C to exit JVM"); try { router=new GossipRouter(port, bind_addr, jmx); if(backlog > 0) router.setBacklog(backlog); if(soTimeout >= 0) router.setSocketReadTimeout(soTimeout); if(soLinger >= 0) router.setLingerTimeout(soLinger); if(expiry_time > 0) router.setExpiryTime(expiry_time); router.start(); } catch(Exception e) { System.err.println(e); } } static void help() { System.out.println(); System.out.println("GossipRouter [-port <port>] [-bind_addr <address>] [options]"); System.out.println(); System.out.println("Options:"); System.out.println(); System.out.println(" -backlog <backlog> - Max queue size of backlogged connections. Must be"); System.out.println(" greater than zero or the default of 1000 will be"); System.out.println(" used."); System.out.println(); System.out.println(" -jmx <true|false> - Expose attributes and operations via JMX."); System.out.println(); System.out.println(" -solinger <msecs> - Time for setting SO_LINGER on connections. 0"); System.out.println(" means do not set SO_LINGER. Must be greater than"); System.out.println(" or equal to zero or the default of 2000 will be"); System.out.println(" used."); System.out.println(); System.out.println(" -sotimeout <msecs> - Time for setting SO_TIMEOUT on connections. 0"); System.out.println(" means don't set SO_TIMEOUT. Must be greater than"); System.out.println(" or equal to zero or the default of 3000 will be"); System.out.println(" used."); System.out.println(); System.out.println(" -expiry <msecs> - Time for closing idle connections. 0"); System.out.println(" means don't expire."); System.out.println(); } }