netty通訊

阿寧你好啊發表於2022-03-17

學習netty之前,要先了解作業系統中的IO零拷貝(已經附上鍊接了)

一、netty的簡單介紹

  • Netty 是由 JBOSS 提供的一個 Java 開源框架,現為 Github 上的獨立專案。
  • Netty 是一個非同步的、基於事件驅動的網路應用框架,用以快速開發高效能、高可靠性的網路 IO 程式。
  • Netty 主要針對在 TCP 協議下,面向 Client 端的高併發應用,或者 **Peer-to-Peer **場景下的大量資料持續傳輸的應用。
  • Netty 本質是一個 NIO 框架,適用於伺服器通訊相關的多種應用場景。
  • Dubbo 協議預設使用 Netty 作為基礎通訊元件,用於實現各程式節點之間的內部通訊

為什麼有了netty框架

原生的NIO存在問題:

  • NIO的類庫和API繁雜:需要熟練掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer
  • 要熟悉Java多執行緒程式設計,因為NIO程式設計涉及到Reactor模式
  • Selector可能出現空輪詢,佔用CPU資源

netty是對NIO進行封裝,解決了上述問題:

  • 設計優雅:
    適用於各種傳輸型別的統一API,阻塞和非阻塞Socket;
    基於靈活且可擴充套件的事件模型,可以清晰地分離關注點;
    高度可定製的執行緒模型
  • 高效能、吞吐量更高:
    延遲更低;
    減少資源消耗;
    最小化不必要的記憶體複製。
  • 安全:完整的 SSL/TLS 和 StartTLS 支援。
  • 社群活躍、不斷更新

二、執行緒模式

下面講解的是netty執行緒模式的由來

現有的執行緒模式:

  • BIO的傳統阻塞IO服務模型
  • NIO的Reactor模型
    單 Reactor 單執行緒;
    單 Reactor多執行緒;
    主從 Reactor多執行緒

2.1 BIO的傳統阻塞IO服務模型

1、每個連線都需要獨立的執行緒完成資料的輸入,業務處理,資料返回。當併發數很大,就會建立大量的執行緒,佔用很大系統資源。
2、連線建立後,如果當前執行緒暫時沒有資料可讀,該執行緒會阻塞在 Handler物件中的read 操作,導致上面的處理執行緒資源浪費。
image

2.2 NIO的Reactor模型

基於I/O多路複用模型:多個客戶端進行連線,先把連線請求給Reactor,多個連線共用一個阻塞物件Reactor,由Reactor負責監聽和分發,當客戶端連線沒有資料時不會阻塞執行緒。
基於執行緒池複用執行緒資源:不必再為每個連線建立執行緒,將連線完成後的業務處理任務分配給執行緒進行處理,一個執行緒可以處理多個連線的業務。(解決了當併發數很大時,會建立大量執行緒,佔用很大系統資源)

Reactor模式中核心組成:

  • Reactor:在一個單獨的執行緒中執行,負責監聽和分發事件,分發給適當的處理執行緒來對IO事件做出反應。
  • Handlers:處理執行緒執行IO事件。

單Reactor單執行緒

特點:

  • 該模型簡單沒有多執行緒的競爭,由一個執行緒完成所有的操作(監聽、分發、執行),沒有充分利用多核CPU
  • 因為Reactor是單執行緒執行,因此在處理某個handler的IO事件時,其他的handler需要進行等待,等待時間長因為該執行緒還要處理業務;
  • 當Reactor出現問題,就會造成業務模組不可用

image
上圖解析:

  • Reactor物件通過select監控客戶端請求事件,收到事件後通過dispatch 進行分發
  • 如果是建立連線請求事件,則由Acceptor通過accept處理連線請求,然後建立一個 Handler 物件處理連線完成後的後續業務處理
  • 如果不是建立連線事件,則Reactor會分發處理執行緒來處理Handler的IO事件(完成 Read → 業務處理 → send 的完整業務流程)

單Reactor多執行緒

特點:

  • 充分利用多核CPU,可能出現多執行緒競爭
  • 因為Reactor是單執行緒執行,因此在處理某個handler的IO事件時,其他的handler需要進行等待,等待時間短因為處理業務交給worker執行緒池執行;
  • Reactor承擔所有的事件的監聽和響應,它是單執行緒執行,在高併發場景容易出現效能瓶頸
  • 當Reactor出現問題,就會造成業務模組不可用

