Netty-核心模塊組件-4

Netty 核心模塊組件

一、Bootstrap、ServerBootstrap

1、Bootstrap 意思是引導,一個 Netty 應用通常由一個 Bootstrap 開始,主要作用是配置整個 Netty 程序,串聯各個組件Netty中

  • Bootstrap 類是客戶端程序的啓動引導類
  • ServerBootstrap 是服務端啓動引導類

2、常見的方法

public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup),該方法用於服務器端,用來設置兩個 EventLoop
public B group(EventLoopGroup group) ,該方法用於客戶端,用來設置一個 EventLoop
public B channel(Class<? extends C> channelClass),該方法用來設置一個服務器端的通道實現
public < T > B option(ChannelOption option, T value),用來給 ServerChannel 添加配置
public < T > ServerBootstrap childOption(ChannelOption childOption, T value),用來給接收到的通道添加配置
public ServerBootstrap childHandler(ChannelHandler childHandler),該方法用來設置業務處理類(自定義的handler)針對WorkerGroup(幹活的類)
public ServerBootstrap handler(ChannelHandler handler)針對BoosGroup(專門負責接收客戶端請求的類)
public ChannelFuture bind(int inetPort) ,該方法用於服務器端,用來設置佔用的端口號
public ChannelFuture connect(String inetHost, int inetPort) ,該方法用於客戶端,用來連接服務器端

二、Future、ChannelFuture

Netty 中所有的 IO 操作都是異步的,不能立刻得知消息是否被正確處理。

但是可以過一會等它執行完成或者直接註冊一個監聽,具體的實現就是通過 Future 和 ChannelFutures,他們可以註冊一個監聽,當操作執行成功或失敗時監聽會自動觸發註冊的監聽事件。

常見的方法:

  • Channel channel(),返回當前正在進行 IO 操作的通道
  • ChannelFuture sync(),等待異步操作執行完畢

三、Channel

1、Netty 網絡通信的組件,能夠用於執行網絡 I/O 操作

2、通過 Channel 可獲得當前網絡連接的通道的狀態

3、通過 Channel 可獲得 網絡連接的配置參數 (例如接收緩衝區大小)

4、Channel 提供異步的網絡 I/O 操作(如建立連接,讀寫,綁定端口),異步調用意味着任何 I/O 調用都將立即返回,並且不保證在調用結束時所請求的 I/O 操作已完成

5、調用立即返回一個 ChannelFuture 實例,通過註冊監聽器到 ChannelFuture 上,可以 I/O 操作成功、失敗或取消時回調通知調用方

6、支持關聯 I/O 操作與對應的處理程序

7、不同協議、不同的阻塞類型的連接都有不同的 Channel 類型與之對應,常用的 Channel 類型

  1. NioSocketChannel,異步的客戶端 TCP Socket 連接。
  2. NioServerSocketChannel,異步的服務器端 TCP Socket 連接。
  3. NioDatagramChannel,異步的 UDP 連接。
  4. NioSctpChannel,異步的客戶端 Sctp 連接。
  5. NioSctpServerChannel,異步的 Sctp 服務器端連接,這些通道涵蓋了 UDP 和 TCP 網絡 IO 以及文件 IO。

四、Selector

1、Netty 基於 Selector 對象實現 I/O 多路複用,通過 Selector 一個線程可以監聽多個連接的 Channel 事件

2、當向一個 Selector 中註冊 Channel 後,Selector 內部的機制就可以自動不斷地查詢(Select) 這些註冊的Channel 是否有已就緒的 I/O 事件(例如可讀,可寫,網絡連接完成等),這樣程序就可以很簡單地使用一個線程高效地管理多個 Channel

五、ChannelHandler 及其實現類

1、ChannelHandler 是一個接口,處理 I/O 事件或攔截 I/O 操作,並將其轉發到其 ChannelPipeline(業務處理鏈)中的下一個處理程序。

2、ChannelHandler 本身並沒有提供很多方法,因爲這個接口有許多的方法需要實現,方便使用期間,可以繼承它的子類

