前言
Netty是一個異步事件驅動的網絡應用程序框架,用於快速開發可維護的高性能協議服務器和客戶端。
Netty4的官方網站是:http://netty.io/
Netty 是一個廣泛使用的 Java 網絡編程框架(Netty 在 2011 年獲得了Duke's Choice Award,見https://www.java.net/dukeschoice/2011)。它活躍和成長於用戶社區,像大型公司 Facebook 和 Instagram 以及流行 開源項目如 Infinispan, HornetQ, Vert.x, Apache Cassandra 和 Elasticsearch 等,都利用其強大的對於網絡抽象的核心代碼。
- 設計
- 針對多種傳輸類型的統一接口 - 阻塞和非阻塞
- 簡單但更強大的線程模型
- 真正的無連接的數據報套接字支持
- 鏈接邏輯支持複用
- 易用性
- 完善的Javadoc
- 全面的代碼示例
- 性能
- 比核心的 Java API 更好的吞吐量,較低的延時
- 資源消耗更少,這個得益於共享池和重用
- 減少內存拷貝
- 健壯性
- 消除由於慢、快、或重載連接產生的OutOfMemoryError
- 消除經常發現在 NIO 在高速網絡中的應用中的不公平讀/寫比
- 安全
- 完整的 SSL/ TLS 和 StartTLS 的支持
- 運行在受限的環境例如 Applet 或 OSGI
- 社區
- 社區完善、更新/發佈頻繁
背景1 - Reactor模型
wiki:
The reactor design pattern is an event handling pattern for handling service requests delivered concurrently to a service handler by one or more inputs. The service handler then demultiplexes the incoming requests and dispatches them synchronously to the associated request handlers.
幾個關鍵點:
- 事件驅動(event handling)
- 可以處理一個或多個輸入源(one or more inputs)
- 通過Service Handler同步的將輸入事件(Event)採用多路複用分發給相應的Request Handler(多個)處理
更多參考: https://my.oschina.net/u/1859679/blog/1844109
背景2 - Java網絡編程(BIO)
經典的BIO服務端:
- 一個主線程監聽某個port,等待客戶端連接
- 當接收到客戶端發起的連接時,創建一個新的線程去處理客戶端請求
- 主線程重新回到監聽port,等待下一個客戶端連接
缺點:
- 每個新的客戶端Socket連接,都需要創建一個Thread處理,將會創建大量的線程
- 線程開銷較大,連接多時,內存耗費大,CPU上下文切換開銷也大
背景3 - Java NIO
Java NIO 由以下幾個核心部分組成:
- Channels
- Buffers
- Selectors
傳統IO基於字節流和字符流進行操作,而NIO基於Channel和Buffer(緩衝區)進行操作,數據總是從通道讀取到緩衝區中,或者從緩衝區寫入到通道中。Selector(選擇區)用於監聽多個通道的事件(比如:連接打開,數據到達)。因此,單個線程可以監聽多個數據通道。
NIO和傳統IO(一下簡稱IO)之間第一個最大的區別是,IO是面向流的,NIO是面向緩衝區的。
Netty的重要組件
下面枚舉所有的Netty應用程序的基本構建模塊,包括客戶端和服務端。
BOOTSTRAP
Netty 應用程序通過設置bootstrap(引導)類的開始,該類提供了一個用於應用程序網絡配置的容器。Netty有兩種類型的引導: 客戶端(Bootstrap)和服務端(ServerBootstrap)
CHANNEL
底層網絡傳輸API必須提供給應用I/O操作的接口,傳入(入站)或者傳出(出站)數據的載體,如讀,寫,連接,綁定等等。對於我們來說,這結構幾乎總是會成爲一個"socket"。
CHANNELHANDLER
ChannelHandler 支持很多協議,並且提供用於數據處理的容器。我們已經知道ChannelHandler由特定事件觸發。ChannelHandler可專用於幾乎所有的動作,包括一個對象轉爲字節,執行過程中拋出的異常處理。
常用的一個接口是 ChannelInboundHandler,這個類型接收到入站事件(包括接收到的數據)可以處理應用程序邏輯。
當你需要提供相應時,你也可以從ChannelInboundHandler沖刷數據。一句話,業務邏輯經常存活於一個或者多個ChannelInboundHandler。
CHANNELPIPELINE
ChannelPipline提供了一個容器給 ChannelHandler鏈並提供了一個API用於管理沿着鏈入站和出站事件的流動。每個Channel都有自己的ChannelPipeline,當Channel創建時自動創建的。
EVENTLOOP
EventLoop 用於處理 Channel 的 I/O 操作,控制流、多線程和併發。一個單一的 EventLoop通常會處理多個 Channel 事件。一個 EventLoopGroup 可以含有多於一個的 EventLoop 和 提供了一種迭代用於檢索清單中的下一個。
CHANNELFUTURE
Netty 所有的 I/O 操作都是異步。因爲一個操作可能無法立即返回,我們需要有一種方法在以後確定它的結果。
出於這個目的,Netty 提供了接口 ChannelFuture,它的 addListener 方法註冊了一個 ChannelFutureListener ,當操作完成時,可以被異步通知(不管成功與否)。
以上組件的關係:
[站外圖片上傳中...(image-67dbed-1563459279939)]
幾點重要的約定:
- 一個EventLoopGroup包含一個或多個EventLoop
- 一個EventLoop在其生命週期內只能和一個Thread綁定
- EventLoop處理的I/O事件都由它綁定的Thread處理
- 一個Channel在其生命週期內,只能註冊於一個EventLoop
- 一個EventLoop可能被分配處理多個Channel。也就是EventLoop與Channel是1:n的關係
- 一個Channel上的所有ChannelHandler的事件由綁定的EventLoop中的I/O線程處理
- 不要阻塞Channel的I/O線程,可能會影響該EventLoop中其他Channel事件處理
第一個 Netty 應用: Echo client / server
本應用的源碼請見 netty倉庫中的example目錄。
接下來,我們來構建一個完整的Netty客戶端和服務器,更完整地瞭解Netty的API是如何實現客戶端和服務器的。
先來看看 Netty 應用 - Echo client/server 總覽:
[站外圖片上傳中...(image-5996c5-1563459279939)]
echo應用的客服端和服務器的交互很簡單: 客戶端啓動後,建立一個連接併發送一個或多個消息到服務端,服務端接受到的每個消息再返回給客戶端。
服務端代碼
- 一個信息處理器(handler): 這個實現是服務端的業務邏輯部分,當連接創建後和接收信息後的處理類。
- 服務器: 主要通過
ServerBootstrap
設置服務器的監聽端口等啓動部分。
EchoServerHandler
通過繼承ChannelInboundHandlerAdapter
,這個類提供了默認的ChannelInboundHandler
實現,只需覆蓋以下的方法:
- channelRead() - 每個消息入站都會調用
- channelReadComplete() - 通知處理器最後的channelRead()是當前批處理中的最後一條消息時調用
- exceptionCaught() - 捕獲到異常時調用
@ChannelHandler.Sharable // 標識這類的實例之間可以在 channel 裏面共享
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf in = (ByteBuf) msg;
System.out.println("Server received: " + in.toString(CharsetUtil.UTF_8));
ctx.write(in); // 將所接收的消息返回給發送者
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER) // 沖刷所有待審消息到遠程節點。關閉通道後,操作完成
.addListener(ChannelFutureListener.CLOSE);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
EchoServer
創建ServerBootstrap
實例來引導服務器,本服務端分配了一個NioEventLoopGroup
實例來處理事件的處理,如接受新的連接和讀/寫數據,然後綁定本地端口,分配EchoServerHandler
實例給Channel,這樣服務器初始化完成,可以使用了。
public class EchoServer {
private final int port;
public EchoServer(int port) {
this.port = port;
}
public void start() throws Exception {
NioEventLoopGroup group = new NioEventLoopGroup(); // 創建 EventLoopGroup
try {
ServerBootstrap bootstrap = new ServerBootstrap(); // 創建 ServerBootstrap
bootstrap.group(group)
.channel(NioServerSocketChannel.class) // 指定使用 NIO 的傳輸 Channel
.localAddress(new InetSocketAddress(port)) // 設置 socket 地址使用所選的端口
.childHandler(new ChannelInitializer<SocketChannel>() { // 添加 EchoServerHandler 到 Channel 的 ChannelPipeline
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new EchoServerHandler());
}
});
ChannelFuture future = bootstrap.bind().sync(); // 綁定的服務器;sync 等待服務器關閉
System.out.println(EchoServer.class.getName() + " started and listen on " + future.channel().localAddress());
future.channel().closeFuture().sync(); // 關閉 channel 和 塊,直到它被關閉
} finally {
group.shutdownGracefully().sync(); // 關閉 EventLoopGroup,釋放所有資源。
}
}
public static void main(String[] args) throws Exception {
int port = 4567;
if (args.length == 1) {
port = Integer.parseInt(args[0]);
}
new EchoServer(port).start(); // 設計端口、啓動服務器
}
}
客戶端代碼
客戶端要做的是:
- 連接服務器
- 發送消息
- 等待和接受服務器返回的消息
- 關閉連接
EchoClientHandler
繼承SimpleChannelInboundHandler
來處理所有的事情,只需覆蓋三個方法:
- channelActive() - 服務器的連接被建立後調用
- channelRead0() - 從服務器端接受到消息調用
- exceptionCaught() - 捕獲異常處理調用
@ChannelHandler.Sharable // @Sharable 標記這個類的實例可以在channel裏共享
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!", CharsetUtil.UTF_8)); // 當被通知該 channel 是活動的時候就發送信息
}
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {
System.out.println("Client received: " + byteBuf.toString(CharsetUtil.UTF_8)); // 記錄接收到的消息
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// 記錄日誌錯誤並關閉 channel
cause.printStackTrace();
ctx.close();
}
}
EchoClient
通過Bootstrap
引導創建客戶端,另外需要 host 、port 兩個參數連接服務器。
public class EchoClient {
private final String host;
private final int port;
public EchoClient(String host, int port) {
this.host = host;
this.port = port;
}
public void start() throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap(); // 創建 Bootstrap
bootstrap.group(group) // 指定EventLoopGroup來處理客戶端事件。由於我們使用NIO傳輸,所以用到了 NioEventLoopGroup 的實現
.channel(NioSocketChannel.class) // 使用的channel類型是一個用於NIO傳輸
.remoteAddress(new InetSocketAddress(host, port)) // 設置服務器的InetSocketAddr
.handler(new ChannelInitializer<SocketChannel>() { // 當建立一個連接和一個新的通道時。創建添加到EchoClientHandler實例到 channel pipeline
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new EchoClientHandler());
}
});
ChannelFuture future = bootstrap.connect().sync(); // 連接到遠程;等待連接完成
future.channel().closeFuture().sync(); // 阻塞到遠程; 等待連接完成
} finally {
group.shutdownGracefully().sync(); // 關閉線程池和釋放所有資源
}
}
public static void main(String[] args) throws Exception {
final String host = "127.0.0.1";
final int port = 4567;
new EchoClient(host, port).start();
}
}
編譯和運行 Echo
首先編譯、運行服務端,會看到以下log:
me.icro.samples.echo.server.EchoServer started and listen on /0:0:0:0:0:0:0:0:4567
下一步是編譯、運行客服端後,服務端會先接收到信息:
Server received: Netty rocks!
然後客戶端收到反饋:
Client received: Netty rocks!
總結
以上,構建並運行你的第一 個Netty 的客戶端和服務器。雖然這是一個簡單的應用程序,它可以擴展到幾千個併發連接。
我們可以在Netty的Github倉庫看到的更多 Netty 如何簡化可擴展和多線程的例子。
下一步的深入學習,網上教程很多,大夥可以參考:
(完)