Netty高階應用及聊天室實戰

女友在高考發表於2022-02-18

Netty 高階應用

1. 編解碼器

概念:在網路應用中,需要實現某種編解碼器。將原始位元組資料與自定義訊息資料進行相互轉換。網路中都是以位元組碼的形式傳輸的。

對Netty而言,編解碼器由兩部分組成:編碼器、解碼器

  • 編碼器:將訊息物件轉為位元組或其他序列形式在網路上傳輸
  • 解碼器:負責將位元組或其他序列形式轉為指定的訊息物件

Netty的編解碼器實現了ChannelHandlerAdapter,也是一種特殊的ChannelHandler,所以依賴與ChannelPipeline,可以將多個編解碼器連結在一起,以實現複雜的轉換邏輯。

  1. 解碼器
  • ByteToMessageDecoder:用於將位元組轉為訊息,需要檢查緩衝區是否有足夠的位元組
  • ReplayingDecoder:繼承ByteToMessageDecoder,不需要檢查緩衝區是否有足夠的位元組,但是ReplayingDecoder速度略慢於ByteToMessageDecoder,同時不是所有的ByteBuf都支援。專案複雜性高則使用ReplayingDecoder,否則使用ByteToMessageDecode
  • MessageToMessageDecoder:用於從一種訊息解碼為另一種訊息(如POJO到POJO)

解碼器示例:

public class DemoDecoder extends MessageToMessageDecoder<ByteBuf> {
    
    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
        String msg = byteBuf.toString(CharsetUtil.UTF_8);
        list.add(msg);
    }
}

通道里加入解碼器:

 protected void initChannel(SocketChannel socketChannel) throws Exception {
                        socketChannel.pipeline().addLast(new DemoDecoder());
                        socketChannel.pipeline().addLast(new DemoNettyServerHandle());
                    }
  1. 編碼器
  • MessageToByteEncoder:將訊息轉為位元組
  • MessageToMessageEncoder:用於從一種訊息編碼為另外一種訊息(例如POJO到POJO)

編碼器示例:

public class DemoEncoder extends MessageToMessageEncoder<String> {
    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, String s, List<Object> list) throws Exception {
        list.add(Unpooled.copiedBuffer(s,CharsetUtil.UTF_8));
    }
}
  1. 編碼解碼器Codec

同時具備編碼與解碼功能

  • ByteToMessageCodec
  • MessageToMessageCodec

2. 基於Netty的HTTP伺服器開發

效果如圖:

程式碼如下:

public class NettyHttpServer {

    private int port;

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

    public static void main(String[] args) {
        new NettyHttpServer(8090).run();
    }

    public void run(){
        EventLoopGroup bossGroup=null;
        EventLoopGroup workerGroup=null;
        try{
            bossGroup=new NioEventLoopGroup(1);
            workerGroup=new NioEventLoopGroup();
            ServerBootstrap serverBootstrap=new ServerBootstrap();
            serverBootstrap.group(bossGroup,workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG,128)
                    .childOption(ChannelOption.SO_KEEPALIVE,Boolean.TRUE)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            //新增編解碼器
                            socketChannel.pipeline().addLast(new HttpServerCodec());
                            socketChannel.pipeline().addLast(new NettyHttpServerHandler());

                        }
                    });
            ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
            channelFuture.channel().closeFuture().sync();

        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}
public class NettyHttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, HttpObject httpObject) throws Exception {
        if(httpObject instanceof HttpRequest){
            DefaultHttpRequest request=(DefaultHttpRequest)httpObject;
            if(request.uri().equals("/favicon.ico")){
                //圖示不響應
                return;
            }
            System.out.println("接收到請求:"+request.uri());
            ByteBuf byteBuf = Unpooled.copiedBuffer("你好,我是服務端", CharsetUtil.UTF_8);
            DefaultFullHttpResponse response=new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,HttpResponseStatus.OK,byteBuf);
            //設定響應頭
            response.headers().set(HttpHeaderNames.CONTENT_TYPE,"text/html;charset=utf-8");
            response.headers().set(HttpHeaderNames.CONTENT_LENGTH,byteBuf.readableBytes());
            channelHandlerContext.writeAndFlush(response);
        }
    }
}

3. 粘包和拆包

簡介:粘包和拆包是TCP網路程式設計中不可避免的,無論客戶端還是服務端,當我們讀取或傳送訊息的時候都要考慮TCP底層的粘包/拆包機制。

粘包產生的原因:

  • 應用程式寫入的資料小於套接字緩衝區大小,網路卡將應用多次寫入的資料傳送到網路上
  • 接收方不及時讀取套接字緩衝區資料
  • TCP預設使用Nagle演算法,將小資料包合併

拆包產生的原因:

  • 資料太大超過剩餘緩衝區的大小
  • 資料太大超過MSS最大報文長度