3、ChannelHandler 及其實現類一覽圖

4、我們經常需要自定義一個 Handler 類去繼承 ChannelInboundHandlerAdapter

5、然後通過重寫相應方法實現業務邏輯,我們接下來看看一般都需要重寫哪些方法

六、Pipeline 和 ChannelPipeline

1、ChannelPipeline 是一個重點

1、ChannelPipeline 是一個 Handler 的集合

2、它負責處理和攔截 inbound 或者 outbound 的事件和操作,相當於一個貫穿 Netty 的鏈。(也可以這樣理解:ChannelPipeline 是 保存 ChannelHandler 的 List,用於處理或攔截Channel 的入站事件和出站操作)

3、ChannelPipeline 實現了一種高級形式的攔截過濾器模式,使用戶可以完全控制事件的處理方式,以及 Channel中各個的 ChannelHandler 如何相互交互

4、在 Netty 中每個 Channel 都有且僅有一個 ChannelPipeline 與之對應,它們的組成關係如下

  • channel能拿到他對應的channelPipeline
  • channelPipeline也可以獲取到對應的channel
  • channelPipeline中包含一個個的ChannelHandlerContext的雙向鏈表
  • 每個ChannelHandlerContext(保存 Channel 相關的所有上下文信息)裏面包含對應具體的channelHandler

2、常用方法

1、ChannelPipeline addFirst(ChannelHandler… handlers)  把一個業務處理類(handler)添加到鏈中的第一個位置

2、ChannelPipeline addLast(ChannelHandler… handlers)  把一個業務處理類(handler)添加到鏈中的最後一個位置

七、ChannelHandlerContext

1、保存 Channel 相關的所有上下文信息,同時關聯一個 ChannelHandler 對象

2、即 ChannelHandlerContext 中 包 含 一 個 具 體 的 事 件 處 理 器 ChannelHandler , 同 時ChannelHandlerContext 中也綁定了對應的 pipeline 和 Channel 的信息,方便對 ChannelHandler 進行調用.

3、常用方法

八、ChannelOption

1、Netty 在創建 Channel 實例後,一般都需要設置 ChannelOption 參數。

2、ChannelOption 參數如下:

九、EventLoopGroup 和其實現類 NioEventLoopGroup

1、EventLoopGroup 是一組 EventLoop(就是對應線程) 的抽象,Netty 爲了更好的利用多核 CPU 資源,一般會有多個 EventLoop同時工作,每個 EventLoop 維護着一個 Selector 實例。

2、EventLoopGroup 提供 next 接口,可以從組裏面按照一定規則獲取其中一個 EventLoop 來處理任務。

3、在 Netty服務器端編程中 ,我們一般 都 需 要 提 供 兩 個 EventLoopGroup , 例如 :

  1. BossEventLoopGroup
  2. WorkerEventLoopGroup

4、通常一個服務端口即一個 ServerSocketChannel 對應一個 Selector 和一個 EventLoop 線程。

5、服務端中,BossEventLoop 負責接收客戶端的連接並將 SocketChannel 交給 WorkerEventLoopGroup 來進行 IO 處理,如下圖所示

6、常用方法:

  • public NioEventLoopGroup(),構造方法

  • public Future<?> shutdownGracefully(),斷開連接,關閉線程

十、 Unpooled 類

1、Netty 提供一個專門用來操作緩衝區(即 Netty 的數據容器)的工具類

2、內部維護了對應的readerIndexwriterIndex

3、相比NIO的ByteBuffer,Netty 提供的ByteBuf不用考慮flip反轉去操作讀寫

4、常用方法如下所示

5、舉例說明 Unpooled 獲取 Netty 的數據容器 ByteBuf 的基本使用

6、案例一

package com.sun.netty.components;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;

/**
 * @Author: sunguoqiang
 * @Description: TODO
 * @DateTime: 2023/1/4 17:14
 **/
