本博客 貓叔的博客,轉載請申明出處html
閱讀本文約 「15分鐘」java
適讀人羣:Java 中級git
學習筆記,Netty系列的學習教程,可能不少部份內容是摘抄,不過本身從新作了整理,相關案例也更新了本身的理解。github
官方解釋:Netty是一個異步的事件驅動的網絡應用程序框架,用於快速開發可維護的高性能的協議服務器和客戶端。——摘自Netty:Homeapache
詞義拆解:編程
零拷貝:TCP接收和發送緩衝區使用直接內存代替堆內存,避免了內存複製bootstrap
高效的併發編程:經過讀寫鎖、volatile、線程安全容器等提高併發性能windows
無鎖化的串行設計:爲了儘量地避免鎖競爭帶來的性能損耗,能夠經過串行化設計,既消息的處理儘量在同一個線程內完成,期間不進行線程切換,這樣就避免了多線程競爭和同步鎖api
持續維護:其修復了已經發現的JDK NIO BUG,下降了開發人員的編程難度數組
鏈路的有效性檢測:TCP層面、協議層、應用層的心跳檢測
規避NIO BUG(Netty的解決策略)
Netty能夠適用的行業很是廣,由於設計優雅、高性能,其能夠在多個行業有所應用,好比在互聯網行業通常會做爲PRC框架使用,而遊戲行業中也進場須要其做爲通訊組件即多協議棧特色,能夠在遊戲行業發揮其高性能通訊,還有在通訊行業裏,因其異步高性能、高可靠性等,它在互聯網上也有許多開源或者教學的IM案例,等等...
若是你或你的團隊在找尋一個高性能且成熟穩定的NIO框架,那麼必定要選擇Netty!
我在第一次瞭解Netty是由於項目須要支持TCP併發長鏈接的解決方案,而在互聯網上找尋了許久,由於服務端是Java寫的,且在瞭解Netty的機制與服務能力後,便開始了和Netty的不解之緣。
但願你也能有所收穫!
即便你是初學編程的小白,你能夠跟着這個小節一步一步構建本身的開發環境,以便於後續學習Netty相關代碼實戰環境。
本小節選擇的開發工具是IDEA,其是一款目前Java開發工程師比較經常使用的開發工具,而java與Maven的版本的選擇,本着學有所成的目的,但願你們能夠和課程保持一致。
若是你的電腦已經安裝了JDK,那麼請你先驗證一下它的版本。
打開電腦cmd,輸入:
java -version
複製代碼
以下是本次演示的java版本。
若是你還沒安裝JDK,那麼能夠到Oracle官網下載。
下載地址:www.oracle.com/technetwork…
注意:咱們僅需下載JDK便可
下載後進行安裝便可。
安裝後還要進行環境變量的配置,在系統變量中新建JAVA_HOME、CLASSPATH兩個。
JAVA_HOME : C:\Program Files\Java\jdk1.8.0_191(Windows上安裝的默認值)
CLASSPATH : .;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tolls.jar;
還有在Path中添加兩個地址
%JAVA_HOME%\bin
%JAVA_HOME%\jre\bin
在Linux上則將**${JAVA_HOME}/bin**添加到執行路徑上。
以上配置好後,請再運行cmd,輸入:Java -version,驗證電腦的Java版本是否顯示正常。
若是你的電腦已經安裝了Maven,那麼請你先驗證一下它的版本。
打開電腦cmd,輸入:
mvn -v
複製代碼
以下是本次演示的Maven版本。
若是你還沒安裝Maven,那麼能夠到官網下載。
將文件解壓到指定的目錄下,如:F:\Maven\apache-maven-3.6.3
在系統變量中新建MAVEN_HOME
MAVEN_HOME : F:\Maven\apache-maven-3.6.3
還有在Path中添加一個地址
%MAVEN_HOME%\bin
在Linux上則將**${MAVEN_HOME}/bin**添加到執行路徑上。
以上配置好後,請再運行cmd,輸入:mvn -v,驗證電腦的Maven版本是否顯示正常。
以後能夠再打開:F:\Maven\apache-maven-3.6.3\conf 下的settings,修改成阿里鏡像
<mirrors>
<mirror>
<id>nexus-aliyun</id>
<mirrorOf>central</mirrorOf>
<name>Nexus aliyun</name>
<url>http://maven.aliyun.com/nexus/content/groups/public</url>
</mirror>
</mirrors>
複製代碼
本次教程使用IDEA,你們請根據本身的電腦狀況下載對應的版本,本次教程的IDEA版本是Windows的Community。
若是你已經有本身熟練的IDE,那麼也能夠用於學習,並不會影響學習質量。
安裝完成後,須要再配置下IDE的Maven,以下圖,你能夠修改Maven路徑、settings文件及本地倉庫。
用IDEA構建一個簡單的SpringBoot項目,你們這時能夠在Project SDK的配置上選擇咱們一開始配置JDK,以下圖
項目新建後,請打開pom.xml文件,引入Netty資源,關於相關框架的Maven資源均可以到如下網站搜索
Netty地址:mvnrepository.com/artifact/io…
netty-all的maven地址
<!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.44.Final</version>
</dependency>
複製代碼
引入到Pom.xml文件中,以下
<dependencies>
<!--省略部分代碼-->
<!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.44.Final</version>
</dependency>
<!--省略部分代碼-->
</dependencies>
複製代碼
以上,netty引入成功,環境搭建完成。
一、javac顯示不是內部或外部命令
操做系統win十、win7
配置如上文所示時,能夠將PATH的路徑改成絕對路徑
C:\Program Files\Java\jdk1.8.0_191\bin
C:\Program Files\Java\jdk1.8.0_191\jre\bin
二、mvn顯示不是內部或外部命令
jdk的環境變量配置有錯,或者是M2_HOME路徑有錯
操做系統win十、win7
配置如上文所示時,能夠將PATH的路徑改成絕對路徑
F:\Maven\apache-maven-3.6.3\bin
三、系統存在兩個JDK版本
將默認啓動的JDK版本中java.exe所在路徑加入到操做系統PATH的首位
四、maven版本不兼容
輸入:mvn -v,若是報「Exception in thread "main" java.lang.UnsupportedClassVersionError: org/apache/maven/cli/MavenCli : Unsupported major.minor version 51.0」的錯誤,能夠更新新版的maven,解決問題。
下圖簡單講述一個Netty服務端構建的基本流程操做,並不涉及具體的業務邏輯。
以下展現部分代碼與上圖流程一致。
EventLoopGroup acceptorGroup = new NioEventLoopGroup();
try {
//一、ServerBootstrap引導類
ServerBootstrap b = new ServerBootstrap();
//二、NioEventLoopGroup接受新鏈接及讀/寫處理
b.group(acceptorGroup)
//三、指定傳輸類型Channel
.channel(NioServerSocketChannel.class)
//四、添加業務Handler
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(...業務Handler);
}
});
//五、綁定服務器及端口
ChannelFuture f = b.bind(port).sync();
//六、監聽服務器Channel關閉
f.channel().closeFuture().sync();
}finally {
//六、釋放資源
acceptorGroup.shutdownGracefully();
}
複製代碼
對於一個基本的Netty服務端而言,他須要綁定到對應的服務器上同時在其上監聽端口以保證能夠接受傳入的鏈接請求,還須要給他配置Channel,將入站消息及時通知給咱們所定義的業務Handler中進行處理。
咱們一開始建立了一個ServerBootStrap實例,他是Netty啓動NIO服務端的輔助啓動類,能夠爲咱們下降服務端的開發難度,同時構建了一個NioEventLoopGroup來接受和處理新的鏈接,固然你也能夠建立兩個Reactor線程組,一個用於服務端接受客戶端的鏈接,一個用於進行SocketChannel的網絡讀寫。咱們還指定了Channel的類型爲NioServerSocketChannel,其功能對應JDK NIO 類庫中的ServerSocketChannel類,在大部分狀況下,你能夠配置NioServerSocketChannel的TCP參數,好比將它的backlog設置爲1024等。
複雜的點來了,綁定I/O事件的ChildChannelHandler類,其實它有點相似Reactor中的Handler類,主要用於處理網絡I/O,例如對消息的編解碼等。但是爲何還有一個ChannelInitializer類呢?
這也是Netty美妙的地方,當一個新的鏈接被服務端接受時,一個新的子Channel會建立,而ChannelInitializer就會把咱們的業務Handler實例添加到這個子Channel的ChannelPipeline中。
接下來是綁定服務器及端口,經過sync()咱們能夠阻塞當前的Thread,直到綁定操做完成爲止,同時咱們還要繼續使用sync()去監聽服務端的Channel,直到它被關閉,咱們才能去關閉EventLoopGroup還有其餘的全部資源。
以上也是Netty服務端的建立流程,相比其傳統的NIO服務端,其大大簡化了開發的複雜程度。
如下是下文演示案例的服務端代碼。
package com.demo.timer;
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;
/** * @ClassName MkTimeServer * @Description 服務端 * @Author Java貓說 * @Date 2020/1/4 0004 14:02 **/
public class MkTimeServer {
public static void main(String[] args) throws Exception {
int port = 8080;
//啓動服務端
new MkTimeServer().run(port);
}
void run(int port) throws Exception{
EventLoopGroup acceptorGroup = new NioEventLoopGroup();
try {
//引導類
ServerBootstrap b = new ServerBootstrap();
b.group(acceptorGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//MkTimeServerHandler屬於業務Handler
socketChannel.pipeline().addLast(new MkTimeServerHandler());
}
});
//阻塞直到異步綁定服務器完成
ChannelFuture f = b.bind(port).sync();
//阻塞直到Channel關閉
f.channel().closeFuture().sync();
}finally {
acceptorGroup.shutdownGracefully();
}
}
}
複製代碼
下圖簡單講述一個Netty客戶端構建的基本流程操做,並不涉及具體的業務邏輯。
以下展現部分代碼與上圖流程一致。
EventLoopGroup acceptorGroup = new NioEventLoopGroup();
try {
//一、Bootstrap引導類
Bootstrap b = new Bootstrap();
//二、NioEventLoopGroup建立鏈接及處理出/入站數據
b.group(group)
//三、指定傳輸類型爲NioSocketChannel類型
.channel(NioSocketChannel.class)
//四、添加業務Handler
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(...業務Handler);
}
});
//五、鏈接遠程指定的host、port節點
ChannelFuture f = b.connect(host,port).sync();
//六、監聽服務器的Channel關閉
f.channel().closeFuture().sync();
}finally {
//六、釋放資源
group.shutdownGracefully();
}
複製代碼
對於一個基本的Netty客戶端而言,其實咱們能夠發現它與服務端的流程很類似,若是說ServerBootStrap實例,他是Netty啓動NIO服務端的輔助啓動類,能夠爲咱們下降服務端的開發難度,那麼其實BootStrap也是Netty爲咱們提供的NIO客戶端的輔助啓動類。
AbstractBootstrap類是ServerBootstrap及Bootstrap的基類,感興趣的朋友能夠看看後續章節的源碼解析。
與服務端不一樣的是,客戶端的Channel須要設置爲NioSocketChannel,一樣你能夠設置相關的選擇,而後爲其添加Handler。
當客戶端完成並啓動BootStrap輔助類,咱們須要調用connect()方法發起異步鏈接,而後調用sync()同步等待鏈接成功。
當客戶端鏈接關閉時,釋放線程資源,並退出客戶端主函數。
如下是下文演示案例的服務端代碼。
package com.demo.timer;
import io.netty.bootstrap.Bootstrap;
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.NioSocketChannel;
/** * @ClassName MkTimeClient * @Description 客戶端 * @Author Java貓說 * @Date 2020/1/4 0004 14:03 **/
public class MkTimeClient {
public static void main(String[] args) throws Exception {
int port = 8080;
String host = "127.0.0.1";
new MkTimeClient().connect(port, host);
}
void connect(int port, String host) throws Exception{
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group).channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//MkTimeClientHandler屬於業務Handler
socketChannel.pipeline().addLast(new MkTimeClientHandler());
}
});
ChannelFuture f = b.connect(host,port).sync();
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
}
複製代碼
在閱讀本章以前,請先學習前兩節,即Netty的服務端與客戶端建立。
若是你能瞭解服務端與客戶端的建立流程與步驟,那麼接下來的學習能夠更加高效。
這個例子以服務端和客戶端的時間傳輸爲例,給各位讀者介紹Netty的服務端與客戶端,讓各位能夠簡單的搭建並運行。
編碼過程涉及的部分知識點也會有所介紹,若是各位對於部分組件有興趣也能夠跳到對應的詞條閱讀相關的源碼介紹。
如上圖的項目架構邏輯圖,本次學習的項目案例是使用Netty構建一個主動給鏈接發送時間戳的服務端,服務端的做用是啓動後監聽鏈接的客戶端,每當有一個客戶端鏈接時,新建一個Channel並在其ChannelPipeline後追加服務業務MkTimeServerHandler實例,業務Handler負責檢測到新鏈接時主動向客戶端發送當前系統時間戳。
客戶端的做用是啓動後根據指定的Host和Port去鏈接服務端,同時客戶端Channel的ChannelPipeline也會有對應處理時間戳的客戶業務MkTimeClientHandler實例,負責主動鏈接服務端,讀取服務端發送的系統時間戳並打印到控制檯並關閉客戶端服務。
咱們的MkTimeServerHandler類第一步就是須要繼承ChannelInboundHandlerAdapter類,首先了解下爲何須要繼承它,ChannelInboundHandlerAdapter是ChannelHandler的適配器之一,其對應的還有ChannelOutboundHandlerAdapter,其中ChannelInboundHandler負責處理處理進站數據和全部狀態更改事件,而ChannelOutboundHandler負責處理出站數據,容許攔截各類操做。
由於服務端須要第一時間判斷新鏈接並主動向客戶端發送系統時間戳,所以咱們繼承了ChannelInboundHandlerAdapter。
那麼ChannelInboundHandlerAdapter負責的全部狀態更改事件是挺多的,咱們今天就先學習演示Demo中的兩個。
適配器提供的多個與Channel生命週期相關的方法之一,channelActive指當Channel處於活躍時,即Channel鏈接且準備就緒時。這也知足咱們業務場景的需求,在檢測到鏈接的時候,獲取當前的系統時間戳,並建立一個Netty定義的ByteBuf字節串用於保存時間戳,經過ChannelHandlerContext調用writeAndFlush方法發送給客戶端。
這裏的ChannelHandlerContext是ChannelPipeline用來直接管理ChannelHandler的「替身」。
通常咱們在寫這一類的業務Handler時,爲了不鏈接異常,通常都會實現exceptionCaught方法,其能夠在捕獲到異常時,打印並關閉這個Channel通道,這也是加強了代碼的健壯性。
package com.demo.timer;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import java.util.Date;
/** * @ClassName MkTimeServerHandler * @Description 發送系統時間 * @Author Java貓說 * @Date 2020/1/4 0004 14:03 **/
public class MkTimeServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("One Channel Connect");
//獲取系統時間戳的字符串
String serverTime = new Date(System.currentTimeMillis()).toString();
//建立一個 ByteBuf 保存特定字節串
ByteBuf resp = Unpooled.copiedBuffer(serverTime.getBytes());
//將 ByteBuf 發送給客戶端
ctx.writeAndFlush(resp);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
//異常關閉
ctx.close();
}
}
複製代碼
在客戶端的MkTimeClientHandler類中,咱們也一樣繼承了ChannelInboundHandlerAdapter,這裏就再也不贅述了。
channelRead也是Channel生命週期相關的方法之一,當Channel讀取到消息時調用,即服務端發送消息,咱們能夠經過這個方法獲取到服務端的時間消息。
咱們經過ByteBuf接收,由於在服務端發送時也同一個了這個類型,並建立一個等長的byte數組,同時在將ByteBuf中的內容傳輸到byte數組,最後轉化爲String打印在控制檯。
在這個業務中,咱們在獲取到服務端系統時間戳後就關閉了鏈接,咱們經過調用ChannelHandlerContext的close方法,關閉鏈接,並在操做完成後通知ChannelFuture,由於不管關閉成功或失敗,都沒法再使用這個鏈接。
package com.demo.timer;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
/** * @ClassName MkTimeClientHandler * @Description 接受時間消息 * @Author Java貓說 * @Date 2020/1/4 0004 14:04 **/
public class MkTimeClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf m = (ByteBuf) msg;
byte[] req = new byte[m.readableBytes()];
m.readBytes(req);
String serverTime = new String(req);
System.out.println(serverTime + " From Server.");
ctx.close();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
複製代碼
一、運行服務端,在IDEA中啓動MkTimeServer的main函數。
啓動成功!
二、運行客戶端,在IDEA中啓動MkTimeClient的main函數。
啓動成功!
同時鏈接服務端,並接收到服務端發送的時間消息,打印到了控制檯,且自動關閉客戶端。
服務端在控制檯也檢測到客戶端的鏈接併發送了系統時間,從控制檯日誌中能夠看到,以下圖。
normanmaurer:exceptionCaught(...) is only called for inbound exceptions. All outbound exceptions must by handled in a listener by design. If you always want to have exceptions handled in exceptionCaught(...) just add a ChannelOutboundHandler that will an listener for every outbound operation.
在這個issues中曾提過,exceptionCaught 只會捕獲 inbound handler的exception, outbound exceptions 須要在writeAndFlush方法里加上listener來監聽消息是否發送成功。
我是MySelf,還在堅持學習技術與產品經理相關的知識,但願本文能給你帶來新的知識點。
學習交流羣:728698035
現架構設計(碼農)兼創業技術顧問,不羈平庸,熱愛開源,雜談程序人生與不按期乾貨。