Netty學習筆記(四):Netty應用(羣聊、心跳機制、長連接)、Protobfuf、編解碼器、TCP粘包和拆包

第 5 章 Netty 應用實例

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

1、要求

編寫一個 Netty 羣聊系統,實現服務器端和客戶端之間的數據簡單通訊(非阻塞) ,實現多人羣聊 。

服務器端:可以監測用戶上線,離線,並實現消息轉發功能

客戶端:通過 channel 可以無阻塞發送消息給其它所有用戶,同時可以接受其它用戶發送的消息(有服務器轉發 得到)

2、實現

服務器端

public class ChatGroupServer {

    private static final int PORT = 6666;

    public ChatGroupServer() {
    }

    public static void main(String[] args) throws InterruptedException {
        ChatGroupServer chatGroupServer = new ChatGroupServer();
        chatGroupServer.run();
    }

    public void run() throws InterruptedException {

        //創建兩個線程組
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            //創建初始化器
            ServerBootstrap bootstrap = new ServerBootstrap();

            //設置初始化參數
            bootstrap.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();

                            //向pipelinez中添加handler
                            //添加解碼器
                            pipeline.addLast(new StringDecoder());
                            //添加編碼器
                            pipeline.addLast(new StringEncoder());
                            //添加自定義業務處理handler
                            pipeline.addLast(new ChatGroupServerHandler());
                        }
                    });

            System.out.println("服務器啓動");
            ChannelFuture channelFuture = bootstrap.bind(PORT).sync();

            //監聽關閉
            channelFuture.channel().closeFuture().sync();

        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

服務器端Handler

public class ChatGroupServerHandler extends SimpleChannelInboundHandler<String> {

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


    //處理客戶端的連接事件。表示連接已建立,一旦建立會調用此方法
    //將當前channel加入channelGroup中
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        //將客戶加入的消息推送給其他客戶
        //channelGroup.writeAndFlush的作用是將消息發送給channelGroup中所有的channel
        channelGroup.writeAndFlush("[客戶端 " + channel.remoteAddress() + "] : 加入聊天");
        //將新上線的客戶加入channelGroup
        channelGroup.add(channel);
    }

    //表示斷開連接,將離開消息推送給其他在線客戶端
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        channelGroup.writeAndFlush("[客戶端 " + channel.remoteAddress() + "] : 離開羣聊");
//        //ChannelGroup會自動執行該方法
//        channelGroup.remove(channel);
        System.out.println("channelGroup size : " + channelGroup.size());
    }

    //表示 channel 處於活動狀態,服務器端提示xx上線
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        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 sender = ctx.channel();
        String s = "[客戶端 " + sender.remoteAddress() + "] : " + msg;
        System.out.println(s);

        //將消息轉發給其他人,並且排除發送者
        channelGroup.forEach((receiver) ->{
            if (receiver != sender) {
                receiver.writeAndFlush(s);
            }
        });
    }

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

客戶端:

public class ChatGroupClient {

    private static final String HOST = "127.0.0.1";
    private static final int PORT = 6666;

    public ChatGroupClient() {
    }

    public static void main(String[] args) throws InterruptedException {
        ChatGroupClient chatGroupClient = new ChatGroupClient();
        chatGroupClient.run();
    }

    public void run() throws InterruptedException {

        //創建線程組
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();

        try {
            //創建初始化器,並設置
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(eventLoopGroup)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            //加入相關handler
                            pipeline.addLast(new StringDecoder());
                            pipeline.addLast(new StringEncoder());
                            pipeline.addLast(new ChatGroupClientHandler());
                        }
                    });

            //連接服務器
            ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress(HOST, PORT)).sync();
            Channel channel = channelFuture.channel();
            System.out.println("----------" + channel.localAddress() + "-----------");

            //創建scanner,循環發送信息
            Scanner scanner = new Scanner(System.in);
            while (scanner.hasNextLine()) {
                String s = scanner.nextLine();
                channel.writeAndFlush(s + "\n");
            }

            //監聽關閉事件
            channel.closeFuture().sync();
        } finally {
            eventLoopGroup.shutdownGracefully();
        }
    }
}

客戶端Handler:

public class ChatGroupClientHandler extends SimpleChannelInboundHandler<String> {
    //接收客戶端轉發的消息
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        System.out.println(msg);
    }
}

結果:

客戶端1輸出:

----------/127.0.0.1:13240-----------
[客戶端 /127.0.0.1:13257] : 加入聊天
你好啊
[客戶端 /127.0.0.1:13257] : 你好,你叫什麼名字
我是你爸爸啊
[客戶端 /127.0.0.1:13257] : 誰?我兒子?

客戶端2輸出:

----------/127.0.0.1:13257-----------
[客戶端 /127.0.0.1:13240] : 你好啊
你好,你叫什麼名字
[客戶端 /127.0.0.1:13240] : 我是你爸爸啊
誰?我兒子?
[客戶端 /127.0.0.1:13240] : 離開羣聊

服務器端輸出:

[客戶端 /127.0.0.1:13240] : 上線
[客戶端 /127.0.0.1:13257] : 上線
[客戶端 /127.0.0.1:13240] : 你好啊
[客戶端 /127.0.0.1:13257] : 你好,你叫什麼名字
[客戶端 /127.0.0.1:13240] : 我是你爸爸啊
[客戶端 /127.0.0.1:13257] : 誰?我兒子?
[客戶端 /127.0.0.1:13240] : 離線
channelGroup size : 1
[客戶端 /127.0.0.1:13257] : 離線
channelGroup size : 0

二、 Netty 應用實例-心跳檢測機制

1、要求

編寫一個 Netty 心跳檢測機制案例, 當服務器超過 3 秒沒有讀時,就提示讀空閒

當服務器超過 5 秒沒有寫操作時,就提示寫空閒

實現當服務器超過 7 秒沒有讀或者寫操作時,就提示讀寫空閒

2、實現

public class HeartBeatServer {

    public static void main(String[] args) {

        //創建兩個線程組
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup 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 ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            /**
                             * 相關管道中加入一個 netty 提供的 IdleStateHandler
                             * 說明:
                             *      IdleStateHandler 是 netty 處理空閒狀態的處理器
                             *      其中參數包括:
                             *      readerIdleTime:表示如果多久沒有讀,就會發送一個心跳檢測包檢測是否連接
                             *      writerIdleTime:表示如果多久沒有寫,就會發送一個心跳檢測包檢測是否連接
                             *      allIdleTime:表示如果多久沒有讀寫,就會發送一個心跳檢測包檢測是否連接
                             *      當IdleStateHandler觸發之後,就會傳遞給管道中的下一個handler進行處理
                             *      通過調用下一個handler的userEventTriggered去處理讀空閒、寫空閒、都寫空閒
                             */
                            pipeline.addLast(new IdleStateHandler(
                                    3,5,7, TimeUnit.SECONDS));
                            pipeline.addLast(new HeartBeatServerHandler());
                        }
                    });

            ChannelFuture channelFuture = serverBootstrap.bind(6666).sync();

            channelFuture.channel().closeFuture().sync();

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}
public class HeartBeatServerHandler 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;

            String evtType = null;
            //判斷事件類型
            switch (event.state()) {
                case READER_IDLE:
                    evtType = "讀空閒";
                    break;
                case WRITER_IDLE:
                    evtType = "寫空閒";
                    break;
                case ALL_IDLE:
                    evtType = "讀寫空閒";
                    break;
            }
            System.out.println(ctx.channel().remoteAddress() + " : " + evtType);
            //TODO 做相應的處理...
        }
    }
}

使用一個客戶連接心跳檢測服務端之後,輸出結果:

/127.0.0.1:1728 : 讀空閒
/127.0.0.1:1728 : 寫空閒
/127.0.0.1:1728 : 讀空閒
/127.0.0.1:1728 : 讀寫空閒
/127.0.0.1:1728 : 讀空閒
/127.0.0.1:1728 : 寫空閒
/127.0.0.1:1728 : 讀空閒

三、Netty 應用實例-實現長連接

1、要求

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

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

改變 Http 協議多次請求的約束,實現長連接了, 服務器可以發送消息給瀏覽器。客戶端瀏覽器和服務器端會相互感知,比如服務器關閉了,瀏覽器會感知,同樣瀏覽器關閉了,服務器會感知

2、實現

//實現長連接
public class WebSocketServer {

