本篇翻譯自netty官方Get Start教程,一方面能把好的文章分享給各位,另外一方面能鞏固所學的知識。如有錯誤和遺漏,歡迎各位指出。html
https://netty.io/wiki/user-gu...java
咱們通常使用專用軟件或者是開源庫和其餘系統通訊。舉個例子,咱們一般使用 http 客戶端從 web 服務器獲取信息,或者經過 web service 執行一個 remote procedure call (遠程調用)。然而,一個通用的協議時常不具有很好的擴展性,例如咱們不會使用一個通用 http 服務器去作以下類型的數據交換——大文件,電子郵件,近實時的金融數據或者是遊戲數據。所以,一個高度優化的致力於解決某些問題的通信協議是頗有必要的,例如你但願實現一臺優化過的 http 服務器,致力於聊天應用,流媒體傳輸,大文件傳輸等。你甚至能夠爲已有需求量身定作一個全新的通訊協議。另外一個不可避免的狀況是,你必須處理一個古老的專用協議,使用他去跟遺留系統通訊,問題是咱們該如何快速實現協議,同時不犧牲應用的穩定性和性能。web
The Netty project is an effort to provide an asynchronous event-driven network application framework and tooling for the rapid development of maintainable high-performance · high-scalability protocol servers and clients.編程
In other words, Netty is an NIO client server framework that enables quick and easy development of network applications such as protocol servers and clients. It greatly simplifies and streamlines network programming such as TCP and UDP socket server development.bootstrap
'Quick and easy' does not mean that a resulting application will suffer from a maintainability or a performance issue. Netty has been designed carefully with the experiences learned from the implementation of a lot of protocols such as FTP, SMTP, HTTP, and various binary and text-based legacy protocols. As a result, Netty has succeeded to find a way to achieve ease of development, performance, stability, and flexibility without a compromise.api
Some users might already have found other network application framework that claims to have the same advantage, and you might want to ask what makes Netty so different from them. The answer is the philosophy it is built on. Netty is designed to give you the most comfortable experience both in terms of the API and the implementation from the day one. It is not something tangible but you will realize that this philosophy will make your life much easier as you read this guide and play with Netty.promise
本章使用簡單的例子帶你瀏覽 netty 的核心構造,你快速上手。在本章事後,你就能夠寫出一個基於 netty 的客戶端和服務器。若是你但願有更好的學習體驗,你能夠先瀏覽 Chapter 2, Architectural Overview 後再回來本章學習 (先看這裏也是OK的)。緩存
能跑通本章例子的最低要求:最新版本的 netty(4.x) 和 JDK 1.6 或以上的版本。
在閱讀時,當你對本章中出現的 class 感到疑惑,請查閱他們的 api 文檔。而本章幾乎全部的 class 都會連接到他們的 api 文檔。若是你發現本章中有什麼錯誤的信息、代碼語法錯誤、或者有什麼好的想法,也請聯繫 netty 社區通知咱們。服務器
世界上最簡單的協議並非 hello world,而是Discard
。這種協議會拋棄掉全部接收到的數據,不會給客戶端任何響應,因此實現Discard協議惟一要作的是忽略全部接收到的數據。接下來讓咱們着手寫一個 handler,用來處理I/O events
(I/O事件)。網絡
package io.netty.example.discard; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; /** * Handles a server-side channel. */ public class DiscardServerHandler extends ChannelInboundHandlerAdapter { // (1) @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { // (2) // Discard the received data silently. ((ByteBuf) msg).release(); // (3) } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (4) // Close the connection when an exception is raised. cause.printStackTrace(); ctx.close(); } }
1.DiscardServerHandler 繼承ChannelInboundHandlerAdapter
,並間接實現了ChannelInboundHandler
。ChannelInboundHandler
接口提供了多種 event handler method (事件處理方法),你可能要逐個實現接口中的方法,但直接繼承ChannelInboundHandlerAdapter
會是更好的選擇。
2.這裏咱們重寫了channelRead()
,當有新數據到達時該方法就會被調用,並附帶接收到的數據做爲方法參數。在本例中,接收到的數據類型是ByteBuf
。
3.要實現 Discard 協議,這裏 handler 會忽略接收到的數據。ByteBuf
做爲 reference-counted (引用計數) 對象,經過調用方法release()
釋放資源,請記住這個 release 動做在 handler 中完成 (原文:是handler的職責)。一般,咱們會像下面那樣實現channelRead()
@Override public void channelRead(ChannelHandlerContext ctx, Object msg) { try { // Do something with msg } finally { ReferenceCountUtil.release(msg); } }
4.當 netty 發生 I/O 錯誤,或者 handler 在處理 event (事件) 拋出異常時,exceptionCaught()
就會被調用。大多數狀況下咱們應該記錄下被捕獲的異常,並關閉與之關聯的channel
(通道),但同時你也能夠作一些額外的異常處理,例如在關閉鏈接以前,你可能會發送一條帶有錯誤代碼的響應消息。
目前爲止,咱們已經完成了一半的工做,剩下的就是在main()
方法中啓動Discard
服務器。
package io.netty.example.discard; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; /** * Discards any incoming data. */ public class DiscardServer { private int port; public DiscardServer(int port) { this.port = port; } public void run() throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1) EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); // (2) b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) // (3) .childHandler(new ChannelInitializer<SocketChannel>() { // (4) @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new DiscardServerHandler()); } }) .option(ChannelOption.SO_BACKLOG, 128) // (5) .childOption(ChannelOption.SO_KEEPALIVE, true); // (6) // Bind and start to accept incoming connections. ChannelFuture f = b.bind(port).sync(); // (7) // Wait until the server socket is closed. // In this example, this does not happen, but you can do that to gracefully // shut down your server. f.channel().closeFuture().sync(); } finally { workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } } public static void main(String[] args) throws Exception { int port; if (args.length > 0) { port = Integer.parseInt(args[0]); } else { port = 8080; } new DiscardServer(port).run(); } }
NioEventLoopGroup
是一個處理I/O操做的事件循環器 (實際上是個線程池)。netty爲不一樣類型的傳輸協議提供了多種NioEventLoopGroup
的實現。在本例中咱們要實現一個服務端應用,並使用了兩個NioEventLoopGroup
。第一個一般被稱爲boss,負責接收已到達的 connection。第二個被稱做 worker,當 boss 接收到 connection 並把它註冊到 worker 後,worker 就能夠處理 connection 上的數據通訊。要建立多少個線程,這些線程如何匹配到Channel
上會隨着EventLoopGroup
實現的不一樣而改變,或者你能夠經過構造器去配置他們。ServerBootstrap
是用來搭建 server 的協助類。你也能夠直接使用Channel
搭建 server,然而這樣作步驟冗長,不是一個好的實踐,大多數狀況下建議使用ServerBootstrap
。NioServerSocketChannel
類,用來初始化一個新的Channel
去接收到達的connection。Channel
。ChannelInitializer
是一個特殊的 handler,幫助開發者配置Channel
,而多數狀況下你會配置Channel
下的ChannelPipeline
,往 pipeline 添加一些 handler (例如DiscardServerHandler) 從而實現你的應用邏輯。當你的應用變得複雜,你可能會向 pipeline 添加更多的 handler,並把這裏的匿名類抽取出來做爲一個單獨的類。Channel
配置特有的參數。這裏咱們寫的是 TCP/IP 服務器,因此能夠配置一些 socket 選項,例如 tcpNoDeply 和 keepAlive。請參考ChannelOption
和ChannelConfig
文檔來獲取更多可用的 Channel 配置選項,並對此有個大概的瞭解。option()
和childOption()
了嗎?option()
用來配置NioServerSocketChannel
(負責接收到來的connection),而childOption()
是用來配置被ServerChannel
(這裏是NioServerSocketChannel
) 所接收的Channel
bind()
(基於不一樣的地址)。剛剛,你使用 netty 完成了第一個服務端程序,可喜可賀!
既然咱們完成了第一個服務端程序,接下來要就要對它進行測試。最簡單的方法是使用命令行 telnet,例如在命令行輸入telnet localhost 8080
,而後再輸入點別的東西。
然而咱們並不知道服務端是否真的在工做,由於他是 Discard Server,咱們得不到任何響應。爲了證實他真的在工做,咱們讓服務端打印接收到的數據。
咱們知道當接收到數據時,channelRead()
會被調用。因此讓咱們加點代碼到 DiscardServerHandler 的channelRead()
中:
@Override public void channelRead(ChannelHandlerContext ctx, Object msg) { ByteBuf in = (ByteBuf) msg; try { while (in.isReadable()) { // (1) System.out.print((char) in.readByte()); System.out.flush(); } } finally { ReferenceCountUtil.release(msg); // (2) } }
System.out.println(in.toString(io.netty.util.CharsetUtil.US_ASCII))
in.release()
替換這裏的代碼。若是你再運行 telnet 命令,服務端會打印出接收到的數據。
Discard Server 完整的源碼放在io.netty.example.discard
這個包中。
目前爲止,咱們寫的服務程序消費了數據但沒有給出任何響應,而做爲一臺服務器理應要對每個請求做出響應。接下來讓咱們實現 ECHO 協議,學習如何響應消息並把接收到的數據發回客戶端。
Echo Server 跟 Discard Server 惟一不一樣的地方在於,他把接收到的數據返回給客戶端,而不是把他們打印到控制檯。因此這裏咱們只須要修改channelRead()
就好了:
@Override public void channelRead(ChannelHandlerContext ctx, Object msg) { ctx.write(msg); // (1) ctx.flush(); // (2) }
ChannelHandlerContext
能觸發多種 I/O 事件和操做,這裏咱們調用write()
方法逐字寫回接收到的數據。請注意咱們並無釋放接收到的消息Object msg
,由於在寫數據時ctx.write(msg)
,netty 已經幫你釋放它了。ctx.write()
關沒有把消息寫到網絡上,他在內部被緩存起來,你須要調用ctx.flush()
把他刷新到網絡上。ctx.writeAndFlush(msg)
是個更簡潔的方法。若是再次使用命令行 telnet,你會看到服務端返回了你輸入過的東西。完整的 Echo Server 源碼放在io.netty.example.echo
包下面。
咱們這一小節要實現 TIME 協議。跟前面的例子不一樣,Timer Server 在鏈接創建時 (收到請求前) 就返回一個32位 (4字節) 整數,並在發送成功後關閉鏈接。在本例中,將會學習到如何構造和發送一個消息,在發送完成時關閉鏈接。
由於要在剛創建鏈接時發送消息而無論後來接收到的數據,此次咱們不能使用channelRead()
,取而代之的是channelActive
方法,如下是具體實現:
package io.netty.example.time; public class TimeServerHandler extends ChannelInboundHandlerAdapter { @Override public void channelActive(final ChannelHandlerContext ctx) { // (1) final ByteBuf time = ctx.alloc().buffer(4); // (2) time.writeInt((int) (System.currentTimeMillis() / 1000L + 2208988800L)); final ChannelFuture f = ctx.writeAndFlush(time); // (3) f.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) { assert f == future; ctx.close(); } }); // (4) } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } }
channelActive()
方法會被調用,咱們在方法體中發送一個32位的表明當前時間的整數。buffer
(緩衝區) 去包含這個消息。咱們要寫一個32位的整數,所以緩衝區ByteBuf
的容量至少是4個字節。經過ChannelHandlerContext.alloc()
獲取ByteBufAllocator
(字節緩衝區分配器),用他來分配一個新的buffer
像往常同樣把消息寫到網絡上。
等一下Σ( ° △ °|||),flip()
方法哪去了?還記不記得在NIO中曾經使用過的java.nio.ByteBuffer.flip()
(簡單總結就是把ByteBuffer
從寫模式變成讀模式)?ByteBuf
並無這個方法,由於他包含了兩個指針——讀指針和寫指針 (讀寫標記,不要理解成C裏的指針)。當你往ByteBuf
寫數據時,寫指針會移動而讀指針不變。這兩個指針剛好標記着數據的起始、終止位置。
與之相反,原生 NIO 並無提供一個簡潔的方式去標記數據的起始和終止位置,你必需要調用flip
方法。有 時候你極可能忘記調用flip
方法,致使發送不出數據或發送了錯誤數據。這樣的錯誤並不會發生在 netty,由於 netty 有不一樣的指針去應對不一樣的操做 (讀寫操做),這使得編程更加簡單,由於你再也不須要 flipping out (瘋狂輸出原生 NIO)
其餘須要注意的是ChannelHandlerContext.write()/writeAndFlush()
方法返回了ChannelFuture
。ChannelFuture
表示一個還沒發生的 I/O 操做。這意味着你請求的一些 I/O 操做可能還沒被處理,由於 netty 中全部的操做都是異步的。舉個例子,下面的代碼可能在消息發送以前就關閉了鏈接:
Channel ch = ...; ch.writeAndFlush(message); ch.close();
因此,你要在 (write()
返回的)ChannelFuture
完成以後再調用close()
。當write
操做完成後,ChannelFuture
會通知到他的listeners
(監聽器)。需加註意,close()
方法可能不會當即關閉連接,一樣close()
也會返回一個ChannelFuture
那麼咱們如何知道寫操做完成了?很簡單,只要向ChannelFuture
註冊監聽器 (ChannelFutureListener
) 就行。這一步,咱們建立了ChannelFutureListener
的匿名類,在寫操做完成時關閉連接。
你也可使用已經定義好的監聽器,例如這樣:
f.addListener(ChannelFutureListener.CLOSE);
爲了測試 Time server 是否如期工做,你可使用 unix 的命令行:
$ rdate -o <port> -p <host>
跟 DISCARD 和 ECHO 服務器不一樣,咱們要寫一個客戶端程序應對 TIME 協議,由於你沒法把一個32位整數翻譯成日期。本節中,咱們確保服務端正常工做,並學習如何使用 netty 寫一個客戶端程序。
netty 客戶端和服務器最大的不一樣在於,客戶端使用了不一樣的Bootstrap
和Channel
實現類。請看下面的例子:
package io.netty.example.time; public class TimeClient { public static void main(String[] args) throws Exception { String host = args[0]; int port = Integer.parseInt(args[1]); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); // (1) b.group(workerGroup); // (2) b.channel(NioSocketChannel.class); // (3) b.option(ChannelOption.SO_KEEPALIVE, true); // (4) b.handler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new TimeClientHandler()); } }); // Start the client. ChannelFuture f = b.connect(host, port).sync(); // (5) // Wait until the connection is closed. f.channel().closeFuture().sync(); } finally { workerGroup.shutdownGracefully(); } } }
Bootstrap
跟ServerBootstrap
類似,但他是做用在客戶端或無鏈接模式的 Channel (通道)。EventLoopGroup
,他會同時做爲 boss group 和 worker group,雖然客戶端並無 boss worker 這個概念。NioSocketChannel
而不是NioServerSocketChannel
。NioSocketChannel
會被用來建立客戶端Channel
ServerBootstrap
不一樣,這裏咱們沒有使用childOption()
,由於客戶端的SocketChannel
沒有父Channel
connect()
代替bind()
方法。正如你所見,客戶端代碼跟服務端代碼沒有很大的區別。那麼接下來就是實現ChannelHandler
,他會從服務端接收一個32位整數,翻譯成可讀的日期格式並打印出來,最後關閉鏈接:
package io.netty.example.time; import java.util.Date; public class TimeClientHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { ByteBuf m = (ByteBuf) msg; // (1) try { long currentTimeMillis = (m.readUnsignedInt() - 2208988800L) * 1000L; System.out.println(new Date(currentTimeMillis)); ctx.close(); } finally { m.release(); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } }
TCP/IP
時, netty 把讀取的數據放到ByteBuf
看起來很是簡單,跟服務端沒有多大區別。然而有時候 handler 會發生錯誤,例如拋出異常IndexOutOfBoundsException
,在下一章節咱們會做具體討論。
像TCP/IP
這種基於流的傳輸協議,接收的數據會存儲到socket
緩衝區。不幸的是,這類緩衝區不是數據包隊列,而是字節流隊列。這意味着,即便你想發送兩個消息並打包成兩個數據包,操做系統只會把他們看成一連串字節。所以,這不能保證你讀到的數據剛好是遠程發送端寫出的數據。舉個例子,假設操做系統TCP/IP
棧收到三個數據包:
由於這種流式協議的特性,應用程序頗有可能像下圖的方式那樣讀取數據碎片:
因此,做爲接收端 (無論是服務端仍是客戶端),應把接收到的數據 (字節流) 整理成一個或多個易於理解的數據貞。對於上述的例子,整理以下:
讓咱們回到 TIME Client 這個例子。一32位整數的數據量很是小,在本例中不該用被分割。然而,問題在於他確實有可能被分割,可能性隨着通訊數據量的增大而增大。
一個簡單的方法是建立一個內部的cumulative buffer
(累積緩衝區),等待數據直到接收到4個字節爲止。下面是修改過的TimeClientHandler
:
package io.netty.example.time; import java.util.Date; public class TimeClientHandler extends ChannelInboundHandlerAdapter { private ByteBuf buf; @Override public void handlerAdded(ChannelHandlerContext ctx) { buf = ctx.alloc().buffer(4); // (1) } @Override public void handlerRemoved(ChannelHandlerContext ctx) { buf.release(); // (1) buf = null; } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { ByteBuf m = (ByteBuf) msg; buf.writeBytes(m); // (2) m.release(); if (buf.readableBytes() >= 4) { // (3) long currentTimeMillis = (buf.readUnsignedInt() - 2208988800L) * 1000L; System.out.println(new Date(currentTimeMillis)); ctx.close(); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } }
ChannelHandler
有兩個與生命週期有關的監聽方法:handlerAdded()
和handlerRemove()
。你能夠在裏面執行任意的初始化或析構任務,只要他們不會阻塞程序很長時間。buf
是否接收到足夠的數據 (4個字節),如果,則進行實際業務處理。不然當有更多數據到達時,netty 會再次調用channelRead()
,直到緩衝區累積到4個字節。雖然方案一解決了問題,但修改過的 handler 看上去不是那麼簡潔。想像一下協議變得更爲複雜,例如包含多個可變長字段,你的ChannelInboundHandler
很快會變得不可維護。
你可能會注意到,能夠向ChannelPipeline
添加多個ChannelHandler
。因此,你能夠把一個龐大複雜的ChannelHandler
分割成多個小模塊,從而減少應用的複雜性。舉個例子,你能夠把TimeClientHandler
分割成兩個handler:
TimeDecoder
TimeClientHandler
幸運的是,netty 提供了一個可擴展的父類,幫助你書寫TimeDecoder
package io.netty.example.time; public class TimeDecoder extends ByteToMessageDecoder { // (1) @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { // (2) if (in.readableBytes() < 4) { return; // (3) } out.add(in.readBytes(4)); // (4) } }
ByteToMessageDecoder
實現了ChannelInboundHandler
,使你更容易去處理數據碎片。ByteToMessageDecoder
調用decode()
方法並維護了一個內部的cumulative buffer
(累積緩衝區)decode()
方法不會添加任何東西到 out 列表。當有更多數據到達時,ByteToMessageDecoder
會再次調用decode()
方法。decode()
方法向 out 列表添加了一個對象,這表示decoder
(解碼器) 成功解析了一個消息。ByteToMessageDecoder
會拋棄掉cumulative buffer
(累積緩衝區)中已讀數據。請記住,你不須要去解析多個消息,由於ByteToMessageDecoder
會持續調用decode()
,直到他沒有往 out 列表添加對象。既然但願往ChannelPipeline
添加其餘 handler (上面的TimeDecoder
),咱們要修改TimeClient
中的ChannelInitializer
:
b.handler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new TimeDecoder(), new TimeClientHandler()); } });
若是你充滿冒險精神,你能夠嘗試使用ReplayingDecoder
,他會使代碼更加簡潔:
public class TimeDecoder extends ReplayingDecoder<Void> { @Override protected void decode( ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { out.add(in.readBytes(4)); } }
此外,netty 提供了不少開箱即用的decoder
,他們已經實現了大多數的網絡協議,避免你本身去實現一個龐大的難以維護的handler。請參考下面的包獲取更多詳細例子:
io.netty.example.factorial
二進制協議io.netty.example.telnet
文本協議上面全部例子都使用了ByteBuf
做爲協議中基本的數據結構。在本小節,咱們將要升級 TIME 協議中的客戶端和服務端,使用 POJO 代替ByteBuf
使用 POJO 的優點是顯而易見的:你的 handler 變得易於維護和可重用,經過把 (從ByteBuf
中抽取信息的) 代碼分離出來。在 TIME 協議的例子裏,咱們僅僅讀取一個32位的整數,直接使用ByteBuf
並不會有太大問題。然而,當實現一個真實的網絡協議時,你會發現作代碼分離頗有必要。
首先,讓咱們定義一個新的類型UnixTime
:
package io.netty.example.time; import java.util.Date; public class UnixTime { private final long value; public UnixTime() { this(System.currentTimeMillis() / 1000L + 2208988800L); } public UnixTime(long value) { this.value = value; } public long value() { return value; } @Override public String toString() { return new Date((value() - 2208988800L) * 1000L).toString(); } }
如今修改TimeDecoder
,讓他向out列表添加一個UnixTime
而不是ByteBuf
@Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { if (in.readableBytes() < 4) { return; } out.add(new UnixTime(in.readUnsignedInt())); }
既然修改了TimeDecoder
,TimeClientHandler
也不能再使用ByteBuf
了:
@Override public void channelRead(ChannelHandlerContext ctx, Object msg) { UnixTime m = (UnixTime) msg; System.out.println(m); ctx.close(); }
是否是更加簡單、優雅?相同的竅門一樣能使用在服務端。此次讓咱們先修改TimeServerHandler
:
@Override public void channelActive(ChannelHandlerContext ctx) { ChannelFuture f = ctx.writeAndFlush(new UnixTime()); f.addListener(ChannelFutureListener.CLOSE); }
如今只剩下encoder
(編碼器),他須要實現ChannelOutboundHandler
,把UnixTime
翻譯回ByteBuf
。這裏比書寫decoder
更加簡單,由於咱們再也不須要處理數據包碎片並把他們組裝起來了。
package io.netty.example.time; public class TimeEncoder extends ChannelOutboundHandlerAdapter { @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) { UnixTime m = (UnixTime) msg; ByteBuf encoded = ctx.alloc().buffer(4); encoded.writeInt((int)m.value()); ctx.write(encoded, promise); // (1) } }
ChannelPromise
傳遞到write()
,以便 netty 把他標記爲成功或失敗 (當數據真正寫到網絡時)。ctx.flush()
,由於ChannelOutboundHandlerAdapter
中有一個單獨的方法void flush(ChannelHandlerContext ctx)
專門用來處理flush
操做。你可使用MessageToByteEncoder
更加地簡化代碼:
public class TimeEncoder extends MessageToByteEncoder<UnixTime> { @Override protected void encode(ChannelHandlerContext ctx, UnixTime msg, ByteBuf out) { out.writeInt((int)msg.value()); } }
最後一步就是把TimeEncoder
添加到服務端ChannelPipeline
,留做練習。
關閉netty應用很簡單——經過shutdownGracefully()
去關閉全部建立的EventLoopGroup
。他返回一個Future
去通知你何時EventLoopGroup
和他從屬的 Channel 已經徹底關閉了。
本章,咱們快速瀏覽了 netty,使用他書寫了一個可用的網絡應用。接下來的章節中會介紹更多關於 netty 的詳細資料,咱們也但願你去重溫io.netty.example package
包中的例子。netty 社區的大門會向你敞開,你能夠向社區提出問題和意見,您的的反饋會幫助netty項目變得更加完善。