Netty 從入門到精通(一)

什麼是netty

Netty是由JBOSS提供的一個java開源框架。Netty提供異步的、事件驅動的網絡應用程序框架和工具,用以快速開發高性能、高可靠性的網絡服務器和客戶端程序。

也就是說,Netty 是一個基於NIO的客戶、服務器端編程框架,使用Netty 可以確保你快速和簡單的開發出一個網絡應用,例如實現了某種協議的客戶、服務端應用。Netty相當於簡化和流線化了網絡應用的編程開發過程,例如:基於TCP和UDP的socket服務開發。

“快速”和“簡單”並不用產生維護性或性能上的問題。Netty 是一個吸收了多種協議(包括FTP、SMTP、HTTP等各種二進制文本協議)的實現經驗,並經過相當精心設計的項目。最終,Netty 成功的找到了一種方式,在保證易於開發的同時還保證了其應用的性能,穩定性和伸縮性。

netty的特點

性能

比核心 Java API 更好的吞吐量,較低的延時

資源消耗更少,這個得益於共享池和重用

減少內存拷貝

健壯性

消除由於慢,快,或重載連接產生的 OutOfMemoryError

消除經常發現在 NIO 在高速網絡中的應用中的不公平的讀/寫比

安全

完整的 SSL / TLS 和 StartTLS 的支持

行在受限的環境例如 Applet 或 OSGI

netty的用途


Netty 從入門到精通(一)


在微服務的大潮之中, 架構師小明把系統拆分成了多個服務,根據需要部署在多個機器上,這些服務非常靈活,可以隨著訪問量彈性擴展。


Netty 從入門到精通(一)


世界上沒有免費的午餐, 拆分成多個“微服務”以後雖然增加了彈性,但也帶來了一個巨大的挑戰:服務之間互相調用的開銷

比如說:原來用戶下一個訂單需要登錄,瀏覽產品詳情,加入購物車,支付,扣庫存等一系列操作,在單體應用的時候它們都在一臺機器的同一個進程中,說白了就是模塊之間的函數調用,效率超級高

現在好了,服務被安置到了不同的服務器上,一個訂單流程,幾乎每個操作都要越網絡,都是遠程過程調用(RPC), 那執行時間、執行效率可遠遠比不上以前了。

遠程過程調用的第一版實現使用了HTTP協議,也就是說各個服務對外提供HTTP接口。 小明發現,HTTP協議雖然簡單明瞭,但是廢話太多,僅僅是給服務器發個簡單的消息都會附帶一大堆無用信息:

GET /orders/1

HTTP/1.1

Host: order.myshop.com

User-Agent: Mozilla/5.0 (Windows NT 6.1; )

Accept: text/html;

Accept-Language: en-US,en;

Accept-Encoding: gzip

Connection: keep-alive

......

看看那User-Agent,Accept-Language ,這個協議明顯是為瀏覽器而生的!但是我這裡是程序之間的調用,用這個HTTP有點虧。

能不能自定義一個精簡的協議? 在這個協議中我只需要把要調用方法名和參數發給服務器即可,根本不用這麼多亂七八糟的額外信息。

但是自定義協議客戶端和服務器端就得直接使用“低級”的Socket了,尤其是服務器端,得能夠處理高併發的訪問請求才行。

小明覆習了一下服務器端的socket編程,最早的Java是所謂的阻塞IO(Blocking IO), 想處理多個socket的連接的話需要創建多個線程, 一個線程對應一個。


Netty 從入門到精通(一)



這種方式寫起來倒是挺簡單的,但是連接(socket)多了就受不了了,如果真的有成千上萬個線程同時處理成千上萬個socket,佔用大量的空間不說,光是線程之間的切換就是一個巨大的開銷。

更重要的是,雖然有大量的socket,但是真正需要處理的(可以讀寫數據的socket)卻不多,大量的線程處於等待數據狀態(這也是為什麼叫做阻塞的原因),資源浪費得讓人心疼。

後來Java為了解決這個問題,又搞了一個非阻塞IO(NIO:Non-Blocking IO,有人也叫做New IO), 改變了一下思路:通過多路複用的方式讓一個線程去處理多個Socket。


Netty 從入門到精通(一)


這樣一來,只需要使用少量的線程就可以搞定多個socket了,線程只需要通過Selector去查一下它所管理的socket集合,哪個Socket的數據準備好了,就去處理哪個Socket,一點兒都不浪費。

好了,就是Java NIO了!

小明先定義了一套精簡的RPC的協議,裡邊規定了如何去調用一個服務,方法名和參數該如何傳遞,返回值用什麼格式......等等。然後雄心勃勃地要把這個協議用Java NIO給實現了。

