Netty
什麼是Netty?
Netty 是一個利用 Java 的高級網絡的能力,隱藏其背後的複雜性而提供一個易於使用的 API 的客戶端/服務器框架。
Netty 是一個廣泛使用的 Java 網絡編程框架(Netty 在 2011 年獲得了Duke's Choice Award,見https://www.java.net/dukeschoice/2011)。它活躍和成長於用戶社區,像大型公司 Facebook 和 Instagram 以及流行 開源項目如 Infinispan, HornetQ, Vert.x, Apache Cassandra 和 Elasticsearch 等,都利用其強大的對於網絡抽象的核心代碼。
以上是摘自《Essential Netty In Action》這本書,本文的內容也是本人讀了這本書之後的一些整理心得,如有不當之處歡迎大蝦們指正
Netty和Tomcat有什麼區別?
Netty和Tomcat最大的區別就在於通信協議,Tomcat是基於Http協議的,他的實質是一個基於http協議的web容器,但是Netty不一樣,他能通過編程自定義各種協議,因爲netty能夠通過codec自己來編碼/解碼字節流,完成類似redis訪問的功能,這就是netty和tomcat最大的不同。
有人說netty的性能就一定比tomcat性能高,其實不然,tomcat從6.x開始就支持了nio模式,並且後續還有APR模式——一種通過jni調用apache網絡庫的模式,相比於舊的bio模式,併發性能得到了很大提高,特別是APR模式,而netty是否比tomcat性能更高,則要取決於netty程序作者的技術實力了。
爲什麼Netty受歡迎?
如第一部分所述,netty是一款收到大公司青睞的框架,在我看來,netty能夠受到青睞的原因有三:
- 併發高
- 傳輸快
- 封裝好
Netty爲什麼併發高
Netty是一款基於NIO(Nonblocking I/O,非阻塞IO)開發的網絡通信框架,對比於BIO(Blocking I/O,阻塞IO),他的併發性能得到了很大提高,兩張圖讓你瞭解BIO和NIO的區別:
阻塞IO的通信方式
非阻塞IO的通信方式
從這兩圖可以看出,NIO的單線程能處理連接的數量比BIO要高出很多,而爲什麼單線程能處理更多的連接呢?原因就是圖二中出現的Selector
。
當一個連接建立之後,他有兩個步驟要做,第一步是接收完客戶端發過來的全部數據,第二步是服務端處理完請求業務之後返回response給客戶端。NIO和BIO的區別主要是在第一步。
在BIO中,等待客戶端發數據這個過程是阻塞的,這樣就造成了一個線程只能處理一個請求的情況,而機器能支持的最大線程數是有限的,這就是爲什麼BIO不能支持高併發的原因。
而NIO中,當一個Socket建立好之後,Thread並不會阻塞去接受這個Socket,而是將這個請求交給Selector,Selector會不斷的去遍歷所有的Socket,一旦有一個Socket建立完成,他會通知Thread,然後Thread處理完數據再返回給客戶端——這個過程是不阻塞的,這樣就能讓一個Thread處理更多的請求了。
下面兩張圖是基於BIO的處理流程和netty的處理流程,輔助你理解兩種方式的差別:
BIO的處理流程
NIO的處理流程
除了BIO和NIO之外,還有一些其他的IO模型,下面這張圖就表示了五種IO模型的處理流程:
五種常見的IO模型
- BIO,同步阻塞IO,阻塞整個步驟,如果連接少,他的延遲是最低的,因爲一個線程只處理一個連接,適用於少連接且延遲低的場景,比如說數據庫連接。
- NIO,同步非阻塞IO,阻塞業務處理但不阻塞數據接收,適用於高併發且處理簡單的場景,比如聊天軟件。
- 多路複用IO,他的兩個步驟處理是分開的,也就是說,一個連接可能他的數據接收是線程a完成的,數據處理是線程b完成的,他比BIO能處理更多請求。
- 信號驅動IO,這種IO模型主要用在嵌入式開發,不參與討論。
- 異步IO,他的數據請求和數據處理都是異步的,數據請求一次返回一次,適用於長連接的業務場景。
以上摘自Linux IO模式及 select、poll、epoll詳解
Netty爲什麼傳輸快
Netty的傳輸快其實也是依賴了NIO的一個特性——零拷貝。我們知道,Java的內存有堆內存、棧內存和字符串常量池等等,其中堆內存是佔用內存空間最大的一塊,也是Java對象存放的地方,一般我們的數據如果需要從IO讀取到堆內存,中間需要經過Socket緩衝區,也就是說一個數據會被拷貝兩次才能到達他的的終點,如果數據量大,就會造成不必要的資源浪費。
Netty針對這種情況,使用了NIO中的另一大特性——零拷貝,當他需要接收數據的時候,他會在堆內存之外開闢一塊內存,數據就直接從IO讀到了那塊內存中去,在netty裏面通過ByteBuf可以直接對這些數據進行直接操作,從而加快了傳輸速度。
下兩圖就介紹了兩種拷貝方式的區別,摘自Linux 中的零拷貝技術,第 1 部分
傳統數據拷貝
零拷貝
上文介紹的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(); //2
System.out.println("Accepted connection from " + clientSocket);
new Thread(new Runnable() { //3
@Override
public void run() {
OutputStream out;
try {
out = clientSocket.getOutputStream();
out.write("Hi!\r\n".getBytes(Charset.forName("UTF-8"))); //4
out.flush();
clientSocket.close(); //5
} catch (IOException e) {
e.printStackTrace();
try {
clientSocket.close();
} catch (IOException ex) {
// ignore on close
}
}
}
}).start(); //6
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 非阻塞IO
public class PlainNioServer {
public void serve(int port) throws IOException {
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
ServerSocket ss = serverChannel.socket();
InetSocketAddress address = new InetSocketAddress(port);
ss.bind(address); //1
Selector selector = Selector.open(); //2
serverChannel.register(selector, SelectionKey.OP_ACCEPT); //3
final ByteBuffer msg = ByteBuffer.wrap("Hi!\r\n".getBytes());
for (;;) {
try {
selector.select(); //4
} catch (IOException ex) {
ex.printStackTrace();
// handle exception
break;
}
Set<SelectionKey> readyKeys = selector.selectedKeys(); //5
Iterator<SelectionKey> iterator = readyKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
try {
if (key.isAcceptable()) { //6
ServerSocketChannel server =
(ServerSocketChannel)key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_WRITE |
SelectionKey.OP_READ, msg.duplicate()); //7
System.out.println(
"Accepted connection from " + client);
}
if (key.isWritable()) { //8
SocketChannel client =
(SocketChannel)key.channel();
ByteBuffer buffer =
(ByteBuffer)key.attachment();
while (buffer.hasRemaining()) {
if (client.write(buffer) == 0) { //9
break;
}
}
client.close(); //10
}
} catch (IOException ex) {
key.cancel();
try {
key.channel().close();
} catch (IOException cex) {
// 在關閉時忽略
}
}
}
}
}
}
- 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(); //1
b.group(group) //2
.channel(OioServerSocketChannel.class)
.localAddress(new InetSocketAddress(port))
.childHandler(new ChannelInitializer<SocketChannel>() {//3
@Override
public void initChannel(SocketChannel ch)
throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { //4
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(buf.duplicate()).addListener(ChannelFutureListener.CLOSE);//5
}
});
}
});
ChannelFuture f = b.bind().sync(); //6
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully().sync(); //7
}
}
}
從代碼量上來看,Netty就已經秒殺傳統Socket編程了,但是這一部分博大精深,僅僅貼幾個代碼豈能說明問題,在這裏給大家介紹一下Netty的一些重要概念,讓大家更理解Netty。
-
Channel
數據傳輸流,與channel相關的概念有以下四個,上一張圖讓你瞭解netty裏面的Channel。Channel一覽
- Channel,表示一個連接,可以理解爲每一個請求,就是一個Channel。
- ChannelHandler,核心處理業務就在這裏,用於處理業務請求。
- ChannelHandlerContext,用於傳輸業務數據。
- ChannelPipeline,用於保存處理過程需要用到的ChannelHandler和ChannelHandlerContext。
- ByteBuf
ByteBuf是一個存儲字節的容器,最大特點就是使用方便,它既有自己的讀索引和寫索引,方便你對整段字節緩存進行讀寫,也支持get/set,方便你對其中每一個字節進行讀寫,他的數據結構如下圖所示:
ByteBuf數據結構
他有三種使用模式:
- Heap Buffer 堆緩衝區
堆緩衝區是ByteBuf最常用的模式,他將數據存儲在堆空間。 - Direct Buffer 直接緩衝區
直接緩衝區是ByteBuf的另外一種常用模式,他的內存分配都不發生在堆,jdk1.4引入的nio的ByteBuffer類允許jvm通過本地方法調用分配內存,這樣做有兩個好處- 通過免去中間交換的內存拷貝, 提升IO處理速度; 直接緩衝區的內容可以駐留在垃圾回收掃描的堆區以外。
- DirectBuffer 在 -XX:MaxDirectMemorySize=xxM大小限制下, 使用 Heap 之外的內存, GC對此”無能爲力”,也就意味着規避了在高負載下頻繁的GC過程對應用線程的中斷影響.
- Composite Buffer 複合緩衝區
複合緩衝區相當於多個不同ByteBuf的視圖,這是netty提供的,jdk不提供這樣的功能。
除此之外,他還提供一大堆api方便你使用,在這裏我就不一一列出了,具體參見ByteBuf字節緩存。
- Codec
Netty中的編碼/解碼器,通過他你能完成字節與pojo、pojo與pojo的相互轉換,從而達到自定義協議的目的。
在Netty裏面最有名的就是HttpRequestDecoder和HttpResponseEncoder了。
Netty入門教程2——動手搭建HttpServer
Netty入門教程3——Decoder和Encoder
Netty入門教程4——如何實現長連接
以上就是我對《Netty實戰》這本書的一些心得和書外的一些相關知識整理,如果有不同的見解,歡迎討論!
作者:追那個小女孩
鏈接:https://www.jianshu.com/p/b9f3f6a16911
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。