├── .gitignore ├── README.md ├── customcodecs ├── README.md ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── biasedbit │ └── nettytutorials │ └── customcodecs │ ├── client │ ├── Client.java │ ├── ClientHandler.java │ ├── ClientHandlerListener.java │ └── ClientJavaSerialization.java │ ├── common │ ├── ByteCounter.java │ ├── Decoder.java │ ├── Encoder.java │ ├── Envelope.java │ ├── Type.java │ └── Version.java │ └── server │ ├── Server.java │ ├── ServerHandler.java │ └── ServerJavaSerialization.java └── handshake ├── README.md ├── pom.xml └── src └── main └── java └── com └── biasedbit └── nettytutorials └── handshake ├── ClientRunner.java ├── ServerRunner.java ├── client ├── Client.java ├── ClientHandler.java ├── ClientHandshakeHandler.java └── ClientListener.java ├── common ├── ByteCounter.java ├── Challenge.java ├── HandshakeEvent.java └── MessageCounter.java └── server ├── Server.java ├── ServerHandler.java ├── ServerHandshakeHandler.java └── ServerListener.java /.gitignore: -------------------------------------------------------------------------------- 1 | */.project 2 | */.classpath 3 | */.settings 4 | */target 5 | */reports 6 | */*.ipr 7 | */*.iws 8 | */*.iml 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | netty tutorials 2 | =============== 3 | Series of tutorials on Netty, mostly to back articles at http://bruno.biasedbit.com/blag/ 4 | -------------------------------------------------------------------------------- /customcodecs/README.md: -------------------------------------------------------------------------------- 1 | Custom codecs tutorial 2 | ====================== 3 | 4 | This is the backing code for a basic tutorial on netty's replaying decoder at http://d.pr/tso8 5 | -------------------------------------------------------------------------------- /customcodecs/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | com.biasedbit.nettytutorials 6 | customcodecs 7 | 1.0.0 8 | jar 9 | 10 | netty tutorial - customcodecs 11 | http://bruno.biasedbit.com/blag/ 12 | 13 | Netty tutorials - custom codecs 14 | 15 | 2010 16 | 17 | 18 | Bruno de Carvalho 19 | bruno@biasedbit.com 20 | http://bruno.biasedbit.com 21 | 22 | 23 | 24 | 25 | 26 | Apache License, Version 2.0 27 | http://www.apache.org/licenses/LICENSE-2.0 28 | 29 | 30 | 31 | 32 | UTF-8 33 | 34 | 35 | 36 | 37 | repository.jboss.org 38 | http://repository.jboss.org/nexus/payload/groups/public/ 39 | 40 | false 41 | 42 | 43 | 44 | 45 | 46 | 47 | org.jboss.netty 48 | netty 49 | 3.2.3.Final 50 | compile 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | maven-compiler-plugin 59 | 2.0.2 60 | 61 | 1.6 62 | 1.6 63 | 64 | 65 | 66 | 67 | 68 | maven-surefire-plugin 69 | 2.4.3 70 | 71 | never 72 | 73 | **/Abstract* 74 | **/*TestUtil* 75 | 76 | 77 | 78 | 79 | 80 | 81 | org.apache.maven.plugins 82 | maven-idea-plugin 83 | 2.2 84 | 85 | true 86 | 7 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /customcodecs/src/main/java/com/biasedbit/nettytutorials/customcodecs/client/Client.java: -------------------------------------------------------------------------------- 1 | package com.biasedbit.nettytutorials.customcodecs.client; 2 | 3 | import com.biasedbit.nettytutorials.customcodecs.common.ByteCounter; 4 | import com.biasedbit.nettytutorials.customcodecs.common.Envelope; 5 | import com.biasedbit.nettytutorials.customcodecs.common.Decoder; 6 | import com.biasedbit.nettytutorials.customcodecs.common.Encoder; 7 | import com.biasedbit.nettytutorials.customcodecs.common.Type; 8 | import com.biasedbit.nettytutorials.customcodecs.common.Version; 9 | import org.jboss.netty.bootstrap.ClientBootstrap; 10 | import org.jboss.netty.channel.ChannelFactory; 11 | import org.jboss.netty.channel.ChannelPipeline; 12 | import org.jboss.netty.channel.ChannelPipelineFactory; 13 | import org.jboss.netty.channel.Channels; 14 | import org.jboss.netty.channel.group.ChannelGroup; 15 | import org.jboss.netty.channel.group.DefaultChannelGroup; 16 | import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory; 17 | 18 | import java.net.InetSocketAddress; 19 | import java.util.concurrent.Executors; 20 | import java.util.concurrent.atomic.AtomicInteger; 21 | 22 | public class Client implements ClientHandlerListener { 23 | 24 | // configuration -------------------------------------------------------------------------------------------------- 25 | 26 | private final String host; 27 | private final int port; 28 | private final int messages; 29 | private final int floods; 30 | 31 | // internal vars -------------------------------------------------------------------------------------------------- 32 | 33 | private ChannelFactory clientFactory; 34 | private ChannelGroup channelGroup; 35 | private ClientHandler handler; 36 | private final AtomicInteger received; 37 | private int flood; 38 | private long startTime; 39 | 40 | // constructors --------------------------------------------------------------------------------------------------- 41 | 42 | public Client(String host, int port, int messages, int floods) { 43 | this.host = host; 44 | this.port = port; 45 | this.messages = messages; 46 | this.floods = floods; 47 | this.received = new AtomicInteger(0); 48 | this.flood = 0; 49 | } 50 | 51 | // ClientHandlerListener ------------------------------------------------------------------------------------------ 52 | 53 | @Override 54 | public void messageReceived(Envelope message) { 55 | // System.err.println("Received message " + message); 56 | if (this.received.incrementAndGet() == this.messages) { 57 | long stopTime = System.currentTimeMillis(); 58 | float timeInSeconds = (stopTime - this.startTime) / 1000f; 59 | System.err.println("Sent and received " + this.messages + " in " + timeInSeconds + "s"); 60 | System.err.println("That's " + (this.messages / timeInSeconds) + " echoes per second!"); 61 | 62 | // ideally, this should be sent to another thread, since this method is called by a netty worker thread. 63 | if (this.flood < this.floods) { 64 | this.received.set(0); 65 | this.flood++; 66 | this.flood(); 67 | } 68 | } 69 | } 70 | 71 | // public methods ------------------------------------------------------------------------------------------------- 72 | 73 | public boolean start() { 74 | 75 | // For production scenarios, use limited sized thread pools 76 | this.clientFactory = new NioClientSocketChannelFactory(Executors.newCachedThreadPool(), 77 | Executors.newCachedThreadPool()); 78 | this.channelGroup = new DefaultChannelGroup(this + "-channelGroup"); 79 | this.handler = new ClientHandler(this, this.channelGroup); 80 | ChannelPipelineFactory pipelineFactory = new ChannelPipelineFactory() { 81 | 82 | @Override 83 | public ChannelPipeline getPipeline() throws Exception { 84 | ChannelPipeline pipeline = Channels.pipeline(); 85 | pipeline.addLast("byteCounter", new ByteCounter("clientByteCounter")); 86 | pipeline.addLast("encoder", Encoder.getInstance()); 87 | pipeline.addLast("decoder", new Decoder()); 88 | pipeline.addLast("handler", handler); 89 | return pipeline; 90 | } 91 | }; 92 | 93 | ClientBootstrap bootstrap = new ClientBootstrap(this.clientFactory); 94 | bootstrap.setOption("reuseAddress", true); 95 | bootstrap.setOption("tcpNoDelay", true); 96 | bootstrap.setOption("keepAlive", true); 97 | bootstrap.setPipelineFactory(pipelineFactory); 98 | 99 | 100 | boolean connected = bootstrap.connect(new InetSocketAddress(host, port)).awaitUninterruptibly().isSuccess(); 101 | if (!connected) { 102 | this.stop(); 103 | } 104 | 105 | return connected; 106 | } 107 | 108 | public void stop() { 109 | if (this.channelGroup != null) { 110 | this.channelGroup.close(); 111 | } 112 | if (this.clientFactory != null) { 113 | this.clientFactory.releaseExternalResources(); 114 | } 115 | } 116 | 117 | private void flood() { 118 | if ((this.channelGroup == null) || (this.clientFactory == null)) { 119 | return; 120 | } 121 | 122 | this.startTime = System.currentTimeMillis(); 123 | for (int i = 0; i < this.messages; i++) { 124 | this.handler.sendMessage(new Envelope(Version.VERSION1, Type.REQUEST, new byte[175])); 125 | } 126 | } 127 | 128 | // main ----------------------------------------------------------------------------------------------------------- 129 | 130 | public static void main(String[] args) throws InterruptedException { 131 | final Client client = new Client("localhost", 9999, 100000, 1); 132 | 133 | if (!client.start()) { 134 | 135 | System.exit(-1); 136 | return; // not really needed... 137 | } 138 | 139 | System.out.println("Client started..."); 140 | 141 | client.flood(); 142 | 143 | Runtime.getRuntime().addShutdownHook(new Thread() { 144 | @Override 145 | public void run() { 146 | client.stop(); 147 | } 148 | }); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /customcodecs/src/main/java/com/biasedbit/nettytutorials/customcodecs/client/ClientHandler.java: -------------------------------------------------------------------------------- 1 | package com.biasedbit.nettytutorials.customcodecs.client; 2 | 3 | import com.biasedbit.nettytutorials.customcodecs.common.Envelope; 4 | import org.jboss.netty.channel.Channel; 5 | import org.jboss.netty.channel.ChannelHandlerContext; 6 | import org.jboss.netty.channel.ChannelStateEvent; 7 | import org.jboss.netty.channel.MessageEvent; 8 | import org.jboss.netty.channel.SimpleChannelUpstreamHandler; 9 | import org.jboss.netty.channel.group.ChannelGroup; 10 | 11 | public class ClientHandler extends SimpleChannelUpstreamHandler { 12 | 13 | // internal vars -------------------------------------------------------------------------------------------------- 14 | 15 | private final ClientHandlerListener listener; 16 | private final ChannelGroup channelGroup; 17 | private Channel channel; 18 | 19 | // constructors --------------------------------------------------------------------------------------------------- 20 | 21 | public ClientHandler(ClientHandlerListener listener, ChannelGroup channelGroup) { 22 | this.listener = listener; 23 | this.channelGroup = channelGroup; 24 | } 25 | 26 | // SimpleChannelUpstreamHandler ----------------------------------------------------------------------------------- 27 | 28 | @Override 29 | public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { 30 | if (e.getMessage() instanceof Envelope) { 31 | this.listener.messageReceived((Envelope) e.getMessage()); 32 | } else { 33 | super.messageReceived(ctx, e); 34 | } 35 | } 36 | 37 | @Override 38 | public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception { 39 | this.channel = e.getChannel(); 40 | this.channelGroup.add(e.getChannel()); 41 | } 42 | 43 | // public methods ------------------------------------------------------------------------------------------------- 44 | 45 | public void sendMessage(Envelope envelope) { 46 | if (this.channel != null) { 47 | this.channel.write(envelope); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /customcodecs/src/main/java/com/biasedbit/nettytutorials/customcodecs/client/ClientHandlerListener.java: -------------------------------------------------------------------------------- 1 | package com.biasedbit.nettytutorials.customcodecs.client; 2 | 3 | import com.biasedbit.nettytutorials.customcodecs.common.Envelope; 4 | 5 | public interface ClientHandlerListener { 6 | 7 | void messageReceived(Envelope message); 8 | } 9 | -------------------------------------------------------------------------------- /customcodecs/src/main/java/com/biasedbit/nettytutorials/customcodecs/client/ClientJavaSerialization.java: -------------------------------------------------------------------------------- 1 | package com.biasedbit.nettytutorials.customcodecs.client; 2 | 3 | import com.biasedbit.nettytutorials.customcodecs.common.ByteCounter; 4 | import com.biasedbit.nettytutorials.customcodecs.common.Decoder; 5 | import com.biasedbit.nettytutorials.customcodecs.common.Encoder; 6 | import com.biasedbit.nettytutorials.customcodecs.common.Envelope; 7 | import com.biasedbit.nettytutorials.customcodecs.common.Type; 8 | import com.biasedbit.nettytutorials.customcodecs.common.Version; 9 | import org.jboss.netty.bootstrap.ClientBootstrap; 10 | import org.jboss.netty.channel.ChannelFactory; 11 | import org.jboss.netty.channel.ChannelPipeline; 12 | import org.jboss.netty.channel.ChannelPipelineFactory; 13 | import org.jboss.netty.channel.Channels; 14 | import org.jboss.netty.channel.group.ChannelGroup; 15 | import org.jboss.netty.channel.group.DefaultChannelGroup; 16 | import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory; 17 | import org.jboss.netty.handler.codec.serialization.ObjectDecoder; 18 | import org.jboss.netty.handler.codec.serialization.ObjectEncoder; 19 | 20 | import java.net.InetSocketAddress; 21 | import java.util.concurrent.Executors; 22 | import java.util.concurrent.atomic.AtomicInteger; 23 | 24 | public class ClientJavaSerialization implements ClientHandlerListener { 25 | 26 | // configuration -------------------------------------------------------------------------------------------------- 27 | 28 | private final String host; 29 | private final int port; 30 | private final int messages; 31 | private final int floods; 32 | 33 | // internal vars -------------------------------------------------------------------------------------------------- 34 | 35 | private ChannelFactory clientFactory; 36 | private ChannelGroup channelGroup; 37 | private ClientHandler handler; 38 | private final AtomicInteger received; 39 | private int flood; 40 | private long startTime; 41 | 42 | // constructors --------------------------------------------------------------------------------------------------- 43 | 44 | public ClientJavaSerialization(String host, int port, int messages, int floods) { 45 | this.host = host; 46 | this.port = port; 47 | this.messages = messages; 48 | this.floods = floods; 49 | this.received = new AtomicInteger(0); 50 | this.flood = 0; 51 | } 52 | 53 | // ClientHandlerListener ------------------------------------------------------------------------------------------ 54 | 55 | @Override 56 | public void messageReceived(Envelope message) { 57 | // System.err.println("Received message " + message); 58 | if (this.received.incrementAndGet() == this.messages) { 59 | long stopTime = System.currentTimeMillis(); 60 | float timeInSeconds = (stopTime - this.startTime) / 1000f; 61 | System.err.println("Sent and received " + this.messages + " in " + timeInSeconds + "s"); 62 | System.err.println("That's " + (this.messages / timeInSeconds) + " echoes per second!"); 63 | 64 | // ideally, this should be sent to another thread, since this method is called by a netty worker thread. 65 | if (this.flood < this.floods) { 66 | this.received.set(0); 67 | this.flood++; 68 | this.flood(); 69 | } 70 | } 71 | } 72 | 73 | // public methods ------------------------------------------------------------------------------------------------- 74 | 75 | public boolean start() { 76 | 77 | // For production scenarios, use limited sized thread pools 78 | this.clientFactory = new NioClientSocketChannelFactory(Executors.newCachedThreadPool(), 79 | Executors.newCachedThreadPool()); 80 | this.channelGroup = new DefaultChannelGroup(this + "-channelGroup"); 81 | this.handler = new ClientHandler(this, this.channelGroup); 82 | ChannelPipelineFactory pipelineFactory = new ChannelPipelineFactory() { 83 | 84 | @Override 85 | public ChannelPipeline getPipeline() throws Exception { 86 | ChannelPipeline pipeline = Channels.pipeline(); 87 | pipeline.addLast("byteCounter", new ByteCounter("clientByteCounter")); 88 | pipeline.addLast("encoder", new ObjectEncoder()); 89 | pipeline.addLast("decoder", new ObjectDecoder()); 90 | pipeline.addLast("handler", handler); 91 | return pipeline; 92 | } 93 | }; 94 | 95 | ClientBootstrap bootstrap = new ClientBootstrap(this.clientFactory); 96 | bootstrap.setOption("reuseAddress", true); 97 | bootstrap.setOption("tcpNoDelay", true); 98 | bootstrap.setOption("keepAlive", true); 99 | bootstrap.setPipelineFactory(pipelineFactory); 100 | 101 | 102 | boolean connected = bootstrap.connect(new InetSocketAddress(host, port)).awaitUninterruptibly().isSuccess(); 103 | if (!connected) { 104 | this.stop(); 105 | } 106 | 107 | return connected; 108 | } 109 | 110 | public void stop() { 111 | if (this.channelGroup != null) { 112 | this.channelGroup.close(); 113 | } 114 | if (this.clientFactory != null) { 115 | this.clientFactory.releaseExternalResources(); 116 | } 117 | } 118 | 119 | private void flood() { 120 | if ((this.channelGroup == null) || (this.clientFactory == null)) { 121 | return; 122 | } 123 | 124 | this.startTime = System.currentTimeMillis(); 125 | for (int i = 0; i < this.messages; i++) { 126 | this.handler.sendMessage(new Envelope(Version.VERSION1, Type.REQUEST, new byte[175])); 127 | } 128 | } 129 | 130 | // main ----------------------------------------------------------------------------------------------------------- 131 | 132 | public static void main(String[] args) throws InterruptedException { 133 | final ClientJavaSerialization client = new ClientJavaSerialization("localhost", 9999, 100000, 10); 134 | 135 | if (!client.start()) { 136 | 137 | System.exit(-1); 138 | return; // not really needed... 139 | } 140 | 141 | System.out.println("Client started..."); 142 | 143 | client.flood(); 144 | 145 | Runtime.getRuntime().addShutdownHook(new Thread() { 146 | @Override 147 | public void run() { 148 | client.stop(); 149 | } 150 | }); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /customcodecs/src/main/java/com/biasedbit/nettytutorials/customcodecs/common/ByteCounter.java: -------------------------------------------------------------------------------- 1 | package com.biasedbit.nettytutorials.customcodecs.common; 2 | 3 | import org.jboss.netty.buffer.ChannelBuffer; 4 | import org.jboss.netty.channel.ChannelHandlerContext; 5 | import org.jboss.netty.channel.ChannelStateEvent; 6 | import org.jboss.netty.channel.MessageEvent; 7 | import org.jboss.netty.channel.SimpleChannelUpstreamHandler; 8 | import org.jboss.netty.channel.WriteCompletionEvent; 9 | 10 | import java.util.concurrent.atomic.AtomicLong; 11 | 12 | public class ByteCounter extends SimpleChannelUpstreamHandler { 13 | 14 | // internal vars -------------------------------------------------------------------------------------------------- 15 | 16 | private final String id; 17 | private final AtomicLong writtenBytes; 18 | private final AtomicLong readBytes; 19 | 20 | // constructors --------------------------------------------------------------------------------------------------- 21 | 22 | public ByteCounter(String id) { 23 | this.id = id; 24 | this.writtenBytes = new AtomicLong(); 25 | this.readBytes = new AtomicLong(); 26 | } 27 | 28 | // SimpleChannelUpstreamHandler ----------------------------------------------------------------------------------- 29 | 30 | @Override 31 | public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) 32 | throws Exception { 33 | if (e.getMessage() instanceof ChannelBuffer) { 34 | this.readBytes.addAndGet(((ChannelBuffer) e.getMessage()) 35 | .readableBytes()); 36 | } 37 | 38 | super.messageReceived(ctx, e); 39 | } 40 | 41 | @Override 42 | public void writeComplete(ChannelHandlerContext ctx, WriteCompletionEvent e) 43 | throws Exception { 44 | super.writeComplete(ctx, e); 45 | this.writtenBytes.addAndGet(e.getWrittenAmount()); 46 | } 47 | 48 | @Override 49 | public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) 50 | throws Exception { 51 | super.channelClosed(ctx, e); 52 | System.out.println(this.id + ctx.getChannel() + " -> sent: " + 53 | this.getWrittenBytes() + "b, recv: " + 54 | this.getReadBytes() + "b"); 55 | } 56 | 57 | // getters & setters ---------------------------------------------------------------------------------------------- 58 | 59 | public long getWrittenBytes() { 60 | return writtenBytes.get(); 61 | } 62 | 63 | public long getReadBytes() { 64 | return readBytes.get(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /customcodecs/src/main/java/com/biasedbit/nettytutorials/customcodecs/common/Decoder.java: -------------------------------------------------------------------------------- 1 | package com.biasedbit.nettytutorials.customcodecs.common; 2 | 3 | import org.jboss.netty.buffer.ChannelBuffer; 4 | import org.jboss.netty.channel.Channel; 5 | import org.jboss.netty.channel.ChannelHandlerContext; 6 | import org.jboss.netty.handler.codec.replay.ReplayingDecoder; 7 | 8 | public class Decoder extends ReplayingDecoder { 9 | 10 | // internal vars -------------------------------------------------------------------------------------------------- 11 | 12 | private Envelope message; 13 | 14 | // constructors --------------------------------------------------------------------------------------------------- 15 | 16 | public Decoder() { 17 | this.reset(); 18 | } 19 | 20 | // ReplayingDecoder ----------------------------------------------------------------------------------------------- 21 | 22 | @Override 23 | protected Object decode(ChannelHandlerContext ctx, Channel channel, 24 | ChannelBuffer buffer, DecodingState state) 25 | throws Exception { 26 | // notice the switch fall-through 27 | switch (state) { 28 | case VERSION: 29 | this.message.setVersion(Version.fromByte(buffer.readByte())); 30 | checkpoint(DecodingState.TYPE); 31 | case TYPE: 32 | this.message.setType(Type.fromByte(buffer.readByte())); 33 | checkpoint(DecodingState.PAYLOAD_LENGTH); 34 | case PAYLOAD_LENGTH: 35 | int size = buffer.readInt(); 36 | if (size <= 0) { 37 | throw new Exception("Invalid content size"); 38 | } 39 | // pre-allocate content buffer 40 | byte[] content = new byte[size]; 41 | this.message.setPayload(content); 42 | checkpoint(DecodingState.PAYLOAD); 43 | case PAYLOAD: 44 | // drain the channel buffer to the message content buffer 45 | // I have no idea what the contents are, but I'm sure you'll figure out how to turn these 46 | // bytes into useful content. 47 | buffer.readBytes(this.message.getPayload(), 0, 48 | this.message.getPayload().length); 49 | 50 | // This is the only exit point of this method (except for the two other exceptions that 51 | // should never occur). 52 | // Whenever there aren't enough bytes, a special exception is thrown by the channel buffer 53 | // and automatically handled by netty. That's why all conditions in the switch fall through 54 | try { 55 | // return the instance var and reset this decoder state after doing so. 56 | return this.message; 57 | } finally { 58 | this.reset(); 59 | } 60 | default: 61 | throw new Exception("Unknown decoding state: " + state); 62 | } 63 | } 64 | 65 | // private helpers ------------------------------------------------------------------------------------------------ 66 | 67 | private void reset() { 68 | checkpoint(DecodingState.VERSION); 69 | this.message = new Envelope(); 70 | } 71 | 72 | // private classes ------------------------------------------------------------------------------------------------ 73 | 74 | public enum DecodingState { 75 | 76 | // constants -------------------------------------------------------------------------------------------------- 77 | 78 | VERSION, 79 | TYPE, 80 | PAYLOAD_LENGTH, 81 | PAYLOAD, 82 | } 83 | } -------------------------------------------------------------------------------- /customcodecs/src/main/java/com/biasedbit/nettytutorials/customcodecs/common/Encoder.java: -------------------------------------------------------------------------------- 1 | package com.biasedbit.nettytutorials.customcodecs.common; 2 | 3 | import org.jboss.netty.buffer.ChannelBuffer; 4 | import org.jboss.netty.buffer.ChannelBuffers; 5 | import org.jboss.netty.channel.Channel; 6 | import org.jboss.netty.channel.ChannelHandler; 7 | import org.jboss.netty.channel.ChannelHandlerContext; 8 | import org.jboss.netty.handler.codec.oneone.OneToOneEncoder; 9 | 10 | /** 11 | * OneToOneEncoder implementation that converts an Envelope instance into a ChannelBuffer. 12 | * 13 | * Since the encoder is stateless, a single instance can be shared among all pipelines, hence the @Sharable annotation 14 | * and the singleton instantiation. 15 | */ 16 | @ChannelHandler.Sharable 17 | public class Encoder extends OneToOneEncoder { 18 | 19 | // constructors --------------------------------------------------------------------------------------------------- 20 | 21 | private Encoder() { 22 | } 23 | 24 | // public static methods ------------------------------------------------------------------------------------------ 25 | 26 | public static Encoder getInstance() { 27 | return InstanceHolder.INSTANCE; 28 | } 29 | 30 | public static ChannelBuffer encodeMessage(Envelope message) throws IllegalArgumentException { 31 | // you can move these verifications "upper" (before writing to the channel) in order not to cause a 32 | // channel shutdown. 33 | if ((message.getVersion() == null) || (message.getVersion() == Version.UNKNOWN)) { 34 | throw new IllegalArgumentException("Message version cannot be null or UNKNOWN"); 35 | } 36 | 37 | if ((message.getType() == null) || (message.getType() == Type.UNKNOWN)) { 38 | throw new IllegalArgumentException("Message type cannot be null or UNKNOWN"); 39 | } 40 | 41 | if ((message.getPayload() == null) || (message.getPayload().length == 0)) { 42 | throw new IllegalArgumentException("Message payload cannot be null or empty"); 43 | } 44 | 45 | // version(1b) + type(1b) + payload length(4b) + payload(nb) 46 | int size = 6 + message.getPayload().length; 47 | 48 | ChannelBuffer buffer = ChannelBuffers.buffer(size); 49 | buffer.writeByte(message.getVersion().getByteValue()); 50 | buffer.writeByte(message.getType().getByteValue()); 51 | buffer.writeInt(message.getPayload().length); 52 | buffer.writeBytes(message.getPayload()); 53 | 54 | return buffer; 55 | } 56 | 57 | // OneToOneEncoder ------------------------------------------------------------------------------------------------ 58 | 59 | @Override 60 | protected Object encode(ChannelHandlerContext channelHandlerContext, Channel channel, Object msg) throws Exception { 61 | if (msg instanceof Envelope) { 62 | return encodeMessage((Envelope) msg); 63 | } else { 64 | return msg; 65 | } 66 | } 67 | 68 | // private classes ------------------------------------------------------------------------------------------------ 69 | 70 | private static final class InstanceHolder { 71 | private static final Encoder INSTANCE = new Encoder(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /customcodecs/src/main/java/com/biasedbit/nettytutorials/customcodecs/common/Envelope.java: -------------------------------------------------------------------------------- 1 | package com.biasedbit.nettytutorials.customcodecs.common; 2 | 3 | import java.io.Serializable; 4 | 5 | public class Envelope implements Serializable { 6 | 7 | // internal vars -------------------------------------------------------------------------------------------------- 8 | 9 | private Version version; 10 | private Type type; 11 | private byte[] payload; 12 | 13 | // constructors --------------------------------------------------------------------------------------------------- 14 | 15 | public Envelope() { 16 | } 17 | 18 | public Envelope(Version version, Type type, byte[] payload) { 19 | this.version = version; 20 | this.type = type; 21 | this.payload = payload; 22 | } 23 | 24 | public Version getVersion() { 25 | return version; 26 | } 27 | 28 | public void setVersion(Version version) { 29 | this.version = version; 30 | } 31 | 32 | public Type getType() { 33 | return type; 34 | } 35 | 36 | public void setType(Type type) { 37 | this.type = type; 38 | } 39 | 40 | public byte[] getPayload() { 41 | return payload; 42 | } 43 | 44 | public void setPayload(byte[] payload) { 45 | this.payload = payload; 46 | } 47 | 48 | // low level overrides -------------------------------------------------------------------------------------------- 49 | 50 | @Override 51 | public String toString() { 52 | return new StringBuilder() 53 | .append("Envelope{") 54 | .append("version=").append(version) 55 | .append(", type=").append(type) 56 | .append(", payload=").append(payload == null ? null : payload.length + "bytes") 57 | .append('}').toString(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /customcodecs/src/main/java/com/biasedbit/nettytutorials/customcodecs/common/Type.java: -------------------------------------------------------------------------------- 1 | package com.biasedbit.nettytutorials.customcodecs.common; 2 | 3 | /** 4 | * Envelope type enum. 5 | */ 6 | public enum Type { 7 | 8 | // constants ------------------------------------------------------------------------------------------------------ 9 | 10 | REQUEST((byte) 0x01), 11 | RESPONSE((byte) 0x02), 12 | KEEP_ALIVE((byte) 0x03), 13 | // put last since it's the least likely one to be encountered in the fromByte() function 14 | UNKNOWN((byte) 0x00); 15 | 16 | // internal vars -------------------------------------------------------------------------------------------------- 17 | 18 | private final byte b; 19 | 20 | // constructors --------------------------------------------------------------------------------------------------- 21 | 22 | private Type(byte b) { 23 | this.b = b; 24 | } 25 | 26 | // public static methods ------------------------------------------------------------------------------------------ 27 | 28 | public static Type fromByte(byte b) { 29 | for (Type code : values()) { 30 | if (code.b == b) { 31 | return code; 32 | } 33 | } 34 | 35 | return UNKNOWN; 36 | } 37 | 38 | // getters & setters ---------------------------------------------------------------------------------------------- 39 | 40 | public byte getByteValue() { 41 | return b; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /customcodecs/src/main/java/com/biasedbit/nettytutorials/customcodecs/common/Version.java: -------------------------------------------------------------------------------- 1 | package com.biasedbit.nettytutorials.customcodecs.common; 2 | 3 | /** 4 | * Envelope version enum. 5 | */ 6 | public enum Version { 7 | 8 | // constants ------------------------------------------------------------------------------------------------------ 9 | 10 | VERSION1((byte) 0x01), 11 | VERSION2((byte) 0x02), 12 | // put last since it's the least likely one to be encountered in the fromByte() function 13 | UNKNOWN((byte) 0x00); 14 | 15 | // internal vars -------------------------------------------------------------------------------------------------- 16 | 17 | private final byte b; 18 | 19 | // constructors --------------------------------------------------------------------------------------------------- 20 | 21 | private Version(byte b) { 22 | this.b = b; 23 | } 24 | 25 | // public static methods ------------------------------------------------------------------------------------------ 26 | 27 | public static Version fromByte(byte b) { 28 | for (Version code : values()) { 29 | if (code.b == b) { 30 | return code; 31 | } 32 | } 33 | 34 | return UNKNOWN; 35 | } 36 | 37 | // getters & setters ---------------------------------------------------------------------------------------------- 38 | 39 | public byte getByteValue() { 40 | return b; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /customcodecs/src/main/java/com/biasedbit/nettytutorials/customcodecs/server/Server.java: -------------------------------------------------------------------------------- 1 | package com.biasedbit.nettytutorials.customcodecs.server; 2 | 3 | import com.biasedbit.nettytutorials.customcodecs.common.Decoder; 4 | import com.biasedbit.nettytutorials.customcodecs.common.Encoder; 5 | import org.jboss.netty.bootstrap.ServerBootstrap; 6 | import org.jboss.netty.channel.Channel; 7 | import org.jboss.netty.channel.ChannelPipeline; 8 | import org.jboss.netty.channel.ChannelPipelineFactory; 9 | import org.jboss.netty.channel.Channels; 10 | import org.jboss.netty.channel.ServerChannelFactory; 11 | import org.jboss.netty.channel.group.DefaultChannelGroup; 12 | import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory; 13 | 14 | import java.net.InetSocketAddress; 15 | import java.util.concurrent.Executors; 16 | 17 | public class Server { 18 | 19 | // configuration -------------------------------------------------------------------------------------------------- 20 | 21 | private final String host; 22 | private final int port; 23 | private DefaultChannelGroup channelGroup; 24 | private ServerChannelFactory serverFactory; 25 | 26 | // constructors --------------------------------------------------------------------------------------------------- 27 | 28 | public Server(String host, int port) { 29 | this.host = host; 30 | this.port = port; 31 | } 32 | 33 | // public methods ------------------------------------------------------------------------------------------------- 34 | 35 | public boolean start() { 36 | 37 | this.serverFactory = new NioServerSocketChannelFactory(Executors.newCachedThreadPool(), 38 | Executors.newCachedThreadPool()); 39 | this.channelGroup = new DefaultChannelGroup(this + "-channelGroup"); 40 | ChannelPipelineFactory pipelineFactory = new ChannelPipelineFactory() { 41 | 42 | @Override 43 | public ChannelPipeline getPipeline() throws Exception { 44 | ChannelPipeline pipeline = Channels.pipeline(); 45 | pipeline.addLast("encoder", Encoder.getInstance()); 46 | pipeline.addLast("decoder", new Decoder()); 47 | pipeline.addLast("handler", new ServerHandler(channelGroup)); 48 | return pipeline; 49 | } 50 | }; 51 | 52 | ServerBootstrap bootstrap = new ServerBootstrap(this.serverFactory); 53 | bootstrap.setOption("reuseAddress", true); 54 | bootstrap.setOption("child.tcpNoDelay", true); 55 | bootstrap.setOption("child.keepAlive", true); 56 | bootstrap.setPipelineFactory(pipelineFactory); 57 | 58 | Channel channel = bootstrap.bind(new InetSocketAddress(this.host, this.port)); 59 | if (!channel.isBound()) { 60 | this.stop(); 61 | return false; 62 | } 63 | 64 | this.channelGroup.add(channel); 65 | return true; 66 | } 67 | 68 | public void stop() { 69 | if (this.channelGroup != null) { 70 | this.channelGroup.close(); 71 | } 72 | if (this.serverFactory != null) { 73 | this.serverFactory.releaseExternalResources(); 74 | } 75 | } 76 | 77 | // main ----------------------------------------------------------------------------------------------------------- 78 | 79 | public static void main(String[] args) { 80 | final Server server = new Server("localhost", 9999); 81 | 82 | if (!server.start()) { 83 | 84 | System.exit(-1); 85 | return; // not really needed... 86 | } 87 | 88 | System.out.println("Server started..."); 89 | 90 | Runtime.getRuntime().addShutdownHook(new Thread() { 91 | @Override 92 | public void run() { 93 | server.stop(); 94 | } 95 | }); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /customcodecs/src/main/java/com/biasedbit/nettytutorials/customcodecs/server/ServerHandler.java: -------------------------------------------------------------------------------- 1 | package com.biasedbit.nettytutorials.customcodecs.server; 2 | 3 | import com.biasedbit.nettytutorials.customcodecs.common.Envelope; 4 | import org.jboss.netty.channel.ChannelHandlerContext; 5 | import org.jboss.netty.channel.ChannelStateEvent; 6 | import org.jboss.netty.channel.MessageEvent; 7 | import org.jboss.netty.channel.SimpleChannelUpstreamHandler; 8 | import org.jboss.netty.channel.group.ChannelGroup; 9 | 10 | public class ServerHandler extends SimpleChannelUpstreamHandler { 11 | 12 | // internal vars -------------------------------------------------------------------------------------------------- 13 | 14 | private final ChannelGroup channelGroup; 15 | 16 | // constructors --------------------------------------------------------------------------------------------------- 17 | 18 | public ServerHandler(ChannelGroup channelGroup) { 19 | this.channelGroup = channelGroup; 20 | } 21 | 22 | // SimpleChannelUpstreamHandler ----------------------------------------------------------------------------------- 23 | 24 | @Override 25 | public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception { 26 | this.channelGroup.add(e.getChannel()); 27 | } 28 | 29 | @Override 30 | public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { 31 | if (e.getMessage() instanceof Envelope) { 32 | // echo it... 33 | e.getChannel().write(e.getMessage()); 34 | } else { 35 | super.messageReceived(ctx, e); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /customcodecs/src/main/java/com/biasedbit/nettytutorials/customcodecs/server/ServerJavaSerialization.java: -------------------------------------------------------------------------------- 1 | package com.biasedbit.nettytutorials.customcodecs.server; 2 | 3 | import com.biasedbit.nettytutorials.customcodecs.common.Decoder; 4 | import com.biasedbit.nettytutorials.customcodecs.common.Encoder; 5 | import org.jboss.netty.bootstrap.ServerBootstrap; 6 | import org.jboss.netty.channel.Channel; 7 | import org.jboss.netty.channel.ChannelPipeline; 8 | import org.jboss.netty.channel.ChannelPipelineFactory; 9 | import org.jboss.netty.channel.Channels; 10 | import org.jboss.netty.channel.ServerChannelFactory; 11 | import org.jboss.netty.channel.group.DefaultChannelGroup; 12 | import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory; 13 | import org.jboss.netty.handler.codec.serialization.ObjectDecoder; 14 | import org.jboss.netty.handler.codec.serialization.ObjectEncoder; 15 | 16 | import java.net.InetSocketAddress; 17 | import java.util.concurrent.Executors; 18 | 19 | public class ServerJavaSerialization { 20 | 21 | // configuration -------------------------------------------------------------------------------------------------- 22 | 23 | private final String host; 24 | private final int port; 25 | private DefaultChannelGroup channelGroup; 26 | private ServerChannelFactory serverFactory; 27 | 28 | // constructors --------------------------------------------------------------------------------------------------- 29 | 30 | public ServerJavaSerialization(String host, int port) { 31 | this.host = host; 32 | this.port = port; 33 | } 34 | 35 | // public methods ------------------------------------------------------------------------------------------------- 36 | 37 | public boolean start() { 38 | 39 | this.serverFactory = new NioServerSocketChannelFactory(Executors.newCachedThreadPool(), 40 | Executors.newCachedThreadPool()); 41 | this.channelGroup = new DefaultChannelGroup(this + "-channelGroup"); 42 | ChannelPipelineFactory pipelineFactory = new ChannelPipelineFactory() { 43 | 44 | @Override 45 | public ChannelPipeline getPipeline() throws Exception { 46 | ChannelPipeline pipeline = Channels.pipeline(); 47 | pipeline.addLast("encoder", new ObjectEncoder()); 48 | pipeline.addLast("decoder", new ObjectDecoder()); 49 | pipeline.addLast("handler", new ServerHandler(channelGroup)); 50 | return pipeline; 51 | } 52 | }; 53 | 54 | ServerBootstrap bootstrap = new ServerBootstrap(this.serverFactory); 55 | bootstrap.setOption("reuseAddress", true); 56 | bootstrap.setOption("child.tcpNoDelay", true); 57 | bootstrap.setOption("child.keepAlive", true); 58 | bootstrap.setPipelineFactory(pipelineFactory); 59 | 60 | Channel channel = bootstrap.bind(new InetSocketAddress(this.host, this.port)); 61 | if (!channel.isBound()) { 62 | this.stop(); 63 | return false; 64 | } 65 | 66 | this.channelGroup.add(channel); 67 | return true; 68 | } 69 | 70 | public void stop() { 71 | if (this.channelGroup != null) { 72 | this.channelGroup.close(); 73 | } 74 | if (this.serverFactory != null) { 75 | this.serverFactory.releaseExternalResources(); 76 | } 77 | } 78 | 79 | // main ----------------------------------------------------------------------------------------------------------- 80 | 81 | public static void main(String[] args) { 82 | final ServerJavaSerialization server = new ServerJavaSerialization("localhost", 9999); 83 | 84 | if (!server.start()) { 85 | 86 | System.exit(-1); 87 | return; // not really needed... 88 | } 89 | 90 | System.out.println("Server started..."); 91 | 92 | Runtime.getRuntime().addShutdownHook(new Thread() { 93 | @Override 94 | public void run() { 95 | server.stop(); 96 | } 97 | }); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /handshake/README.md: -------------------------------------------------------------------------------- 1 | Netty tutorials - handshaking 2 | ============================= 3 | 4 | This is a tutorial that demonstrates how to factor out handshaking logic into a specialized client/server handshake handler so that it can be removed as soon as it's unnecessary, keeping the final handlers as simple as they should be. 5 | 6 | It's the backing code for the post at http://bruno.biasedbit.com/blag/2010/07/15/handshaking-tutorial-with-netty/ -------------------------------------------------------------------------------- /handshake/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | com.biasedbit.nettytutorials 6 | handshake 7 | 1.0.0 8 | jar 9 | 10 | netty tutorial - handshaking 11 | http://bruno.biasedbit.com/blag/ 12 | 13 | Netty tutorials - handshaking 14 | 15 | 2010 16 | 17 | 18 | Bruno de Carvalho 19 | bruno@biasedbit.com 20 | http://bruno.biasedbit.com 21 | 22 | 23 | 24 | 25 | 26 | Apache License, Version 2.0 27 | http://www.apache.org/licenses/LICENSE-2.0 28 | 29 | 30 | 31 | 32 | UTF-8 33 | 34 | 35 | 36 | 37 | repository.jboss.org 38 | http://repository.jboss.org/nexus/content/groups/public/ 39 | 40 | false 41 | 42 | 43 | 44 | 45 | 46 | 47 | junit 48 | junit 49 | 4.7 50 | test 51 | 52 | 53 | org.jboss.netty 54 | netty 55 | 3.2.3.Final 56 | compile 57 | 58 | 59 | org.slf4j 60 | slf4j-api 61 | 1.6.1 62 | compile 63 | 64 | 65 | 66 | 67 | true 68 | org.slf4j 69 | slf4j-log4j12 70 | 1.6.1 71 | runtime 72 | 73 | 74 | true 75 | log4j 76 | log4j 77 | 1.2.16 78 | runtime 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | maven-compiler-plugin 87 | 2.0.2 88 | 89 | 1.6 90 | 1.6 91 | 92 | 93 | 94 | 95 | 96 | maven-surefire-plugin 97 | 2.4.3 98 | 99 | never 100 | 101 | **/Abstract* 102 | **/*TestUtil* 103 | 104 | 105 | 106 | 107 | 108 | 109 | org.apache.maven.plugins 110 | maven-idea-plugin 111 | 2.2 112 | 113 | true 114 | 7 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /handshake/src/main/java/com/biasedbit/nettytutorials/handshake/ClientRunner.java: -------------------------------------------------------------------------------- 1 | package com.biasedbit.nettytutorials.handshake; 2 | 3 | import com.biasedbit.nettytutorials.handshake.client.Client; 4 | import com.biasedbit.nettytutorials.handshake.client.ClientListener; 5 | 6 | import java.util.concurrent.CountDownLatch; 7 | import java.util.concurrent.ExecutorService; 8 | import java.util.concurrent.Executors; 9 | import java.util.concurrent.TimeUnit; 10 | import java.util.concurrent.atomic.AtomicInteger; 11 | 12 | /** 13 | * @author Bruno de Carvalho 14 | */ 15 | public class ClientRunner { 16 | 17 | public static void runClient(final String id, final String serverId, 18 | final int nMessages) 19 | throws InterruptedException { 20 | 21 | final AtomicInteger cLast = new AtomicInteger(); 22 | final AtomicInteger clientCounter = new AtomicInteger(); 23 | final CountDownLatch latch = new CountDownLatch(1); 24 | 25 | // Create a client with custom id, that connects to a server with given 26 | // id and has a message listener that ensures that ALL messages are 27 | // received in perfect order. 28 | Client c = new Client(id, serverId, new ClientListener() { 29 | @Override 30 | public void messageReceived(String message) { 31 | int num = Integer.parseInt(message.trim()); 32 | if (num != (cLast.get() + 1)) { 33 | System.err.println("--- CLIENT-LISTENER(" + id + ") " + 34 | ":: OUT OF ORDER!!! expecting " + 35 | (cLast.get() + 1) + " and got " + 36 | message); 37 | } else { 38 | cLast.set(num); 39 | } 40 | 41 | if (clientCounter.incrementAndGet() >= nMessages) { 42 | latch.countDown(); 43 | } 44 | } 45 | }); 46 | 47 | if (!c.start()) { 48 | return; 49 | } 50 | 51 | for (int i = 0; i < nMessages; i++) { 52 | // This sleep here prevents all messages to be instantly queued 53 | // in the handshake message queue. Since handshake takes some time, 54 | // all messages sent during handshake will be queued (and later on 55 | // flushed). 56 | // Since we want to test the effect of removing the handshake 57 | // handler from the pipeline (and ensure that message order is 58 | // preserved), this sleep helps us accomplish that with a random 59 | // factor. 60 | // If lucky, a couple of messages will even hit the handshake 61 | // handler *after* the handshake has been completed but right 62 | // before the handshake handler is removed from the pipeline. 63 | // Worry not, that case is also covered :) 64 | Thread.sleep(1L); 65 | c.sendMessage((i + 1) + "\n"); 66 | } 67 | 68 | // Run the client for some time, then shut it down. 69 | latch.await(10, TimeUnit.SECONDS); 70 | c.stop(); 71 | } 72 | 73 | public static void main(String[] args) throws InterruptedException { 74 | // More clients will test robustness of the server, but output becomes 75 | // more confusing. 76 | int nClients = 1; 77 | final int nMessages = 10000; 78 | // Changing this value to something different than the server's id 79 | // will cause handshaking to fail. 80 | final String serverId = "server1"; 81 | ExecutorService threadPool = Executors.newCachedThreadPool(); 82 | for (int i = 0; i < nClients; i++) { 83 | final int finalI = i; 84 | threadPool.submit(new Runnable() { 85 | @Override 86 | public void run() { 87 | try { 88 | ClientRunner.runClient("client" + finalI, serverId, 89 | nMessages); 90 | } catch (InterruptedException e) { 91 | e.printStackTrace(); 92 | } 93 | } 94 | }); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /handshake/src/main/java/com/biasedbit/nettytutorials/handshake/ServerRunner.java: -------------------------------------------------------------------------------- 1 | package com.biasedbit.nettytutorials.handshake; 2 | 3 | import com.biasedbit.nettytutorials.handshake.server.Server; 4 | import com.biasedbit.nettytutorials.handshake.server.ServerHandler; 5 | import com.biasedbit.nettytutorials.handshake.server.ServerListener; 6 | 7 | import java.util.Map; 8 | import java.util.concurrent.ConcurrentHashMap; 9 | import java.util.concurrent.atomic.AtomicInteger; 10 | 11 | /** 12 | * @author Bruno de Carvalho 13 | */ 14 | public class ServerRunner { 15 | 16 | public static void main(String[] args) { 17 | final Map lastMap = 18 | new ConcurrentHashMap(); 19 | 20 | // Create a new server with id "server1" with a listener that ensures 21 | // that for each handler, perfect message order is guaranteed. 22 | final Server s = new Server("server1", new ServerListener() { 23 | 24 | @Override 25 | public void messageReceived(ServerHandler handler, 26 | String message) { 27 | AtomicInteger last = lastMap.get(handler); 28 | int num = Integer.parseInt(message.trim()); 29 | if (num != (last.get() + 1)) { 30 | System.err.println("+++ SERVER-LISTENER(" + 31 | handler.getRemoteId() + ") :: " + 32 | "OUT OF ORDER!!! expecting " + 33 | (last.get() + 1) + " and got " + 34 | message); 35 | } else { 36 | last.set(num); 37 | } 38 | 39 | handler.sendMessage(message); 40 | } 41 | 42 | @Override 43 | public void connectionOpen(ServerHandler handler) { 44 | System.err.println("+++ SERVER-LISTENER(" + 45 | handler.getRemoteId() + 46 | ") :: Connection with " + 47 | handler.getRemoteId() + 48 | " opened & ready to send/receive data."); 49 | AtomicInteger counter = new AtomicInteger(); 50 | lastMap.put(handler, counter); 51 | } 52 | }); 53 | 54 | if (!s.start()) { 55 | return; 56 | } 57 | 58 | Runtime.getRuntime().addShutdownHook(new Thread() { 59 | @Override 60 | public void run() { 61 | s.stop(); 62 | } 63 | }); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /handshake/src/main/java/com/biasedbit/nettytutorials/handshake/client/Client.java: -------------------------------------------------------------------------------- 1 | package com.biasedbit.nettytutorials.handshake.client; 2 | 3 | import com.biasedbit.nettytutorials.handshake.common.ByteCounter; 4 | import com.biasedbit.nettytutorials.handshake.common.MessageCounter; 5 | import org.jboss.netty.bootstrap.ClientBootstrap; 6 | import org.jboss.netty.channel.Channel; 7 | import org.jboss.netty.channel.ChannelFactory; 8 | import org.jboss.netty.channel.ChannelFuture; 9 | import org.jboss.netty.channel.ChannelPipeline; 10 | import org.jboss.netty.channel.ChannelPipelineFactory; 11 | import org.jboss.netty.channel.Channels; 12 | import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory; 13 | import org.jboss.netty.handler.codec.frame.DelimiterBasedFrameDecoder; 14 | import org.jboss.netty.handler.codec.frame.Delimiters; 15 | import org.jboss.netty.handler.codec.string.StringDecoder; 16 | import org.jboss.netty.handler.codec.string.StringEncoder; 17 | 18 | import java.net.InetSocketAddress; 19 | import java.util.concurrent.Executor; 20 | import java.util.concurrent.Executors; 21 | 22 | /** 23 | * @author Bruno de Carvalho 24 | */ 25 | public class Client { 26 | 27 | // internal vars ---------------------------------------------------------- 28 | 29 | private final String id; 30 | private final String serverId; 31 | private final ClientListener listener; 32 | private ClientBootstrap bootstrap; 33 | private Channel connector; 34 | 35 | // constructors ----------------------------------------------------------- 36 | 37 | public Client(String id, String serverId, ClientListener listener) { 38 | this.id = id; 39 | this.serverId = serverId; 40 | this.listener = listener; 41 | } 42 | 43 | // public methods --------------------------------------------------------- 44 | 45 | public boolean start() { 46 | // Standard netty bootstrapping stuff. 47 | Executor bossPool = Executors.newCachedThreadPool(); 48 | Executor workerPool = Executors.newCachedThreadPool(); 49 | ChannelFactory factory = 50 | new NioClientSocketChannelFactory(bossPool, workerPool); 51 | this.bootstrap = new ClientBootstrap(factory); 52 | 53 | // Declared outside to fit under 80 char limit 54 | final DelimiterBasedFrameDecoder frameDecoder = 55 | new DelimiterBasedFrameDecoder(Integer.MAX_VALUE, 56 | Delimiters.lineDelimiter()); 57 | this.bootstrap.setPipelineFactory(new ChannelPipelineFactory() { 58 | public ChannelPipeline getPipeline() throws Exception { 59 | ByteCounter byteCounter = 60 | new ByteCounter("--- CLIENT-COUNTER :: "); 61 | MessageCounter messageCounter = 62 | new MessageCounter("--- CLIENT-MSGCOUNTER :: "); 63 | ClientHandshakeHandler handshakeHandler = 64 | new ClientHandshakeHandler(id, serverId, 5000); 65 | 66 | return Channels.pipeline(byteCounter, 67 | frameDecoder, 68 | new StringDecoder(), 69 | new StringEncoder(), 70 | messageCounter, 71 | handshakeHandler, 72 | new ClientHandler(listener)); 73 | } 74 | }); 75 | 76 | ChannelFuture future = this.bootstrap 77 | .connect(new InetSocketAddress("localhost", 12345)); 78 | if (!future.awaitUninterruptibly().isSuccess()) { 79 | System.out.println("--- CLIENT - Failed to connect to server at " + 80 | "localhost:12345."); 81 | this.bootstrap.releaseExternalResources(); 82 | return false; 83 | } 84 | 85 | this.connector = future.getChannel(); 86 | return this.connector.isConnected(); 87 | } 88 | 89 | public void stop() { 90 | if (this.connector != null) { 91 | this.connector.close().awaitUninterruptibly(); 92 | } 93 | this.bootstrap.releaseExternalResources(); 94 | System.out.println("--- CLIENT - Stopped."); 95 | } 96 | 97 | public boolean sendMessage(String message) { 98 | if (this.connector.isConnected()) { 99 | // Append \n if it's not present, because of the frame delimiter 100 | if (!message.endsWith("\n")) { 101 | this.connector.write(message + '\n'); 102 | } else { 103 | this.connector.write(message); 104 | } 105 | return true; 106 | } 107 | 108 | return false; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /handshake/src/main/java/com/biasedbit/nettytutorials/handshake/client/ClientHandler.java: -------------------------------------------------------------------------------- 1 | package com.biasedbit.nettytutorials.handshake.client; 2 | 3 | import com.biasedbit.nettytutorials.handshake.common.HandshakeEvent; 4 | import org.jboss.netty.channel.ChannelEvent; 5 | import org.jboss.netty.channel.ChannelHandlerContext; 6 | import org.jboss.netty.channel.ChannelStateEvent; 7 | import org.jboss.netty.channel.MessageEvent; 8 | import org.jboss.netty.channel.SimpleChannelUpstreamHandler; 9 | 10 | import java.util.concurrent.atomic.AtomicInteger; 11 | 12 | /** 13 | * @author Bruno de Carvalho 14 | */ 15 | public class ClientHandler extends SimpleChannelUpstreamHandler { 16 | 17 | // internal vars ---------------------------------------------------------- 18 | 19 | private final AtomicInteger counter; 20 | private final ClientListener listener; 21 | 22 | // constructors ----------------------------------------------------------- 23 | 24 | public ClientHandler(ClientListener listener) { 25 | this.listener = listener; 26 | this.counter = new AtomicInteger(); 27 | } 28 | 29 | // SimpleChannelUpstreamHandler ------------------------------------------- 30 | 31 | @Override 32 | public void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e) 33 | throws Exception { 34 | if (e instanceof HandshakeEvent) { 35 | if (((HandshakeEvent) e).isSuccessful()) { 36 | out("--- CLIENT-HANDLER :: Handshake successful, connection " + 37 | "to " + ((HandshakeEvent) e).getRemoteId() + " is up."); 38 | } else { 39 | out("--- CLIENT-HANDLER :: Handshake failed."); 40 | } 41 | return; 42 | } 43 | 44 | super.handleUpstream(ctx, e); 45 | } 46 | 47 | @Override 48 | public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) 49 | throws Exception { 50 | this.counter.incrementAndGet(); 51 | this.listener.messageReceived(e.getMessage().toString()); 52 | } 53 | 54 | @Override 55 | public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) 56 | throws Exception { 57 | super.channelClosed(ctx, e); 58 | out("--- CLIENT-HANDLER :: Channel closed, received " + 59 | this.counter.get() + " messages: " + e.getChannel()); 60 | } 61 | 62 | // private static helpers ------------------------------------------------- 63 | 64 | private static void out(String s) { 65 | System.out.println(s); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /handshake/src/main/java/com/biasedbit/nettytutorials/handshake/client/ClientHandshakeHandler.java: -------------------------------------------------------------------------------- 1 | package com.biasedbit.nettytutorials.handshake.client; 2 | 3 | import com.biasedbit.nettytutorials.handshake.common.Challenge; 4 | import com.biasedbit.nettytutorials.handshake.common.HandshakeEvent; 5 | import org.jboss.netty.channel.Channel; 6 | import org.jboss.netty.channel.ChannelFuture; 7 | import org.jboss.netty.channel.ChannelFutureListener; 8 | import org.jboss.netty.channel.ChannelHandlerContext; 9 | import org.jboss.netty.channel.ChannelStateEvent; 10 | import org.jboss.netty.channel.Channels; 11 | import org.jboss.netty.channel.DownstreamMessageEvent; 12 | import org.jboss.netty.channel.ExceptionEvent; 13 | import org.jboss.netty.channel.MessageEvent; 14 | import org.jboss.netty.channel.SimpleChannelHandler; 15 | 16 | import java.util.ArrayDeque; 17 | import java.util.Queue; 18 | import java.util.concurrent.CountDownLatch; 19 | import java.util.concurrent.TimeUnit; 20 | import java.util.concurrent.atomic.AtomicBoolean; 21 | 22 | /** 23 | * @author Bruno de Carvalho 24 | */ 25 | public class ClientHandshakeHandler extends SimpleChannelHandler { 26 | 27 | // internal vars ---------------------------------------------------------- 28 | 29 | private final long timeoutInMillis; 30 | private final String localId; 31 | private final String remoteId; 32 | private final AtomicBoolean handshakeComplete; 33 | private final AtomicBoolean handshakeFailed; 34 | private final CountDownLatch latch = new CountDownLatch(1); 35 | private final Queue messages = new ArrayDeque(); 36 | private final Object handshakeMutex = new Object(); 37 | private String challenge; 38 | 39 | // constructors ----------------------------------------------------------- 40 | 41 | public ClientHandshakeHandler(String localId, String remoteId, 42 | long timeoutInMillis) { 43 | this.localId = localId; 44 | this.remoteId = remoteId; 45 | this.timeoutInMillis = timeoutInMillis; 46 | this.handshakeComplete = new AtomicBoolean(false); 47 | this.handshakeFailed = new AtomicBoolean(false); 48 | } 49 | 50 | // SimpleChannelHandler --------------------------------------------------- 51 | 52 | @Override 53 | public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) 54 | throws Exception { 55 | if (this.handshakeFailed.get()) { 56 | // Bail out fast if handshake already failed 57 | return; 58 | } 59 | 60 | if (this.handshakeComplete.get()) { 61 | // If handshake succeeded but message still came through this 62 | // handler, then immediately send it upwards. 63 | // Chances are it's the last time a message passes through 64 | // this handler... 65 | super.messageReceived(ctx, e); 66 | return; 67 | } 68 | 69 | synchronized (this.handshakeMutex) { 70 | // Recheck conditions after locking the mutex. 71 | // Things might have changed while waiting for the lock. 72 | if (this.handshakeFailed.get()) { 73 | return; 74 | } 75 | 76 | if (this.handshakeComplete.get()) { 77 | super.messageReceived(ctx, e); 78 | return; 79 | } 80 | 81 | // Parse the challenge. 82 | // Expected format is "clientId:serverId:challenge" 83 | String[] params = ((String) e.getMessage()).trim().split(":"); 84 | if (params.length != 3) { 85 | out("--- CLIENT-HS :: Invalid handshake: expected 3 params, " + 86 | "got " + params.length); 87 | this.fireHandshakeFailed(ctx); 88 | return; 89 | } 90 | 91 | // Silly validations... 92 | // 1. Validate that server replied correctly to this client's id. 93 | if (!params[0].equals(this.localId)) { 94 | out("--- CLIENT-HS == Handshake failed: local id is " + 95 | this.localId +" but challenge response is for '" + 96 | params[0] + "'"); 97 | this.fireHandshakeFailed(ctx); 98 | return; 99 | } 100 | 101 | // 2. Validate that asserted server id is its actual id. 102 | if (!params[1].equals(this.remoteId)) { 103 | out("--- CLIENT-HS :: Handshake failed: expecting remote id " + 104 | this.remoteId + " but got " + params[1]); 105 | this.fireHandshakeFailed(ctx); 106 | return; 107 | } 108 | 109 | // 3. Ensure that challenge response is correct. 110 | if (!Challenge.isValidResponse(params[2], this.challenge)) { 111 | out("--- CLIENT-HS :: Handshake failed: '" + params[2] + 112 | "' is not a valid response for challenge '" + 113 | this.challenge + "'"); 114 | this.fireHandshakeFailed(ctx); 115 | return; 116 | } 117 | 118 | // Everything went okay! 119 | out("--- CLIENT-HS :: Challenge validated, flushing messages & " + 120 | "removing handshake handler from pipeline."); 121 | 122 | // Flush messages *directly* downwards. 123 | // Calling ctx.getChannel().write() here would cause the messages 124 | // to be inserted at the top of the pipeline, thus causing them 125 | // to pass through this class's writeRequest() and be re-queued. 126 | out("--- CLIENT-HS :: " + this.messages.size() + 127 | " messages in queue to be flushed."); 128 | for (MessageEvent message : this.messages) { 129 | ctx.sendDownstream(message); 130 | } 131 | 132 | // Remove this handler from the pipeline; its job is finished. 133 | ctx.getPipeline().remove(this); 134 | 135 | // Finally fire success message upwards. 136 | this.fireHandshakeSucceeded(this.remoteId, ctx); 137 | } 138 | } 139 | 140 | @Override 141 | public void channelConnected(final ChannelHandlerContext ctx, 142 | ChannelStateEvent e) throws Exception { 143 | out("--- CLIENT-HS :: Outgoing connection established to: " + 144 | e.getChannel().getRemoteAddress()); 145 | 146 | // Write the handshake & add a timeout listener. 147 | ChannelFuture f = Channels.future(ctx.getChannel()); 148 | f.addListener(new ChannelFutureListener() { 149 | @Override 150 | public void operationComplete(ChannelFuture future) 151 | throws Exception { 152 | // Once this message is sent, start the timeout checker. 153 | new Thread() { 154 | @Override 155 | public void run() { 156 | // Wait until either handshake completes (releases the 157 | // latch) or this latch times out. 158 | try { 159 | latch.await(timeoutInMillis, TimeUnit.MILLISECONDS); 160 | } catch (InterruptedException e1) { 161 | out("--- CLIENT-HS :: Handshake timeout checker: " + 162 | "interrupted!"); 163 | e1.printStackTrace(); 164 | } 165 | 166 | // Informative output, do nothing... 167 | if (handshakeFailed.get()) { 168 | out("--- CLIENT-HS :: (pre-synchro) Handshake " + 169 | "timeout checker: discarded " + 170 | "(handshake failed)"); 171 | return; 172 | } 173 | 174 | // More informative output, do nothing... 175 | if (handshakeComplete.get()) { 176 | out("--- CLIENT-HS :: (pre-synchro) Handshake " + 177 | "timeout checker: discarded" + 178 | "(handshake completed)"); 179 | return; 180 | } 181 | 182 | // Handshake has neither failed nor completed, time 183 | // to do something! (trigger failure). 184 | // Lock on the mutex first... 185 | synchronized (handshakeMutex) { 186 | // Same checks as before, conditions might have 187 | // changed while waiting to get a lock on the 188 | // mutex. 189 | if (handshakeFailed.get()) { 190 | out("--- CLIENT-HS :: (synchro) Handshake " + 191 | "timeout checker: already failed."); 192 | return; 193 | } 194 | 195 | if (!handshakeComplete.get()) { 196 | // If handshake wasn't completed meanwhile, 197 | // time to mark the handshake as having failed. 198 | out("--- CLIENT-HS :: (synchro) Handshake " + 199 | "timeout checker: timed out, " + 200 | "killing connection."); 201 | fireHandshakeFailed(ctx); 202 | } else { 203 | // Informative output; the handshake was 204 | // completed while this thread was waiting 205 | // for a lock on the handshakeMutex. 206 | // Do nothing... 207 | out("--- CLIENT-HS :: (synchro) Handshake " + 208 | "timeout checker: discarded " + 209 | "(handshake OK)"); 210 | } 211 | } 212 | } 213 | }.start(); 214 | } 215 | }); 216 | 217 | this.challenge = Challenge.generateChallenge(); 218 | String handshake = 219 | this.localId + ':' + this.remoteId + ':' + challenge + '\n'; 220 | Channel c = ctx.getChannel(); 221 | // Passing null as remoteAddress, since constructor in 222 | // DownstreamMessageEvent will use remote address from the channel if 223 | // remoteAddress is null. 224 | // Also, we need to send the data directly downstream rather than 225 | // call c.write() otherwise the message would pass through this 226 | // class's writeRequested() method defined below. 227 | ctx.sendDownstream(new DownstreamMessageEvent(c, f, handshake, null)); 228 | } 229 | 230 | @Override 231 | public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) 232 | throws Exception { 233 | out("--- CLIENT-HS :: Channel closed."); 234 | if (!this.handshakeComplete.get()) { 235 | this.fireHandshakeFailed(ctx); 236 | } 237 | } 238 | 239 | @Override 240 | public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) 241 | throws Exception { 242 | out("--- CLIENT-HS :: Exception caught."); 243 | e.getCause().printStackTrace(); 244 | if (e.getChannel().isConnected()) { 245 | // Closing the channel will trigger handshake failure. 246 | e.getChannel().close(); 247 | } else { 248 | // Channel didn't open, so we must fire handshake failure directly. 249 | this.fireHandshakeFailed(ctx); 250 | } 251 | } 252 | 253 | @Override 254 | public void writeRequested(ChannelHandlerContext ctx, MessageEvent e) 255 | throws Exception { 256 | // Before doing anything, ensure that noone else is working by 257 | // acquiring a lock on the handshakeMutex. 258 | synchronized (this.handshakeMutex) { 259 | if (this.handshakeFailed.get()) { 260 | // If the handshake failed meanwhile, discard any messages. 261 | return; 262 | } 263 | 264 | // If the handshake hasn't failed but completed meanwhile and 265 | // messages still passed through this handler, then forward 266 | // them downwards. 267 | if (this.handshakeComplete.get()) { 268 | out("--- CLIENT-HS :: Handshake already completed, not " + 269 | "appending '" + e.getMessage().toString().trim() + 270 | "' to queue!"); 271 | super.writeRequested(ctx, e); 272 | } else { 273 | // Otherwise, queue messages in order until the handshake 274 | // completes. 275 | this.messages.offer(e); 276 | } 277 | } 278 | } 279 | 280 | // private static helpers ------------------------------------------------- 281 | 282 | private static void out(String s) { 283 | System.out.println(s); 284 | } 285 | 286 | // private helpers -------------------------------------------------------- 287 | 288 | private void fireHandshakeFailed(ChannelHandlerContext ctx) { 289 | this.handshakeComplete.set(true); 290 | this.handshakeFailed.set(true); 291 | this.latch.countDown(); 292 | ctx.getChannel().close(); 293 | ctx.sendUpstream(HandshakeEvent.handshakeFailed(ctx.getChannel())); 294 | } 295 | 296 | private void fireHandshakeSucceeded(String server, 297 | ChannelHandlerContext ctx) { 298 | this.handshakeComplete.set(true); 299 | this.handshakeFailed.set(false); 300 | this.latch.countDown(); 301 | ctx.sendUpstream(HandshakeEvent 302 | .handshakeSucceeded(server, ctx.getChannel())); 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /handshake/src/main/java/com/biasedbit/nettytutorials/handshake/client/ClientListener.java: -------------------------------------------------------------------------------- 1 | package com.biasedbit.nettytutorials.handshake.client; 2 | 3 | /** 4 | * @author Bruno de Carvalho 5 | */ 6 | public interface ClientListener { 7 | 8 | void messageReceived(String message); 9 | } 10 | -------------------------------------------------------------------------------- /handshake/src/main/java/com/biasedbit/nettytutorials/handshake/common/ByteCounter.java: -------------------------------------------------------------------------------- 1 | package com.biasedbit.nettytutorials.handshake.common; 2 | 3 | import org.jboss.netty.buffer.ChannelBuffer; 4 | import org.jboss.netty.channel.ChannelHandlerContext; 5 | import org.jboss.netty.channel.ChannelStateEvent; 6 | import org.jboss.netty.channel.MessageEvent; 7 | import org.jboss.netty.channel.SimpleChannelUpstreamHandler; 8 | import org.jboss.netty.channel.WriteCompletionEvent; 9 | 10 | import java.util.concurrent.atomic.AtomicLong; 11 | 12 | /** 13 | * @author Bruno de Carvalho 14 | */ 15 | public class ByteCounter extends SimpleChannelUpstreamHandler { 16 | 17 | // internal vars ---------------------------------------------------------- 18 | 19 | private final String id; 20 | private final AtomicLong writtenBytes; 21 | private final AtomicLong readBytes; 22 | 23 | // constructors ----------------------------------------------------------- 24 | 25 | public ByteCounter(String id) { 26 | this.id = id; 27 | this.writtenBytes = new AtomicLong(); 28 | this.readBytes = new AtomicLong(); 29 | } 30 | 31 | // SimpleChannelUpstreamHandler ------------------------------------------- 32 | 33 | @Override 34 | public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) 35 | throws Exception { 36 | if (e.getMessage() instanceof ChannelBuffer) { 37 | this.readBytes.addAndGet(((ChannelBuffer) e.getMessage()) 38 | .readableBytes()); 39 | } 40 | 41 | super.messageReceived(ctx, e); 42 | } 43 | 44 | @Override 45 | public void writeComplete(ChannelHandlerContext ctx, WriteCompletionEvent e) 46 | throws Exception { 47 | super.writeComplete(ctx, e); 48 | this.writtenBytes.addAndGet(e.getWrittenAmount()); 49 | } 50 | 51 | @Override 52 | public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) 53 | throws Exception { 54 | super.channelClosed(ctx, e); 55 | System.out.println(this.id + ctx.getChannel() + " -> sent: " + 56 | this.getWrittenBytes() + "b, recv: " + 57 | this.getReadBytes() + "b"); 58 | } 59 | 60 | // getters & setters ------------------------------------------------------ 61 | 62 | public long getWrittenBytes() { 63 | return writtenBytes.get(); 64 | } 65 | 66 | public long getReadBytes() { 67 | return readBytes.get(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /handshake/src/main/java/com/biasedbit/nettytutorials/handshake/common/Challenge.java: -------------------------------------------------------------------------------- 1 | package com.biasedbit.nettytutorials.handshake.common; 2 | 3 | /** 4 | * @author Bruno de Carvalho 5 | */ 6 | public class Challenge { 7 | 8 | // public static methods -------------------------------------------------- 9 | 10 | public static String generateChallenge() { 11 | return "challenge?"; 12 | } 13 | 14 | public static boolean isValidChallenge(String challenge) { 15 | return "challenge?".equals(challenge); 16 | } 17 | 18 | public static String generateResponse(String challenge) { 19 | if ("challenge?".equals(challenge)) { 20 | return "response!"; 21 | } else { 22 | return "invalidResponse!"; 23 | } 24 | } 25 | 26 | public static boolean isValidResponse(String response, String challenge) { 27 | return "response!".equals(response) && "challenge?".equals(challenge); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /handshake/src/main/java/com/biasedbit/nettytutorials/handshake/common/HandshakeEvent.java: -------------------------------------------------------------------------------- 1 | package com.biasedbit.nettytutorials.handshake.common; 2 | 3 | import org.jboss.netty.channel.Channel; 4 | import org.jboss.netty.channel.ChannelEvent; 5 | import org.jboss.netty.channel.ChannelFuture; 6 | import org.jboss.netty.channel.Channels; 7 | 8 | /** 9 | * @author Bruno de Carvalho 10 | */ 11 | public class HandshakeEvent implements ChannelEvent { 12 | 13 | // internal vars ---------------------------------------------------------- 14 | 15 | private final boolean successful; 16 | private final String remoteId; 17 | private final Channel channel; 18 | 19 | // constructors ----------------------------------------------------------- 20 | 21 | private HandshakeEvent(String remoteId, Channel channel) { 22 | this.remoteId = remoteId; 23 | this.successful = remoteId != null; 24 | this.channel = channel; 25 | } 26 | 27 | // public static methods -------------------------------------------------- 28 | 29 | public static HandshakeEvent handshakeSucceeded(String remoteId, 30 | Channel channel) { 31 | return new HandshakeEvent(remoteId, channel); 32 | } 33 | 34 | public static HandshakeEvent handshakeFailed(Channel channel) { 35 | return new HandshakeEvent(null, channel); 36 | } 37 | 38 | // ChannelEvent ----------------------------------------------------------- 39 | 40 | @Override 41 | public Channel getChannel() { 42 | return this.channel; 43 | } 44 | 45 | @Override 46 | public ChannelFuture getFuture() { 47 | return Channels.succeededFuture(this.channel); 48 | } 49 | 50 | // getters & setters ------------------------------------------------------ 51 | 52 | public boolean isSuccessful() { 53 | return successful; 54 | } 55 | 56 | public String getRemoteId() { 57 | return remoteId; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /handshake/src/main/java/com/biasedbit/nettytutorials/handshake/common/MessageCounter.java: -------------------------------------------------------------------------------- 1 | package com.biasedbit.nettytutorials.handshake.common; 2 | 3 | import org.jboss.netty.channel.ChannelHandlerContext; 4 | import org.jboss.netty.channel.ChannelStateEvent; 5 | import org.jboss.netty.channel.MessageEvent; 6 | import org.jboss.netty.channel.SimpleChannelHandler; 7 | 8 | import java.util.concurrent.atomic.AtomicLong; 9 | 10 | /** 11 | * @author Bruno de Carvalho 12 | */ 13 | public class MessageCounter extends SimpleChannelHandler { 14 | 15 | // internal vars ---------------------------------------------------------- 16 | 17 | private final String id; 18 | private final AtomicLong writtenMessages; 19 | private final AtomicLong readMessages; 20 | 21 | // constructors ----------------------------------------------------------- 22 | 23 | public MessageCounter(String id) { 24 | this.id = id; 25 | this.writtenMessages = new AtomicLong(); 26 | this.readMessages = new AtomicLong(); 27 | } 28 | 29 | // SimpleChannelHandler --------------------------------------------------- 30 | 31 | @Override 32 | public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) 33 | throws Exception { 34 | this.readMessages.incrementAndGet(); 35 | super.messageReceived(ctx, e); 36 | } 37 | 38 | @Override 39 | public void writeRequested(ChannelHandlerContext ctx, MessageEvent e) 40 | throws Exception { 41 | this.writtenMessages.incrementAndGet(); 42 | super.writeRequested(ctx, e); 43 | } 44 | 45 | @Override 46 | public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) 47 | throws Exception { 48 | super.channelClosed(ctx, e); 49 | System.out.println(this.id + ctx.getChannel() + " -> sent: " + 50 | this.getWrittenMessages() + ", recv: " + 51 | this.getReadMessages()); 52 | } 53 | 54 | // getters & setters ------------------------------------------------------ 55 | 56 | public long getWrittenMessages() { 57 | return writtenMessages.get(); 58 | } 59 | 60 | public long getReadMessages() { 61 | return readMessages.get(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /handshake/src/main/java/com/biasedbit/nettytutorials/handshake/server/Server.java: -------------------------------------------------------------------------------- 1 | package com.biasedbit.nettytutorials.handshake.server; 2 | 3 | import com.biasedbit.nettytutorials.handshake.common.ByteCounter; 4 | import com.biasedbit.nettytutorials.handshake.common.MessageCounter; 5 | import org.jboss.netty.bootstrap.ServerBootstrap; 6 | import org.jboss.netty.channel.Channel; 7 | import org.jboss.netty.channel.ChannelFactory; 8 | import org.jboss.netty.channel.ChannelHandler; 9 | import org.jboss.netty.channel.ChannelPipeline; 10 | import org.jboss.netty.channel.ChannelPipelineFactory; 11 | import org.jboss.netty.channel.Channels; 12 | import org.jboss.netty.channel.group.ChannelGroup; 13 | import org.jboss.netty.channel.group.DefaultChannelGroup; 14 | import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory; 15 | import org.jboss.netty.handler.codec.frame.DelimiterBasedFrameDecoder; 16 | import org.jboss.netty.handler.codec.frame.Delimiters; 17 | import org.jboss.netty.handler.codec.string.StringDecoder; 18 | import org.jboss.netty.handler.codec.string.StringEncoder; 19 | 20 | import java.net.InetSocketAddress; 21 | import java.util.concurrent.Executor; 22 | import java.util.concurrent.Executors; 23 | 24 | /** 25 | * @author Bruno de Carvalho 26 | */ 27 | public class Server { 28 | 29 | // internal vars ---------------------------------------------------------- 30 | 31 | private final String id; 32 | private final ServerListener listener; 33 | private ServerBootstrap bootstrap; 34 | private ChannelGroup channelGroup; 35 | 36 | // constructors ----------------------------------------------------------- 37 | 38 | public Server(String id, ServerListener listener) { 39 | this.id = id; 40 | this.listener = listener; 41 | } 42 | 43 | // public methods --------------------------------------------------------- 44 | 45 | public boolean start() { 46 | // Pretty standard Netty startup stuff... 47 | // boss/worker executors, channel factory, channel group, pipeline, ... 48 | Executor bossPool = Executors.newCachedThreadPool(); 49 | Executor workerPool = Executors.newCachedThreadPool(); 50 | ChannelFactory factory = 51 | new NioServerSocketChannelFactory(bossPool, workerPool); 52 | this.bootstrap = new ServerBootstrap(factory); 53 | 54 | this.channelGroup = new DefaultChannelGroup(this.id + "-all-channels"); 55 | 56 | 57 | // declared here to fit under the 80 char limit 58 | final ChannelHandler delimiter = 59 | new DelimiterBasedFrameDecoder(Integer.MAX_VALUE, 60 | Delimiters.lineDelimiter()); 61 | this.bootstrap.setPipelineFactory(new ChannelPipelineFactory() { 62 | 63 | @Override 64 | public ChannelPipeline getPipeline() throws Exception { 65 | ByteCounter counter = 66 | new ByteCounter("+++ SERVER-COUNTER :: "); 67 | MessageCounter messageCounter = 68 | new MessageCounter("+++ SERVER-MSGCOUNTER :: "); 69 | ServerHandshakeHandler handshakeHandler = 70 | new ServerHandshakeHandler(id, channelGroup, 5000); 71 | return Channels.pipeline(counter, 72 | delimiter, 73 | new StringDecoder(), 74 | new StringEncoder(), 75 | messageCounter, 76 | handshakeHandler, 77 | new ServerHandler(listener)); 78 | } 79 | }); 80 | 81 | Channel acceptor = this.bootstrap.bind(new InetSocketAddress(12345)); 82 | if (acceptor.isBound()) { 83 | System.err.println("+++ SERVER - bound to *:12345"); 84 | this.channelGroup.add(acceptor); 85 | return true; 86 | } else { 87 | System.err.println("+++ SERVER - Failed to bind to *:12345"); 88 | this.bootstrap.releaseExternalResources(); 89 | return false; 90 | } 91 | } 92 | 93 | public void stop() { 94 | this.channelGroup.close().awaitUninterruptibly(); 95 | this.bootstrap.releaseExternalResources(); 96 | System.err.println("+++ SERVER - Stopped."); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /handshake/src/main/java/com/biasedbit/nettytutorials/handshake/server/ServerHandler.java: -------------------------------------------------------------------------------- 1 | package com.biasedbit.nettytutorials.handshake.server; 2 | 3 | import com.biasedbit.nettytutorials.handshake.common.HandshakeEvent; 4 | import org.jboss.netty.channel.Channel; 5 | import org.jboss.netty.channel.ChannelEvent; 6 | import org.jboss.netty.channel.ChannelHandlerContext; 7 | import org.jboss.netty.channel.ChannelStateEvent; 8 | import org.jboss.netty.channel.MessageEvent; 9 | import org.jboss.netty.channel.SimpleChannelUpstreamHandler; 10 | 11 | import java.util.concurrent.atomic.AtomicInteger; 12 | 13 | /** 14 | * @author Bruno de Carvalho 15 | */ 16 | public class ServerHandler extends SimpleChannelUpstreamHandler { 17 | 18 | // internal vars ---------------------------------------------------------- 19 | 20 | private final AtomicInteger counter; 21 | private final ServerListener listener; 22 | private String remoteId; 23 | private Channel channel; 24 | 25 | // constructors ----------------------------------------------------------- 26 | 27 | public ServerHandler(ServerListener listener) { 28 | this.listener = listener; 29 | this.counter = new AtomicInteger(); 30 | } 31 | 32 | // SimpleChannelUpstreamHandler ------------------------------------------- 33 | 34 | @Override 35 | public void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e) 36 | throws Exception { 37 | if (e instanceof HandshakeEvent) { 38 | if (((HandshakeEvent) e).isSuccessful()) { 39 | out("+++ SERVER-HANDLER :: Handshake successful, connection " + 40 | "to " + ((HandshakeEvent) e).getRemoteId() + " is up."); 41 | this.remoteId = ((HandshakeEvent) e).getRemoteId(); 42 | this.channel = ctx.getChannel(); 43 | // Notify the listener that a new connection is now READY 44 | this.listener.connectionOpen(this); 45 | } else { 46 | out("+++ SERVER-HANDLER :: Handshake failed."); 47 | } 48 | return; 49 | } 50 | 51 | super.handleUpstream(ctx, e); 52 | } 53 | 54 | @Override 55 | public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) 56 | throws Exception { 57 | this.counter.incrementAndGet(); 58 | this.listener.messageReceived(this, e.getMessage().toString()); 59 | } 60 | 61 | @Override 62 | public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) 63 | throws Exception { 64 | super.channelClosed(ctx, e); 65 | out("+++ SERVER-HANDLER :: Channel closed, received " + 66 | this.counter.get() + " messages: " + e.getChannel()); 67 | } 68 | 69 | // public methods --------------------------------------------------------- 70 | 71 | public void sendMessage(String message) { 72 | if (!message.endsWith("\n")) { 73 | this.channel.write(message + '\n'); 74 | } else { 75 | this.channel.write(message); 76 | } 77 | } 78 | 79 | public String getRemoteId() { 80 | return remoteId; 81 | } 82 | 83 | // private static helpers ------------------------------------------------- 84 | 85 | private static void out(String s) { 86 | System.err.println(s); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /handshake/src/main/java/com/biasedbit/nettytutorials/handshake/server/ServerHandshakeHandler.java: -------------------------------------------------------------------------------- 1 | package com.biasedbit.nettytutorials.handshake.server; 2 | 3 | import com.biasedbit.nettytutorials.handshake.common.Challenge; 4 | import com.biasedbit.nettytutorials.handshake.common.HandshakeEvent; 5 | import org.jboss.netty.channel.Channel; 6 | import org.jboss.netty.channel.ChannelFuture; 7 | import org.jboss.netty.channel.ChannelHandlerContext; 8 | import org.jboss.netty.channel.ChannelStateEvent; 9 | import org.jboss.netty.channel.Channels; 10 | import org.jboss.netty.channel.DownstreamMessageEvent; 11 | import org.jboss.netty.channel.ExceptionEvent; 12 | import org.jboss.netty.channel.MessageEvent; 13 | import org.jboss.netty.channel.SimpleChannelHandler; 14 | import org.jboss.netty.channel.group.ChannelGroup; 15 | 16 | import java.net.SocketAddress; 17 | import java.util.ArrayDeque; 18 | import java.util.Queue; 19 | import java.util.concurrent.CountDownLatch; 20 | import java.util.concurrent.TimeUnit; 21 | import java.util.concurrent.atomic.AtomicBoolean; 22 | 23 | /** 24 | * @author Bruno de Carvalho 25 | */ 26 | public class ServerHandshakeHandler extends SimpleChannelHandler { 27 | 28 | // internal vars ---------------------------------------------------------- 29 | 30 | private final long timeoutInMillis; 31 | private final String localId; 32 | private final ChannelGroup group; 33 | private final AtomicBoolean handshakeComplete; 34 | private final AtomicBoolean handshakeFailed; 35 | private final Object handshakeMutex = new Object(); 36 | private final Queue messages = new ArrayDeque(); 37 | private final CountDownLatch latch = new CountDownLatch(1); 38 | 39 | // constructors ----------------------------------------------------------- 40 | 41 | public ServerHandshakeHandler(String localId, ChannelGroup group, 42 | long timeoutInMillis) { 43 | this.localId = localId; 44 | this.group = group; 45 | this.timeoutInMillis = timeoutInMillis; 46 | this.handshakeComplete = new AtomicBoolean(false); 47 | this.handshakeFailed = new AtomicBoolean(false); 48 | } 49 | 50 | // SimpleChannelHandler --------------------------------------------------- 51 | 52 | @Override 53 | public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) 54 | throws Exception { 55 | if (this.handshakeFailed.get()) { 56 | // Bail out fast if handshake already failed 57 | return; 58 | } 59 | 60 | if (this.handshakeComplete.get()) { 61 | // If handshake succeeded but message still came through this 62 | // handler, then immediately send it upwards. 63 | super.messageReceived(ctx, e); 64 | return; 65 | } 66 | 67 | synchronized (this.handshakeMutex) { 68 | // Recheck conditions after locking the mutex. 69 | // Things might have changed while waiting for the lock. 70 | if (this.handshakeFailed.get()) { 71 | return; 72 | } 73 | 74 | if (this.handshakeComplete.get()) { 75 | super.messageReceived(ctx, e); 76 | return; 77 | } 78 | 79 | // Validate handshake 80 | String handshake = (String) e.getMessage(); 81 | // 1. Validate expected clientId:serverId:challenge format 82 | String[] params = handshake.trim().split(":"); 83 | if (params.length != 3) { 84 | out("+++ SERVER-HS :: Invalid handshake: expecting 3 params, " + 85 | "got " + params.length + " -> '" + handshake + "'"); 86 | this.fireHandshakeFailed(ctx); 87 | return; 88 | } 89 | 90 | // 2. Validate the asserted serverId = localId 91 | String client = params[0]; 92 | if (!this.localId.equals(params[1])) { 93 | out("+++ SERVER-HS :: Invalid handshake: this is " + 94 | this.localId + " and client thinks it's " + params[1]); 95 | this.fireHandshakeFailed(ctx); 96 | return; 97 | } 98 | 99 | // 3. Validate the challenge format. 100 | if (!Challenge.isValidChallenge(params[2])) { 101 | out("+++ SERVER-HS :: Invalid handshake: invalid challenge '" + 102 | params[2] + "'"); 103 | this.fireHandshakeFailed(ctx); 104 | return; 105 | } 106 | 107 | // Success! Write the challenge response. 108 | out("+++ SERVER-HS :: Challenge validated, flushing messages & " + 109 | "removing handshake handler from pipeline."); 110 | String response = params[0] + ':' + params[1] + ':' + 111 | Challenge.generateResponse(params[2]) + '\n'; 112 | this.writeDownstream(ctx, response); 113 | 114 | // Flush any pending messages (in this tutorial, no messages will 115 | // ever be queued because the server does not take the initiative 116 | // of sending messages to clients on its own... 117 | out("+++ SERVER-HS :: " + this.messages.size() + 118 | " messages in queue to be flushed."); 119 | for (MessageEvent message : this.messages) { 120 | ctx.sendDownstream(message); 121 | } 122 | 123 | // Finally, remove this handler from the pipeline and fire success 124 | // event up the pipeline. 125 | out("+++ SERVER-HS :: Removing handshake handler from pipeline."); 126 | ctx.getPipeline().remove(this); 127 | this.fireHandshakeSucceeded(client, ctx); 128 | } 129 | } 130 | 131 | @Override 132 | public void channelConnected(final ChannelHandlerContext ctx, 133 | ChannelStateEvent e) throws Exception { 134 | this.group.add(ctx.getChannel()); 135 | out("+++ SERVER-HS :: Incoming connection established from: " + 136 | e.getChannel().getRemoteAddress()); 137 | 138 | // Fire up the handshake handler timeout checker. 139 | // Wait X seconds for the handshake then disconnect. 140 | new Thread() { 141 | 142 | @Override 143 | public void run() { 144 | try { 145 | latch.await(timeoutInMillis, TimeUnit.MILLISECONDS); 146 | } catch (InterruptedException e1) { 147 | out("+++ SERVER-HS :: Handshake timeout checker: " + 148 | "interrupted!"); 149 | e1.printStackTrace(); 150 | } 151 | 152 | if (handshakeFailed.get()) { 153 | out("+++ SERVER-HS :: (pre-synchro) Handshake timeout " + 154 | "checker: discarded (handshake failed)"); 155 | return; 156 | } 157 | 158 | if (handshakeComplete.get()) { 159 | out("+++ SERVER-HS :: (pre-synchro) Handshake timeout " + 160 | "checker: discarded (handshake complete)"); 161 | return; 162 | } 163 | 164 | synchronized (handshakeMutex) { 165 | if (handshakeFailed.get()) { 166 | out("+++ SERVER-HS :: (synchro) Handshake timeout " + 167 | "checker: already failed."); 168 | return; 169 | } 170 | 171 | if (!handshakeComplete.get()) { 172 | out("+++ SERVER-HS :: (synchro) Handshake timeout " + 173 | "checker: timed out, killing connection."); 174 | ctx.sendUpstream(HandshakeEvent 175 | .handshakeFailed(ctx.getChannel())); 176 | handshakeFailed.set(true); 177 | ctx.getChannel().close(); 178 | } else { 179 | out("+++ SERVER-HS :: (synchro) Handshake timeout " + 180 | "checker: discarded (handshake OK)"); 181 | } 182 | } 183 | } 184 | }.start(); 185 | } 186 | 187 | @Override 188 | public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) 189 | throws Exception { 190 | out("+++ SERVER-HS :: Channel closed."); 191 | if (!this.handshakeComplete.get()) { 192 | this.fireHandshakeFailed(ctx); 193 | } 194 | } 195 | 196 | @Override 197 | public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) 198 | throws Exception { 199 | out("+++ SERVER-HS :: Exception caught."); 200 | e.getCause().printStackTrace(); 201 | if (e.getChannel().isConnected()) { 202 | // Closing the channel will trigger handshake failure. 203 | e.getChannel().close(); 204 | } else { 205 | // Channel didn't open, so we must fire handshake failure directly. 206 | this.fireHandshakeFailed(ctx); 207 | } 208 | } 209 | 210 | 211 | @Override 212 | public void writeRequested(ChannelHandlerContext ctx, MessageEvent e) 213 | throws Exception { 214 | // Before doing anything, ensure that noone else is working by 215 | // acquiring a lock on the handshakeMutex. 216 | synchronized (this.handshakeMutex) { 217 | if (this.handshakeFailed.get()) { 218 | // If the handshake failed meanwhile, discard any messages. 219 | return; 220 | } 221 | 222 | // If the handshake hasn't failed but completed meanwhile and 223 | // messages still passed through this handler, then forward 224 | // them downwards. 225 | if (this.handshakeComplete.get()) { 226 | out("+++ SERVER-HS :: Handshake already completed, not " + 227 | "appending '" + e.getMessage().toString().trim() + 228 | "' to queue!"); 229 | super.writeRequested(ctx, e); 230 | } else { 231 | // Otherwise, queue messages in order until the handshake 232 | // completes. 233 | this.messages.offer(e); 234 | } 235 | } 236 | } 237 | 238 | // private static helpers ------------------------------------------------- 239 | 240 | private static void out(String s) { 241 | System.err.println(s); 242 | } 243 | 244 | // private helpers -------------------------------------------------------- 245 | 246 | private void writeDownstream(ChannelHandlerContext ctx, Object data) { 247 | // Just declaring these variables so that last statement in this 248 | // method fits inside the 80 char limit... I typically use 120 :) 249 | ChannelFuture f = Channels.succeededFuture(ctx.getChannel()); 250 | SocketAddress address = ctx.getChannel().getRemoteAddress(); 251 | Channel c = ctx.getChannel(); 252 | 253 | ctx.sendDownstream(new DownstreamMessageEvent(c, f, data, address)); 254 | } 255 | 256 | private void fireHandshakeFailed(ChannelHandlerContext ctx) { 257 | this.handshakeComplete.set(true); 258 | this.handshakeFailed.set(true); 259 | this.latch.countDown(); 260 | ctx.getChannel().close(); 261 | ctx.sendUpstream(HandshakeEvent.handshakeFailed(ctx.getChannel())); 262 | } 263 | 264 | private void fireHandshakeSucceeded(String client, 265 | ChannelHandlerContext ctx) { 266 | this.handshakeComplete.set(true); 267 | this.handshakeFailed.set(false); 268 | this.latch.countDown(); 269 | ctx.sendUpstream(HandshakeEvent 270 | .handshakeSucceeded(client, ctx.getChannel())); 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /handshake/src/main/java/com/biasedbit/nettytutorials/handshake/server/ServerListener.java: -------------------------------------------------------------------------------- 1 | package com.biasedbit.nettytutorials.handshake.server; 2 | 3 | /** 4 | * @author Bruno de Carvalho 5 | */ 6 | public interface ServerListener { 7 | 8 | void messageReceived(ServerHandler handler, String message); 9 | 10 | void connectionOpen(ServerHandler serverHandler); 11 | } 12 | --------------------------------------------------------------------------------