Netty

一、什麼是Netty?

Netty是一個提供異步事件驅動(asynchronous event-driven)的網絡應用框架,是一個用於快速開發高性能、可擴展協議的服務器和客戶端。本質上,Netty就是一個NIO客戶端服務器框架,我們可以用它快速簡單地開發網絡應用程序。Netty大大簡化了網絡程序開發過程。

二、如何使用Netty?

(一)、建立一個Maven項目

建議使用JDK1.6以上版本,這個步驟我就不舉例了。假設我創建的項目名稱是Netty。

(二)、導入maven依賴

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.6.Final</version>
</dependency>

(三)、編寫我們的第一個Discard Server服務器

世上最簡單的協議不是’Hello, World!’ 而是 DISCARD(拋棄服務)。這個協議將會拋棄任何收到 的數據,而不響應。 爲了實現 DISCARD 協議,你只需忽略所有收到的數據。讓我們從 handler(處理器)的實現開始,handler 是由 Netty 生成用來處理 I/O 事件的。

代碼如下:

package com.netty;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

/**
 * Class DiscardServerHandler ...
 *
 * @author LiJun
 * Created on 2018/8/15
 * 處理服務端 channel
 */
public class DiscardServerHandler  extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 丟棄收到的數據
        ((ByteBuf)msg).release();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        // 出現異常就關閉連接
        cause.printStackTrace();
        ctx.close();
    }
}

步驟如下:

  1. 創建一個類繼承ChannelInboundHandlerAdapter,在這個例子上我們創建了一個DiscardServerHandler繼承ChannelInboundHandlerAdapter類,ChannelInboundHandlerAdapter這個類實現了ChannelInboundHandler接口(這個接口中提供了多種事件處理的接口方法)。

  2. 我們覆蓋了channelRead方法,每當從客戶端接收到新的數據的時候,這個方法就會被調用。

  3. 實現DISCARD協議,處理器需要忽略掉收到的信息。ByteBuf 是一個引用計數 對象,這個對象必須顯示地調用 release() 方法來釋放。

    來一個channelRead()方法的模板吧:

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
       try {
           // Do something with msg
       } finally {
           ReferenceCountUtil.release(msg);
       }
    }
  4. exceptionCaught()事件處理方法是當出現 Throwable 對象纔會被調用,即當 Netty 由於 IO 錯誤或者處理器在處理事件時拋出的異常時。在大部分情況下,捕獲的異常應該被記錄下來 並且把關聯的 channel 給關閉掉。然而這個方法的處理方式會在遇到不同異常的情況下有不 同的實現,比如你可能想在關閉連接之前發送一個錯誤碼的響應消息。

注:處理器的職責是釋放所有傳遞到處理器的引用計數對象

(四)、啓動服務端的DiscardServerHandler

package com.netty;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

/**
 * Class DiscardServer ...
 *
 * @author LiJun
 * Created on 2018/8/15
 */
public class DiscardServer {
    private int port;

    public DiscardServer() {
    }

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

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

        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) {
                            socketChannel.pipeline().addLast(new DiscardServerHandler());
                        }
                    })
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true);
            // 綁定端口,並開始接收進來的連接
            ChannelFuture channelFuture = b.bind(port).sync();

            // 等待服務器,socket關閉
            // 在這裏,不會發生什麼
            channelFuture.channel().closeFuture().sync();
        }finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }

    }

    public static void main(String[] args) throws Exception {
        int port;
        if(args.length > 0){
            port = Integer.parseInt(args[0]);
        }else {
            port = 8080;
        }
        new DiscardServer(port).run();
    }

}