public class NettyByteBuf01 {
    public static void main(String[] args) {
        /**
         *  創建一個 ByteBuf
         *  說明:
         *  1. 創建 對象,該對象包含一個數組 arr , 是一個 byte[10]
         *  2. 在 netty 的 buffer 中,不需要使用 flip 進行反轉 , 底層維護了 readerindex 和 writerIndex
         *  3. 通過 readerindex 和 writerIndex 和 capacity, 將 buffer 分成三個區域
         *      1、0---readerindex 已經讀取的區域、
         *      2、readerindex---writerIndex,可讀的區域、
         *      3、writerIndex---capacity, 可寫的區域
         */
        ByteBuf buffer = Unpooled.buffer(10);
        for (int i = 0; i < 10; i++) {
            buffer.writeByte(i);
        }
        System.out.println("capacity=" + buffer.capacity());//10
        // 輸出
        for (int i = 0; i < buffer.capacity(); i++) { // 此方式readerindex不會改變
            System.out.println(buffer.getByte(i));
        }
        for (int i = 0; i < buffer.capacity(); i++) { // 此方式readerindex會改變
            System.out.println(buffer.readByte());
        }
        System.out.println("執行完畢");
    }
}

7、案例二

package com.sun.netty.components;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;

import java.nio.charset.Charset;

/**
 * @Author: sunguoqiang
 * @Description: TODO
 * @DateTime: 2023/1/4 17:14
 **/
public class NettyByteBuf02 {
    public static void main(String[] args) {
        // 創建 ByteBuf
        ByteBuf byteBuf = Unpooled.copiedBuffer("hello,world!", Charset.forName("utf-8"));
        // 使用相關的方法
        if (byteBuf.hasArray()) { // true
            byte[] content = byteBuf.array();
            // 將 content 轉成字符串
            System.out.println(new String(content, Charset.forName("utf-8")));
            // 輸出ByteBuf對象
            System.out.println("byteBuf=" + byteBuf);
            // 輸出byteBuf偏移量
            System.out.println(byteBuf.arrayOffset()); // 0
            // byteBuf讀取索引位置
            System.out.println(byteBuf.readerIndex()); // 0
            // byteBuf寫入索引位置
            System.out.println(byteBuf.writerIndex()); // 12
            // byteBuf容量
            System.out.println(byteBuf.capacity());   // 36
            // byteBuf可讀取的第一個字節
            System.out.println(byteBuf.readByte()); // 104
            // byteBuf指定索引位置的字節
            System.out.println(byteBuf.getByte(0)); // 104
            // byteBuf可讀取字節數
            int len = byteBuf.readableBytes(); // 12
            System.out.println("len=" + len);
            //使用 for 取出各個字節
            for (int i = 0; i < len; i++) {
                System.out.println((char) byteBuf.getByte(i));
            }
            // 按照某個範圍讀取
            System.out.println(byteBuf.getCharSequence(0, 4, Charset.forName("utf-8")));
            System.out.println(byteBuf.getCharSequence(4, 6, Charset.forName("utf-8")));
        }
    }
}

十一、Netty 應用實例-羣聊系統

1、實例要求

1、編寫一個 Netty 羣聊系統,實現服務器端和客戶端之間的數據簡單通訊(非阻塞)
2、實現多人羣聊
3、服務器端:可以監測用戶上線,離線,並實現消息轉發功能
4、客戶端:通過 channel 可以無阻塞發送消息給其它所有用戶,同時可以接受其它用戶發送的消息(有服務器轉發得到)
5、目的:進一步理解 Netty 非阻塞網絡編程機制

2、服務端

GroupChatServer 服務端

package com.sun.netty.groupChat;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
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;

/**
 * @Author: sunguoqiang
 * @Description: 羣聊系統服務端
 * @DateTime: 2023/1/9 14:25
 **/
public class GroupChatServer {
    // 監聽端口
    private final Integer port;

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