粘包和拆包的解決方案

  1. 訊息長度固定,累計讀取到定長的報文就認為是一個完整的資訊
  2. 將換行符作為訊息結束符
  3. 將特殊的分隔符作為訊息的結束標誌
  4. 通過在訊息頭中定義長度欄位來標識訊息總長度

Netty中粘包和拆包的解決方案

Netty提供了4種解碼器來解決:

  1. 固定長度拆包器FixedLengthFrameDecoder
  2. 行拆包器LineBasedFrameDecoder,以換行符作為分隔符
  3. 分隔符拆包器DelimiterBasedFrameDecoder,通過自定義的分隔符進行拆分
  4. 基於資料包長度的拆包器LengthFieldBasedFrameDecoder,將應用層資料包的長度最為拆分一句。要求應用層協議中包含資料包的長度。

DelimiterBasedFrameDecoder示例:

ByteBuf byteBuf =
Unpooled.copiedBuffer("$".getBytes(StandardCharsets.UTF_8));
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(2048, byteBuf));

LengthFieldBasedFrameDecoder構造器引數講解:
public LengthFieldBasedFrameDecoder(
ByteOrder byteOrder,
int lengthFieldOffset,
int lengthFieldLength,
int lengthAdjustment,
int initialBytesToStrip,
boolean failFast)

  • byteOrder是指明Length欄位是大端序還是小端序,因為Netty要讀取Length欄位的值,所以大端小端要設定好,預設Netty是大端序ByteOrder.BIG_ENDIAN。

  • maxFrameLength是指最大包長度,如果Netty最終生成的資料包超過這個長度,Netty就會報錯。

  • lengthFieldOffset是指明Length的偏移位

  • lengthFieldLength是Length欄位長度

  • lengthAdjustment 這個引數很多時候設為負數,這是最讓小夥伴們迷惑的。下面我用一整段話來解釋這個引數

當Netty利用lengthFieldOffset(偏移位)和lengthFieldLength(Length欄位長度)成功讀出Length欄位的值後,Netty認為這個值是指從Length欄位之後,到包結束一共還有多少位元組,如果這個值是13,那麼Netty就會再等待13個Byte的資料到達後,拼接成一個完整的包。但是更多時候,Length欄位的長度,是指整個包的長度,如果是這種情況,當Netty讀出Length欄位的時候,它已經讀取了包的4個Byte的資料,所以,後續未到達的資料只有9個Byte,即13 - 4 = 9,這個時候,就要用lengthAdjustment來告訴Netty,後續的資料並沒有13個Byte,要減掉4個Byte,所以lengthAdjustment要設為 -4!!!

  • initialBytesToStrip,跳過的個數。比如這裡initialBytesToStrip設定為4,那麼Netty就會跳過前4位解析後面的內容

  • failFast 引數一般設定為true,當這個引數為true時,netty一旦讀到Length欄位,並判斷Length超過maxFrameLength,就立即丟擲異常。

示例:

 @Override
    public void channelActive(ChannelHandlerContext channelHandlerContext) throws Exception {
        for (int i=0;i<100;i++){
            byte[] bytes = "你好,我是客戶端".getBytes(CharsetUtil.UTF_8);
            ByteBuf byteBuf = Unpooled.buffer();
            byteBuf.writeInt(bytes.length);
            byteBuf.writeBytes(bytes);
            channelHandlerContext.writeAndFlush(byteBuf);
        }
    }

第2個引數和第三個參數列示:0-4個位元組是內容長度欄位,第五個引數的4代表跳過前4個位元組。

 socketChannel.pipeline().addLast(new LengthFieldBasedFrameDecoder(60535,0,4,0,4));

最後輸出的內容:

4. 基於Netty和WebSocket的聊天室案例

1. WebSocket簡介

WebSocket是一種在單個TCP連線上進行全雙工通訊的協議。相比HTTP協議,WebSocket具備如下特點:

  1. 支援雙向通訊,實時性更強
  2. 更好的二進位制支援
  3. 較少的開銷:協議控制的資料包頭部較小

應用場景:

  • 社交訂閱
  • 協同編輯
  • 股票基金報價
  • 體育實況更新
  • 多媒體聊天
  • 線上教育

2. 服務端開發

  1. 引入依賴

基於SpringBoot環境

 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--新增thymeleaf依賴 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.72.Final</version>
        </dependency>
  1. 核心後端程式碼
@Component
public class NettyWebSocketServer implements Runnable {


    @Autowired
    private NettyConfig nettyConfig;

    @Autowired
    private WebSocketChannelInit webSocketChannelInit;

    private EventLoopGroup bossGroup = new NioEventLoopGroup(1);
    private EventLoopGroup wokerGroup = new NioEventLoopGroup();


    @PreDestroy
    public void close(){
        bossGroup.shutdownGracefully();
        wokerGroup.shutdownGracefully();
    }