解釋:

  1. NioEventLoopGroup 是用來處理I/O操作的多線程事件循環器,Netty 提供了許多不同的EventLoopGroup 的實現用來處理不同的傳輸。在這個例子中我們實現了一個服務端的應用, 因此會有2個NioEventLoopGroup 會被使用。第一個經常被叫做‘boss’,用來接收進來的連 接。第二個經常被叫做‘worker’,用來處理已經被接收的連接,一旦‘boss’接收到連接,就會 把連接信息註冊到‘worker’上。如何知道多少個線程已經被使用,如何映射到已經創建的 Channel上都需要依賴於 EventLoopGroup 的實現,並且可以通過構造函數來配置他們的關係。

  2. ServerBootstrap 是一個啓動 NIO 服務的輔助啓動類。你可以在這個服務中直接使用 Channel,但是這會是一個複雜的處理過程,在很多情況下你並不需要這樣做。

  3. 這裏我們指定使用 NioServerSocketChannel 類來舉例說明一個新的Channel如何接收進來的連接。

  4. 這裏的事件處理類經常會被用來處理一個最近的已經接收的ChannelChannelInitializer 是 一個特殊的處理類,他的目的是幫助使用者配置一個新的 Channel。也許你想通過增加一些 處理類比如DiscardServerHandler 來配置一個新的 Channel 或者其對應的ChannelPipeline 來實現你的網絡程序。當你的程序變的複雜時,可能你會增加更多的處理類到 pipeline 上,然後提取這些匿名類到最頂層的類上。

  5. 你可以設置這裏指定的 Channel 實現的配置參數。我們正在寫一個TCP/IP 的服務端,因此 我們被允許設置 socket 的參數選項比如tcpNoDelaykeepAlive

  6. 你關注過 option()childOption()嗎?option()是提供給NioServerSocketChannel用來接 收進來的連接。childOption()是提供給由父管道 ServerChannel接收到的連接,在這個例子 中也是 NioServerSocketChannel

  7. 我們繼續,剩下的就是綁定端口然後啓動服務。這裏我們在機器上綁定了機器所有網卡上的 8080 端口。當然現在你可以多次調用 bind() 方法(基於不同綁定地址) .

(五)、測試

先運行main方法哦

爲了我們便於觀察到結果,

我們將channelRead()方法內容改一下

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf in = (ByteBuf)msg;
        try{
            while(in.isReadable()){
                System.out.print((char) in.readByte());
                System.out.flush();
            }
        }finally {
            ReferenceCountUtil.release(msg);
        }
    }

1. 介紹下telnet

Telnet協議是TCP/IP協議族中的一員,是Internet遠程登陸服務的標準協議和主要方式。它爲用戶提供了在本地計算機上完成遠程主機工作的能力。在終端使用者的電腦上使用telnet程序,用它連接到服務器。終端使用者可以在telnet程序中輸入命令,這些命令會在服務器上運行,就像直接在服務器的控制檯上輸入一樣。可以在本地就能控制服務器。要開始一個telnet會話,必須輸入用戶名和密碼來登錄服務器。Telnet是常用的遠程控制Web服務器的方法。

2. win10如何開啓telnet

控制面板 -> 程序 -> 啓用或關閉Windows功能 -> 找到Telnet客戶端勾選上,然後點確定。

打開cmd,輸入Telnet

我們可以輸入?/help查看幫助

3.連接

輸入 open localhost 8080 顯示正在連接localhost…就表示連接上了

當我們在telnet輸入內容的時候就能在idea的控制檯山看到響應的信息了。

三、利用Netty編寫一個Echo(應答)服務器

channelRead()方法內容改成如下:

ctx.writeAndFlush(msg);

這個表示把傳入的數據傳回去,並顯示。

writeAndFlush(msg); 等於 ctx.write(msg)+ctx.flush()

調用wirte方法不會使消息寫入到通道上,它被緩衝在了內部,我們必須要調用ctx.flush()方法把緩衝區的數據強行輸出。

四、利用Netty編寫一個Time(時間) 服務器

TimeServerHandler類如下:

package com.netty;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

/**
 * Class TimeServerHandler ...
 *
 * @author LiJun
 * Created on 2018/8/15
 */
public class TimeServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        final ByteBuf time = ctx.alloc().buffer(4);
        time.writeInt((int)(System.currentTimeMillis()/1000L+220898800L));
        final ChannelFuture f = ctx.writeAndFlush(time);
        f.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture channelFuture) throws Exception {
                assert f == channelFuture;
                ctx.close();
            }
        });
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        // 出現異常就關閉連接
        cause.printStackTrace();
        ctx.close();
    }
}

