Netty4.0 開發指導 1
原文: http://netty.io/wiki/user-guide-for-4.x.html
前言
1. 問題
如今我們使用通用的程序或者第三方的庫去與對方交互. 比如,我們經常使用HTTP庫從web服務器檢索信息或者通過web服務調用遠程方法.
然而,通用的協議或者它的實現有時擴展性不好. 這就像我們不使用通用的HTTP服務器去交換大文件, e-mail消息, 以及近實時消息像股票信息和多人遊戲數據. 多麼需要一個專門爲了特殊用途高度優化的協議實現. 比如, 你可能想實現一個優化的HTTP服務器用於基於AJAX的聊天應用, 媒體流傳輸, 或者大文件傳輸. 你甚至可能想去設計和實現一個完整的新協議,爲你的需求量身定製.
另一個不可避免的情況是當你必須確保一個遺留專有協議和一個老的系統交互. 在這種情況下, 重要的是多快我們能實現這個協議, 還不犧牲應用程序所產生的穩定性和性能.
2.解決方案
Netty項目努力提供一個異步的事件驅動網絡程序框架和工具, 爲快速開發易於維護的高性能, 高可擴展性的協議服務器和客戶端.
換句話說, Netty是一個NIO客戶端/服務器架構, 可以快速和容易的開發網絡程序就像協議服務器和客戶端.它極大的簡化了網絡開發, 如TCP和UDP套接字服務器的開發.
"快速和容易"不是意味着產生的程序將受到來自於可維護性和性能問題的損害. 帶着來自於大量協議如FTP, SMTP, HTTP以及各種二進制和基於文本的傳統協議的實現的經驗, Netty被精心設計. 所以, Netty成功的找到一種方法去實現簡易開發, 性能, 穩定性和靈活性不衝突.
一些用戶可能已經發現其他的一些網絡程序框架聲稱有相同的優勢, 你可能想問什麼使Netty與他們如此的不同. 答案在它建立的理念. Netty的設計給你來自於API條款和實施之日起兩者最舒適的體驗. 這不是有形的東西,但你將意識到這個理念將使你的生活更容易當你閱讀這個指南和玩轉Netty.
第1章 Getting Started
這章圍繞着Netty的核心結構和一些簡單例子可以讓你快速上手. 當你讀完本章你將能夠馬上寫一個基於Netty的客戶端和服務端.
在開始之前
在本章介紹的例子運行的最低需求只有兩個, 最新版本的Netty和JDK1.6或以上.
寫一個Discard服務端
世界最簡單的協議不是"Hello World!"是DISCARD. 這個協議會丟棄任務接收到的數據沒有響應.
去實現DISCARD協議, 唯一需要做的是忽略所有接收到的數據. 讓我們直接從handler的實現開始, 它處理Netty產生的I/O事件.
01 |
package io.netty.example.discard; |
03 |
import io.netty.channel.ChannelHandlerContext; |
04 |
import io.netty.channel.ChannelInboundHandlerAdapter; |
05 |
import io.netty.channel.MessageList; |
08 |
* Handles a server-side channel. |
10 |
public class DiscardServerHandler extends ChannelInboundHandlerAdapter
{ |
13 |
public void messageReceived(ChannelHandlerContext ctx, MessageList<Object>
msgs) { |
15 |
msgs.releaseAllAndRecycle(); |
19 |
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
{ |
21 |
cause.printStackTrace(); |
1. DiscardServerHandler繼承了ChannelInboundHandlerAdapter, 這是一個ChannelInboundHandler的實現. ChannelInboundHandlerAdapter提供了各種事件處理方法, 這些方法你可以覆蓋. 暫時, 它僅僅足夠擴展ChannelInboundHandlerAdapter而不是實現自己的處理接口.
2. 我們在這重寫messageReceived事件處理方法. 每當接收到來自客戶端的新數據, 這個方法被調用時會傳進去一個MessageList的參數, 這個參數包含着收到字節列表. 在這個例子中, 我們僅僅通過調用releaseAllAndRecycle方法丟棄接收到的數據去實現DISCARD協議.
3. exceptionCaught()方法會帶着Throwable調用,當Netty發生一個異常.你可能想去發送一個帶着錯誤編碼的返回消息在關閉連接前.
到目前爲止進展順利, 我們已經實現了DISCARD服務端的一半. 現在剩下的是寫main()方法.
01 |
package io.netty.example.discard; |
03 |
import io.netty.bootstrap.ServerBootstrap; |
04 |
import io.netty.channel.ChannelFuture; |
05 |
import io.netty.channel.ChannelInitializer; |
06 |
import io.netty.channel.EventLoopGroup; |
07 |
import io.netty.channel.nio.NioEventLoopGroup; |
08 |
import io.netty.channel.socket.SocketChannel; |
09 |
import io.netty.channel.socket.nio.NioServerSocketChannel; |
12 |
* Discards any incoming data. |
14 |
public class DiscardServer { |
16 |
private final int port; |
18 |
public DiscardServer( int port) { |
22 |
public void run() throws Exception
{ |
23 |
EventLoopGroup bossGroup = new NioEventLoopGroup(); |
24 |
EventLoopGroup workerGroup = new NioEventLoopGroup(); |
26 |
ServerBootstrap b = new ServerBootstrap(); |
27 |
b.group(bossGroup, workerGroup) |
28 |
.channel(NioServerSocketChannel. class ) |
29 |
.childHandler( new ChannelInitializer<SocketChannel>() { |
31 |
public void initChannel(SocketChannel ch) throws Exception
{ |
32 |
ch.pipeline().addLast( new DiscardServerHandler()); |
35 |
.option(ChannelOption.SO_BACKLOG, 128 ) |
36 |
.childOption(ChannelOption.SO_KEEPALIVE, true ); |
39 |
ChannelFuture f = b.bind(port).sync(); |
44 |
f.channel().closeFuture().sync(); |
46 |
workerGroup.shutdownGracefully(); |
47 |
bossGroup.shutdownGracefully(); |
51 |
public static void main(String[]
args) throws Exception { |
53 |
if (args.length > 0 ) { |
54 |
port = Integer.parseInt(args[ 0 ]); |
58 |
new DiscardServer(port).run(); |
1. NioEventLoopGroup是一個多線程事件循環處理I/O操作. Netty提供各種EventLoopGroup爲實現不同的傳輸協議. 在這個例子中, 我們實現了服務端的程序, 因此兩個NioEventLoopGroup被使用. 第一個叫作"boss", 被用來處理接收到的新連接, 第二個叫作"worker", 一旦"boss"接受了連接並且註冊了這個連接就會交給"worker"處理. 使用了多少線程以及根據實現他們是如何映射到被創建的channel並且可以通過構造方法設置.
2. ServerBootstrap是設置服務端的輔助類. 你可以設置服務端直接用channel. 然而, 請注意這一個繁鎖的過程,大多數情況下你不需要這樣做.
3. 這裏, 我們指定使用NioServerSocketChannel類, 這個類被用來初始化一個新的Channel以接用連上來的連接.
4. 這個處理單元將一直被一個新的連接上來的Channel運行. ChannelInitialize是被指定的處理單元, 用來幫助用戶設置一個新的Channel. 這裏最有可能的操作是通過添加一些處理單元到一個新的Channel的ChannelPipeline, 這些處理單元像 就像DiscardServerHandler來實現你的網絡程序. 由於程序變的複雜, 很可能你將添加更多的handlers到流水線上,
最終擴展這個匿名類成爲頂級類.
5. 你也可以設置參數指定channel的實現. 我們是在寫一個TCP/IP的服務端,所以我們允許設置套接字選項就像tcpNoDelay和keepAlive.
6. 你有注意到option()和childOption()嗎? option()是針對NioServerSocketChannel用來接受連上來的連接. childOption()是針對通過上層ServerChannel接受到所有Channel.
7. 我們準備要開始了.剩下的就是綁定端口和運行server了. 我們綁定本機所有網卡的8080端口. 你可以調用多次bind()方法只要你想.
觀察接收到的數據
現在我們已經寫完了第一個服務端程序, 我們需要測試他是否真的可以工作. 最簡單的測試方式是使用telnet命令. 例如, 你可以在命令行輸入"telnet localhost 8080" 然後再輸入一些東西.
然而,我們能說這個服務端程序工作的很好嗎? 我們不能真正的知道因爲這是一個丟棄協議的服務端程序. 你得不到任務的響應. 爲了證明他真的可以工作, 讓我們修改一下服務端程序讓他打印他接收到的數據.
我們已經知道每當收到數據MessageList會被填充並且messageReceived方法會被調用. 讓我們加一個代碼到DiscardServerHandler中的messageReceived方法:
02 |
public void messageReceived(ChannelHandlerContext ctx, MessageList<Object> msgs) { |
03 |
MessageList<ByteBuf> messages = msgs.cast(); |
05 |
for (ByteBuf in: messages) { |
06 |
while (in.readable()) { |
07 |
System.out.println(( char ) buf.readByte()); |
12 |
msgs.releaseAllAndRecycle(); |
1. 這個循環很低效實際上可以簡化爲: System.out.println(buf.toString(io.netty.util.CharsetUtil.US_ASCII))
2. MessageList.releaseAllAndRecycle()將釋放池化消息的所有引用計數並將自己返回到對象池中.
如果你再次運行telnet命令, 你將看到服務端接收到的數據.
寫一個Echo服務端
到目前爲止, 我們一直消費數據沒有任何的響應. 服務端通常情況是響應請求. 讓我們學習如何寫一個通過實現ECHO協議返回消息到客戶端, ECHO協議是收到任何數據都發送回來.
和DISCARD服務端程序唯一的不同的是,之前章節我們已經實現的將收到的數據打印到控制檯替換爲將收到的數據發送回去.
2 |
public void messageReceived(ChannelHandlerContext ctx, MessageList<Object> msgs) { |
1. ChannelHandlerContext對象有一個與他相關聯的Channel的引用. 這裏, 返回的Channel代表的是收到MessageList的連接. 我們可以拿到這個Channel並調用write()方法往遠程節點寫點東西.
Time協議服務端
這部分實現的是TIME協議. 這裏與之前例子的不同是, 發送一個消息, 他包含一個32位整型, 一旦這個消息發出後將不接收任何請求和斷開連接. 在這個例子裏, 你將學習如何構建和發送消息, 以及在完成後關閉連接.
因爲我們將忽略任務收到的數據但是一旦建立了連接就會發送消息, 我們不能使用messageReceived()方法. 我們應該覆蓋channelActivie()方法代替. 下面是實現:
01 |
package io.netty.example.time; |
03 |
public class TimeServerHandler extends ChannelInboundHandlerAdapter
{ |
06 |
public void channelActive( final ChannelHandlerContext
ctx) { |
07 |
final ByteBuf time = ctx.alloc().buffer( 4 ); |
08 |
time.writeInt(( int ) (System.currentTimeMillis() / 1000L + 2208988800L); |
10 |
final ChannelFuture f = ctx.write(time); |
11 |
f.addListener( new ChannelFutureListener() { |
13 |
public void operationComplete(ChannelFuture future) { |
21 |
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
{ |
22 |
cause.printStackTrace(); |
1. 正如上文, 當有連接建立將會調用channelActive()方法以及準備產生的交互消息. 讓我們在這個方法裏寫一個32位的整型數字代表當前時間.
2. 爲了發送消息, 我們需要分配一個新的包含消息的buffer. 我們將寫一個32位整型, 因此我們需要一個容量至少4字節的ByteBuf. 通過ChannelHandlerContext.alloc()拿到當前的ByteBufAllocator用來分配一個新的 buffer.
3. 通常, 我們會寫一個構造好的消息.
但 是等等, flip在哪? 在NIO我們在發送消息沒有調用ByteBuffer.flip()方法? ByteBuf沒有這樣一個方法因爲他有兩個指針; 一個是讀操作另一個是寫操作. 當你寫東西到一個ByteBuf時寫索引將增長而讀索引不改變. 讀索引和寫索引分別代表的是消息的開始和結束.
比較之下, NIO的緩衝區沒有提供一個清楚的方式的找出消息內容的開始和結束沒有調用flip方法. 當你忘記flip緩衝區時你將有大麻煩因爲沒有數據或者錯誤的數據將被髮送. 類似這種錯誤在Netty中不會發生因爲我們有不同的指針對應不同的操作類型.
另一個注意的要點是寫方法會返回一個ChannelFuture. ChannelFuture代表一個還沒有完成的I/O操作. 這意味着, 請求操作可能還沒有被執行完成因爲在Netty中所有的操作都是異步的. 例如, 下面的代碼可能關閉操作甚至在發送消息之前:
1 |
ChannelHandlerContext ctx = ...; |
因爲, 你需要在寫操作完成通知你後再調用關閉方法. 請注意, 關閉操作也不是立即關閉, 而是返回一個ChannelFuture.
4. 當你寫請求完成後我們如何得到通知? 這是儘量簡單的給返回的ChannelFuture添加一個ChannelFutureListener. 這裏, 當操作結束我們創建了一個匿名ChannelFutureListener關閉Channel.
或者, 你可以簡化代碼使用預定義listener:
1 |
f.addListener(ChannelFutureListener.CLOSE); |
Time協議客戶端
不像DISCARD和ECHO服務端, 我們需要給TIME協議寫一個客戶端因爲普通人不能轉換32位二進制的日曆數據. 這部分, 我們討論如何確實服務端工作正常以及學習如何寫一個Netty的客戶端.
在Netty裏服務端和客戶端最大以及唯一的不同是需要的Bootstrap. 看一下接下來的代碼段:
01 |
package io.netty.example.time; |
04 |
public class TimeClient { |
05 |
public static void main(String[]
args) throws Exception { |
06 |
String host = args[ 0 ]; |
07 |
int port = Integer.parseInt(args[ 1 ]); |
08 |
EventLoopGroup workerGroup = new NioEventLoopGroup(); |
11 |
Bootstrap b = new Bootstrap(); |
12 |
b.group(workerGroup); ( 2 ) |
13 |
b.channel(NioSocketChannel. class ); |
14 |
b.option(ChannelOption.SO_KEEPALIVE, true ); |
15 |
b.handler( new ChannelInitializer<SocketChannel>() { |
17 |
public void initChannel(SocketChannel ch) throws Exception
{ |
18 |
ch.pipeline().addLast( new TimeClientHandler()); |
23 |
ChannelFuture f = b.connect(host, port).sync(); |
26 |
f.channel().closeFuture().sync(); |
28 |
workerGroup.shudownGracefully(); |
1. Boostrap與ServerBootstrap很相似, 除了他是針對非服務端channel像客戶端或者無連接模式的channel.
2. 如果只指定了一個EventLoopGroup, 他將被用來作爲"boss"組和"worker"組. 雖然針對客戶端"boss"組是不會被使用的.
3. 替換NioServerSocketChannel, NioSocketChannel被用來創建客戶端的Channel.
4. 注意我們沒有使用childOption(), 這裏不像ServerBootstrap, 因爲客戶端的SocketChannel沒有父級.
5. 我們應該調用connect()方法而不是bind()方法.
正如你能看到的, 他與服務端的代碼不是真的不同. 實現了什麼樣的ChannelHandler? 他應該從服務端收到一個32位整型數字, 轉換爲普通人可以讀懂的格式, 打印後關閉連接:
01 |
package io.netty.example.time; |
03 |
import java.util.Date; |
05 |
public class TimeClientHandler extends ChannelInboundHandlerAdapter
{ |
07 |
public void messageReceived(ChannelHandlerContext ctx, MessageList<Object>
msgs) { |
08 |
ByteBuf m = (ByteBuf) msgs.get( 0 ); |
09 |
long currentTimeMillis = (buf.readInt() - 2208988800L) * 1000L; |
10 |
System.out.println( new Date(currentTimeMillis)); |
11 |
msgs.releaseAllAndRecycle(); |
16 |
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
{ |
17 |
cause.printStackTrace(); |
1. 只處理第一條消息. 注意MessageList的大小是大於0的.
這個看起來很簡單, 和服務端的例子看起來沒有任何的不同. 然而, 這個處理邏輯有時將拒絕工作會發現IndexOutOfBoundsException. 我們將在以後章節討論爲什麼會發生.
處理基於流的傳輸協議
套接字緩衝區的小警告
在基於流的傳輸協議裏就像TCP/IP, 收到的數據會存儲到套接字緩衝區. 不幸的是, 基於流傳輸的緩衝區不是一個數據包隊列而是一個字節隊列. 這意味着, 即使你發送了兩條消息作爲兩條獨立的數據包, 操作系統也不會像兩條消息一樣處理他們而是爲一串字節. 所以, 不保證你讀到的正是你遠程節點寫的. 例如, 讓我們假設TCP/IP協議棧的操作系統已經收到三個數據包:
因爲基於流協議的一般性質, 在你的程序裏有很高的機會會將以下面這種零散的形式讀到他們:
因此, 收到的部分, 無論是服務端或者客戶端, 應該整理零散的收到的數據到一個或多個有意義的框(frames)通過程序邏輯可以容易的理解. 在上面的例子, 收到的數據應該像下面這樣被裝框:
第一個解決方案
現在讓我們回去看一下TIME客戶端的例子. 也有相同的問題. 32位整型數字是非常小的數據, 不太可能經常被分段. 然而, 問題是他也可能被分段, 隨着流量的增加分段的可能性也將增加.
簡單的解決方案是創建一個內部的累積緩衝區並且一直等到緩衝區收到超過4個字節的數據. 下面是更改TimeClientHandler的實現並解決了問題:
01 |
package io.netty.example.time; |
03 |
import java.util.Date; |
05 |
public class TimeClientHandler extends ChannelInboundHandlerAdapter
{ |
09 |
public void handlerAdded(ChannelHandlerContext ctx) { |
10 |
buf = ctx.alloc().buffer( 4 ); |
14 |
public void handlerRemoved(ChannelHandlerContext ctx) { |
20 |
public void messageReceived(ChannelHandlerContext ctx, MessageList<Object>
msgs) { |
21 |
for (ByteBuf m: msgs.<ByteBuf>cast()) { |
24 |
msgs.releaseAllAndRecycle(); |
26 |
if (buf.readableBytes() >= 4 ) { |
27 |
long currentTimeMillis = (buf.readInt() - 2208988800L) * 1000L; |
28 |
System.out.println( new Date(currentTimeMillis)); |
34 |
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
{ |
35 |
cause.printStackTrace(); |
1. 一個ChannelHandler有兩個生命週期的監聽器方法: handlerAdded() 和 handlerRemoved(). 你可以執行任意初始化(反初始化)任務只要不要阻塞太長時間.
2. MessageList.<T>cast()能夠讓你轉換MessageList的參數類型而不用看到煩人未檢查類型警告.
3. 首先, 所有接收到的數據應該累積到緩衝區.
4. 然後, 這個處理單元檢查是否有足夠的數據, 這人例子是4個字節, 然後進行實際的業務邏輯. 此外, 當有更多的數據收到將調用messageReceived方法.
第二個解決方案
雖然第一個解決方案已經解決TIME客戶端的問題, 被修改後的處理單元看起來不清晰. 想像一下一個更復雜的協議組合多個字段就像可變長度字段. 你的ChannelHandler的實現將很快變的難以維護.
正如你注意到的, 你可以添加超過一個ChannelHandler到ChannelPipeline, 所以, 你可以分割一個複雜龐大的ChannelHandler到多個模塊去減少你程序的複雜性. 例如, 你可以分割TimeClientHandler到兩個處理單元:
- TimeDecoder處理分段問題
- TimeClientHandler初始化的簡化版本
幸好, Netty提供了一個可擴展類幫助你寫第一個立即可用.
01 |
package io.netty.example.time; |
03 |
public class TimeDecoder extends ByteToMessageDecoder
{ |
05 |
protected void decode(ChannelHandlerContext ctx, ByteBuf in, MessageList<Object>
out) { |
06 |
if (in.readableBytes() < 4 ) { |
10 |
out.add(in.readBytes( 4 )); |
1. ByteToMessageDecoder是一個ChannelHandler的實現, 他使處理分段問題更容易.
2. 每當有新數據收到, ByteToMessageDecoder都會調用decode()方法並帶着一個內部維護的累積緩衝區.
3. 如果累積緩衝區沒有足夠的數據decode()方法將不會添加任何東西到out中. 當再有數據收到ByteToMessageDecoder將再次調用decode()方法.
4. 如果decode()方法添加一個對象到out, 這意味着解碼器成功解碼一條消息. ByteToMessageDecoder將丟棄內部累積緩衝區讀到的部分. 請記住你不需要解碼多條消息. ByteToMessageDecoder將一直調用decoder方法直接沒有任何可讀數據.
現在我們有另一個處理單元需要插入到ChannelPipeline, 我們應該更改ChannelInitialized的實現:
1 |
b.handler( new ChannelInitializer<SocketChannel>() { |
3 |
public void initChannel(SocketChannel ch) throws Exception
{ |
4 |
ch.pipeline().addLast( new TimeDecoder(), new TimeClientHandler()); |
另外, Netty提供了立即可用的解碼器讓你更容易的實現更多的協議以及幫你避免處理一個寵大且難以維護的處理單元的實現. 請查看以面的包獲取更多信息:
- io.netty.example.factorial 針對二進制協議
- io.netty.example.telnet 針對基於文本行協議