image

上圖解析:

  • Reactor物件通過select監控客戶端請求事件,收到事件後,通過Dispatch 進行分發
  • 如果是建立連線請求事件,則由Acceptor通過accept處理連線請求,然後建立一個Handler物件處理完成連線後的各種事件
  • 如果不是連線請求事件,則Reactor會分發處理執行緒來處理Handler的IO事件,handler只負責響應事件(read、send),不做具體的業務處理交給worker執行緒池執行(這樣不會使handler阻塞太久)
  • worker執行緒池會分配獨立的worker執行緒完成真正的業務,並將結果返回給handler,handler收到響應後,通過send將結果返回給client

主從Reactor多執行緒

特點:

  • 主Reactor可已有多個,從Reactor也可以有多個
  • 主Reactor負責處理建立連線請求事件,從Reactor負責處理業務請求事件
  • 當從Reactor出現問題,可以呼叫其他的從Reactor提供服務

image

上圖解析:

  • Reactor主執行緒 MainReactor物件通過select監聽連線事件,收到事件後,通過Acceptor的accept處理連線事件
  • 當Acceptor處理連線事件後,MainReactor將連線分配給SubReactor,subreactor將連線加入到連線佇列進行監聽,並建立handler進行各種事件處理
  • 當有新事件發生時,subreactor就會分發處理執行緒來處理handler,handler通過read讀取資料,分發給後面的worker執行緒池處理。
  • worker執行緒池分配獨立的worker執行緒進行業務處理,並返回結果。handler 收到響應的結果後,再通過send將結果返回給client
  • Reactor主執行緒可以對應多個Reactor子執行緒,即MainRecator可以關聯多個 SubReactor

生活中的體現:
單 Reactor 單執行緒:前臺接待員和服務員是同一個人,全程為顧客服務
單 Reactor 多執行緒:1 個前臺接待員,多個服務員,接待員只負責接待
主從 Reactor 多執行緒:多個前臺接待員,多個服務生

2.3 netty執行緒模型

netty執行緒模型主要是依據主從Reactor多執行緒
image

  • BossGroup中的NioEventLoop就像MainReactor可以有多個,WorkerGroup中的NioEventLoop就像SubReactor一樣可以有多個。

  • Netty抽象出兩組執行緒池,BossGroup專門負責接收客戶端的連線,WorkerGroup專門負責網路的讀寫

  • BossGroup和WorkerGroup型別都是NioEventLoopGroup,NioEventLoopGroup相當於一個事件迴圈組,這個組中含有多個事件迴圈,每一個事件迴圈是NioEventLoop(NioEventLoopGroup可以有多個執行緒,即可以含有多個NioEventLoop)

  • NioEventLoop表示一個不斷迴圈的執行處理任務的執行緒,每個 NioEventLoop都有一個Selector,用於監聽繫結在其上的socket的網路通訊

  • 每個BossGroup下面的NioEventLoop迴圈執行的步驟有3步:
    1、輪詢 accept 事件
    2、處理 accept 事件,與 client 建立連線,生成NioSocketChannel,並將其註冊到某個WorkerGroup的NioEventLoop上的Selector
    3、繼續處理任務佇列的任務,即runAllTasks

  • 每個WorkerGroup下面的NioEventLoop迴圈執行的步驟有3步:
    1、輪詢 read,write 事件
    2、處理 I/O 事件,即 read,write 事件,在對應NioSocketChannel處理
    3、處理任務佇列的任務,即runAllTasks

  • 每個WorkerGroup的NioEventLoop處理業務時,會使用pipeline(管道),pipeline中包含了channel(通道),即通過pipeline可以獲取到對應通道,每個通道中都有一個channelPipeline維護了很多的處理器channelhandler。

netty案例-TCP服務

服務端:
NettyServer

NettyServer程式碼
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