解釋:

  1. 我們表示的是在連接被創建的時候發送一個消息,所以使用的是channelActive()方法

  2. channelActive()方法將會在連接被建立並且準備進行通信時被調用。因此讓我們在這個 法裏完成一個代表當前時間的32位整數消息的構建工作。

  3. 爲了發送一個新的消息,我們需要分配一個包含這個消息的新的緩衝。因爲我們需要寫入一 個32位的整數,因此我們需要一個至少有4個字節的 ByteBuf。通過 ChannelHandlerContext.alloc()得到一個當前的ByteBufAllocator,然後分配一個新的緩衝。

  4. 在這我們不需要flip()方法了,ByteBuf中沒有這個方法,它含有兩個指針,一個對應讀操作,一個對應寫操作。當我們想ByteBuf中寫入數據的時候,寫指針的索引就會增加,讀指針不變,讀的時候讀指針變化,寫指針不變。所以讀指針索引和寫指針所以分別代表了消息的開始和消息和結束。

    注意:ChannelHandlerContext.write()writeAndFlush()方法會返回一 個 ChannelFuture 對象,一個ChannelFuture 代表了一個還沒有發生的 I/O 操作。這意味着 任何一個請求操作都不會馬上被執行,因爲在 Netty 裏所有的操作都是異步的。所以我們需要在write方法返回的ChannelFuture完成後調用close方法,然後當他的寫操 作已經完成他會通知他的監聽者。請注意,close() 方法也可能不會立馬關閉,他也會返回一個 ChannelFuture

  5. 當一個寫請求已經完成是如何通知到我們?這個只需要簡單地在返回的 ChannelFuture 上 增加一個ChannelFutureListener。這裏我們構建了一個匿名的 ChannelFutureListener類用來 在操作完成時關閉 Channel。 或者,你可以使用簡單的預定義監聽器代碼:

    f.addListener(ChannelFutureListener.CLOSE);

    爲了測試我們的time服務如我們期望的一樣工作,你可以使用 UNIX 的 rdate 命令

    $ rdate -o <port> -p <host>

    Port 是你在main()函數中指定的端口,host 使用 localhost 就可以了。

五、時間客戶端

在Netty中,編寫服務端和客戶端唯一也是最大的不同就是使用了不同的BootStrapChannel的實現。

先看一下代碼吧:

TimeClient

“`java
package com.netty;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

/**
* Class TimeClient …
*
* @author LiJun
* Created on 2018/8/27
*/
public class TimeClient {

public static void main(String[] args) {
    //String  host = args[0];
    //int port = Integer.parseInt(args[1]);
    String host = "localhost";
    int port = 8080;
    EventLoopGroup workerGroup = new NioEventLoopGroup();

    try {
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(workerGroup);
        bootstrap.channel(NioSocketChannel.class);
        bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
        bootstrap.handler(new ChannelInitializer<SocketChannel>() {

            @Override
            protected void initChannel(SocketChannel socketChannel) throws Exception {
                socketChannel.pipeline().addLast(new TimeDecoder(), new TimeClientHandler());
            }
        });

        //啓動客戶端
        ChannelFuture channelFuture = bootstrap.connect(host, port).sync();

        //等待關閉
        channelFuture.channel().closeFuture().sync();

    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        workerGroup.shutdownGracefully();
    }

}

}

“`

解釋下:

  1. BootStrapServerBootStrap類似,但是它針對的是非服務端的channel,例如客戶端或者無連接傳輸模式的channel
  2. 指定了EventLoopGroup,那麼這既是一個boss group,也是worker group,儘管客戶端不需要boss worker
  3. NioSocketChannel這個類在客戶端channel被創建時使用。
  4. 不需要像使用ServerBootStrap一樣使用childOption()方法,因爲客戶端的SocketChannel沒有父親。
  5. 我們用connect()方法代替了bind()方法。

    最後我們需要一個TimeClientHandler類來處理信息,翻譯時間。

TimeClientHandler

“`java
package com.netty;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

import java.util.Date;

/**
* Class TimeClientHandler …
*
* @author LiJun
* Created on 2018/8/27
*/
public class TimeClientHandler extends ChannelInboundHandlerAdapter {

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    ByteBuf byteBuf = (ByteBuf)msg;

    try {
        long currentTimeMillis = (byteBuf.readUnsignedInt() - 2208988800L)*1000L;
        System.out.println(new Date(currentTimeMillis));
        ctx.close();
    }finally {
        byteBuf.release();
    }
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
    cause.printStackTrace();
    ctx.close();
}

}

“`

解釋下:

  1. 在TCP/IP中,Netty會把讀到的數據放到ByteBuf這個數據結構中。

    啓動時間服務器:

TimeServer

