程序員:Netty系列入門案例

本博客 貓叔的博客,轉載請申明出處html

閱讀本文約 「15分鐘」java

適讀人羣:Java 中級git

學習筆記,Netty系列的學習教程,可能不少部份內容是摘抄,不過本身從新作了整理,相關案例也更新了本身的理解。github

目錄狀況

Image Text

Netty簡介

官方解釋:Netty是一個異步的事件驅動的網絡應用程序框架,用於快速開發可維護的高性能的協議服務器和客戶端。——摘自Netty:Homeapache

詞義拆解:編程

  • 異步:無需阻塞等待線程執行結果,容許後續操做,直到其餘線程執行完成後,再回調通知此線程。
  • 事件驅動:事件驅動體系結構(Event-Driven Architecture)是使用事件來觸發解耦後服務之間的通訊,事件是狀態的更改或更新,例如加入到購物車中的課程這一事件。通常具備三個核心組件:事件生產者、事件路由器、事件使用者,路由器負責將事件進行過濾並推送給使用者,生產者與使用者分離,使他們能夠獨立擴展、部署。
  • 協議:網絡通訊的參與方必須遵循相同的規則,這套規則稱爲協議(protocol),它最終體現爲在網絡上傳輸的數據包的格式。

特色

設計優雅

  • 統一接口:提供了統一的異步I/O編程接口Channel,可針對多種傳輸類型的統一接口(阻塞和非阻塞套接字)
  • 異步非阻塞:採用異步非阻塞的I/O類庫,基於Reactor模式實現
  • 事件驅動模型:基於靈活且可擴展的事件模型,讓咱們能夠專一關注業務邏輯層
  • UDP協議:除了支持TCP也支持UDP(用戶數據報協議),因UDP不用在客戶和服務器之間創建一個鏈接,且沒有超時重發等機制,因此傳輸速度很快
  • 責任鏈模式:ChannelPipeline基於責任鏈模式開發,便於業務邏輯的攔截,定製和擴展

上手易用

  • 自主配置:容許用戶經過啓動參數配置的形式選擇Reactor單線程模型、Reactor多線程模型或Reactor主從多線程模型
  • 資料豐富:你能夠在其官網查看詳細的Javadoc用戶指南或者部分案例

高性能

  • 零拷貝:TCP接收和發送緩衝區使用直接內存代替堆內存,避免了內存複製bootstrap

  • 高效的併發編程:經過讀寫鎖、volatile、線程安全容器等提高併發性能windows

  • 無鎖化的串行設計:爲了儘量地避免鎖競爭帶來的性能損耗,能夠經過串行化設計,既消息的處理儘量在同一個線程內完成,期間不進行線程切換,這樣就避免了多線程競爭和同步鎖api

健壯抗壓

  • 持續維護:其修復了已經發現的JDK NIO BUG,下降了開發人員的編程難度數組

  • 鏈路的有效性檢測:TCP層面、協議層、應用層的心跳檢測

  • 規避NIO BUG(Netty的解決策略)

    • 一、根據該BUG的特徵,首先偵測該BUG是否發生
    • 二、將問題Selector上註冊的Channel轉移到新建的Selector上
    • 三、老問題的Selector關閉,使用新建的Selector替換

安全穩定

  • 協議:完整的 SSL / TLS 和 StartTLS 的支持
  • 範圍廣:運行在受限的環境例如 Applet 或 OSGI
  • 穩定:Netty對JDK的線程池進行了封裝和改造,可是,本質上仍然是利用了線程池和線程安全隊列簡化了多線程編程,同時經過對象計數器對Netty的ByteBuf等內置對象進行細粒度的內存申請和釋放,對非法的對象引用進行檢測和保護

行業應用

Netty能夠適用的行業很是廣,由於設計優雅、高性能,其能夠在多個行業有所應用,好比在互聯網行業通常會做爲PRC框架使用,而遊戲行業中也進場須要其做爲通訊組件即多協議棧特色,能夠在遊戲行業發揮其高性能通訊,還有在通訊行業裏,因其異步高性能、高可靠性等,它在互聯網上也有許多開源或者教學的IM案例,等等...

我的經驗

若是你或你的團隊在找尋一個高性能且成熟穩定的NIO框架,那麼必定要選擇Netty

我在第一次瞭解Netty是由於項目須要支持TCP併發長鏈接的解決方案,而在互聯網上找尋了許久,由於服務端是Java寫的,且在瞭解Netty的機制與服務能力後,便開始了和Netty的不解之緣。

但願你也能有所收穫!