    public static void main(String[] args) {

        //創建兩個線程組
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {

            ServerBootstrap serverBootstrap = new ServerBootstrap();

            serverBootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();

                            //加入netty提供的httpServerCodec =》(code + decode)
                            //HttpServerCodec 是netty提供的http編碼-解碼器
                            pipeline.addLast(new HttpServerCodec());
                            //是以塊的方式寫,所以添加ChunkedWriteHandler處理器
                            pipeline.addLast(new ChunkedWriteHandler());
                            /*
                                http數據在傳輸的過程中是分段的,這就是爲什麼,當瀏覽器發送大量數據時,會發出多次http請求
                                HttpObjectAggregator就是可以將多個段進行聚合
                             */
                            pipeline.addLast(new HttpObjectAggregator(8192));
                            /*說明:
                                1、對應websocket,它的數據是以 幀(frame)的形式傳遞
                                2、WebSocketFrame 下面有6個子類
                                3、WebSocketServerProtocolHandler的核心功能是將http協議升級爲ws協議,保持長連接
                                4、參數對應其uri
                             */
                            pipeline.addLast(new WebSocketServerProtocolHandler("/hello"));

                            //自定義Handler處理業務邏輯
                            pipeline.addLast(new TextWebSocketFrameHandler());
                        }
                    });

            ChannelFuture channelFuture = serverBootstrap.bind(6666).sync();

            channelFuture.channel().closeFuture().sync();

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}
//TextWebSocketFrame表示一個文本幀
public class TextWebSocketFrameHandler
        extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        System.out.println("服務器收到消息:" + msg.text());

        //回覆消息
        String s = "[服務器時間:" + LocalDateTime.now() + "] : " + msg.text();
        ctx.channel().writeAndFlush(new TextWebSocketFrame(s));
    }

    //當web客戶端連接後出發該方法
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        //id 表示唯一的值,LongText 是唯一的 ShortText 不是唯一
        System.out.println("handlerAdded 被調用" + ctx.channel().id().asLongText());
        System.out.println("handlerAdded 被調用" + ctx.channel().id().asShortText());
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        System.out.println("handlerRemoved 被調用" + ctx.channel().id().asLongText());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println("出現異常:" + cause.getMessage());
        ctx.close();
    }
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script>
    var socket;
    //判斷當前瀏覽器是否支持websocket
    if(window.WebSocket) {
        //go on
        socket = new WebSocket("ws://localhost:6666/hello");
        //相當於channelReado, ev 收到服務器端回送的消息
        socket.onmessage = function (ev) {
            var rt = document.getElementById("responseText");
            rt.value = rt.value + "\n" + ev.data;
        }

        //相當於連接開啓(感知到連接開啓)
        socket.onopen = function (ev) {
            var rt = document.getElementById("responseText");
            rt.value = "連接開啓了.."
        }

        //相當於連接關閉(感知到連接關閉)
        socket.onclose = function (ev) {

            var rt = document.getElementById("responseText");
            rt.value = rt.value + "\n" + "連接關閉了.."
        }
    } else {
        alert("當前瀏覽器不支持websocket")
    }

    //發送消息到服務器
    function send(message) {
        if(!window.socket) { //先判斷socket是否創建好
            return;
        }
        if(socket.readyState == WebSocket.OPEN) {
            //通過socket 發送消息
            socket.send(message)
        } else {
            alert("連接沒有開啓");
        }
    }
</script>
    <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>

結果:首先在頁面發送信息:

在這裏插入圖片描述

可以看到消息順利的回顯到了頁面,並且服務器端的控制檯也顯示出來了:

handlerAdded 被調用5ce0c5fffe05000f-00003dc0-00000001-3a2525e615eafff5-5b0722ed
handlerAdded 被調用5b0722ed
服務器收到消息:你好
服務器收到消息:你也好

當頁面關閉時,服務器端也出現了斷開連接的消息

handlerAdded 被調用5ce0c5fffe05000f-00003dc0-00000001-3a2525e615eafff5-5b0722ed
handlerAdded 被調用5b0722ed
服務器收到消息:你好
服務器收到消息:你也好
出現異常:遠程主機強迫關閉了一個現有的連接。
handlerRemoved 被調用5ce0c5fffe05000f-00003dc0-00000001-3a2525e615eafff5-5b0722ed

四、 Log4j 整合到 Netty

1、在Maven 中添加對Log4j的依賴 在 pom.xml

        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.25</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.25</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.25</version>
            <scope>test</scope>
        </dependency>

2、配置 Log4j , 添加resources/log4j.properties文件,文件內容如下:

log4j.rootLogger=debug,appender1

log4j.appender.appender1=org.apache.log4j.ConsoleAppender

log4j.appender.appender1.layout=org.apache.log4j.TTCCLayout

log4j.appender.stdout.layout.ConversionPattern=[%p] %C{1} -%m%n

3、輸出:

[main] DEBUG io.netty.buffer.ByteBufUtil - -Dio.netty.allocator.type: pooled
[main] DEBUG io.netty.buffer.ByteBufUtil - -Dio.netty.threadLocalDirectBufferSize: 65536
[main] DEBUG io.netty.buffer.ByteBufUtil - -Dio.netty.maxThreadLocalCharBufferSize: 16384
CodecClientHandler::channelActive 發送數據
[nioEventLoopGroup-2-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.maxCapacityPerThread: 32768
[nioEventLoopGroup-2-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.maxSharedCapacityFactor: 2
[nioEventLoopGroup-2-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.linkCapacity: 16
[nioEventLoopGroup-2-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.ratio: 8

第 6 章 Google Protobuf

一、編碼和解碼介紹

1、基本介紹

編寫網絡應用程序時,因爲數據在網絡中傳輸的都是二進制字節碼數據,在發送數據時就需要編碼,接收數據時就需要解碼

codec(編解碼器) 的組成部分有兩個:decoder(解碼器)和 encoder(編碼器)。 encoder 負責把業務數據轉換成字節 碼數據,decoder 負責把字節碼數據轉換成業務數據

在這裏插入圖片描述

2、Netty 本身的編碼解碼的機制

Netty 自身提供了一些 codec(編解碼器):

  • StringEncoder,對字符串數據進行編碼
  • ObjectEncoder,對 Java 對象進行編碼
  • StringDecoder, 對字符串數據進行解碼
  • ObjectDecoder,對 Java 對象進行解碼

Netty 本身自帶的 ObjectDecoder 和 ObjectEncoder 可以用來實現 POJO 對象或各種業務對象的編碼和解碼,底層使用的仍是 Java 序列化技術 , 而 Java 序列化技術本身效率就不高,存在如下問題:

  • 無法跨語言
  • 序列化後的體積太大,是二進制編碼的 5 倍多。
  • 序列化性能太低

對於上面的這些問題,可以使用Protobuf進行解決

二、Protobuf

1、基本介紹

Protobuf 是 Google 發佈的開源項目,全稱 Google Protocol Buffers,是一種輕便高效的結構化數據存儲格式, 可以用於結構化數據串行化,或者說序列化。它很適合做數據存儲RPC [遠程過程調用 remote procedure call] 數據交換格式

參考文檔 :https://developers.google.com/protocol-buffers/docs/proto

Protobuf 是以 message 的方式來管理數據的.

支持跨平臺、跨語言,即[客戶端和服務器端可以是不同的語言編寫的] (支持目前絕大多數語言,例如 C++、 C#、Java、python 等) ,高性能,高可靠性

使用 protobuf 編譯器能自動生成代碼,Protobuf 是將類的定義使用.proto 文件進行描述,然後通過 protoc.exe 編譯器根據.proto 自動生成.java 文件
在這裏插入圖片描述

2、Protobuf 入門實例

參考

第 7 章 Netty 編解碼器和 handler 的調用機制

一、基本說明

netty 的組件設計:Netty 的主要組件有 Channel、EventLoop、ChannelFuture、ChannelHandler、ChannelPipe 等

ChannelHandler 充當了處理入站和出站數據的應用程序邏輯的容器。例如,實現 ChannelInboundHandler 接口(或 ChannelInboundHandlerAdapter),你就可以接收入站事件和數據,這些數據會被業務邏輯處理。當要給客戶端 發 送 響 應 時 , 也 可 以 從 ChannelInboundHandler 衝 刷 數 據 。 業 務 邏 輯 通 常 寫 在 一 個 或 者 多 個 ChannelInboundHandler 中。ChannelOutboundHandler 原理一樣,只不過它是用來處理出站數據的

ChannelPipeline 提供了 ChannelHandler 鏈的容器。以客戶端應用程序爲例,如果事件的運動方向是從客戶端到 服務端的,那麼我們稱這些事件爲出站的,即客戶端發送給服務端的數據會通過 pipeline 中的一系列 ChannelOutboundHandler,並被這些 Handler 處理,反之則稱爲入站的

在這裏插入圖片描述

二、編碼解碼器

1、基本介紹

當 Netty 發送或者接受一個消息的時候,就將會發生一次數據轉換。入站消息會被解碼:從字節轉換爲另一種 格式(比如 java 對象);如果是出站消息,它會被編碼成字節。

Netty 提供一系列實用的編解碼器,他們都實現了 ChannelInboundHadnler 或者 ChannelOutboundHandler 接口。 在這些類中,channelRead 方法已經被重寫了。以入站爲例,對於每個從入站 Channel 讀取的消息,這個方法會被調用。隨後,它將調用由解碼器所提供的 decode()方法進行解碼,並將已經解碼的字節轉發給 ChannelPipeline 中的下一個 ChannelInboundHandler

2、解碼器-ByteToMessageDecoder

ByteToMessageDecoder是用於解碼的,所以繼承了ChannelInboundHadnler。它的作用是將通過二進制字節碼發送過來的數據轉換爲相應的數據,他的繼承關係如下圖所示:

在這裏插入圖片描述

由於不可能知道遠程節點是否會一次性發送一個完整的信息,tcp 有可能出現粘包拆包的問題,這個類會對入 站數據進行緩衝,直到它準備好被處理,下面用一個例子說明:

這個例子,每次入站從ByteBuf中讀取4字節,將其解碼爲一個int,然後將它添加到下一個List中。當沒有更多元素可以被添加到該List中時,它的內容將會被髮送給下一個ChannelInboundHandler。int在被添加到List中時,會被自動裝箱爲Integer。

public class ToIntegerDecoder extends ByteToMessageDecoder {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        if (in.readableBytes() >= 4) {
            out.add(in.readInt());
        }
    }
}

在調用readInt()方法前必須驗證所輸入的ByteBuf是否具有足夠的數據(4個字節)來生成int,流程如下圖所示:
在這裏插入圖片描述

3、自定義的編碼器和解碼器

使用自定義的編碼器和解碼器來說明 Netty 的 handler 調用機制:

  • 客戶端發送 long-> 服務器
  • 服務端發送 long-> 客戶端

案例分析圖例:

在這裏插入圖片描述

實現代碼:

編解碼器:

public class MyByteToLongDecoder extends ByteToMessageDecoder {

    /**
     *  將字節碼解析爲Long。該方法會被調用多次,知道沒有新的元素被添加進List
     *  或者ByteBuf中沒有更多的可讀。之後List中的數據會被傳遞給下一個InBoundHandler,
     *  同時該InBoundHandler也會被調用多次
     * @param ctx 上下文
     * @param in ByteBuf
     * @param out 用於存放解碼得到的數據,會傳遞給下一個Handler
     * @throws Exception
     */
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {

        System.out.println("MyByteToLongDecoder::decode 被調用");
        //對於字節碼依次讀取8個字節生成Long
        if (in.readableBytes() >= 8) {
            out.add(in.readLong());
        }

    }
}
public class MyLongToByteEncoder extends MessageToByteEncoder<Long> {
    @Override
    protected void encode(ChannelHandlerContext ctx, Long msg, ByteBuf out) throws Exception {
        System.out.println("MyLongToByteEncoder::encode 被調用");
        System.out.println("msg = " + msg);
        System.out.println("-------------------------------------");
        out.writeLong(msg);
    }
}

服務器端:

public class CodecServer {

    public static void main(String[] args) {

        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {

            ServerBootstrap bootstrap = new ServerBootstrap();

            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new CodecServerChannelInitializer());

            ChannelFuture channelFuture = bootstrap.bind(6666).sync();

            channelFuture.channel().closeFuture().sync();

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}
public class CodecServerChannelInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();

        //加入InboundHandler進行解碼
        pipeline.addLast(new MyByteToLongDecoder());

        //加加入OutboundHandler進行解碼
        pipeline.addLast(new MyLongToByteEncoder());

        //加入自定InboundHandler將解碼的數據進行輸出
        pipeline.addLast(new CodecServerHandler());

    }
}
public class CodecServerHandler extends SimpleChannelInboundHandler<Long> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Long msg) throws Exception {

        System.out.println("CodecServerHandler::channelRead0 被調用");

        System.out.println("[客戶端" + ctx.channel().remoteAddress() + "] : " + msg);

        System.out.println("-----------------------------------------");

        //給客戶端回送一個long
        ctx.writeAndFlush(654321L);
    }

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

客戶端:

public class CodecClient {

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

        try {
            Bootstrap bootstrap = new Bootstrap();

            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new CodecClientChannelInitializer());

            ChannelFuture channelFuture = bootstrap.connect(
                    new InetSocketAddress("127.0.0.1", 6666)).sync();

            channelFuture.channel().closeFuture().sync();

        } finally {
            group.shutdownGracefully();
        }
    }
}
public class CodecClientChannelInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();

        //向管道中加入出棧的 handler 對進行編碼
        pipeline.addLast(new MyLongToByteEncoder());

        //向管道中加入出棧的 handler 對進行解碼
        pipeline.addLast(new MyByteToLongDecoder());

        //加入處理業務邏輯的handler
        pipeline.addLast(new CodecClientHandler());
    }
}
public class CodecClientHandler extends SimpleChannelInboundHandler<Long> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Long msg) throws Exception {
        //對服務器端回送的數據進行讀取
        System.out.println("CodecClientHandler::channelRead0");
        System.out.println("[服務器 " + ctx.channel().remoteAddress() + "] : " + msg);
    }
    
    //發送數據
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("CodecClientHandler::channelActive " + "發送數據");
        ctx.writeAndFlush(123456L);
    }
}

輸出結果:

服務器端:

MyByteToLongDecoder::decode 被調用
CodecServerHandler::channelRead0 被調用
[客戶端/127.0.0.1:13502] : 123456
-----------------------------------------
MyLongToByteEncoder::encode 被調用
msg = 654321

客戶端:

CodecClientHandler::channelActive 發送數據
MyLongToByteEncoder::encode 被調用
msg = 123456
-------------------------------------
MyByteToLongDecoder::decode 被調用
CodecClientHandler::channelRead0
[服務器 /127.0.0.1:6666] : 654321

要點

  • 不論解碼器handler 還是 編碼器handler 。接收的消息類型必須與待處理的消息類型一致,否則該handler不會被執行。
  • 在解碼器進行數據解碼時,需要判斷緩存區(ByteBuf)的數據是否足夠 ,否則接收到的結果會期望結果可能不一致

4、解碼器-ReplayingDecoder

ReplayingDecoder 擴展了 ByteToMessageDecoder 類,使用這個類,我們不必調用 readableBytes()方法。參數 S 指定了用戶狀態管理的類型,其中 Void 代表不需要狀態管理

public abstract class ReplayingDecoder<S> extends ByteToMessageDecoder 

使用:

public class MyByteToLongDecoder2 extends ReplayingDecoder<Void> {

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {

        System.out.println("MyByteToLongDecoder::decode 被調用");
        //在 ReplayingDecoder 不需要判斷數據是否足夠讀取,內部會進行處理判斷
        out.add(in.readLong());

    }
}

ReplayingDecoder 使用方便,但它也有一些侷限性:

  • 並 不 是 所 有 的 ByteBuf 操 作 都 被 支 持 , 如 果 調 用 了 一 個 不 被 支 持 的 方 法 , 將 會 拋 出 一 個 UnsupportedOperationException。
  • ReplayingDecoder 在某些情況下可能稍慢於 ByteToMessageDecoder,例如網絡緩慢並且消息格式複雜時,消息會被拆成了多個碎片,速度變慢

5、 其它編解碼器

解碼器:

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

上面這些解碼器都有對應的編碼器:
在這裏插入圖片描述

第 8 章 TCP 粘包和拆包 及解決方案

一、TCP 粘包和拆包基本介紹

TCP 是面向連接的,面向流的,提供高可靠性服務。

收發兩端(客戶端和服務器端)都要有一一成對的 socket, 因此,發送端爲了將多個發給接收端的包,更有效的發給對方,使用了優化方法(Nagle 算法),將多次間隔較小且數據量小的數據,合併成一個大的數據塊,然後進行封包。

這樣做雖然提高了效率,但是接收端就難於分辨出完整的數據包了,因爲面向流的通信是無消息保護邊界的

由於 TCP 無消息保護邊界, 需要在接收端處理消息邊界問題,也就是我們所說的粘包、拆包問題。下面用一張圖解釋這個問題:

在這裏插入圖片描述

假設客戶端分別發送了兩個數據包 D1 和 D2 給服務端,由於服務端一次讀取到字節數是不確定的,故可能存在以
下四種情況:

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

二、TCP 粘包和拆包現象實例

在編寫 Netty 程序時,如果沒有做處理,就會發生粘包和拆包的問題。下面以一個實例,來展示粘包和拆包現象(這裏只粘貼了Handler的代碼,其餘代碼與之前無異):

public class MyServerHandler extends SimpleChannelInboundHandler<ByteBuf> {

    private int cnt;

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
        byte[] buffer = new byte[msg.readableBytes()];
        msg.readBytes(buffer);

        String s = new String(buffer, CharsetUtil.UTF_8);

        System.out.println("服務器收到消息:" + s);
        System.out.println("服務器收到的消息量" + (++cnt));

        //服務器端回覆隨機id給客戶端
        ByteBuf response = Unpooled.copiedBuffer(UUID.randomUUID().toString(), CharsetUtil.UTF_8);
        ctx.writeAndFlush(response);
    }
}
public class MyClientHandler extends SimpleChannelInboundHandler<ByteBuf> {

    private int cnt;

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //發送10條數據給服務器端
        for (int i = 0; i < 10; i++) {
            ByteBuf buf = Unpooled.copiedBuffer("hello,server " + i, CharsetUtil.UTF_8);
            ctx.writeAndFlush(buf);
        }
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
        byte[] buffer = new byte[msg.readableBytes()];
        msg.readBytes(buffer);

        String s = new String(buffer, CharsetUtil.UTF_8);

        System.out.println("服務器收到消息: " + s);
        System.out.println("服務器收到的消息量" + (++cnt));
    }
}

第一次運行客戶端,服務器端的輸出:

服務器收到消息:hello,server 0hello,server 1hello,server 2hello,server 3hello,server 4hello,server 5hello,server 6hello,server 7hello,server 8hello,server 9
服務器收到的消息量1

第二次運行客戶端,服務器端的輸出:

服務器收到消息:hello,server 0
服務器收到的消息量1
服務器收到消息:hello,server 1
服務器收到的消息量2
服務器收到消息:hello,server 2hello,server 3hello,server 4
服務器收到的消息量3
服務器收到消息:hello,server 5hello,server 6
服務器收到的消息量4
服務器收到消息:hello,server 7
服務器收到的消息量5
服務器收到消息:hello,server 8
服務器收到的消息量6
服務器收到消息:hello,server 9
服務器收到的消息量7

第三次運行客戶端,服務器端的輸出:

服務器收到消息:hello,server 0
服務器收到的消息量1
服務器收到消息:hello,server 1
服務器收到的消息量2
服務器收到消息:hello,server 2hello,server 3hello,server 4
服務器收到的消息量3
服務器收到消息:hello,server 5
服務器收到的消息量4
服務器收到消息:hello,server 6
服務器收到的消息量5
服務器收到消息:hello,server 7hello,server 8hello,server 9
服務器收到的消息量6

可以看到三次的運行結果都不相同,這就是TCP的粘包和拆包引起。下面我們來看一下如何使用Netty解決這種問題。

三、TCP 粘包和拆包解決方案

關鍵就是要解決 服務器端每次讀取數據長度的問題, 如果這個問題解決了,就不會出現服務器多讀或少讀數據的問 題,從而避免的 TCP 粘包、拆包 。

可以使用自定義協議 + 編解碼器 來解決 。

實例:

要求客戶端發送 5 個 Message 對象, 客戶端每次發送一個 Message 對象

服務器端每次接收一個Message, 分5次進行解碼, 每讀取到 一個Message

代碼:

傳輸類:

public class MessageProtocal {

    private int len;
    private byte[] content;

    public MessageProtocal() {
    }

    public MessageProtocal(int len, byte[] content) {
        this.len = len;
        this.content = content;
    }

    public int getLen() {
        return len;
    }

    public void setLen(int len) {
        this.len = len;
    }

    public byte[] getContent() {
        return content;
    }

    public void setContent(byte[] content) {
        this.content = content;
    }
}

編碼器:

public class MyMessageEncoder extends MessageToByteEncoder<MessageProtocal> {
    //對MessageProtocal進行編碼,轉換爲二進制字節流
    @Override
    protected void encode(ChannelHandlerContext ctx, MessageProtocal msg, ByteBuf out) throws Exception {

        System.out.println("MyMessageEncoder::encode 被調用");
        //放入長度和內容
        out.writeInt(msg.getLen());
        out.writeBytes(msg.getContent());
    }
}