public class NettyServer {
    public static void main(String[] args) throws Exception {


        //建立BossGroup 和 WorkerGroup
        //說明
        //1. 建立兩個執行緒組 bossGroup 和 workerGroup
        //2. bossGroup 只是處理連線請求 , 真正的和客戶端業務處理,會交給 workerGroup完成
        //3. 兩個都是無限迴圈
        //4. bossGroup 和 workerGroup 含有的子執行緒(NioEventLoop)的個數
        //   預設實際 cpu核數 * 2
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup(); //8



        try {
            //建立伺服器端的啟動物件,配置引數
            ServerBootstrap bootstrap = new ServerBootstrap();

            //使用鏈式程式設計來進行設定
            bootstrap.group(bossGroup, workerGroup) //設定兩個執行緒組
                    .channel(NioServerSocketChannel.class) //使用NioSocketChannel 作為伺服器的通道實現
                    .option(ChannelOption.SO_BACKLOG, 128) // 設定執行緒佇列等待連線個數
                    .childOption(ChannelOption.SO_KEEPALIVE, true) //設定保持活動連線狀態
//                    .handler(null) // 該 handler對應 bossGroup , childHandler 對應 workerGroup
                    .childHandler(new ChannelInitializer<SocketChannel>() {//建立一個通道初始化物件(匿名物件)
                        //給pipeline 設定處理器
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            System.out.println("客戶socketchannel hashcode=" + ch.hashCode()); //可以使用一個集合管理 SocketChannel, 再推送訊息時,可以將業務加入到各個channel 對應的 NIOEventLoop 的 taskQueue 或者 scheduleTaskQueue
                            ch.pipeline().addLast(new NettyServerHandler());
                        }
                    }); // 給我們的workerGroup 的 EventLoop 對應的管道設定處理器

            System.out.println(".....伺服器 is ready...");

            //繫結一個埠並且同步生成了一個 ChannelFuture 物件(也就是立馬返回這樣一個物件)
            //啟動伺服器(並繫結埠)
            ChannelFuture cf = bootstrap.bind(6668).sync();

            //給cf 註冊監聽器,監控我們關心的事件

            cf.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    if (cf.isSuccess()) {
                        System.out.println("監聽埠 6668 成功");
                    } else {
                        System.out.println("監聽埠 6668 失敗");
                    }
                }
            });


            //對關閉通道事件  進行監聽
            cf.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }

}


NettyServerHandler

NettyServerHandler程式碼
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelPipeline;
import io.netty.util.CharsetUtil;

import java.util.concurrent.TimeUnit;

/*
說明
1. 我們自定義一個Handler 需要繼承netty 規定好的某個HandlerAdapter(規範)
2. 這時我們自定義一個Handler , 才能稱為一個handler
 */
public class NettyServerHandler extends ChannelInboundHandlerAdapter {