“`java
package com.netty;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

/**
* Class TimeServer …
*
* @author LiJun
* Created on 2018/8/15
*/
public class TimeServer{
private int port;

public TimeServer() {
}

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

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

    try {
        ServerBootstrap b = new ServerBootstrap();
        b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) {
                        socketChannel.pipeline().addLast(new DiscardServerHandler());
                    }
                })
                .option(ChannelOption.SO_BACKLOG, 128)
                .childOption(ChannelOption.SO_KEEPALIVE, true);
        // 綁定端口,並開始接收進來的連接
        ChannelFuture channelFuture = b.bind(port).sync();

        // 等待服務器,socket關閉
        // 在這裏,不會發生什麼
        channelFuture.channel().closeFuture().sync();
    }finally {
        workerGroup.shutdownGracefully();
        bossGroup.shutdownGracefully();
    }

}

public static void main(String[] args) throws Exception {
    int port;
    if(args.length > 0){
        port = Integer.parseInt(args[0]);
    }else {
        port = 8080;
    }
    new TimeServer(port).run();
}

}

“`

六、處理一個基於流的傳輸

注意事項:基於流的傳輸(例如TCP/IP)接收到的數據是存在於socket接收的buffer中的,而且這個基於流的傳輸不是數據包隊列,而是字節隊列,所以不能保證遠程寫入數據的準確讀取。由於這個原因,接收部分需要將接收的數據進行碎片整理,整理成一個或多個可以被應用邏輯輕鬆識別的有意義框架。

例如:

  • 遠程傳輸的數據爲 ABC DEF GHI
  • 沒處理接收到的數據就可能爲 AB CDEFG H I 因爲是字節,數據錯亂了
  • 我們需要將數據處理成 ABC DEF GHI 這種能被識別的數據。

由此:

(一)、解決方法一

以TIME客戶端爲例。32位整型是非常小的數據,可能會被拆分到不同的數據段內,並且,碎片化的可能性會隨着流量的增加而增加。所以我們可以構造一個內部的可積累的緩衝,知道4個字節全部接收到了內部緩衝區。

修改後的TimeClientHandler:

package com.netty;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

import java.util.Date;

/**
 * Class TimeClientHandler2 ...
 *
 * @author LiJun
 * Created on 2018/8/27
 */
public class TimeClientHandler2  extends ChannelInboundHandlerAdapter {
    private ByteBuf byteBuf;

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        byteBuf = ctx.alloc().buffer(4);
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        byteBuf.release();
        byteBuf = null;

    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf m = (ByteBuf)msg;
        byteBuf.writeBytes(m);
        m.release();
        if(byteBuf.readableBytes() >= 4){
            long currentTimeMills = (byteBuf.readUnsignedInt() - 2208988800L)*1000L;
            System.out.println(new Date(currentTimeMills));
            ctx.close()
        }
    }

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

解釋下:

  1. ChannelHandler有兩個生命週期的監聽方法:handlerAdded()和handlerRemoved()。我們可以完成任意初始化。
  2. 所有接收的數據都被積累在了byteBuf變量中。
  3. 處理器需要檢查byteBuf照片那個是否有足夠數據,如果有,則進行邏輯處理,否則就重複調用channelRead()方法獲得更多數據,知道數據夠了爲止。

(二)、解決方法二

上面的方法維護起來麻煩。我們可以吧ChannelHandler拆分成多個模塊以減少應用的複雜度。

我們可以把上面的TimeClientHandler拆分成;兩個處理器:

  • TimeDecoder處理數據拆分的問題
  • TimeClientHandler 原始版本的實現。

這就要用到Netty提供的可擴展類了:ByteToMessageDecoder

TimeDecoder

package com.netty;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;

import java.util.List;

/**
 * Class TimeDecoder ...
 *
 * @author LiJun
 * Created on 2018/8/27
 */
public class TimeDecoder extends ByteToMessageDecoder {
    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
        if(byteBuf.readableBytes() < 4){
            return;
        }
        list.add(byteBuf.readBytes(4));

    }
}

解釋下:

  1. ByteToMessageDecoderChannelInboundHandler的· 一個實現可,可以吧處理數據拆分的問題變得簡單。
  2. 每當有新數據接收的時候,ByteToMessageDecoder都會調用decode()方法來處理內部的累積緩衝byteBuf
  3. decode()方法可以決定累積緩衝裏有沒有足夠數據可以往out對象裏放任意數據。當有更多的數據被接收了,ByteToMessageDecoder會再次調用decode()方法。
  4. 如果在 decode()方法裏增加了一個對象到 out 對象裏,這意味着解碼器解碼消息成功。 ByteToMessageDecoder 將會丟棄在累積緩衝裏已經被讀過的數據。我們不需要對多條消息調用 decode(),ByteToMessageDecoder 會持續調用 decode() 直到不放任何數據到 out 裏。

修改TImeClient的ChannelInitializer實現:

bootstrap.handler(new ChannelInitializer<SocketChannel>() {
    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        socketChannel.pipeline().addLast(new TimeDecoder(), new TimeClientHandler());
    }
});

這還有一個解碼器:ReplayingDecoder,需要去了解API獲取更多信息。

package com.netty;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ReplayingDecoder;

import java.util.List;

/**
 * Class TimeDecoder2 ...
 *
 * @author LiJun
 * Created on 2018/8/27
 */
public class TimeDecoder2  extends ReplayingDecoder<Void> {


    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
        list.add(byteBuf.readBytes(4));
    }
}

Netty還提供了不少開箱即用的解碼器

七、用POJO代替ByteBuf

在此之前我們都是使用ByteBu作爲協議消息的數據結構。我們接下來就要使用POJO代替ByteBuf。

這樣做的好處:

  • 通過從ChannelHandler中提取出ByteBuf的代碼,使ChannelHandler的實現變得更加可維護和可重用。

我們改善我們的時間客戶端:

首先我們定義一個新的類:UnixTime

package com.netty;

import java.util.Date;

/**
 * Class UnixTime ...
 *
 * @author LiJun
 * Created on 2018/8/27
 */
public class UnixTime {
    private final long value;

    public UnixTime() {
        this(System.currentTimeMillis()/1000L + 2208988800L);
    }

    public UnixTime(long value) {
        this.value = value;
    }

    public long getValue() {
        return value;
    }

    @Override
    public String toString() {
        return new Date((getValue() - 2208988800L)*1000L).toString();
    }

}

修改TimeDecoder類,返回UnixTime:

package com.netty;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;

import java.util.List;

/**
 * Class TimeDecoder3 ...
 *
 * @author LiJun
 * Created on 2018/8/27
 */
public class TimeDecoder3 extends ByteToMessageDecoder {
    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
        if(byteBuf.readableBytes() < 4){
            return;
        }
        list.add(new UnixTime(byteBuf.readUnsignedInt()));
    }
}

修改TimeClientHandler繼承的channelRead()方法:

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    UnixTime unixTime = (UnixTime)msg;
    System.out.println(unixTime);
    ctx.close();
}

同樣我們服務端也可以改:

java
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ChannelFuture future = ctx.writeAndFlush(new UnixTime());
future.addListener(ChannelFutureListener.CLOSE);
}

現在我們就編寫一個編碼器實現ChannelOutboundHandler,, 用來將UnixTime對象轉化爲一個ByteBuf:

package com.netty;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOutboundBuffer;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.ChannelPromise;

/**
 * Class TimeEncoder ...
 *
 * @author LiJun
 * Created on 2018/8/27
 */
public class TimeEncoder extends ChannelOutboundHandlerAdapter {
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        UnixTime unixTime = (UnixTime)msg;
        ByteBuf encoded = ctx.alloc().buffer(4);
        encoded.writeInt((int)unixTime.getValue());
        ctx.write(encoded, promise);

    }
}

注意下:

  1. 通過ChannelPromise,當編碼後的數據被寫到通道上Netty可以通過這個對象標記成功還是失敗。
  2. 我們不需要調用ctx.flush(),因爲處理器單獨分離除了一個void flush()方法了。

如果要進一步簡化代碼的話:

package com.netty;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOutboundBuffer;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.ChannelPromise;
import io.netty.handler.codec.MessageToByteEncoder;

/**
 * Class TimeEncoder ...
 *
 * @author LiJun
 * Created on 2018/8/27
 */
public class TimeEncoder extends MessageToByteEncoder<UnixTime> {
    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, UnixTime unixTime, ByteBuf byteBuf) throws Exception {
        byteBuf.writeInt((int)unixTime.getValue());
    }
}

最後,我們需要在TimeServerHandler之前把TimeEncoder插入到ChannelPipeline。

八、關閉我們的應用

關閉一個 Netty 應用往往只需要簡單地通過 shutdownGracefully() 方法來關閉你構建的所有的EventLoopGroup。當EventLoopGroup 被完全地終止,並且對應的所有 channel 都已經被關閉時,Netty 會返回一個Future對象來通知你。

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