    public void run() throws InterruptedException {
        // 創建兩個線程組
        NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
        NioEventLoopGroup workerGroup = new NioEventLoopGroup(8);
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.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 socketChannel) throws Exception {
                            // 向pipeline加入處理器
                            ChannelPipeline pipeline = socketChannel.pipeline();
                            pipeline.addLast("decoder", new StringDecoder());
                            // 如果不加這個編碼解碼器的  無法直接傳輸字符串
                            pipeline.addLast("encoder", new StringEncoder());
                            pipeline.addLast("myHandler", new GroupChatServerHandler());
                        }
                    });
            System.out.println("服務器啓動完成,綁定端口:" + port);
            ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
            // 監聽關閉時間
            channelFuture.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        GroupChatServer groupChatServer = new GroupChatServer(8989);
        groupChatServer.run();
    }

}
View Code

GroupChatServerHandler 服務端處理器

package com.sun.netty.groupChat;

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.Date;

/**
 * @Author: sunguoqiang
 * @Description: 自定義服務器處理器
 * @DateTime: 2023/1/9 14:26
 **/
public class GroupChatServerHandler extends SimpleChannelInboundHandler<String> {

    //定義管理每個客戶端的channel組
    //GlobalEventExecutor.INSTANCE 全局的事件執行器,單例的
    private static final ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

    // 當連接建立會第一個執行該方法,【客戶端連接事件】
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        // 當客戶端連接,第一時間將每個客戶端的channel加入到channelGroup統一管理
        Channel channel = ctx.channel();
        // 給當前channelGroup管理的所有channel的客戶端都發送消息
        channelGroup.writeAndFlush(sdf.format(new Date())+"[客戶端]"+channel.remoteAddress()+"進入聊天室...");
        channelGroup.add(channel);
    }

    // channel活躍時觸發,監測上線
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        System.out.println(sdf.format(new Date())+"[客戶端]"+channel.remoteAddress()+"上線...");
//        channelGroup.writeAndFlush(sdf.format(new Date())+"[客戶端]"+channel.remoteAddress()+"上線...");
    }

    // 當某個channel處於活動狀態,就會觸發,用於發送某某上線【客戶端上線活動狀態事件】
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        channelGroup.writeAndFlush(sdf.format(new Date())+"[客戶端]"+channel.remoteAddress()+"離線...");
    }

    // 當某個channel離開狀態,就會觸發,用於發送某某下線【客戶端離開非活動狀態事件】
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        channelGroup.writeAndFlush(sdf.format(new Date())+"[客戶端]"+channel.remoteAddress()+"離開聊天室...");
    }

    // 客戶端發送消息會觸發【讀取客戶端數據事件】
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, String msg) throws Exception {
        // 獲取當前發送消息的用戶
        Channel channel = channelHandlerContext.channel();
        // 排除發送者自己,不給他轉發消息
        for (Channel channel1 : channelGroup) {
            if(channel.equals(channel1)){
                channel1.writeAndFlush(sdf.format(new Date()) + "[自己發送]:"+msg);
            }else{
                channel1.writeAndFlush(sdf.format(new Date()) + "[客戶端-"+channel.remoteAddress()+"]:"+msg);
            }
        }
    }

    // 當發生異常會觸發【異常觸發事件】
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        Channel channel = ctx.channel();
        channel.close();
    }
}
View Code

3、客戶端

GroupChatClient 客戶端

package com.sun.netty.groupChat;

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;

/**
 * @Author: sunguoqiang
 * @Description: 羣聊系統客戶端
 * @DateTime: 2023/1/9 14:59
 **/
public class GroupChatClient {

    private final String ip;
    private final Integer port;

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

    public void run() throws InterruptedException {
        NioEventLoopGroup clientGroup = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(clientGroup)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline pipeline = socketChannel.pipeline();
                            pipeline.addLast("decoder", new StringDecoder());
                            pipeline.addLast("encoder", new StringEncoder());
                            pipeline.addLast("myHandler", new GroupChatClientHandler());
                        }
                    });
            ChannelFuture channelFuture = bootstrap.connect(ip, port);
            // 監聽連接是否成功
            channelFuture.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture channelFuture) throws Exception {
                    if (channelFuture.isSuccess()) {
                        System.out.println("連接成功...");
                    } else {
                        System.out.println("連接失敗...");
                    }
                }
            });
            Scanner scanner = new Scanner(System.in);
            while (scanner.hasNextLine()) {
                String nextLine = scanner.nextLine();
                channelFuture.channel().writeAndFlush(nextLine);
            }
            channelFuture.channel().close().sync();
        } finally {

        }
    }

    public static void main(String[] args) throws InterruptedException {
        GroupChatClient groupChatClient = new GroupChatClient("127.0.0.1", 8989);
        groupChatClient.run();
    }
}
View Code

