package net.jxta.impl.endpoint.netty; import java.io.StringWriter; import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.Enumeration; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; import java.util.concurrent.Executors; import java.util.logging.Level; import java.util.logging.Logger; import net.jxta.document.Advertisement; import net.jxta.document.AdvertisementFactory; import net.jxta.document.StructuredDocument; import net.jxta.document.XMLDocument; import net.jxta.document.XMLElement; import net.jxta.endpoint.EndpointAddress; import net.jxta.endpoint.EndpointService; import net.jxta.exception.PeerGroupException; import net.jxta.id.ID; import net.jxta.impl.endpoint.IPUtils; import net.jxta.impl.endpoint.TransportUtils; import net.jxta.impl.protocol.TCPAdv; import net.jxta.logging.Logging; import net.jxta.peergroup.PeerGroup; import net.jxta.platform.Module; import net.jxta.protocol.ConfigParams; import net.jxta.protocol.ModuleImplAdvertisement; import net.jxta.protocol.TransportAdvertisement; import org.jboss.netty.channel.socket.ClientSocketChannelFactory; import org.jboss.netty.channel.socket.ServerSocketChannelFactory; import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory; import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory; /** * JBoss Netty based endpoint transport. Utilizes netty client and server channel * factories to implement the standard JXTA messaging protocol described in * Section 7.1 "TCP/IP Message Transport" of the JXTA v2.0 protocols specification. * * By default, this implementation uses the Netty NIO socket factories, and so * requires an unfettered TCP/IP connection to communicate. Subclasses may use * alternative factories to work around this restriction, for instance by negotiating * the opening of a port using UPnP or STUN/TURN, or utilizing HTTP to tunnel the * data. * * @author iain.mcginniss@onedrum.com */ public class NettyTransport implements Module { private static final Logger LOG = Logger.getLogger(NettyTransport.class.getName()); public static final int MODULE_STARTUP_FAILED = -1; private PeerGroup group; private String protocolName; private boolean serverEnabled = true; private TransportServerComponent server; private boolean clientEnabled = true; private TransportClientComponent client; private boolean started = false; public void init(PeerGroup group, ID assignedID, Advertisement implAdv) throws PeerGroupException { this.group = group; this.protocolName = getDefaultProtocolName(); processStaticConfiguration(implAdv); TCPAdv instanceConfiguration = extractInstanceConfiguration(assignedID); initServer(instanceConfiguration); initClient(instanceConfiguration, getPreferredReturnAddress(instanceConfiguration)); if(Logging.SHOW_CONFIG && LOG.isLoggable(Level.CONFIG)) { LOG.config(buildConfigurationState(assignedID, (ModuleImplAdvertisement)implAdv)); } } private void initServer(TCPAdv instanceConfiguration) throws PeerGroupException { this.serverEnabled = instanceConfiguration.isServerEnabled(); if(!serverEnabled) { this.server = new NullTransportServerComponent(); return; } String interfaceAddress = instanceConfiguration.getInterfaceAddress(); InetAddress bindAddr; if(interfaceAddress != null) { bindAddr = TransportUtils.resolveInterfaceAddress(interfaceAddress); } else { bindAddr = IPUtils.ANYADDRESS; } String publicName = instanceConfiguration.getServer(); EndpointAddress publicAddress = null; if (publicName != null) { publicAddress = new EndpointAddress(protocolName, publicName, null, null); } NettyTransportServer server = new NettyTransportServer(createServerSocketChannelFactory(), new InetSocketAddressTranslator(protocolName), group); int preferredPort = correctPort(instanceConfiguration.getPort(), 1, 65535, getDefaultPort(), getDefaultPort(), "Preferred"); int startPort = correctPort(instanceConfiguration.getStartPort(), 1, preferredPort, getDefaultPortRangeLowerBound(), 1, "Range start"); int endPort = correctPort(instanceConfiguration.getEndPort(), startPort, 65535, getDefaultPortRangeUpperBound(), 65535, "Range end"); List<InetSocketAddress> potentialBindpoints = IPUtils.createRandomSocketAddressList(bindAddr, preferredPort, startPort, endPort); server.init(potentialBindpoints, publicAddress, instanceConfiguration.getPublicAddressOnly()); this.server = server; } private void initClient(TCPAdv instanceConfiguration, EndpointAddress returnAddress) { this.clientEnabled = instanceConfiguration.isClientEnabled(); if(!clientEnabled) { client = new NullTransportClientComponent(); return; } client = new NettyTransportClient(createClientSocketChannelFactory(), new InetSocketAddressTranslator(protocolName), group, returnAddress); } private EndpointAddress getPreferredReturnAddress(TCPAdv instanceConfiguration) { if(server == null || !server.getPublicAddresses().hasNext()) { InetAddress addr = TransportUtils.resolveInterfaceAddress(instanceConfiguration.getInterfaceAddress()); InetSocketAddress socketAddress = new InetSocketAddress(addr, 0); InetSocketAddressTranslator translator = new InetSocketAddressTranslator(protocolName); return translator.toEndpointAddress(socketAddress); } else { // the preferred address is assumed to be the first one return server.getPublicAddresses().next(); } } private String buildConfigurationState(ID assignedID, ModuleImplAdvertisement implAdv) { StringWriter writer = new StringWriter(); writer.append("Configuring ").append(getTransportDescriptiveName()).append(" Transport : ").append(assignedID.toString()); if(implAdv != null) { writer.append("\n\tImplementation: "); writer.append("\n\t\tModule Spec ID: ").append(implAdv.getModuleSpecID().toString()); writer.append("\n\t\tImpl Description: ").append(implAdv.getDescription()); writer.append("\n\t\tImpl URI: ").append(implAdv.getUri()); writer.append("\n\t\tImpl Code: ").append(implAdv.getCode()); } writer.append("\n\tGroup Params:"); writer.append("\n\t\tGroup: ").append(group.toString()); writer.append("\n\t\tPeer ID: ").append(group.getPeerID().toString()); writer.append("\n\tConfiguration:"); writer.append("\n\t\tProtocol: ").append(protocolName); writer.append("\n\tServer enabled: ").append(Boolean.toString(serverEnabled)); if(serverEnabled) { writer.append("\n\t\tServer physical addresses:"); for(EndpointAddress addr : server.getBoundAddresses()) { writer.append("\n\t\t\t" + addr); } writer.append("\n\t\tServer public addresses:"); Iterator<EndpointAddress> publicAddrs = server.getPublicAddresses(); while(publicAddrs.hasNext()) { writer.append("\n\t\t\t" + publicAddrs.next()); } } writer.append("\n\tClient enabled: ").append(Boolean.toString(clientEnabled)); if(clientEnabled) { writer.append("\n\t\tClient return address: ").append(client.getPublicAddress().toString()); } return writer.toString(); } private void processStaticConfiguration(Advertisement implAdv) { if(implAdv == null || !(implAdv instanceof ModuleImplAdvertisement)) { return; } ModuleImplAdvertisement moduleImplAdv = (ModuleImplAdvertisement) implAdv; StructuredDocument<?> parameters = moduleImplAdv.getParam(); if(parameters != null) { Enumeration<?> protoChildren = parameters.getChildren("Proto"); if(protoChildren.hasMoreElements()) { this.protocolName = ((XMLElement<?>) protoChildren.nextElement()).getTextValue(); } } } // Type conversion warnings are disabled due to the unhelpful generics structure of StructuredDocument and Element. @SuppressWarnings("unchecked") private TCPAdv extractInstanceConfiguration(ID assignedID) { ConfigParams configAdvertisement = group.getConfigAdvertisement(); XMLElement instanceParameters = (XMLDocument) configAdvertisement.getServiceParam(assignedID); if(instanceParameters == null) { return null; } Enumeration<XMLElement> adverts = instanceParameters.getChildren(TransportAdvertisement.getAdvertisementType()); if(!adverts.hasMoreElements()) { return null; } try { XMLElement adv = adverts.nextElement(); Advertisement advertisement = AdvertisementFactory.newAdvertisement(adv); if(!(advertisement instanceof TCPAdv)) { throw new IllegalArgumentException("Service parameter for " + assignedID + " should be a " + TCPAdv.getAdvertisementType() + ", but is a " + advertisement.getAdvType()); } return (TCPAdv) advertisement; } catch(NoSuchElementException e) { throw new IllegalArgumentException("Service parameter for " + assignedID + " is not a valid advertisement"); } } /** * @return the socket channel factory to be used for outgoing connections by the client. It is intended * that this be overridden, if a child implementation wishes to change the mechanism used to establish an * outbound connection. */ protected ClientSocketChannelFactory createClientSocketChannelFactory() { return new NioClientSocketChannelFactory(Executors.newCachedThreadPool(), Executors.newCachedThreadPool()); } /** * @return the server socket channel factory to be used for binding and accepting connections from * remote peers. It is intended that this be overridden, if a child implementation wishes to change * the mechanism used to accept an inbound connection. */ protected ServerSocketChannelFactory createServerSocketChannelFactory() { return new NioServerSocketChannelFactory(Executors.newCachedThreadPool(), Executors.newCachedThreadPool()); } /** * Used to take a port from the configuration, and "correct" it to a useable port number. * <ol> * <li>If the port is -1 (meaning "default"), then the default port is returned.</li> * <li>If the port is 0 (meaning "any legal port"), then the anyPort parameter is returned.</li> * <li>Otherwise, the port is constrained to the bounds defined by the min and max parameters.</li> * </ol> */ private int correctPort(int port, int min, int max, int defaultPort, int anyPort, String portName) { if(port == -1) { return defaultPort; } else if(port == 0) { return anyPort; } else if(port < min || port > max) { port = Math.max(min, Math.min(port, max)); if(Logging.SHOW_WARNING && LOG.isLoggable(Level.WARNING)) { LOG.log(Level.WARNING, "{0} port was outside legal range ({1}-{2}), changed to {3}", new Object[] { portName, min, max, port }); } } return port; } public int startApp(String[] args) { if(!serverEnabled && !clientEnabled) { LOG.log(Level.INFO, "Both client and server of transport for {0} are disabled - module not starting", getProtocolName()); return Module.START_DISABLED; } EndpointService endpointService = group.getEndpointService(); if(endpointService == null) { return Module.START_AGAIN_STALLED; } if(!client.start(endpointService) || !server.start(endpointService)) { return MODULE_STARTUP_FAILED; } started = true; return Module.START_OK; } public void stopApp() { if(!started) { return; } started = false; client.beginStop(); server.beginStop(); client.stop(); server.stop(); group = null; } public String getProtocolName() { return protocolName; } /* Methods we expect child classes to override, typically to distinguish * themselves from this default TCP-based implementation */ /** * Returns the protocol name which will be used, if not specified in the "Proto" * element of the module implementation advertisement. */ protected String getDefaultProtocolName() { return "tcp"; } /** * The default port for this transport, if not specified by the instance configuration * for this transport. */ protected int getDefaultPort() { return 7901; } /** * If binding to the preferred or default port fails, we will typically attempt to bind * to any port within a range specified by the configuration or within a default range. * This method should specify the <em>lower</em> end of the default range, used if the * configuration does not specify anything else. */ protected int getDefaultPortRangeLowerBound() { return 7901; } /** * If binding to the preferred or default port fails, we will typically attempt to bind * to any port within a range specified by the configuration or within a default range. * This method should specify the <em>upper</em> end of the default range, used if the * configuration does not specify anything else. */ protected int getDefaultPortRangeUpperBound() { return 7999; } /** * A short human-readable name for this transport, that will be displayed in configuration * logging. */ protected String getTransportDescriptiveName() { return "Netty TCP"; } }