    //讀取資料事件(這裡我們可以讀取客戶端傳送的訊息)
    /*
    1. ChannelHandlerContext ctx:上下文物件, 含有 管道pipeline , 通道channel, 地址
    2. Object msg: 就是客戶端傳送的資料 預設Object
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("伺服器讀取執行緒 " + Thread.currentThread().getName() + " channle =" + ctx.channel());
        System.out.println("server ctx =" + ctx);
        System.out.println("看看channel 和 pipeline的關係");
        Channel channel = ctx.channel();
        ChannelPipeline pipeline = ctx.pipeline(); //本質是一個雙向連結串列


        //將 msg 轉成一個 ByteBuf
        //ByteBuf 是 Netty 提供的,不是 NIO 的 ByteBuffer.
        ByteBuf buf = (ByteBuf) msg;
        System.out.println("客戶端傳送訊息是:" + buf.toString(CharsetUtil.UTF_8));
        System.out.println("客戶端地址:" + channel.remoteAddress());
    }

    //資料讀取完畢
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {

        //writeAndFlush 是 write + flush
        //將資料寫入到快取,並重新整理
        //一般講,我們對這個傳送的資料進行編碼
        ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客戶端~(>^ω^<)喵1", CharsetUtil.UTF_8));
    }

    //發生異常後, 一般是需要關閉通道

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }
}

客戶端:
NettyClient

NettyClient程式碼
import io.netty.bootstrap.Bootstrap;
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;

public class NettyClient {
    public static void main(String[] args) throws Exception {

        //客戶端需要一個事件迴圈組
        EventLoopGroup group = new NioEventLoopGroup();


        try {
            //建立客戶端啟動物件
            //注意客戶端使用的不是 ServerBootstrap 而是 Bootstrap
            Bootstrap bootstrap = new Bootstrap();

            //設定相關引數
            bootstrap.group(group) //設定執行緒組
                    .channel(NioSocketChannel.class) // 設定客戶端通道的實現類(反射)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new NettyClientHandler()); //加入自己的處理器
                        }
                    });

            System.out.println("客戶端 ok..");

            //啟動客戶端去連線伺服器端
            //關於 ChannelFuture 要分析,涉及到netty的非同步模型
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync();
            //對關閉通道事件  進行監聽
            channelFuture.channel().closeFuture().sync();
        }finally {

            group.shutdownGracefully();

        }
    }
}

NettyClientHandler

NettyClientHandler程式碼
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;

public class NettyClientHandler extends ChannelInboundHandlerAdapter {

    //當通道就緒就會觸發該方法
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("client " + ctx);
        ctx.writeAndFlush(Unpooled.copiedBuffer("hello, server: (>^ω^<)喵", CharsetUtil.UTF_8));
    }

    //當通道有讀取事件時,會觸發
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        ByteBuf buf = (ByteBuf) msg;
        System.out.println("伺服器回覆的訊息:" + buf.toString(CharsetUtil.UTF_8));
        System.out.println("伺服器的地址: "+ ctx.channel().remoteAddress());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}


netty中的引數元件

1、Bootstrap、ServerBootstrap

Bootstrap 意思是引導,一個 Netty 應用通常由一個 Bootstrap 開始,主要作用是配置整個 Netty 程式,串聯各個元件,Netty 中 Bootstrap 類是客戶端程式的啟動引導類,ServerBootstrap 是服務端啟動引導類。

2、Future、ChannelFuture

Netty 中所有的 IO 操作都是非同步的,不能立刻得知訊息是否被正確處理。但是可以立即返回一個ChannelFuture,它可以註冊一個監聽,當操作執行成功或失敗時監聽會自動觸發註冊的監聽事件

  • Channel channel(),返回當前正在進行 IO 操作的通道
  • ChannelFuture sync(),等待非同步操作執行完畢,同步執行註冊的監聽事件

Future-Listener 機制
當Future物件剛剛建立時,處於非完成狀態,呼叫者可以通過返回的 ChannelFuture來獲取操作執行的狀態,註冊監聽函式來執行完成後的操作。

  • 通過 isDone 方法來判斷當前操作是否完成;
  • 通過 isSuccess 方法來判斷已完成的當前操作是否成功;
  • 通過 getCause 方法來獲取已完成的當前操作失敗的原因;
  • 通過 isCancelled 方法來判斷已完成的當前操作是否被取消;
  • 通過 addListener 方法來註冊監聽器,當操作已完成(isDone方法返回完成),將會通知指定的監聽器;

3、Channel

  • Netty 網路通訊的元件,能夠用於執行網路 I/O 操作。
  • Netty 網路通訊的元件,能夠用於執行網路 I/O 操作。
  • Channel 提供非同步的網路 I/O 操作(如建立連線,讀寫,繫結埠),非同步呼叫意味著任何 I/O 呼叫都將立即返回一個ChannelFuture,並且不保證在呼叫結束時所請求的 I/O 操作已完(後期通過ChannelFuture的方法檢視非同步執行結果)
  • 不同協議、不同的阻塞型別的連線都有不同的 Channel 型別與之對應
    NioSocketChannel,非同步的客戶端 TCP Socket 連線。
    NioServerSocketChannel,非同步的伺服器端 TCP Socket 連線。
    NioDatagramChannel,非同步的 UDP 連線。

4、Selector

  • Netty 基於 Selector 物件實現 I/O 多路複用,通過 Selector 一個執行緒可以監聽多個連線的 Channel 事件。
  • 當向一個 Selector 中註冊 Channel 後,Selector 內部的機制就可以自動不斷地查詢(Select)這些註冊的 Channel 是否有已就緒的 I/O 事件(例如可讀,可寫,網路連線完成等),這樣程式就可以很簡單地使用一個執行緒高效地管理多個 Channel

5、ChannelHandler 及其實現類

  • ChannelHandler 是一個介面,處理 I/O 事件或攔截 I/O 操作,並將其轉發到其 ChannelPipeline(業務處理鏈)中的下一個處理程式。
  • ChannelHandler 本身並沒有提供很多方法,因為這個介面有許多的方法需要實現,方便使用期間,可以繼承它的子類
  • 當自定義一個handler類處理器時,需要繼承ChannelhandlerAdapter
    image

6、Pipeline 和 ChannelPipeline

  • Pipeline中包含了多個Channel(通道)
  • 一個Channel包含了一個ChannelPipeline,而ChannePipeline中又維護了一個由ChannelHandlerContext組成的雙向連結串列,並且每個channeHandlerContext中又關聯著一個channelHandler
  • ChannelPipeline是儲存ChannelHandler的List,用於處理或攔截Channel 的入站事件和出站事件操作
  • 入站事件和出站事件在一個雙向連結串列中,入站事件會從連結串列head往後傳遞到最後一個入站的 handler,出站事件會從連結串列tail往前傳遞到最前t個出站的handler, c兩種型別的handler互不干擾
  • ChannelPipeline實現了一種高階形式的攔截過濾器模式,使使用者可以完全控制事件的處理方式
    image

ChannelPipeline addFirst(ChannelHandler... handlers),把一個業務處理類(handler)新增到鏈中的第一個位置
ChannelPipeline addLast(ChannelHandler... handlers),把一個業務處理類(handler)新增到鏈中的最後一個位置

7、ChannelHandlerContext

  • 儲存 Channel 相關的所有上下文資訊,同時關聯一個 ChannelHandler 物件
  • 即ChannelHandlerContext中包含一個具體的事件處理器ChannelHandler,同時ChannelHandlerContext中也繫結了對應的pipeline 和 Channel 的資訊,方便對ChannelHandler進行呼叫。

ChannelHandlerContext的常用方法:
1、ChannelFuture close(),關閉通道
2、ChannelOutboundInvoker flush(),重新整理
3、ChannelFuture writeAndFlush(Object msg),將資料寫到ChannelPipeline 中當前 ChannelHandler 的下一個ChannelHandler 開始處理(出站)

8、ChannelOption

Netty在建立Channel例項後,一般都需要設定 ChannelOption 引數
image

9、EventLoopGroup 和其實現類 NioEventLoopGroup

  • EventLoopGroup 是一組 EventLoop 的抽象,Netty 為了更好的利用多核 CPU 資源,一般會有多個 EventLoop 同時工作,每個 EventLoop 維護著一個 Selector 例項。
  • EventLoopGroup 提供 next 介面,可以從組裡面按照一定規則獲取其中一個 EventLoop 來處理任務。在 Netty 伺服器端程式設計中,我們一般都需要提供兩個 EventLoopGroup,例如:BossEventLoopGroup 和 WorkerEventLoopGroup。
  • BossEventLoop負責接收客戶端的連線並將SocketChannel註冊到 WorkerEventLoopGroup的其中一個workerEventLoop的selector上並進行後續的IO事件處理

10、Unpooled類

  • Netty提供一個專門用來操作緩衝區(即 Netty 的資料容器)的工具類

ByteBuf buffer = Unpooled.buffer(10);
ByteBuf byteBuf = Unpooled.copiedBuffer("hello,world!", Charset.forName("utf-8"));

netty的案例-群聊系統

GroupChatServer

GroupChatServer程式碼
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

public class GroupChatServer {

    private int port; //監聽埠


    public GroupChatServer(int port) {
        this.port = port;
    }

    //編寫run方法,處理客戶端的請求
    public void run() throws  Exception{

        //建立兩個執行緒組
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup(); //8個NioEventLoop

        try {
            ServerBootstrap b = new ServerBootstrap();

            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    .childHandler(new ChannelInitializer<SocketChannel>() {

                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {

                            //獲取到pipeline
                            ChannelPipeline pipeline = ch.pipeline();
                            //向pipeline加入解碼器
                            pipeline.addLast("decoder", new StringDecoder());
                            //向pipeline加入編碼器
                            pipeline.addLast("encoder", new StringEncoder());
                            //加入自己的業務處理handler
                            pipeline.addLast(new GroupChatServerHandler());

                        }
                    });

            System.out.println("netty 伺服器啟動");
            ChannelFuture channelFuture = b.bind(port).sync();

            //監聽關閉
            channelFuture.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }

    public static void main(String[] args) throws Exception {

        new GroupChatServer(7000).run();
    }
}

GroupChatServerHandler

GroupChatServerHandler程式碼
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class GroupChatServerHandler extends SimpleChannelInboundHandler<String> {

    //這樣寫還要自己遍歷Channel
    //public static List<Channel> channels = new ArrayList<Channel>();

    //使用一個hashmap 管理私聊(私聊本案例並未實現,只是提供個思路)
    //public static Map<String, Channel> channels = new HashMap<String,Channel>();

    //定義一個channle 組,管理所有的channel
    //GlobalEventExecutor.INSTANCE) 是全域性的事件執行器,是一個單例
    private static ChannelGroup  channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");


    //handlerAdded 表示連線建立,一旦連線,第一個被執行
    //將當前channel 加入到  channelGroup
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        //將該客戶加入聊天的資訊推送給其它線上的客戶端

        //該方法會將 channelGroup 中所有的channel 遍歷,併傳送訊息,我們不需要自己遍歷

        channelGroup.writeAndFlush("[客戶端]" + channel.remoteAddress() + " 加入聊天" + sdf.format(new java.util.Date()) + " \n");
        channelGroup.add(channel);

		//私聊如何實現
//         channels.put("userid100",channel);
		

    }

    //斷開連線, 將xx客戶離開資訊推送給當前線上的客戶
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {

        Channel channel = ctx.channel();
        channelGroup.writeAndFlush("[客戶端]" + channel.remoteAddress() + " 離開了\n");
        System.out.println("channelGroup size" + channelGroup.size());

    }

    //表示channel 處於活動狀態, 提示 xx上線
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //這個是給服務端看的,客戶端上面已經提示xxx加入群聊了
        System.out.println(ctx.channel().remoteAddress() + " 上線了~");
    }

    //表示channel 處於不活動狀態, 提示 xx離線了
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {

        System.out.println(ctx.channel().remoteAddress() + " 離線了~");
    }

    //讀取資料,轉發給線上的每一個客戶端
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {

        //獲取到當前channel
        Channel channel = ctx.channel();
        //這時我們遍歷channelGroup, 根據不同的情況,回送不同的訊息

        channelGroup.forEach(ch -> {
            if(channel != ch) { //不是當前的channel,轉發訊息
                ch.writeAndFlush("[客戶]" + channel.remoteAddress() + " 傳送了訊息" + msg + "\n");
            }else {//回顯自己傳送的訊息給自己
                ch.writeAndFlush("[自己]傳送了訊息" + msg + "\n");
            }
        });
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        //關閉通道
        ctx.close();
    }
}

GroupChatClient

GroupChatClient程式碼
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import java.util.Scanner;


public class GroupChatClient {

    //屬性
    private final String host;
    private final int port;

    public GroupChatClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    public void run() throws Exception{
        EventLoopGroup group = new NioEventLoopGroup();

        try {


        Bootstrap bootstrap = new Bootstrap()
                .group(group)
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<SocketChannel>() {

                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {

                        //得到pipeline
                        ChannelPipeline pipeline = ch.pipeline();
                        //加入相關handler
                        pipeline.addLast("decoder", new StringDecoder());
                        pipeline.addLast("encoder", new StringEncoder());
                        //加入自定義的handler
                        pipeline.addLast(new GroupChatClientHandler());
                    }
                });

        ChannelFuture channelFuture = bootstrap.connect(host, port).sync();
        //得到channel
            Channel channel = channelFuture.channel();
            System.out.println("-------" + channel.localAddress()+ "--------");
            //客戶端需要輸入資訊,建立一個掃描器
            Scanner scanner = new Scanner(System.in);
            while (scanner.hasNextLine()) {
                String msg = scanner.nextLine();
                //通過channel 傳送到伺服器端
                channel.writeAndFlush(msg + "\r\n");
            }
        }finally {
            group.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception {
        new GroupChatClient("127.0.0.1", 7000).run();
    }
}

GroupChatClientHandler

GroupChatClientHandler程式碼
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

public class GroupChatClientHandler extends SimpleChannelInboundHandler<String> {

    //從伺服器拿到的資料
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        System.out.println(msg.trim());
    }
}

netty的心跳檢測機制

IdleStateHandler是netty 提供的處理空閒狀態的處理器(放在ChannelPipeline維護的ChannelHandler的雙向連結串列上):

  • long readerIdleTime : 表示多長時間沒有讀, 就會傳送一個心跳檢測包檢測是否連線
  • long writerIdleTime : 表示多長時間沒有寫, 就會傳送一個心跳檢測包檢測是否連線
  • long allIdleTime : 表示多長時間沒有讀寫, 就會傳送一個心跳檢測包檢測是否連線

netty的編解碼器

編解碼器放在ChannelPipeline維護的ChannelHandler的雙向連結串列上
服務端發資料給客戶端:服務端—>出站(編碼)—>Socket通道—>入站(解碼)—>客戶端
客戶端發資料給服務端:客戶端—>出站(編碼)—>Socket通道—>入站(解碼)—>服務端
image

編解碼器(自定義編解碼器時需要繼承下面的其中一個類):

  • ByteToMessageDecoder
  • LineBasedFrameDecoder:這個類在 Netty 內部也有使用,它使用行尾控制字元(\n或者\r\n)作為分隔符來解析資料。
  • DelimiterBasedFrameDecoder:使用自定義的特殊字元作為訊息的分隔符。
  • HttpObjectDecoder:一個 HTTP 資料的解碼器
  • LengthFieldBasedFrameDecoder:通過指定長度來標識整包訊息,這樣就可以自動的處理黏包和半包訊息。

TCP 粘包和拆包及解決方案

TCP粘包和拆包
使用優化方法(Nagle 演算法),將多次間隔較小且資料量小的資料,合併成一個大的資料塊,然後進行封包。這樣做雖然提高了效率,但是接收端就難於分辨出完整的資料包了,出現了粘包和拆包的問題,因為面向流的通訊是無訊息保護邊界的
image

  • 服務端分兩次讀取到了兩個獨立的資料包,分別是 D1 和 D2,沒有粘包和拆包
  • 服務端一次接受到了兩個資料包,D1 和 D2 粘合在一起,稱之為 TCP 粘包
  • 服務端分兩次讀取到了資料包,第一次讀取到了完整的 D1 包和 D2 包的部分內容,第二次讀取到了 D2 包的剩餘內容,這稱之為 TCP 拆包
  • 服務端分兩次讀取到了資料包,第一次讀取到了 D1 包的部分內容 D1_1,第二次讀取到了 D1 包的剩餘部分內容 D1_2 和完整的 D2 包。

解決方案
使用自定義協議+編解碼器來解決,關鍵就是要解決伺服器端每次讀取資料長度的問題,這個問題解決,就不會出現伺服器多讀或少讀資料的問題,從而避免的 TCP 粘包、拆包。

RPC(基於netty)

1、RPC(Remote Procedure Call)遠端過程呼叫,是一個計算機通訊協議。該協議允許執行於一臺計算機的程式呼叫另一臺計算機的子程式,而程式設計師無需額外地為這個互動作用程式設計
2、兩個或多個應用程式都分佈在不同的伺服器上,它們之間的呼叫都像是本地方法呼叫一樣

RPC的呼叫流程圖
image

  • 服務消費方(client)以本地呼叫方式呼叫服務
  • client stub 接收到呼叫後負責將方法、引數等封裝成能夠進行網路傳輸的訊息體
  • client stub 將訊息進行編碼併傳送到服務端
  • server stub 收到訊息後進行解碼
  • server stub 根據解碼結果呼叫本地的服務
  • 本地服務執行並將結果返回給 server stub
  • server stub 將返回匯入結果進行編碼併傳送至消費方
  • client stub 接收到訊息並進行解碼
  • 服務消費方(client)得到結果

RPC 的目標就是將 2 - 8 這些步驟都封裝起來,使用者無需關心這些細節,可以像呼叫本地方法一樣即可完成遠端服務呼叫

相關文章