GroupChatClientHandler 客戶端處理器

package com.sun.netty.groupChat;

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

/**
 * @Author: sunguoqiang
 * @Description: 聊天室客戶端處理器
 * @DateTime: 2023/1/9 14:59
 **/
public class GroupChatClientHandler extends SimpleChannelInboundHandler<String> {
    // 讀取服務端發來的消息【讀寫消息事件】
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, String msg) throws Exception {
        Channel channel = channelHandlerContext.channel();
        System.out.println(msg);
    }
}
View Code

十二、Netty 心跳檢測機制案例

1、實例要求

  1. 編寫一個 Netty 心跳檢測機制案例, 當服務器超過 3 秒沒有讀時,就提示讀空閒
  2. 當服務器超過 5 秒沒有寫操作時,就提示寫空閒
  3. 實現當服務器超過 7 秒沒有讀或者寫操作時,就提示讀寫空閒

2、服務端

package com.sun.netty.heartbeat;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
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;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.IdleStateHandler;

import java.util.concurrent.TimeUnit;

/**
 * @Author: sunguoqiang
 * @Description: TODO
 * @DateTime: 2023/1/10 11:29
 **/
public class MyHeartBeatServer {

    public static void main(String[] args) throws InterruptedException {
        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline pipeline = socketChannel.pipeline();
                            /**
                             * 加入netty提供的IdleStateHandler
                             * IdleStateHandler;空閒狀態處理器
                             * 構造參數↓
                             *  long readerIdleTime : 表示多長時間沒讀操作了,服務端沒有接受到客戶端的讀操作;(觸發後,服務端就會發送心跳檢測包給客戶端,檢測是否還是連接狀態)
                             *     long writerIdleTime : 表示有多長時間沒有寫操作了,服務端沒有接收到客戶端的寫操作
                             *     long allIdleTime : 表示多長時間既沒有讀操作也沒有寫操作。
                             *     TimeUnit unit : 時間單位
                             */
                            pipeline.addLast(new IdleStateHandler(3, 5, 8, TimeUnit.SECONDS));
                            /**
                             * 加入一個對空閒狀態檢測進一步處理的handler
                             * 時間到了就會觸發對應的事件(IdleStateEvent),我們再寫一個自定義的handler來監聽這個時間就可以做出對應的操作
                             * 當事件觸發之後,就會傳遞給對應的channel管道的下一個handler去處理
                             * 通過調用(觸發)下一個handler的 userEventTiggerd()方法,該方法去處理
                             */
                            pipeline.addLast(new MyHeartBeatServerHandler());
                        }
                    });
            System.out.println("server start ok ...");
            ChannelFuture channelFuture = serverBootstrap.bind(7878).sync();
            channelFuture.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}
服務端
package com.sun.netty.heartbeat;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandler;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleStateEvent;

/**
 * @Author: sunguoqiang
 * @Description: 心跳檢測服務端處理器,因爲不需要接受數據,所以就直接繼承 ChannelInboundHandlerAdapter
 * @DateTime: 2023/1/10 11:35
 **/
public class MyHeartBeatServerHandler extends ChannelInboundHandlerAdapter {

    /**
     * 當觸發心跳事件後,會觸發該方法
     * @param ctx 上下文
     * @param evt 事件
     * @throws Exception 異常
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            switch (event.state()) {
                case ALL_IDLE:
                    System.out.println("【讀寫空閒】事件");
                    break;
                case READER_IDLE:
                    System.out.println("【讀空閒】事件");
                    break;
                case WRITER_IDLE:
                    System.out.println("【寫空閒】事件");
            }
            System.out.println(ctx.channel().remoteAddress() + "發生事件:" + event.state());
//            ctx.channel().close();
        }
    }
}
心跳檢測服務端處理器

3、客戶端

package com.sun.netty.heartbeat;

import com.sun.netty.groupChat.GroupChatClientHandler;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
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;

/**
 * @Author: sunguoqiang
 * @Description: 羣聊系統客戶端
 * @DateTime: 2023/1/9 14:59
 **/
public class MyHeartBeatClient {

    private final String ip;
    private final Integer port;

    public MyHeartBeatClient(String ip, int port) {
        this.ip = ip;
        this.port = port;
    }

    public void run() throws InterruptedException {
        NioEventLoopGroup clientGroup = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(clientGroup)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline pipeline = socketChannel.pipeline();
                            pipeline.addLast("decoder", new StringDecoder());
                            pipeline.addLast("encoder", new StringEncoder());
                            pipeline.addLast("myHandler", new GroupChatClientHandler());
                        }
                    });
            ChannelFuture channelFuture = bootstrap.connect(ip, port);
            // 監聽連接是否成功
            channelFuture.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture channelFuture) throws Exception {
                    if (channelFuture.isSuccess()) {
                        System.out.println("連接成功...");
                    } else {
                        System.out.println("連接失敗...");
                    }
                }
            });
            Scanner scanner = new Scanner(System.in);
            while (scanner.hasNextLine()) {
                String nextLine = scanner.nextLine();
                channelFuture.channel().writeAndFlush(nextLine);
            }
            channelFuture.channel().close().sync();
        } finally {

        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyHeartBeatClient myHeartBeatClient = new MyHeartBeatClient("127.0.0.1", 7878);
        myHeartBeatClient.run();
    }
}
客戶端

十三、Netty 通過 WebSocket 編程實現服務器和客戶端長連接

1、實例要求

1、Http 協議是無狀態的, 瀏覽器和服務器間的請求響應一次,下一次會重新創建連接.

2、要求:實現基於 webSocket 的長連接的全雙工的交互

3、改變 Http 協議多次請求的約束,實現長連接了, 服務器可以發送消息給瀏覽器

4、客戶端瀏覽器和服務器端會相互感知,比如服務器關閉了,瀏覽器會感知,同樣瀏覽器關閉了,服務器會感知

5、運行界面

 

2、代碼

package com.sun.netty.websocket;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;

import java.time.LocalDateTime;

/**
 * @Title: WebSocketServerFrameHandler
 * @Author sunguoqiang
 * @Package com.sun.netty.websocket
 * @Date 2023/1/14 11:22
 * @description: * 自定義處理業務邏輯的處理器
 * * 對於websocket是通過【幀frame】的形式傳遞的(數據鏈路層的傳輸單元是幀)
 * * TextWebSocketFrame:表示一個文本幀,就是傳輸過程中的數據
 */
public class WebSocketServerFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        System.out.println("服務端收到消息:" + msg.text());
        //回覆瀏覽器
        ctx.channel().writeAndFlush(new TextWebSocketFrame("服務器時間:" + LocalDateTime.now() + ":" + msg.text()));
    }

    //客戶端斷開連接時會觸發該事件
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        //id.asLongText表示獲取這channel唯一的值
        System.out.println("handlerRemoved被調用:" + ctx.channel().id().asLongText());
    }

    //客戶端連接時會觸發該事件
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        //id.asLongText表示獲取這channel唯一的值
        System.out.println("handlerAdded被調用:" + ctx.channel().id().asLongText());
        //id.asShortText表示獲取這channel的值,這個不是唯一的。有可能重複
        System.out.println("handlerAdded被調用:" + ctx.channel().id().asShortText());
    }

    //當客戶端發生異常時會觸發該事件
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println("異常發生:" + cause.getMessage());
        ctx.channel().close();
    }
}
WebSocketServerFrameHandler自定義處理業務邏輯的處理器
package com.sun.netty.websocket;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.stream.ChunkedWriteHandler;