    @Override
    public void run() {
        try{
            ServerBootstrap serverBootstrap=new ServerBootstrap();
            serverBootstrap.group(bossGroup,wokerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(webSocketChannelInit);
            ChannelFuture channelFuture = serverBootstrap.bind(nettyConfig.getPort()).sync();
            System.out.println("Netty服務端啟動成功");
            channelFuture.channel().closeFuture().sync();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            bossGroup.shutdownGracefully();
            wokerGroup.shutdownGracefully();
        }

    }
}
@Component
public class WebSocketChannelInit extends ChannelInitializer {

    @Autowired
    private NettyConfig nettyConfig;

    @Autowired
    private WebSocketHandler webSocketHandler;

    @Override
    protected void initChannel(Channel channel) throws Exception {
        ChannelPipeline pipeline = channel.pipeline();
        //對http協議的支援
        pipeline.addLast(new HttpServerCodec());
        //對大資料流的支援
        pipeline.addLast(new ChunkedWriteHandler());
        //post請求分為3部分。request line、request header、body
        //HttpObjectAggregator將多個資訊轉化為單一的request或者response物件
        pipeline.addLast(new HttpObjectAggregator(8000));
        //將http協議升級為ws協議,websocket的支援
        pipeline.addLast(new WebSocketServerProtocolHandler(nettyConfig.getPath()));

        pipeline.addLast(webSocketHandler);
    }
}

@Component
@ChannelHandler.Sharable  //設定通道共享
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    private List<Channel> channels=new ArrayList<>();

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        channels.add(ctx.channel());
        System.out.println("有新的連線了...");
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        channels.remove(ctx.channel());
        System.out.println("連線下線了");
    }

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {

        String text = textWebSocketFrame.text();
        Channel currentChannel = channelHandlerContext.channel();
        for (Channel channel:channels){
            //自己不給自己發訊息
            if(!channel.equals(currentChannel)){
                channel.writeAndFlush(new TextWebSocketFrame(text));
            }
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        Channel channel = ctx.channel();
        channels.remove(channel);
    }
}

3. 前端js程式碼

$(function () {
    //這裡需要注意的是,prompt有兩個引數,前面是提示的話,後面是當對話方塊出來後,在對話方塊裡的預設值
    var username = "";
    while (true) {
        //彈出一個輸入框,輸入一段文字,可以提交
        username = prompt("請輸入您的名字", ""); //將輸入的內容賦給變數 name ,
        if (username.trim() === "")//如果返回的有內容
        {
            alert("名稱不能輸入空")
        } else {
            $("#username").text(username);
            break;
        }
    }

    var ws = new WebSocket("ws://localhost:8081/chatService");
    ws.onopen = function () {
        console.log("連線成功.")
    };
    ws.onmessage = function (evt) {
        showMessage(evt.data);
    };
    ws.onclose = function (){
        console.log("連線關閉")
    };

    ws.onerror = function (){
        console.log("連線異常")
    };

    function showMessage(message) {
        // 張三:你好
        var str = message.split(":");
        $("#msg_list").append('<li class="active"}>\n' +
            '                                  <div class="main">\n' +
            '                                    <img class="avatar" width="30" height="30" src="/img/user.png">\n' +
            '                                    <div>\n' +
            '                                        <div class="user_name">'+str[0]+'</div>\n' +
            '                                        <div class="text">'+str[1]+'</div>\n' +
            '                                    </div>                       \n' +
            '                                   </div>\n' +
            '                              </li>');
        // 置底
        setBottom();
    }

    $('#my_test').bind({
        focus: function (event) {
            event.stopPropagation();
            $('#my_test').val('');
            $('.arrow_box').hide()
        },
        keydown: function (event) {
            event.stopPropagation();
            if (event.keyCode === 13) {
                if ($('#my_test').val().trim() === '') {
                    this.blur();
                    $('.arrow_box').show();
                    setTimeout(this.focus(),1000);
                } else {
                    $('.arrow_box').hide();
                    //傳送訊息
                    sendMsg();
                    this.blur();
                    setTimeout(this.focus())
                }
            }
        }
    });
    $('#send').on('click', function (event) {
        event.stopPropagation();
        if ($('#my_test').val().trim() === '') {
            $('.arrow_box').show()
        } else {
            sendMsg();
        }
    });

    function sendMsg() {
        var message = $("#my_test").val();
        $("#msg_list").append('<li class="active"}>\n' +
            '                                  <div class="main self">\n' +
            '                                      <div class="text">'+message+'</div>\n' +
            '                                  </div>\n' +
            '                              </li>');
        $("#my_test").val('');

        //傳送訊息
        message = username + ":" + message;
        ws.send(message);
        // 置底
        setBottom();
    }

    // 置底
    function setBottom() {
        // 傳送訊息後滾動到底部
        var container = $('.m-message');
        var scroll = $('#msg_list');
        container.animate({
            scrollTop: scroll[0].scrollHeight - container[0].clientHeight + container.scrollTop() + 100
        });
    }
});

相關文章