/** * */ package net.varkhan.serv.p2p.message.transport; import net.varkhan.base.management.report.JMXAverageMonitorReport; import net.varkhan.serv.p2p.message.PeerResolver; import net.varkhan.serv.p2p.message.dispatch.MesgDispatcher; import net.varkhan.serv.p2p.message.dispatch.MesgReceiver; import net.varkhan.serv.p2p.message.dispatch.MesgSender; import net.varkhan.serv.p2p.connect.PeerAddress; import net.varkhan.serv.p2p.connect.transport.TCPAddress; import net.varkhan.serv.p2p.message.*; import net.varkhan.serv.p2p.message.protocol.BinaryEnvelope; import net.varkhan.serv.p2p.message.protocol.BinaryPayload; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import java.io.IOException; import java.io.InterruptedIOException; import java.io.OutputStream; import java.net.*; /** * <b>.</b> * <p/> * @author varkhan * @date Nov 19, 2009 * @time 12:53:57 AM */ @SuppressWarnings( { "UnusedDeclaration" }) public class TCPTransport implements MesgTransport { public static final Logger log=LogManager.getLogger(TCPTransport.class); private static final int RECV_TIMEOUT=500; private static final int BACK_LOG =100; private int timeout=RECV_TIMEOUT; private InetAddress localAddr; private ServerSocket server; protected PeerResolver resolver; protected MesgDispatcher dispatcher; protected PeerAddress local; private JMXAverageMonitorReport stats; private String name; private volatile boolean run =false; private volatile Thread thread=null; /** * Create a TCP PeerChannel. * * @param resolver the point name resolver * @param dispatcher the message dispatcher * @param localPoint the local point node * @param localAddr the local address to bind to (or {@code null} if any) * @param localPort the local port number to bind to (or {@code 0} if any) * @param stats activity statistics collector * @throws java.io.IOException if the TCP listening socket could not be created and bound */ public TCPTransport( PeerResolver resolver, MesgDispatcher dispatcher, PeerAddress localPoint, InetAddress localAddr, int localPort, JMXAverageMonitorReport stats) throws IOException { this.resolver=resolver; this.dispatcher=dispatcher; this.local=localPoint; this.stats=stats; this.localAddr=localAddr; if(this.localAddr!=null) { server=new ServerSocket(localPort, BACK_LOG, this.localAddr); } else { server= new ServerSocket(localPort, BACK_LOG); } this.name = ((server.getInetAddress()==null)?"0.0.0.0": server.getInetAddress().getHostAddress())+ ":"+server.getLocalPort()+ "@"+localPoint.name(); } /** * Get the message reception timeout * * @return the maximum number of milliseconds the channel will wait for the reception of a single message */ public int getTimeout() { return timeout; } /** * Set the message reception timeout * * @param delay the maximum number of milliseconds the channel will wait for the reception of a single message * @throws java.net.SocketException if the underlying transport did not accept reconfiguration */ public void setTimeout(int delay) throws SocketException { timeout = delay; if(server!=null) server.setSoTimeout(timeout); } public InetAddress getAddress() { return server.getInetAddress(); } public int getPort() { return server.getLocalPort(); } /********************************************************************************** ** Lifecycle management **/ @Override public TCPAddress endpoint() { return new TCPAddress() { @Override public InetAddress addrTCP() { return server.getInetAddress(); } @Override public int portTCP() { return server.getLocalPort(); } @Override public String name() { return name; } @Override @SuppressWarnings("unchecked") public <T> T as(Class<T> c) { if(c.isAssignableFrom(TCPAddress.class)) return (T) this; return null; } }; } /** * Indicates whether the channel is started and able to handle messages * * @return {@code true} if the channel transport layer is ready to handle outgoing messages, * and the listening thread is handling incoming messages */ public boolean isStarted() { return run && thread!=null && thread.isAlive(); } /** * Start the channel * * @throws java.io.IOException if the channel could not bind to an address */ public synchronized void start() throws IOException { if(run || (thread!=null && thread.isAlive())) return; if (log.isDebugEnabled()) { log.debug(TCPTransport.class.getSimpleName()+": start "+name + "(" + server.getInetAddress().getHostAddress() + ":" + getPort() + ")"); } thread = new Thread() { public void run() { serverLoop(); } }; thread.setName(TCPTransport.class.getSimpleName()+"("+server.getInetAddress().getHostAddress()+":"+getPort()+")"); thread.setDaemon(true); run = true; thread.start(); } /** * Stop the channel * * @throws java.io.IOException if the channel could not free resources */ public synchronized void stop() throws IOException { run = false; Thread t=thread; if(t==null) return; thread = null; t.interrupt(); try { t.join(2*timeout); } catch(InterruptedException e) { // ignore } if (log.isDebugEnabled()) { log.debug(name+ " stop TCPMessageChannel (address=" + server.getInetAddress().getHostAddress() + ", port=" + server.getLocalPort() + ")"); } } public void serverLoop() { // Main message listening loop while(run) { // Waiting for an incoming connection final Socket socket; try { socket = server.accept(); socket.setSoTimeout(timeout); } catch(SocketTimeoutException e) { stats.inc(TCPTransport.class.getSimpleName()+".Recv.error["+e.getClass().getSimpleName()+"]"); continue; } catch(InterruptedIOException e) { if(!run) return; stats.inc(TCPTransport.class.getSimpleName()+".Recv.error["+e.getClass().getSimpleName()+"]"); if (log.isDebugEnabled()) { log.debug("Interrupted while receiving", e); } continue; } catch(SocketException e) { stats.inc(TCPTransport.class.getSimpleName()+".Recv.error["+e.getClass().getSimpleName()+"]"); if (log.isDebugEnabled()) { log.debug("Protocol error while receiving", e); } continue; } catch(IOException e) { stats.inc(TCPTransport.class.getSimpleName()+".Recv.error["+e.getClass().getSimpleName()+"]"); if (log.isDebugEnabled()) { log.debug("Accept on server socket failed", e); } continue; } catch (Throwable e) { stats.inc(TCPTransport.class.getSimpleName()+".Recv.error["+e.getClass().getSimpleName()+"]"); log.error("Unexpected exception", e); continue; } if (log.isDebugEnabled()) { log.debug("Receiving a message from "+socket.getInetAddress().getHostAddress()); } // Handle the message try { receive(socket); stats.inc(TCPTransport.class.getSimpleName()+".Recv.Success"); } catch (IOException e) { stats.inc(TCPTransport.class.getSimpleName()+".Recv.error["+e.getClass().getSimpleName()+"]"); if (log.isDebugEnabled()) { log.debug("Data read on socket failed", e); } try { socket.close(); } catch (IOException x) { /* ignore */ } } catch (Throwable e) { stats.inc(TCPTransport.class.getSimpleName()+".Recv.error["+e.getClass().getSimpleName()+"]"); log.error("Unexpected exception", e); try { socket.close(); } catch (IOException x) { /* ignore */ } } } } private void receive(Socket socket) throws IOException {// Here we could try to use an executor service. // But the overhead of starting a thread is in most cases higher that just doing the minimal // handling we need here when we are not getting a CALL (and in that case, the dispatcher will do it) // Building received message BinaryEnvelope recv = new BinaryEnvelope(socket.getInputStream()); String method=recv.method(); MesgPayload mesg = recv.message(); long sequence=recv.sequence(); // Passing message to the dispatcher switch(recv.type()) { case BinaryEnvelope.PING: { PeerAddress remote = resolver.update(recv.srcId(),mesg); if(sequence>=0){ // Reply immediately, with decremented ttl ping(socket, local, remote, method, resolver.info(), sequence-1); } } break; case BinaryEnvelope.CALL: { PeerAddress src = resolver.resolve(recv.srcId()); PeerAddress dst = resolver.resolve(recv.dstId()); if(sequence<0) { dispatcher.call(src, dst, method, mesg, null, -1, null); } else { BinaryPayload repl = new BinaryPayload(); dispatcher.call(src, dst, method, mesg, repl, sequence, new WsTCPSender(socket)); } } break; case BinaryEnvelope.REPL: { PeerAddress src = resolver.resolve(recv.srcId()); PeerAddress dst = resolver.resolve(recv.dstId()); dispatcher.repl(src, dst, method, mesg, sequence); } break; } } /********************************************************************************** ** Message transmission handling **/ public boolean call(PeerAddress src, PeerAddress dst, String method, MesgPayload message, MesgReceiver handler) throws IOException { if(!run) { stats.inc(TCPTransport.class.getSimpleName()+".Call.NotRunning"); return false; } // We only know how to send messages from the local addr if(!local.equals(src)) { stats.inc(TCPTransport.class.getSimpleName()+".Call.NotLocal"); return false; } TCPAddress node = dst.as(TCPAddress.class); if(node==null) { stats.inc(TCPTransport.class.getSimpleName()+".Call.Protocol.Unsupported"); return false; } InetAddress addr=node.addrTCP(); int port=node.portTCP(); long sequence = -1; if(handler!=null) sequence = dispatcher.register(handler); if(log.isDebugEnabled()) { log.debug("CALL tcp://"+addr.getHostAddress()+":"+port+"/"+method); } // We don't use the sequence ID system, since the answer will come trough the same socket anyway Socket socket = null; try { socket = setupSocket(node.addrTCP(), node.portTCP(), (int) handler.getMaxDelay()); BinaryEnvelope send = new BinaryEnvelope(BinaryEnvelope.CALL, src.name(), dst.name(), method, sequence, message); OutputStream outStm = socket.getOutputStream(); send.send(outStm); outStm.flush(); // We don't need output to this anymore socket.shutdownOutput(); stats.inc(TCPTransport.class.getSimpleName()+".Call.Success"); } catch (IOException e) { if(log.isInfoEnabled()) log.info("CALL tcp://"+node.addrTCP().getHostAddress()+":"+node.portTCP()+"/"+method+"#"+sequence+" failed", e); stats.inc(TCPTransport.class.getSimpleName()+".Call.error["+e.getClass().getSimpleName()+"]"); throw e; } // We hijack this thread to handle the reply immediately, since it should be available right now try { receive(socket); stats.inc(TCPTransport.class.getSimpleName()+".Recv.Success"); } catch (SocketTimeoutException e) { if(log.isInfoEnabled()) log.info("CALL tcp://"+socket.getInetAddress().getHostAddress()+":"+socket.getPort()+"/"+method+"#"+sequence+" failed", e); stats.inc(TCPTransport.class.getSimpleName()+".Recv.error["+e.getClass().getSimpleName()+"]"); throw e; } catch (IOException e) { if(log.isInfoEnabled()) log.info("CALL tcp://"+socket.getInetAddress().getHostAddress()+":"+socket.getPort()+"/"+method+"#"+sequence+" failed", e); stats.inc(TCPTransport.class.getSimpleName()+".Recv.error["+e.getClass().getSimpleName()+"]"); throw e; } catch (Throwable e) { if(log.isInfoEnabled()) log.info("CALL tcp://"+socket.getInetAddress().getHostAddress()+":"+socket.getPort()+"/"+method+"#"+sequence+" failed", e); stats.inc(TCPTransport.class.getSimpleName()+".Recv.error["+e.getClass().getSimpleName()+"]"); throw new IOException("Unexpected error", e); } finally { if (socket != null) { try { if (!socket.isOutputShutdown() && socket.isConnected()) { socket.shutdownOutput(); } socket.close(); } catch (IOException e) { // ignore exception } } } return true; } public boolean repl(PeerAddress src, PeerAddress dst, String method, MesgPayload message, long sequence) throws IOException { if(!run) { stats.inc(TCPTransport.class.getSimpleName()+".Repl.NotRunning"); return false; } // We only know how to send messages from the local addr if(!local.equals(src)) { stats.inc(TCPTransport.class.getSimpleName()+".Repl.NotLocal"); return false; } TCPAddress node = dst.as(TCPAddress.class); if(node==null) { stats.inc(TCPTransport.class.getSimpleName()+".Repl.Protocol.Unsupported"); return false; } InetAddress addr=node.addrTCP(); int port=node.portTCP(); if(log.isDebugEnabled()) log.debug("REPL tcp://"+addr.getHostAddress()+":"+port+"/"+method+"#"+sequence); // We don't use the sequence ID system, since the answer will come trough the same socket anyway Socket socket = null; try { socket = setupSocket(node.addrTCP(), node.portTCP(), timeout); // We will not need input from this anymore socket.shutdownInput(); BinaryEnvelope send = new BinaryEnvelope(BinaryEnvelope.REPL, src.name(), dst.name(), method, sequence, message); OutputStream outStm = socket.getOutputStream(); send.send(outStm); outStm.flush(); // We don't need output to this anymore socket.shutdownOutput(); stats.inc(TCPTransport.class.getSimpleName()+".Repl.Success"); } catch (IOException e) { if(log.isInfoEnabled()) log.info("REPL tcp://"+node.addrTCP().getHostAddress()+":"+node.portTCP()+"/"+method+"#"+sequence+" failed", e); stats.inc(TCPTransport.class.getSimpleName()+".Repl.error["+e.getClass().getSimpleName()+"]"); throw e; } finally { if (socket!=null) { try { if (!socket.isOutputShutdown() && socket.isConnected()) { socket.shutdownOutput(); } socket.close(); } catch(IOException e) { // ignore exception } } } return true; } public boolean ping(PeerAddress src, PeerAddress dst, String method, MesgPayload message, long sequence) throws IOException { if(!run) { stats.inc(TCPTransport.class.getSimpleName()+".Ping.NotRunning"); return false; } // We only know how to send messages from the local addr if(!local.equals(src)) { stats.inc(TCPTransport.class.getSimpleName()+".Ping.NotLocal"); return false; } TCPAddress node = dst.as(TCPAddress.class); if(node==null) { stats.inc(TCPTransport.class.getSimpleName()+".Ping.Protocol.Unsupported"); return false; } InetAddress addr=node.addrTCP(); int port=node.portTCP(); if(log.isDebugEnabled()) log.debug("PING tcp://"+addr.getHostAddress()+":"+port+"/"+method); // We don't use the sequence ID system, since the answer will come trough the same socket anyway Socket socket = null; try { socket = setupSocket(node.addrTCP(), node.portTCP(), timeout); ping(socket, src, dst, method, message, sequence); } catch (IOException e) { if(log.isInfoEnabled()) log.info("PING tcp://"+node.addrTCP().getHostAddress()+":"+node.portTCP()+"/"+method+"#"+sequence+" failed", e); stats.inc(TCPTransport.class.getSimpleName()+".Ping.error["+e.getClass().getSimpleName()+"]"); throw e; } // We hijack this thread to handle the reply immediately, since it should be available right now try { receive(socket); stats.inc(TCPTransport.class.getSimpleName()+".Ping.Success"); } catch (SocketTimeoutException e) { if(log.isInfoEnabled()) log.info("PING tcp://"+socket.getInetAddress().getHostAddress()+":"+socket.getPort()+"/"+method+"#"+sequence+" failed", e); stats.inc(TCPTransport.class.getSimpleName()+".Ping.error["+e.getClass().getSimpleName()+"]"); throw e; } catch (IOException e) { if(log.isInfoEnabled()) log.info("PING tcp://"+socket.getInetAddress().getHostAddress()+":"+socket.getPort()+"/"+method+"#"+sequence+" failed", e); stats.inc(TCPTransport.class.getSimpleName()+".Ping.error["+e.getClass().getSimpleName()+"]"); throw e; } catch (Throwable e) { if(log.isInfoEnabled()) log.info("PING tcp://"+socket.getInetAddress().getHostAddress()+":"+socket.getPort()+"/"+method+"#"+sequence+" failed", e); stats.inc(TCPTransport.class.getSimpleName()+".Ping.error["+e.getClass().getSimpleName()+"]"); throw new IOException("Unexpected error", e); } finally { if (socket != null) { try { if (!socket.isOutputShutdown() && socket.isConnected()) { socket.shutdownOutput(); } socket.close(); } catch (IOException e) { // ignore exception } } } return true; } private void ping(Socket socket, PeerAddress src, PeerAddress dst, String method, MesgPayload message, long sequence) throws IOException { BinaryEnvelope send = new BinaryEnvelope(BinaryEnvelope.PING, src.name(), dst.name(), method, sequence, message); OutputStream outStm = socket.getOutputStream(); send.send(outStm); outStm.flush(); stats.inc(TCPTransport.class.getSimpleName()+".Ping.Success"); // Expect reply? if(sequence>=0) receive(socket); } private Socket setupSocket(InetAddress addr, int port, int timeout) throws IOException { Socket socket = new Socket(); socket.setReuseAddress(true); socket.setSoTimeout(timeout); // set read timeout if (localAddr != null) { socket.bind(new InetSocketAddress(localAddr, 0)); } socket.connect(new InetSocketAddress(addr, port), timeout); return socket; } private class WsTCPSender implements MesgSender { protected Socket socket; private WsTCPSender(Socket socket) { this.socket=socket; } public void send(PeerAddress src, PeerAddress dst, String method, MesgPayload message, long sequence) throws IOException { try { socket.shutdownInput(); BinaryEnvelope send = new BinaryEnvelope(BinaryEnvelope.REPL, src.name(), dst.name(), method, sequence, message); OutputStream outStm = socket.getOutputStream(); send.send(outStm); outStm.flush(); socket.shutdownOutput(); stats.inc(TCPTransport.class.getSimpleName()+".Repl.Success"); } catch (IOException e) { if(log.isInfoEnabled()) log.info("REPL tcp://"+socket.getInetAddress().getHostAddress()+":"+socket.getPort()+"/"+method+"#"+sequence+" failed", e); stats.inc(TCPTransport.class.getSimpleName()+".Repl.error["+e.getClass().getSimpleName()+"]"); } finally { try { socket.close(); } catch(IOException e) { /* ignore */ } } } public void finalize() throws Throwable { socket.close(); super.finalize(); } } }