From f206c5359bd255405f314b0d6a73774d1ecebb09 Mon Sep 17 00:00:00 2001 From: Trustin Lee Date: Mon, 22 Sep 2008 11:00:18 +0000 Subject: [PATCH] Finished the first chapter --- src/docbook/custom.dtd | 16 +- src/docbook/module/start.xml | 660 +++++++++++++++++++++++++++++++++-- 2 files changed, 638 insertions(+), 38 deletions(-) diff --git a/src/docbook/custom.dtd b/src/docbook/custom.dtd index 79c9ddb546..284443d30e 100644 --- a/src/docbook/custom.dtd +++ b/src/docbook/custom.dtd @@ -30,6 +30,7 @@ ChannelHandlerContext"> ChannelPipeline"> ChannelPipelineCoverage"> +ChannelPipelineFactory"> Channels"> ChannelStateEvent"> ChannelUpstreamHandler"> @@ -39,8 +40,21 @@ -ServerSocketChannel"> +ServerSocketChannel"> +SocketChannel"> +NioClientSocketChannelFactory"> NioServerSocketChannelFactory"> + + + +FrameDecoder"> + + + +ReplayingDecoder"> +VoidEnum"> + + diff --git a/src/docbook/module/start.xml b/src/docbook/module/start.xml index c76cec6f41..11e31fea49 100644 --- a/src/docbook/module/start.xml +++ b/src/docbook/module/start.xml @@ -7,17 +7,19 @@ Getting Started This chapter tours around the core constructs of Netty with simple - examples to let you get started with Netty easily. You should be able to + examples to let you get started with Netty easily. You will be able to write a network application on top of Netty right away when you are at the end of this chapter. - + + +
- Minimum Requirements + Before Getting Started The minimum requirements to run the examples which are introduced in this chapter are just two; the latest version of Netty release and JDK @@ -30,7 +32,16 @@ Is that all? To tell the truth, you should find these two are just enough to implement almost any type of protocols. Otherwise, please feel free to contact the Netty - project team and let us know what's missing. + project community and let us know what's missing. + + + At last but least, please refer to the API reference whenever you want + to know more about the classes introduced here. All class names in + this document are linked to the online API reference for your + convenience. Also, please feel free to contact + the Netty project community and let us know if there's any + incorrect information, errors in grammar and typo, and if you have a + good idea to improve the documentation.
@@ -48,12 +59,6 @@
package org.jboss.netty.example.discard; -import org.jboss.netty.channel.&ChannelHandlerContext;; -import org.jboss.netty.channel.&ChannelPipelineCoverage;; -import org.jboss.netty.channel.&ExceptionEvent;; -import org.jboss.netty.channel.&MessageEvent;; -import org.jboss.netty.channel.&SimpleChannelHandler;; - @&ChannelPipelineCoverage;("all") public class DiscardServerHandler extends &SimpleChannelHandler; { @@ -123,10 +128,6 @@ public class DiscardServerHandler extends &SimpleChannelHandler; {telnet localhost 8080" in the command line and type something. @@ -252,9 +253,9 @@ public void messageReceived(&ChannelHandlerContext; ctx, &MessageEvent; e) { Although it resembles to NIO ByteBuffer a lot, - it is highly recommended to refer to the API reference documentation. - Learning how to use &ChannelBuffer; correctly is a critical step in - using Netty without difficulty. + it is highly recommended to refer to the API reference. Learning how + to use &ChannelBuffer; correctly is a critical step in using Netty + without difficulty. @@ -314,7 +315,7 @@ public void messageReceived(&ChannelHandlerContext; ctx, &MessageEvent; e) {
Writing a Time Server - The protocol to implement time time is the TIME + The protocol to implement in this section is the TIME protocol. It is different from the previous examples in that it sends a message, which contains a 32-bit integer, without receiving any requests and closes the connection once the message is sent. In this example, you @@ -330,14 +331,6 @@ public void messageReceived(&ChannelHandlerContext; ctx, &MessageEvent; e) { package org.jboss.netty.example.time; - -import org.jboss.netty.channel.&ChannelBuffers;; -import org.jboss.netty.channel.&ChannelHandlerContext;; -import org.jboss.netty.channel.&ChannelPipelineCoverage;; -import org.jboss.netty.channel.&ExceptionEvent;; -import org.jboss.netty.channel.&MessageEvent;; -import org.jboss.netty.channel.&SimpleChannelHandler;; - @&ChannelPipelineCoverage;("all") public class TimeServerHandler extends &SimpleChannelHandler; { @@ -361,8 +354,7 @@ public class TimeServerHandler extends &SimpleChannelHandler; { @Override public void exceptionCaught(&ChannelHandlerContext; ctx, &ExceptionEvent; e) { e.getCause().printStackTrace(); - &Channel; ch = e.getChannel(); - ch.close(); + e.getChannel().close(); } } @@ -387,9 +379,10 @@ public class TimeServerHandler extends &SimpleChannelHandler; { On the other hand, it's a good idea to use static imports for &ChannelBuffers;: - import static org.jboss.netty.buffer.ChannelBuffers.*; + import static org.jboss.netty.buffer.&ChannelBuffers;.*; ... -&ChannelBuffer; buf = dynamicBuffer(1024); +&ChannelBuffer; dynamicBuf = dynamicBuffer(256); +&ChannelBuffer; ordinaryBuf = buffer(1024); @@ -419,8 +412,7 @@ public class TimeServerHandler extends &SimpleChannelHandler; { Another point to note is that the write method returns a &ChannelFuture;. A &ChannelFuture; represents an - I/O operation which was not occurred yet. It means, even if you have - called the write method, the requested + I/O operation which was not occurred yet. It means, any requested operation might not have been performed yet because all operations are asynchronous in Netty. For example, the following code might close the connection even before a message is sent: @@ -431,8 +423,10 @@ ch.close(); Therefore, you need to call the close method after the &ChannelFuture;, which was returned by the - write method, tells you that the write - operation has been done. + write method, notifies you when the write + operation has been done. Also, close + might not close the connection immediately, and it returns a + &ChannelFuture;. @@ -443,10 +437,602 @@ ch.close(); which closes the &Channel; when the operation is done. - Alternatively, you could simplify the code like this: + Alternatively, you could simplify the code using a pre-defined + listener: f.addListener(&ChannelFutureListener;.CLOSE);
+ +
+ Writing a Time Client + + Unlike DISCARD and ECHO servers, we need a client for the TIME protocol + because a human can't translate a 32-bit binary data into a date on a + calendar. Let's make sure the server works correctly and learn how to + write a client with Netty. + + + The biggest and only difference between a server and a client in Netty + is that different &Bootstrap; and &ChannelFactory; are required. Please + take a look at the following code: + + package org.jboss.netty.example.time; + +import java.net.InetSocketAddress; +import java.util.concurrent.Executors; + +public class TimeClient { + + public static void main(String[] args) throws Exception { + String host = args[0]; + int port = Integer.parseInt(args[1]); + + &ChannelFactory; factory = + new &NioClientSocketChannelFactory;( + Executors.newCachedThreadPool(), + Executors.newCachedThreadPool()); + + &ClientBootstrap; bootstrap = new &ClientBootstrap;(factory); + + TimeClientHandler handler = new TimeClientHandler(); + bootstrap.getPipeline().addLast("handler", handler); + + bootstrap.setOption("tcpNoDelay", true); + bootstrap.setOption("keepAlive", true); + + bootstrap.connect(new InetSocketAddress(host, port)); + } +} + + + + &NioClientSocketChannelFactory;, instead of &NioServerSocketChannelFactory; + was used to create a client-side &Channel;. + + + + + &ClientBootstrap; is a client-side counterpart of &ServerBootstrap;. + + + + + Please note that there's no "child." prefix. + A client-side &SocketChannel; doesn't have a parent. + + + + + We should call the connect method instead of + the bind method. + + + + + As you saw, it's not really different from the server side startup. What + about the &ChannelHandler; implementation? It should receive a 32-bit + integer from the server, translate it into a human readable format, print + the translated time, and close the connection: + + package org.jboss.netty.example.time; + +@&ChannelPipelineCoverage;("all") +public class TimeClientHandler extends &SimpleChannelHandler; { + + @Override + public void messageReceived(&ChannelHandlerContext; ctx, &MessageEvent; e) { + &ChannelBuffer; buf = (&ChannelBuffer;) e.getMessage(); + long currentTimeMillis = buf.readInt() * 1000L; + System.out.println(new Date(currentTimeMillis())); + e.getChannel().close(); + } + + @Override + public void exceptionCaught(&ChannelHandlerContext; ctx, &ExceptionEvent; e) { + e.getCause().printStackTrace(); + e.getChannel().close(); + } +} + + It looks very simple and doesn't look any different from the server side + example. However, this handler sometimes will refuse to work raising an + IndexOutOfBoundsException. Let's figure + out why in the next section. + +
+ +
+ + Dealing with Packet Fragmentation and Assembly + +
+ + What is Packet Fragmentation and Assembly? + + + In a stream-based transport such as TCP/IP, packets can be fragmented and + reassembled during transmission even in a LAN environment. For example, + let's assume you have received three packets: + + +-----+-----+-----+ +| ABC | DEF | GHI | ++-----+-----+-----+ + + because of the packet fragmentation, a server can receive them like the + following: + + +----+-------+---+---+ +| AB | CDEFG | H | I | ++----+-------+---+---+ + + Therefore, a receiving part, regardless it's server-side or client-side, + should defrag the received packets into one or more meaningful + frames that could be easily understood by the + application logic. In case of the example above, the received packets + should be defragmented back like the following: + + +-----+-----+-----+ +| ABC | DEF | GHI | ++-----+-----+-----+ +
+
+ + The First Solution + + + Now let's get back to the TIME client example. We have the same problem + here. A 32-bit integer is a very small amount of data, and it is not + likely to be fragmented often. However, the problem is that it + can be fragmented, and the possibility of + fragmentation will increase as the traffic goes higher. + + + The simplistic solution is to create an internal cumulative buffer and + wait until all 4 bytes are received into the internal buffer. Here's + the modified TimeClientHandler implementation + that fixes the problem. + + package org.jboss.netty.example.time; + +import static org.jboss.netty.buffer.&ChannelBuffers;.*; + +@&ChannelPipelineCoverage;("one") +public class TimeClientHandler extends &SimpleChannelHandler; { + + private final &ChannelBuffer; buf = dynamicBuffer(); + + @Override + public void messageReceived(&ChannelHandlerContext; ctx, &MessageEvent; e) { + &ChannelBuffer; m = (&ChannelBuffer;) e.getMessage(); + buf.writeBytes(m); + + if (buf.readableBytes() >= 4) { + long currentTimeMillis = buf.readInt() * 1000L; + System.out.println(new Date(currentTimeMillis())); + e.getChannel().close(); + } + } + + @Override + public void exceptionCaught(&ChannelHandlerContext; ctx, &ExceptionEvent; e) { + e.getCause().printStackTrace(); + e.getChannel().close(); + } +} + + + + This time, "one" was used as the value of the + &ChannelPipelineCoverage; annotation. It's because the new + TimeClientHandler has to maintain the internal + buffer and therefore cannot serve multiple &Channel;s. If an + instance of TimeClientHandler is shared by + multiple &Channel;s (and consequently multiple &ChannelPipeline;s), + the content of the buf will be messed up. + + + + + A dynamic buffer is a &ChannelBuffer; which + increases its capacity on demand. It's very useful when you don't + know the length of the message. + + + + + First, all received data should be cumulated into + buf. + + + + + And then, the handler must check if buf has enough + data, 4 bytes in this example, and proceed to the actual business + logic. Otherwise, Netty will call the + messageReceived method again when more + data arrives, and eventually all 4 bytes will be cumulated. + + + + + There's another place that needs a fix. Do you remember that we have + added a TimeClientHandler instance to the + default &ChannelPipeline; of the &ClientBootstrap;? + It means one TimeClientHandler is going to handle + multiple &Channel;s and consequently the date will be messed up. To + create a new TimeClientHandler instance per + &Channel;, we should implement a &ChannelPipelineFactory;: + + package org.jboss.netty.example.time; + +public class TimeClientPipelineFactory implements &ChannelPipelineFactory; { + + public &ChannelPipeline; getPipeline() { + &ChannelPipeline; pipeline = &Channels;.pipeline(); + pipeline.addLast("handler", new TimeClientHandler()); + return pipeline; + } +} + + Now let's replace the following lines of TimeClient: + + TimeClientHandler handler = new TimeClientHandler(); +bootstrap.getPipeline().addLast("handler", handler); + + with the following: + + bootstrap.getPipelineFactory(new TimeClientPipelineFactory()); + + It might look somewhat complicated at the first glance, and it is true + that we don't need to introduce TimeClientPipelineFactory + in this particular case because TimeClient creates + only one connection. + + + However, as your application gets more and more complex, you will end up + with an implementation of &ChannelPipelineFactory;, which yields much + more flexibility. + +
+
+ + The Second Solution + + + Although the first solution has resolved the problem with the TIME + client, the modified handler doesn't look that clean. Imagine a more + complicated protocol which is composed of multiple fields such as a + variable length field. Your &ChannelHandler; implementation will + become unmaintainable very quickly. + + + As you already might have noticed, you can add more than one + &ChannelHandler; to a &ChannelPipeline;, and therefore, you can + split one monolithic &ChannelHandler; into multiple modular ones to + reduce the complexity of your application. For example, you could + split TimeClientHandler into two handlers: + + + + TimeDecoder which deals with the packet + fragmentation and assembly issue, and + + + + + the initial simple version of TimeClientHandler. + + + + + + Fortunately, Netty provides an extensible class which enables you to + write the first one out of the box. + + package org.jboss.netty.example.time; + + +public class TimeDecoder extends &FrameDecoder; { + + @Override + protected Object decode( + &ChannelHandlerContext; ctx, &Channel; channel, &ChannelBuffer; buffer) { + + if (buffer.readableBytes() < 4) { + return null; + } + + return buffer.readBytes(4); + } +} + + + + There's no &ChannelPipelineCoverage; annotation this time because + &FrameDecoder; is already annotated with "one". + + + + + &FrameDecoder; calls decode method with + an internally maintained cumulative buffer whenever new data is + received. + + + + + If null is returned, it means there's not + enough data yet. &FrameDecoder; will call again when more data is + in. + + + + + If non-null is returned, it means the + decode method has decoded a message + successfully. &FrameDecoder; will discard the read part of its + internal cumulative buffer. Please remember that you don't need + to decode multiple messages. &FrameDecoder; will keep calling + the decoder method until it returns + null. + + + + + If you are a adventurous person, you might want to try the + &ReplayingDecoder; which simplifies the decoder even more. You will + need to consult the API reference for more information though. + + package org.jboss.netty.example.time; + +public class TimeDecoder extends &ReplayingDecoder;<&VoidEnum;> { + + @Override + protected Object decode( + &ChannelHandlerContext; ctx, &Channel; channel, &ChannelBuffer; buffer, &VoidEnum; state) { + + return buffer.readBytes(4); + } +} + + Additionally, Netty provides out-of-the-box decoders which enables + you to implement most protocol very easily and helps you avoid from + ending up with a monolithic unmaintainable handler implementation. + You might want to take a look into the following packages for more + detailed examples: + + + + org.jboss.netty.example.factorial for + a binary protocol, and + + + + + org.jboss.netty.example.telnet for + a text line-based protocol. + + + + +
+
+ +
+ + Speaking in POJO instead of ChannelBuffer + + + All the examples we visited so far used a &ChannelBuffer; as a primary + data structure of a protocol message. In this section, we will improve + the TIME protocol client and server example to use a + POJO instead of a + &ChannelBuffer;. + + + The advantage of using a POJO in your &ChannelHandler; is obvious; + your handler becomes more maintainable and reusable by separating the + code which extracts information from &ChannelBuffer; out from the + handler. In the TIME client and server examples, we read only one + 32-bit integer and it's not a big deal to use &ChannelBuffer; directly. + However, you will find it is necessary to make the separation as you + implement a real world protocol. + + + First, let's define a new type called UnixTime. + + package org.jboss.netty.example.time; + +public class UnixTime { + private final int value; + + public UnixTime(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + @Override + public String toString() { + return new Date(value * 1000L).toString(); + } +} + + We can now revise the TimeDecoder to return + a UnixTime instead of a &ChannelBuffer;. + + @Override +protected Object decode( + &ChannelHandlerContext; ctx, &Channel; channel, &ChannelBuffer; buffer) { + if (buffer.readableBytes() < 4) { + return null; + } + + return new UnixTime(buffer.readInt()); +} + + + + &FrameDecoder; and &ReplayingDecoder; allow you to return an object + of any type. If they were restricted to return only a + &ChannelBuffer;, we would have to insert another &ChannelHandler; + which transforms a &ChannelBuffer; into a + UnixTime. + + + + + With the updated decoder, the TimeClientHandler + doesn't use &ChannelBuffer; anymore: + + @Override +public void messageReceived(&ChannelHandlerContext; ctx, &MessageEvent; e) { + UnixTime m = (UnixTime) e.getMessage(); + System.out.println(m); + e.getChannel().close(); +} + + Much simpler and elegant, right? The same technique can be applied on + the server side. Let's update the + TimeServerHandler first this time: + + @Override +public void channelConnected(&ChannelHandlerContext; ctx, &ChannelStateEvent; e) { + UnixTime time = new UnixTime(System.currentTimeMillis() / 1000); + &ChannelFuture; f = e.getChannel().write(time); + f.addListener(&ChannelFutureListener;.CLOSE); +} + + Now, the only missing piece is the &ChannelHandler; which translates a + UnixTime back into a &ChannelBuffer;. It's much + simpler than writing a decoder because there's no need to deal with + packet fragmentation and assembly when encoding a message. + + package org.jboss.netty.example.time; + +import static org.jboss.netty.buffer.&ChannelBuffers;.*; + +@&ChannelPipelineCoverage;("all") +public class TimeEncoder implements &ChannelDownstreamHandler; { + + public void handleDownstream(&ChannelHandlerContext; ctx, &ChannelEvent; e) { + if (!(e instanceof &MessageEvent;)) { + ctx.sendDownstream(e); + return; + } + + UnixTime time = (UnixTime) ((&MessageEvent;) e).getMessage(); + + &ChannelBuffer; buf = buffer(4); + buf.writeInt(time.getValue()); + + &Channels;.write(ctx, e.getChannel(), e.getFuture(), buf); + } +} + + + + The &ChannelPipelineCoverage; value of an encoder is usually + "all" because an encoder is stateless in most + cases. + + + + + An encoder implements a &ChannelDownstreamHandler; to intercept a + write request. &ChannelDownstreamHandler; is a sub-type of + &ChannelHandler; that is used to intercept the downstream events, + which flows in the opposite direction of the &ChannelEvent;s we have + been processing so far. Please refer to the API reference to learn + more about the difference between a upstream event and a downstream + event. + + + + + &MessageEvent; is not the only event that a &ChannelDownstreamHandler; + intercepts. You need to forward the events of a unknown type so that + the other &ChannelDownstreamHandler;s or I/O thread processes them. + + + + + Once done with transforming a POJO into a &ChannelBuffer;, you should + forward the new buffer to the previous &ChannelDownstreamHandler; in + the &ChannelPipeline;. &Channels; provides various helper methods + which generates and fires a &ChannelEvent;. In this example, + &Channels;.write(...) method creates a new + &MessageEvent; and fires it to the previous &ChannelDownstreamHandler; + in the &ChannelPipeline;. + + + On the other hand, it's a good idea to use static imports for + &Channels;: + import static org.jboss.netty.channel.&Channels;.*; +... +&ChannelPipeline; pipeline = pipeline(); +write(ctx, e.getChannel(), e.getFuture(), buf); +fireChannelDisconnected(ctx, e.getChannel()); + + + + + At this point, you might wonder why you didn't have to forward a + &ChannelEvent; to the next &ChannelUpstreamHandler; in the + &ChannelPipeline; when we write the decoders. Actually, it had been + done behind the scene by &FrameDecoder; and &ReplayingDecoder;. + The TimeDecoder would look like the following if + it didn't extend neither &FrameDecoder; nor &SimpleChannelHandler;. + + package org.jboss.netty.example.time; + +import static org.jboss.netty.buffer.&ChannelBuffers;.*; +import static org.jboss.netty.channel.&Channels;.*; + +@&ChannelPipelineCoverage;("one") +public class TimeDecoder implements &ChannelUpstreamHandler; { + + private final &ChannelBuffer; buf = dynamicBuffer(); + + public void handleUpstream(&ChannelHandlerContext; ctx, &ChannelEvent; e) { + if (!(e instanceof &MessageEvent;)) { + ctx.sendUpstream(e); + return; + } + + &ChannelBuffer; m = (&ChannelBuffer;) ((&MessageEvent;) e).getMessage(); + buf.writeBytes(m); + + while (buf.readableBytes() >= 4) { + UnixTime time = new UnixTime(buf.readInt()); + fireMessageReceived(ctx, time); + } + + buf.discardReadBytes(); + } +} + + The last task left is to add the TimeEncoder to + the &ChannelPipeline; of the &ServerBootstrap;, and it's left as a + trivial exercise. + +
+ +
+ + Summary + + + In this section, we had a quick tour of Netty so that you can start to + write a network application on top of Netty right away. I believe you + still have a lot of questions about various topics, and they will be + covered in the upcoming chapters and the revised version of this chapter. + Also, please don't forget that the Netty project + community is always here to help you. + +