可是美好的理想很快被無情的現實給擊碎, 小明努力了一週就意識到自己陷入了一個大坑之中,Java NIO雖然看起來簡單,但是API還是太“低級”了,有太多的複雜性,沒有強悍的、一流的編程能力根本無法駕馭,根本做不到高併發情況下的可靠和高效。

小明不死心,繼續向領導要人要資源,一定要把這個坑給填上,掙扎了6個月以後,終於實現了一個自己的NIO框架,可以執行高併發的RPC調用了。

然後又是長達6個月的修修補補,小明經常半夜被叫醒:生產環境的RPC調用無法返回了! 這樣的Bug不知道改了多少個。

在那些不眠之夜中,小明經常仰天長嘆:我用NIO做個高併發的RPC框架怎麼這麼難吶!

一年之後,自研的框架終於穩定,可是小明也從張大胖那裡聽到了一個讓他崩潰的消息: 小明你知道嗎?有個叫Netty的開源框架,可以快速地開發高性能的面向協議的服務器和客戶端。 易用、健壯、安全、高效,你可以在Netty上輕鬆實現各種自定義的協議!咱們也試試?

小明趕緊研究,看完後不由得“淚流滿面”:這東西怎麼不早點出來啊!

好了,這個故事我快編不下去了,要爛尾了。

Netty 從入門到精通(一)


說說Netty到底是何方神聖, 要解決什麼問題吧。

像上面小明的例子,想使用Java NIO來實現一個高性能的RPC框架,調用協議,數據的格式和次序都是自己定義的,現有的HTTP根本玩不轉,那使用Netty就是絕佳的選擇。

其實遊戲領域是個更好的例子,長連接,自定義協議,高併發,Netty就是絕配。

因為Netty本身就是一個基於NIO的網絡框架, 封裝了Java NIO那些複雜的底層細節,給你提供簡單好用的抽象概念來編程。

下載netty

Netty 從入門到精通(一)

download

點擊這裡即可下載netty

創建一個helloworld項目

在開始之前

運行本章示例的最低要求只有兩個;Netty和JDK 1.6或以上的最新版本。Netty的最新版本可以在項目下載頁面中找到。要下載JDK的正確版本,請參考您首選的JDK供應商的網站。

在閱讀時,您可能對本章介紹的類有更多的問題。如果您想了解更多,請參考API參考資料。為了方便起見,本文中的所有類名都鏈接到在線API引用。另外,請不要猶豫聯繫Netty項目社區,讓我們知道是否有任何不正確的信息、語法錯誤和打印錯誤,以及您是否有改進文檔的好主意。

編寫 Discard Server

世界上最簡單的協議不是“你好,世界!”但 Discard Server。它是一個丟棄任何接收到的數據而沒有任何響應的協議。

要實現丟棄協議,您只需忽略所有接收到的數據。讓我們直接從處理程序實現開始,它處理Netty生成的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();
}
}

DiscardServerHandler擴展了ChannelInboundHandlerAdapter,它是ChannelInboundHandler的實現。ChannelInboundHandler提供了可以覆蓋的各種事件處理程序方法。現在,只需擴展ChannelInboundHandlerAdapter,而不是自己實現處理程序接口。

我們在這裡覆蓋channelRead()事件處理程序方法。每當從客戶端接收到新數據時,就用接收到的消息調用此方法。在本例中,接收到的消息的類型是ByteBuf。

要實現丟棄協議,處理程序必須忽略接收到的消息。ByteBuf是一個引用計數的對象,必須通過release()方法顯式地釋放它。請記住,釋放傳遞給處理程序的任何引用計數的對象是處理程序的責任。通常,channelRead()處理程序方法是這樣實現的:

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {

try {
// Do something with msg
} finally {
ReferenceCountUtil.release(msg);
}
}

exceptionCaught()事件處理程序方法在Netty由於I/O錯誤而引發異常或處理程序實現由於在處理事件時拋出異常而引發異常時使用Throwable調用。在大多數情況下,應該記錄捕獲的異常,並在這裡關閉它的關聯通道,儘管此方法的實現可能不同,這取決於您希望如何處理異常情況。例如,您可能希望在關閉連接之前發送帶有錯誤代碼的響應消息。

到目前為止一切順利。我們已經實現了丟棄服務器的前一半。現在剩下的就是編寫main()方法,該方法使用DiscardServerHandler啟動服務器。

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 = 8080;
if (args.length > 0) {
port = Integer.parseInt(args[0]);
}
new DiscardServer(port).run();
}
}
/<socketchannel>

下面我們來進行一個測試

telnet 127.0.0.1 8080 成功連接!!!


分享到:


相關文章: