首先宣告,本文是為Netty新手準備的,所以事無鉅細的會把步驟列出來,老手們就不用在我這篇文章上浪費時間了,要不然你會嫌我墨跡的。
一、前言
Netty是一個開源的非同步事件驅動的網路應用程式框架,用於快速開發可維護的高效能協議伺服器和客戶端。
Netty的創始人是韓國人trustin lee,他現在韓國line公司工作,早前應用較多的Mina也是這牛人的作品。
Netty目前的專案leader是德國人Norman maurer(之前在Redhat,全職開發Netty),也是《Netty in action》的作者,目前是蘋果公司高階工程師,同時也經常參加netty相關的技術會議,這兩大牛長下面這樣:
Netty的優點,簡單一句話:使用簡單、功能強大、效能強悍。
Netty的特點:
高併發:Netty 是一款基於 NIO(Nonblocking IO,非阻塞IO)開發的網路通訊框架,對比於 BIO(Blocking I/O,阻塞IO),他的併發效能得到了很大提高。
傳輸快:Netty 的傳輸依賴於零拷貝特性,儘量減少不必要的記憶體拷貝,實現了更高效率的傳輸。
封裝好:Netty 封裝了 NIO 操作的很多細節,提供了易於使用呼叫介面。
Netty的優勢:
使用簡單:封裝了 NIO 的很多細節,使用更簡單。
功能強大:預置了多種編解碼功能,支援多種主流協議。
定製能力強:可以通過 ChannelHandler 對通訊框架進行靈活地擴充套件。
效能高:通過與其他業界主流的 NIO 框架對比,Netty 的綜合效能最優。
穩定:Netty 修復了已經發現的所有 NIO 的 bug,讓開發人員可以專注於業務本身。
社群活躍:Netty 是活躍的開源專案,版本迭代週期短,bug 修復速度快。
Netty高效能表現在哪些方面?
IO 執行緒模型:同步非阻塞,用最少的資源做更多的事。
記憶體零拷貝:儘量減少不必要的記憶體拷貝,實現了更高效率的傳輸。
記憶體池設計:申請的記憶體可以重用,主要指直接記憶體。內部實現是用一顆二叉查詢樹管理記憶體分配情況。
串形化處理讀寫:避免使用鎖帶來的效能開銷。
高效能序列化協議:支援 protobuf 等高效能序列化協議。
BIO、NIO和AIO的區別是什麼?
這三個概念分別對應三種通訊模型:阻塞、非阻塞、非阻塞非同步,概念這裡就不寫了,大家可以度娘搜一下,網上好多部落格說Netty對應NIO,準確來說,應該是既可以是NIO,也可以是AIO,就看你怎麼實現,這三個的區別如下:
BIO:一個連線一個執行緒,客戶端有連線請求時伺服器端就需要啟動一個執行緒進行處理,執行緒開銷大。偽非同步IO:將請求連線放入執行緒池,一對多,但執行緒還是很寶貴的資源。
NIO:一個請求一個執行緒,但客戶端傳送的連線請求都會註冊到多路複用器上,多路複用器輪詢到連線有I/O請求時才啟動一個執行緒進行處理。
AIO:一個有效請求一個執行緒,客戶端的I/O請求都是由OS先完成了再通知伺服器應用去啟動執行緒進行處理。
BIO是面向流的,NIO是面向緩衝區的;BIO的各種流是阻塞的。而NIO是非阻塞的;BIO的Stream是單向的,而NIO的channel是雙向的。
NIO的特點:事件驅動模型、單執行緒處理多工、非阻塞I/O,I/O讀寫不再阻塞,而是返回0、基於block的傳輸比基於流的傳輸更高效、更高階的IO函式zero-copy、IO多路複用大大提高了Java網路應用的可伸縮性和實用性。基於Reactor執行緒模型。
二、Netty能做什麼?
學技能都是為了能夠應用到實際工作中去,誰也不是為了學而學、弄著玩不是,那麼Netty能做什麼呢?主要是在兩個方面:
-
現在物聯網的應用無處不在,大量的專案都牽涉到應用感測器和伺服器端的資料通訊,Netty作為基礎通訊元件、能夠輕鬆解決之前有較高門檻的通訊系統開發,你不用再為如何解析各類簡單、或複雜的通訊協議而薅頭發了,有過這方面開發經驗的程式設計師會有更深刻、或者說刻骨銘心的體會。
-
現在網際網路系統講究的都是高併發、分散式、微服務,各類訊息滿天飛,Netty在這類架構裡面的應用可謂是如魚得水,如果你對當前的各種應用伺服器不爽,那麼完全可以基於Netty來實現自己的HTTP伺服器,FTP伺服器,UDP伺服器,RPC伺服器,WebSocket伺服器,Redis的Proxy伺服器,MySQL的Proxy伺服器等等。
三、掌握Netty有什麼好處呢?
直接的好處是:能夠有進大廠、拿高薪的機會,業內好多著名的公司在招聘**高階/資深Java工程師時基本上都要求熟練掌握、或熟悉Netty。
這個名單還可以很長很長。。。
作為一個學Java的,如果沒有研究過Netty,那麼你對Java語言的使用和理解僅僅停留在表面水平,會點SSH,寫幾個MVC,訪問資料庫和快取,這些只是初、中等Java程式設計師乾的事。如果你要進階,想了解Java伺服器的深層高階知識,Netty絕對是一個必須要過的門檻。
間接地好處是:多款開源框架中應用了Netty,掌握了Netty,就具有分析這些開源框架的基礎了,也就是有了成為技術大牛的基礎。
這些開源框架有哪些呢?簡單羅列一些典型的,如下:
阿里分散式服務框架 Dubbo 的 RPC 框架;
淘寶的訊息中介軟體 RocketMQ;
Hadoop 的高效能通訊和序列化元件 Avro 的 RPC 框架;
開源叢集運算框架 Spark;
分散式計算框架 Storm;
併發應用和分散式應用 Akka;
名單依然很長很長。。。。
四、動手開幹、實現一個傳輸字串的簡單例項
在開始動手之前,必要的基礎概念還是要知道的,要不然程式碼敲下來,功能倒是實現了,但對Netty還是一頭霧水,這就不是本文要達到的目的了。
本示例需要用到的基礎知識主要有以下幾方面的東東,這些知識點最好有一個大概的瞭解,要不然,看例項會有一定的困難。
- 掌握Java基礎
- 掌握Maven基礎
- 熟悉IntelliJ IDEA整合開發工具的使用,這個工具簡稱IDEA
- 知道TCP、Socket的基本概念
1、Netty的元件
I/O:各種各樣的流(檔案、陣列、緩衝、管道。。。)的處理(輸入輸出)。
Channel:通道,代表一個連線,每個Client請對會對應到具體的一個Channel。
ChannelPipeline:責任鏈,每個Channel都有且僅有一個ChannelPipeline與之對應,裡面是各種各樣的Handler。
handler:用於處理出入站訊息及相應的事件,實現我們自己要的業務邏輯。
EventLoopGroup:I/O執行緒池,負責處理Channel對應的I/O事件。
ServerBootstrap:伺服器端啟動輔助物件。
Bootstrap:客戶端啟動輔助物件。
ChannelInitializer:Channel初始化器。
ChannelFuture:代表I/O操作的執行結果,通過事件機制,獲取執行結果,通過新增監聽器,執行我們想要的操作。
ByteBuf:位元組序列,通過ByteBuf操作基礎的位元組陣列和緩衝區。
2、基礎環境準備
基礎環境準備主要有三個方面:JDK安裝及環境變數設定、Maven安裝及環境變數設定、IDEA安裝及基本設定。
2.1、JDK安裝及環境變數設定
JDK下載,可以從官方現在,也可以度娘上隨便搜下載連結,最新版是JDK14,我這裡下載的是JDK8,用8還是14哪個版本無所謂,都可以,但要注意一點的是,現在從JDK的官網Oracle下載需要賬號了,沒賬號的可下不了啦,不知道在搞什麼東東。
官網下載地址:https://www.oracle.com ,截圖如下:
下載完,一路Next安裝完,在建立Java環境變數設定,[此電腦]右鍵-->[屬性]-->[高階系統設定]-->[環境變數]-->[系統變數],截圖如下:
Java環境變數建立完畢後,在DOS視窗執行命令:java -version,測試一下是否正常
2.2、Maven安裝及環境變數設定
Maven功能很強大,但大家不用擔心、本例項中僅僅是利用其便利的jar包依賴、jar包依賴傳遞,基本上沒有任何學習成本。
jar包依賴、jar包依賴傳遞的概念如下圖,清楚明瞭,都不用多做解釋:
Maven是下載,解壓縮後,配置環境變數後就能用,不用安裝的。
下載地址https://downloads.apache.org/maven/maven-3/3.6.3/binaries/
安裝:下載壓縮包,解壓,資料夾拷貝到所想儲存的位置(如C盤根目錄)
配置環境變數,和Java的環境變數配置一樣的,建立MAVEN_HOME,指向Maven資料夾,再在path中新增進去就行,如下圖:
由於直接衝Maven的中央倉庫中自動下載jar包較慢,一般在Maven的配置檔案中,增加阿里雲的公共倉庫配置,這樣會顯著加快jar包的下載速度,如下:
上面的環境變數設定完後,通過DOS視窗中輸入命令:mvn -version 進行驗證是否成功,如下:
2.3、IDEA安裝及基本設定
IDEA的下載和安裝就不多說了,其版本分旗艦版和社群版,旗艦版收費,社群版免費,社群版不支援html、js、css等,但對於本例項,社群版就夠用了,但如果你不在乎那點銀子,可以考慮旗艦版,一步到位,萬一後面我們還要做WEB系統開發可以免得折騰。
其安裝不用多說,一路Next就行,安裝完後,在其配置裡面指定一下JDK、Maven的位置就行了,如下圖:
Maven指定:[File]-->[setting]-->[Build,Excution,Deployment]-->[Build Tools]-->[Maven]
JDK指定:[File]-->[Project Structure]-->[Project Setting]-->[Project]
3、在IDEA中建立Maven工程
新建工程
填寫包名及工程名稱
Maven配置
生成工程,自動建立Maven的依賴檔案
在pom.xml中配置Netty依賴
經過上面的步驟,我們的Maven工程就已經建立完畢,現在可以編寫Netty的第一個程式,這個程式很簡單,傳輸一個字串,雖然程式很簡單,但是已經能夠大體上反映Netty開發通訊程式的一個整體流程了。
4、Netty開發的基本流程
Netty開發的基本流程很簡潔,伺服器端和客戶端都是這個套路,如下:
Netty開發的實際過程,這是一個簡化的過程,但已經把大概流程表達出來了,綠色的代表客戶端流程、藍色的代表伺服器端流程,注意標紅的部分,見下圖:
4.1 建立客戶端類
建立Handler
首先建立Handler類,該類用於接收伺服器端傳送的資料,這是一個簡化的類,只重寫了訊息讀取方法channelRead0、捕捉異常方法exceptionCaught。
客戶端的Handler一般繼承的是SimpleChannelInboundHandler,該類有豐富的方法,心跳、超時檢測、連線狀態等等。
程式碼如下:
package com.jcj.helloworld;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.CharsetUtil;
/**
* @Auther: 江成軍
* @Date: 2020/6/1 11:12
* @Description: 通用handler,處理I/O事件
*/
@ChannelHandler.Sharable
public class HandlerClientHello extends SimpleChannelInboundHandler<ByteBuf>
{
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception
{
/**
* @Author 江成軍
* @Date 2020/6/1 11:17
* @Description 處理接收到的訊息
**/
System.out.println("接收到的訊息:"+byteBuf.toString(CharsetUtil.UTF_8));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception
{
/**
* @Author 江成軍
* @Date 2020/6/1 11:20
* @Description 處理I/O事件的異常
**/
cause.printStackTrace();
ctx.close();
}
}
程式碼說明:
@ChannelHandler.Sharable,這個註解是為了執行緒安全,如果你不在乎是否執行緒安全,不加也可以。
SimpleChannelInboundHandler
,這裡的型別可以是ByteBuf,也可以是String,還可以是物件,根據實際情況來。 channelRead0,訊息讀取方法,注意名稱中有個0。
ChannelHandlerContext,通道上下文,代指Channel。
ByteBuf,位元組序列,通過ByteBuf操作基礎的位元組陣列和緩衝區,因為JDK原生操作位元組麻煩、效率低,所以Netty對位元組的操作進行了封裝,實現了指數級的效能提升,同時使用更加便利。
CharsetUtil.UTF_8,這個是JDK原生的方法,用於指定位元組陣列轉換為字串時的編碼格式。
建立客戶端啟動類
客戶端啟動類根據伺服器端的IP和埠,建立連線,連線建立後,實現訊息的雙向傳輸。
程式碼較簡潔,如下:
package com.jcj.helloworld;
import com.sun.org.apache.bcel.internal.generic.ATHROW;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.util.CharsetUtil;
import java.net.InetSocketAddress;
/**
* @Auther: 江成軍
* @Date: 2020/6/1 11:24
* @Description: 客戶端啟動類
*/
public class AppClientHello
{
private final String host;
private final int port;
public AppClientHello(String host, int port)
{
this.host = host;
this.port = port;
}
public void run() throws Exception
{
/**
* @Author 江成軍
* @Date 2020/6/1 11:28
* @Description 配置相應的引數,提供連線到遠端的方法
**/
EventLoopGroup group = new NioEventLoopGroup();//I/O執行緒池
try {
Bootstrap bs = new Bootstrap();//客戶端輔助啟動類
bs.group(group)
.channel(NioSocketChannel.class)//例項化一個Channel
.remoteAddress(new InetSocketAddress(host,port))
.handler(new ChannelInitializer<SocketChannel>()//進行通道初始化配置
{
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception
{
socketChannel.pipeline().addLast(new HandlerClientHello());//新增我們自定義的Handler
}
});
//連線到遠端節點;等待連線完成
ChannelFuture future=bs.connect().sync();
//傳送訊息到伺服器端,編碼格式是utf-8
future.channel().writeAndFlush(Unpooled.copiedBuffer("Hello World", CharsetUtil.UTF_8));
//阻塞操作,closeFuture()開啟了一個channel的監聽器(這期間channel在進行各項工作),直到鏈路斷開
future.channel().closeFuture().sync();
} finally {
group.shutdownGracefully().sync();
}
}
public static void main(String[] args) throws Exception
{
new AppClientHello("127.0.0.1",18080).run();
}
}
由於程式碼中已經新增了詳盡的註釋,這裡只對極個別的進行說明:
ChannelInitializer,通道Channel的初始化工作,如加入多個handler,都在這裡進行。
bs.connect().sync(),這裡的sync()表示採用的同步方法,這樣連線建立成功後,才繼續往下執行。
pipeline(),連線建立後,都會自動建立一個管道pipeline,這個管道也被稱為責任鏈,保證順序執行,同時又可以靈活的配置各類Handler,這是一個很精妙的設計,既減少了執行緒切換帶來的資源開銷、避免好多麻煩事,同時效能又得到了極大增強。
4.2 建立伺服器端類
建立Handler
和客戶端,只重寫了訊息讀取方法channelRead(注意這裡不是channelRead0)、捕捉異常方法exceptionCaught。
另外伺服器端Handler繼承的是ChannelInboundHandlerAdapter,而不是SimpleChannelInboundHandler,至於這兩者的區別,這裡贅述,大家自行百度吧。
程式碼如下:
package com.jcj.helloworld;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
/**
* @Auther: 江成軍
* @Date: 2020/6/1 11:47
* @Description: 伺服器端I/O處理類
*/
@ChannelHandler.Sharable
public class HandlerServerHello extends ChannelInboundHandlerAdapter
{
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception
{
//處理收到的資料,並反饋訊息到到客戶端
ByteBuf in = (ByteBuf) msg;
System.out.println("收到客戶端發過來的訊息: " + in.toString(CharsetUtil.UTF_8));
//寫入併傳送資訊到遠端(客戶端)
ctx.writeAndFlush(Unpooled.copiedBuffer("你好,我是服務端,我已經收到你傳送的訊息", CharsetUtil.UTF_8));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception
{
//出現異常的時候執行的動作(列印並關閉通道)
cause.printStackTrace();
ctx.close();
}
}
以上程式碼很簡潔,大家注意和客戶端Handler類進行比較。
建立伺服器端啟動類
伺服器端啟動類比客戶端啟動類稍顯複雜一點,先貼出程式碼如下:
package com.jcj.helloworld;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import java.net.InetSocketAddress;
/**
* @Auther: 江成軍
* @Date: 2020/6/1 11:51
* @Description: 伺服器端啟動類
*/
public class AppServerHello
{
private int port;
public AppServerHello(int port)
{
this.port = port;
}
public void run() throws Exception
{
EventLoopGroup group = new NioEventLoopGroup();//Netty的Reactor執行緒池,初始化了一個NioEventLoop陣列,用來處理I/O操作,如接受新的連線和讀/寫資料
try {
ServerBootstrap b = new ServerBootstrap();//用於啟動NIO服務
b.group(group)
.channel(NioServerSocketChannel.class) //通過工廠方法設計模式例項化一個channel
.localAddress(new InetSocketAddress(port))//設定監聽埠
.childHandler(new ChannelInitializer<SocketChannel>() {
//ChannelInitializer是一個特殊的處理類,他的目的是幫助使用者配置一個新的Channel,用於把許多自定義的處理類增加到pipline上來
@Override
public void initChannel(SocketChannel ch) throws Exception {//ChannelInitializer 是一個特殊的處理類,他的目的是幫助使用者配置一個新的 Channel。
ch.pipeline().addLast(new HandlerServerHello());//配置childHandler來通知一個關於訊息處理的InfoServerHandler例項
}
});
//繫結伺服器,該例項將提供有關IO操作的結果或狀態的資訊
ChannelFuture channelFuture= b.bind().sync();
System.out.println("在" + channelFuture.channel().localAddress()+"上開啟監聽");
//阻塞操作,closeFuture()開啟了一個channel的監聽器(這期間channel在進行各項工作),直到鏈路斷開
channelFuture.channel().closeFuture().sync();
} finally {
group.shutdownGracefully().sync();//關閉EventLoopGroup並釋放所有資源,包括所有建立的執行緒
}
}
public static void main(String[] args) throws Exception
{
new AppServerHello(18080).run();
}
}
程式碼說明:
EventLoopGroup,實際專案中,這裡建立兩個EventLoopGroup的例項,一個負責接收客戶端的連線,另一個負責處理訊息I/O,這裡為了簡單展示流程,讓一個例項把這兩方面的活都幹了。
NioServerSocketChannel,通過工廠通過工廠方法設計模式例項化一個channel,這個在大家還沒有能夠熟練使用Netty進行專案開發的情況下,不用去深究。
到這裡,我們就把伺服器端和客戶端都寫完了 ,如何執行呢,先在伺服器端啟動類上右鍵,點Run 'AppServerHello.main()'選單執行,見下圖:
然後,再同樣的操作,執行客戶端啟動類,就能看見效果了。
5、尾聲
本文的內容就到這裡結束了,希望本文能夠讓大家對Netty有一個整體的認識,並大概瞭解其開發流程。
Netty的功能很多,本文只是一個入門的介紹,如果大家對於Netty開發有興趣,可以關注我並給我留言,我會根據關注和留言情況,陸續再撰寫Netty實戰開發的文章。
得到肯定和正向反饋,才有繼續寫下去的願望和動力,畢竟寫這種事無鉅細的文章,還是挺費精力的。