解碼器:

public class MyMessageDecoder extends ReplayingDecoder {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {

        System.out.println("---------------------------------------------------");
        System.out.println("MyMessageDecoder::decode 被調用");
        //將二進制字節流轉換爲 MessageProtocal,並放入List中
        int len = in.readInt();
        byte[] content = new byte[len];
        in.readBytes(content);
        out.add(new MessageProtocal(len, content));
    }
}

客戶端handler:

public class MyClientHandler extends SimpleChannelInboundHandler<MessageProtocal> {

    private int cnt;

    //發送數據
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //發送10條數據給服務器端
        for (int i = 0; i < 5; i++) {
            String msg = "你好,服務器";
            byte[] content = msg.getBytes(Charset.forName("utf-8"));
            MessageProtocal messageProtocal = new MessageProtocal(content.length, content);
            ctx.writeAndFlush(messageProtocal);
        }
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MessageProtocal msg) throws Exception {

    }

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

服務端handler:

public class MyServerHandler extends SimpleChannelInboundHandler<MessageProtocal> {

    private int cnt;

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MessageProtocal msg) throws Exception {

        String s = new String(msg.getContent(), CharsetUtil.UTF_8);

        System.out.println("服務器收到消息:" + s + " 長度爲:" + msg.getLen());
        System.out.println("服務器收到的消息量 " + (++cnt));

        //服務器端回覆隨機id給客戶端
        ByteBuf response = Unpooled.copiedBuffer(UUID.randomUUID().toString(), CharsetUtil.UTF_8);
        ctx.writeAndFlush(response);
    }
}

其他代碼與上面類似,這裏不再粘貼,關鍵是對消息的長度要進行定義。下面是服務器端輸出結果:

---------------------------------------------------
MyMessageDecoder::decode 被調用
服務器收到消息:你好,服務器 長度爲:18
服務器收到的消息量 1
---------------------------------------------------
MyMessageDecoder::decode 被調用
服務器收到消息:你好,服務器 長度爲:18
服務器收到的消息量 2
---------------------------------------------------
MyMessageDecoder::decode 被調用
服務器收到消息:你好,服務器 長度爲:18
服務器收到的消息量 3
---------------------------------------------------
MyMessageDecoder::decode 被調用
服務器收到消息:你好,服務器 長度爲:18
服務器收到的消息量 4
---------------------------------------------------
MyMessageDecoder::decode 被調用
服務器收到消息:你好,服務器 長度爲:18
服務器收到的消息量 5

客戶端:

MyMessageEncoder::encode 被調用
MyMessageEncoder::encode 被調用
MyMessageEncoder::encode 被調用
MyMessageEncoder::encode 被調用
MyMessageEncoder::encode 被調用
ntext ctx, MessageProtocal msg) throws Exception {

        String s = new String(msg.getContent(), CharsetUtil.UTF_8);

        System.out.println("服務器收到消息:" + s + " 長度爲:" + msg.getLen());
        System.out.println("服務器收到的消息量 " + (++cnt));

        //服務器端回覆隨機id給客戶端
        ByteBuf response = Unpooled.copiedBuffer(UUID.randomUUID().toString(), CharsetUtil.UTF_8);
        ctx.writeAndFlush(response);
    }
}

其他代碼與上面類似,這裏不再粘貼,關鍵是對消息的長度要進行定義。下面是服務器端輸出結果:

---------------------------------------------------
MyMessageDecoder::decode 被調用
服務器收到消息:你好,服務器 長度爲:18
服務器收到的消息量 1
---------------------------------------------------
MyMessageDecoder::decode 被調用
服務器收到消息:你好,服務器 長度爲:18
服務器收到的消息量 2
---------------------------------------------------
MyMessageDecoder::decode 被調用
服務器收到消息:你好,服務器 長度爲:18
服務器收到的消息量 3
---------------------------------------------------
MyMessageDecoder::decode 被調用
服務器收到消息:你好,服務器 長度爲:18
服務器收到的消息量 4
---------------------------------------------------
MyMessageDecoder::decode 被調用
服務器收到消息:你好,服務器 長度爲:18
服務器收到的消息量 5

客戶端:

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