├── .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 |
--------------------------------------------------------------------------------