Netty環境搭建

即便你是初學編程的小白,你能夠跟着這個小節一步一步構建本身的開發環境,以便於後續學習Netty相關代碼實戰環境。

本小節選擇的開發工具是IDEA,其是一款目前Java開發工程師比較經常使用的開發工具,而javaMaven的版本的選擇,本着學有所成的目的,但願你們能夠和課程保持一致。

配置Java

若是你的電腦已經安裝了JDK,那麼請你先驗證一下它的版本。

打開電腦cmd,輸入:

java -version
複製代碼

以下是本次演示的java版本。

image-20200104111453974

若是你還沒安裝JDK,那麼能夠到Oracle官網下載。

image-20200112071259266

下載地址:www.oracle.com/technetwork…

注意:咱們僅需下載JDK便可

下載後進行安裝便可。

安裝後還要進行環境變量的配置,在系統變量中新建JAVA_HOME、CLASSPATH兩個。

image-20200112071521243

JAVA_HOME : C:\Program Files\Java\jdk1.8.0_191(Windows上安裝的默認值)

CLASSPATH : .;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tolls.jar;

還有在Path中添加兩個地址

image-20200112071550602

%JAVA_HOME%\bin

%JAVA_HOME%\jre\bin

在Linux上則將**${JAVA_HOME}/bin**添加到執行路徑上。

以上配置好後,請再運行cmd,輸入:Java -version,驗證電腦的Java版本是否顯示正常。

配置Maven

若是你的電腦已經安裝了Maven,那麼請你先驗證一下它的版本。

打開電腦cmd,輸入:

mvn -v
複製代碼

以下是本次演示的Maven版本。

image-20200104114438695

若是你還沒安裝Maven,那麼能夠到官網下載。

image-20200112071747829

下載地址:maven.apache.org/download.cg…

將文件解壓到指定的目錄下,如:F:\Maven\apache-maven-3.6.3

在系統變量中新建MAVEN_HOME

image-20200112071814559

MAVEN_HOME : F:\Maven\apache-maven-3.6.3

還有在Path中添加一個地址

image-20200112071836650

%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>
複製代碼

安裝IDE

本次教程使用IDEA,你們請根據本身的電腦狀況下載對應的版本,本次教程的IDEA版本是Windows的Community。

image-20200112071912375

下載地址:www.jetbrains.com/idea/downlo…

若是你已經有本身熟練的IDE,那麼也能夠用於學習,並不會影響學習質量。

安裝完成後,須要再配置下IDE的Maven,以下圖,你能夠修改Maven路徑、settings文件及本地倉庫。

image-20200104122636289

引入Netty

用IDEA構建一個簡單的SpringBoot項目,你們這時能夠在Project SDK的配置上選擇咱們一開始配置JDK,以下圖

image-20200104122909694

項目新建後,請打開pom.xml文件,引入Netty資源,關於相關框架的Maven資源均可以到如下網站搜索

mvnrepository.com/

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服務端建立

流程圖理解

下圖簡單講述一個Netty服務端構建的基本流程操做,並不涉及具體的業務邏輯。

image-20200112134508478

部分代碼

以下展現部分代碼與上圖流程一致。

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客戶端建立

流程圖理解

下圖簡單講述一個Netty客戶端構建的基本流程操做,並不涉及具體的業務邏輯。

image-20200112145419340

部分代碼

以下展現部分代碼與上圖流程一致。

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的服務端與客戶端建立。

若是你能瞭解服務端與客戶端的建立流程與步驟,那麼接下來的學習能夠更加高效。

流程邏輯

image-20200104182900894

這個例子以服務端和客戶端的時間傳輸爲例,給各位讀者介紹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函數。

image-20200112161000732

啓動成功!

image-20200112161109665

二、運行客戶端,在IDEA中啓動MkTimeClient的main函數。

image-20200112161154768

啓動成功!

image-20200112161255226

同時鏈接服務端,並接收到服務端發送的時間消息,打印到了控制檯,且自動關閉客戶端。

服務端在控制檯也檢測到客戶端的鏈接併發送了系統時間,從控制檯日誌中能夠看到,以下圖。

image-20200112161438189

注意點

  • exceptionCaught(...)

github.com/netty/netty…

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,還在堅持學習技術與產品經理相關的知識,但願本文能給你帶來新的知識點。

公衆號:Java貓說

學習交流羣:728698035

現架構設計(碼農)兼創業技術顧問,不羈平庸,熱愛開源,雜談程序人生與不按期乾貨。

Image Text
相關文章
相關標籤/搜索