/**
 * @Author: sunguoqiang
 * @Description: Websocket雙工TCP長連接
 * @DateTime: 2023/1/12 9:59
 **/
public class WebsocketServer {
    public static void main(String[] args) throws InterruptedException {
        NioEventLoopGroup boosGroup = new NioEventLoopGroup();
        NioEventLoopGroup workGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(boosGroup, workGroup)
                    .channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler(LogLevel.INFO))//netty自帶的日誌處理器
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            //基於http協議,使用http的編碼和解碼器
                            pipeline.addLast(new HttpServerCodec());
                            //是以 塊方式 寫,添加ChunkedWriteHandler處理器
                            pipeline.addLast(new ChunkedWriteHandler());
                            /**
                             * 說明:
                             *    1、因爲http的數據在傳輸過程中是【分段】的,HttpObjectAggregator可以將多個分段聚合起來
                             *    2、這就是當瀏覽器發送大量數據時,就會發送多次http請求
                             */
                            pipeline.addLast(new HttpObjectAggregator(8192));
                            /**
                             * 說明:
                             *    1、對於websocket是通過【幀frame】的形式傳遞的(數據鏈路層的傳輸單元是幀)
                             *    2、可以看到WebSocketFrame 下面有6個子類
                             *    3、瀏覽器發送文件、請求時,ws://localhost:7000/websocket 表示請求的uri
                             *    4、WebSocketServerProtocolHandler核心功能:將http協議升級爲ws協議(websocket長連接協議)
                             *    5、爲什麼http能夠升級爲ws協議呢?是通過狀態碼101
                             */
                            pipeline.addLast(new WebSocketServerProtocolHandler("/websocket"));
                            //自定義處理業務邏輯的處理器
                            pipeline.addLast(new WebSocketServerFrameHandler());
                        }
                    });
            System.out.println("服務端綁定端口7000.....啓動成功");

            //啓動服務端
            ChannelFuture cf = serverBootstrap.bind(7000).sync();
            cf.channel().closeFuture().sync();
        } finally {
            boosGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
        }
    }
}
Websocket雙工TCP長連接—服務端
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title><!DOCTYPE html>
        <html lang="en">
        <head>
        <meta charset="UTF-8">
        <title>Title</title>
</head>
<script>
    var socket;
    //判斷當前瀏覽器是否支持websocket編程
    if (window.WebSocket) {
        socket = new WebSocket("ws://localhost:7000/websocket");
        //相當於服務器中的channelRead0事件【客戶端讀取服務端數據事件】,msg爲服務端發來的數據
        socket.onmessage = function (resp) {
            var rt = document.getElementById('responseText');
            rt.value = rt.value + "\n" + resp.data
        }
        //相當於客戶端與服務端連接開啓的事件
        socket.onopen = function (resp) {
            var rt = document.getElementById('responseText');
            rt.value = "連接已開啓";
        }
        //相當於客戶端與服務端連接關閉的事件
        socket.onclose = function (resp) {
            var rt = document.getElementById('responseText');
            rt.value = rt.value + "\n" + "連接已關閉";
        }
    } else {
        alert("當前瀏覽器不支持websocket編程");
    }

    //發送消息給服務端的方法
    function send(msg) {
        //判斷websocket是否創建好了
        if (!window.socket) {
            return
        }
        //判斷當前狀態是否已經連接開啓
        if (socket.readyState == WebSocket.OPEN) {
            //通過socket發送消息
            socket.send(msg);
        } else {
            alert("連接未開啓");
        }
    }
</script>
<body>
<form onsubmit="return false">
    <textarea name="message" style="height: 300px;width: 300px"></textarea>
    <input type="button" value="發送" onclick="send(this.form.message.value)">
    <textarea id="responseText" style="height: 300px;width: 300px"></textarea>
    <input type="button" value="清空" onclick="document.getElementById('responseText').value=''">
</form>

</body>
</html>
前端的簡單頁面-websocket.html

測試效果:

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章