什么是Netty伤疙?
Netty 是一個利用 Java 的高級網(wǎng)絡(luò)的能力悼吱,隱藏其背后的復(fù)雜性而提供一個易于使用的 API 的客戶端/服務(wù)器框架慎框。
Netty 是一個廣泛使用的 Java 網(wǎng)絡(luò)編程框架(Netty 在 2011 年獲得了Duke's Choice Award,見https://www.java.net/dukeschoice/2011)舆绎。它活躍和成長于用戶社區(qū)鲤脏,像大型公司 Facebook 和 Instagram 以及流行 開源項目如 Infinispan, HornetQ, Vert.x, Apache Cassandra 和 Elasticsearch 等,都利用其強大的對于網(wǎng)絡(luò)抽象的核心代碼吕朵。
以上是摘自《Essential Netty In Action》這本書猎醇,本文的內(nèi)容也是本人讀了這本書之后的一些整理心得,如有不當之處歡迎大蝦們指正
Netty和Tomcat有什么區(qū)別努溃?
Netty和Tomcat最大的區(qū)別就在于通信協(xié)議硫嘶,Tomcat是基于Http協(xié)議的,他的實質(zhì)是一個基于http協(xié)議的web容器梧税,但是Netty不一樣沦疾,他能通過編程自定義各種協(xié)議,因為netty能夠通過codec自己來編碼/解碼字節(jié)流第队,完成類似redis訪問的功能哮塞,這就是netty和tomcat最大的不同。
有人說netty的性能就一定比tomcat性能高凳谦,其實不然忆畅,tomcat從6.x開始就支持了nio模式,并且后續(xù)還有APR模式——一種通過jni調(diào)用apache網(wǎng)絡(luò)庫的模式尸执,相比于舊的bio模式家凯,并發(fā)性能得到了很大提高,特別是APR模式如失,而netty是否比tomcat性能更高绊诲,則要取決于netty程序作者的技術(shù)實力了。
為什么Netty受歡迎褪贵?
如第一部分所述掂之,netty是一款收到大公司青睞的框架,在我看來,netty能夠受到青睞的原因有三:
- 并發(fā)高
- 傳輸快
- 封裝好
Netty為什么并發(fā)高
Netty是一款基于NIO(Nonblocking I/O板惑,非阻塞IO)開發(fā)的網(wǎng)絡(luò)通信框架橄镜,對比于BIO(Blocking I/O,阻塞IO)冯乘,他的并發(fā)性能得到了很大提高洽胶,兩張圖讓你了解BIO和NIO的區(qū)別:
從這兩圖可以看出,NIO的單線程能處理連接的數(shù)量比BIO要高出很多裆馒,而為什么單線程能處理更多的連接呢姊氓?原因就是圖二中出現(xiàn)的
Selector
。當一個連接建立之后喷好,他有兩個步驟要做翔横,第一步是接收完客戶端發(fā)過來的全部數(shù)據(jù),第二步是服務(wù)端處理完請求業(yè)務(wù)之后返回response給客戶端梗搅。NIO和BIO的區(qū)別主要是在第一步禾唁。
在BIO中,等待客戶端發(fā)數(shù)據(jù)這個過程是阻塞的无切,這樣就造成了一個線程只能處理一個請求的情況荡短,而機器能支持的最大線程數(shù)是有限的,這就是為什么BIO不能支持高并發(fā)的原因哆键。
而NIO中掘托,當一個Socket建立好之后,Thread并不會阻塞去接受這個Socket籍嘹,而是將這個請求交給Selector闪盔,Selector會不斷的去遍歷所有的Socket,一旦有一個Socket建立完成辱士,他會通知Thread泪掀,然后Thread處理完數(shù)據(jù)再返回給客戶端——這個過程是不阻塞的,這樣就能讓一個Thread處理更多的請求了颂碘。
下面兩張圖是基于BIO的處理流程和netty的處理流程异赫,輔助你理解兩種方式的差別:
除了BIO和NIO之外,還有一些其他的IO模型凭涂,下面這張圖就表示了五種IO模型的處理流程:
- BIO,同步阻塞IO贴妻,阻塞整個步驟切油,如果連接少,他的延遲是最低的名惩,因為一個線程只處理一個連接澎胡,適用于少連接且延遲低的場景,比如說數(shù)據(jù)庫連接。
- NIO攻谁,同步非阻塞IO稚伍,阻塞業(yè)務(wù)處理但不阻塞數(shù)據(jù)接收,適用于高并發(fā)且處理簡單的場景戚宦,比如聊天軟件个曙。
- 多路復(fù)用IO,他的兩個步驟處理是分開的受楼,也就是說垦搬,一個連接可能他的數(shù)據(jù)接收是線程a完成的,數(shù)據(jù)處理是線程b完成的艳汽,他比BIO能處理更多請求猴贰。
- 信號驅(qū)動IO,這種IO模型主要用在嵌入式開發(fā)河狐,不參與討論米绕。
- 異步IO,他的數(shù)據(jù)請求和數(shù)據(jù)處理都是異步的馋艺,數(shù)據(jù)請求一次返回一次栅干,適用于長連接的業(yè)務(wù)場景。
以上摘自Linux IO模式及 select丈钙、poll非驮、epoll詳解
Netty為什么傳輸快
Netty的傳輸快其實也是依賴了NIO的一個特性——零拷貝。我們知道雏赦,Java的內(nèi)存有堆內(nèi)存劫笙、棧內(nèi)存和字符串常量池等等,其中堆內(nèi)存是占用內(nèi)存空間最大的一塊星岗,也是Java對象存放的地方填大,一般我們的數(shù)據(jù)如果需要從IO讀取到堆內(nèi)存,中間需要經(jīng)過Socket緩沖區(qū)俏橘,也就是說一個數(shù)據(jù)會被拷貝兩次才能到達他的的終點允华,如果數(shù)據(jù)量大,就會造成不必要的資源浪費寥掐。
Netty針對這種情況靴寂,使用了NIO中的另一大特性——零拷貝,當他需要接收數(shù)據(jù)的時候召耘,他會在堆內(nèi)存之外開辟一塊內(nèi)存百炬,數(shù)據(jù)就直接從IO讀到了那塊內(nèi)存中去,在netty里面通過ByteBuf可以直接對這些數(shù)據(jù)進行直接操作污它,從而加快了傳輸速度剖踊。
下兩圖就介紹了兩種拷貝方式的區(qū)別庶弃,摘自Linux 中的零拷貝技術(shù),第 1 部分
上文介紹的ByteBuf是Netty的一個重要概念德澈,他是netty數(shù)據(jù)處理的容器歇攻,也是Netty封裝好的一個重要體現(xiàn),將在下一部分做詳細介紹梆造。
為什么說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) {
// 在關(guān)閉時忽略
}
}
}
}
}
}
- 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就已經(jīng)秒殺傳統(tǒng)Socket編程了,但是這一部分博大精深摊聋,僅僅貼幾個代碼豈能說明問題鸡捐,在這里給大家介紹一下Netty的一些重要概念,讓大家更理解Netty麻裁。
-
Channel
數(shù)據(jù)傳輸流箍镜,與channel相關(guān)的概念有以下四個,上一張圖讓你了解netty里面的Channel煎源。
Channel一覽- Channel色迂,表示一個連接,可以理解為每一個請求手销,就是一個Channel歇僧。
- ChannelHandler,核心處理業(yè)務(wù)就在這里锋拖,用于處理業(yè)務(wù)請求诈悍。
- ChannelHandlerContext,用于傳輸業(yè)務(wù)數(shù)據(jù)兽埃。
- ChannelPipeline侥钳,用于保存處理過程需要用到的ChannelHandler和ChannelHandlerContext。
- ByteBuf
ByteBuf是一個存儲字節(jié)的容器柄错,最大特點就是使用方便舷夺,它既有自己的讀索引和寫索引,方便你對整段字節(jié)緩存進行讀寫售貌,也支持get/set给猾,方便你對其中每一個字節(jié)進行讀寫,他的數(shù)據(jù)結(jié)構(gòu)如下圖所示:
他有三種使用模式:
- Heap Buffer 堆緩沖區(qū)
堆緩沖區(qū)是ByteBuf最常用的模式颂跨,他將數(shù)據(jù)存儲在堆空間敢伸。 - Direct Buffer 直接緩沖區(qū)
直接緩沖區(qū)是ByteBuf的另外一種常用模式,他的內(nèi)存分配都不發(fā)生在堆毫捣,jdk1.4引入的nio的ByteBuffer類允許jvm通過本地方法調(diào)用分配內(nèi)存详拙,這樣做有兩個好處- 通過免去中間交換的內(nèi)存拷貝, 提升IO處理速度; 直接緩沖區(qū)的內(nèi)容可以駐留在垃圾回收掃描的堆區(qū)以外。
- DirectBuffer 在 -XX:MaxDirectMemorySize=xxM大小限制下, 使用 Heap 之外的內(nèi)存, GC對此”無能為力”,也就意味著規(guī)避了在高負載下頻繁的GC過程對應(yīng)用線程的中斷影響.
- Composite Buffer 復(fù)合緩沖區(qū)
復(fù)合緩沖區(qū)相當于多個不同ByteBuf的視圖蔓同,這是netty提供的饶辙,jdk不提供這樣的功能。
除此之外斑粱,他還提供一大堆api方便你使用弃揽,在這里我就不一一列出了,具體參見ByteBuf字節(jié)緩存则北。
- Codec
Netty中的編碼/解碼器矿微,通過他你能完成字節(jié)與pojo、pojo與pojo的相互轉(zhuǎn)換尚揣,從而達到自定義協(xié)議的目的涌矢。
在Netty里面最有名的就是HttpRequestDecoder和HttpResponseEncoder了。
Netty入門教程2——動手搭建HttpServer
Netty入門教程3——Decoder和Encoder
Netty入門教程4——如何實現(xiàn)長連接
以上就是我對《Netty實戰(zhàn)》這本書的一些心得和書外的一些相關(guān)知識整理快骗,如果有不同的見解娜庇,歡迎討論!