前言
最近在看ES源碼,Netty貫穿在整個ES之中,想要看懂和更好的理解ES源碼,必須首先對Netty有一定的認識和了解,所以簡單總結了Netty的一些基本架構和認知, 後續再總結更加深入的Netty知識。
什麼是Netty,爲什麼使用Netty
Netty是一個高性能、異步事件驅動的NIO框架,它提供了對TCP、UDP和文件傳輸的支持。
作爲一個異步NIO框架,Netty的所有IO操作都是異步非阻塞的,通過Future-Listener機制,用戶可以方便的主動獲取或者通過通知機制獲得IO操作結果。
作爲當前最流行的NIO框架,Netty在互聯網領域、大數據分佈式計算領域、遊戲行業、通信行業等獲得了廣泛的應用,一些業界著名的開源組件也基於Netty的NIO框架構建。
像大型公司 Facebook 和 Instagram 以及流行 開源項目如 Infinispan, HornetQ,Vert.x, Apache Cassandra 和 Elasticsearch 等,都利用其強大的對於網絡抽象的核心代碼。
Netty受歡迎的原因主要有:
- 併發高
- 傳輸快
- 封裝好
Netty爲什麼併發高
Netty是一款基於NIO(Nonblocking I/O,非阻塞IO)開發的網絡通信框架,對比於BIO(Blocking I/O,阻塞IO),他的併發性能得到了很大提高,兩張圖讓你瞭解BIO和NIO的區別:
從這兩圖可以看出,NIO的單線程能處理連接的數量比BIO要高出很多,而爲什麼單線程能處理更多的連接呢?原因就是圖二中出現的Selector。
當一個連接建立之後,他有兩個步驟要做,第一步是接收完客戶端發過來的全部數據,第二步是服務端處理完請求業務之後返回response給客戶端。
NIO和BIO的區別主要是在第一步。
在BIO中,等待客戶端發數據這個過程是阻塞的,這樣就造成了一個線程只能處理一個請求的情況,而機器能支持的最大線程數是有限的,這就是爲什麼BIO不能支持高併發的原因。
而NIO中,當一個Socket建立好之後,Thread並不會阻塞去接受這個Socket,而是將這個請求交給Selector,Selector會不斷的去遍歷所有的Socket,一旦有一個Socket建立完成,他會通知Thread,然後Thread處理完數據再返回給客戶端——這個過程是阻塞的,這樣就能讓一個Thread處理更多的請求了。
下面兩張圖是基於BIO的處理流程和netty的處理流程,輔助你理解兩種方式的差別:
Netty爲什麼傳輸快
Netty的傳輸快其實也是依賴了NIO的一個特性——零拷貝。
我們知道,Java的內存有堆內存、棧內存和字符串常量池等等,其中堆內存是佔用內存空間最大的一塊,也是Java對象存放的地方,一般我們的數據如果需要從IO讀取到堆內存,中間需要經過Socket緩衝區,也就是說一個數據會被拷貝兩次才能到達他的的終點,如果數據量大,就會造成不必要的資源浪費。
Netty針對這種情況,使用了NIO中的另一大特性——零拷貝,當他需要接收數據的時候,他會在堆內存之外開闢一塊內存,數據就直接從IO讀到了那塊內存中去,在netty裏面通過ByteBuf可以直接對這些數據進行直接操作,從而加快了傳輸速度。
下兩圖就介紹了兩種拷貝方式的區別:
上文介紹的ByteBuf是Netty的一個重要概念,他是netty數據處理的容器,也是Netty封裝好的一個重要體現。
Netty爲什麼封裝好
要說Netty爲什麼封裝好,這種用文字是說不清的,直接上代碼:
阻塞I/O:
public class PlainOioServer {
public void serve(int port) throws IOException {
final ServerSocket socket = new ServerSocket(port); //1
try {
for (;;) {
final Socket clientSocket = socket.accept();
System.out.println("Accepted connection from " + clientSocket);
new Thread(new Runnable() {
@Override
public void run() {
OutputStream out;
try {
out = clientSocket.getOutputStream();
out.write("Hi!\r\n".getBytes(Charset.forName("UTF-8")));
out.flush();
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
try {
clientSocket.close();
} catch (IOException ex) {
// ignore on close
}
}
}
}).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
Netty:
public class NettyOioServer {
public void server(int port) throws Exception {
final ByteBuf buf = Unpooled.unreleasableBuffer(
Unpooled.copiedBuffer("Hi!\r\n", Charset.forName("UTF-8")));
EventLoopGroup group = new OioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(group)
.channel(OioServerSocketChannel.class)
.localAddress(new InetSocketAddress(port))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch)
throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(buf.duplicate()).addListener(ChannelFutureListener.CLOSE);
}
});
}
});
ChannelFuture f = b.bind().sync();
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully().sync();
}
}
}
從代碼量上來看,Netty就已經秒殺傳統Socket編程了,但是這一部分博大精深,僅僅貼幾個代碼豈能說明問題,在這裏給大家介紹一下Netty的一些重要概念,讓大家更理解Netty。
Channel:
數據傳輸流,與channel相關的概念有以下四個,上一張圖讓你瞭解netty裏面的Channel:
Channel,表示一個連接,可以理解爲每一個請求,就是一個Channel。
ChannelHandler,核心處理業務就在這裏,用於處理業務請求。
ChannelHandlerContext,用於傳輸業務數據。
ChannelPipeline,用於保存處理過程需要用到的ChannelHandler和ChannelHandlerContext。
ByteBuf:
ByteBuf是一個存儲字節的容器,最大特點就是使用方便,它既有自己的讀索引和寫索引,方便你對整段字節緩存進行讀寫,也支持get/set,方便你對其中每一個字節進行讀寫,他的數據結構如下圖所示:
他有三種使用模式:
Heap Buffer 堆緩衝區
堆緩衝區是ByteBuf最常用的模式,他將數據存儲在堆空間。
Direct Buffer 直接緩衝區
直接緩衝區是ByteBuf的另外一種常用模式,他的內存分配都不發生在堆,jdk1.4引入的nio的ByteBuffer類允許jvm通過本地方法調用分配內存,這樣做有兩個好處
- 通過免去中間交換的內存拷貝, 提升IO處理速度; 直接緩衝區的內容可以駐留在垃圾回收掃描的堆區以外。
- DirectBuffer 在 -XX:MaxDirectMemorySize=xxM大小限制下, 使用 Heap 之外的內存,,GC對此”無能爲力”,也就意味着規避了在高負載下頻繁的GC過程對應用線程的中斷影響。
Composite Buffer 複合緩衝區
複合緩衝區相當於多個不同ByteBuf的視圖,這是netty提供的,jdk不提供這樣的功能。
參考文檔:
https://www.jianshu.com/p/b9f3f6a16911
https://blog.csdn.net/a953713428/article/details/65629552
https://www.kancloud.cn/kancloud/essential-netty-in-action/52617