Netty實踐-時間伺服器


本教學中實現的協定是TIME協定。 它與先前的範例不同,時間伺服器只傳送包含32位整數的訊息,而不接收任何請求,並在訊息傳送後關閉連線。 在本範例中,您將學習如何構造和傳送訊息,以及在完成時關閉連線。

因為時間伺服器將忽略任何接收到的資料,但是一旦建立連線就傳送訊息,所以我們不能使用channelRead()方法。而是覆蓋channelActive()方法。 以下是程式碼的實現:

package com.yiibai.netty.time;

public class TimeServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelActive(final ChannelHandlerContext ctx) { // (1)
        final ByteBuf time = ctx.alloc().buffer(4); // (2)
        time.writeInt((int) (System.currentTimeMillis() / 1000L + 2208988800L));

        final ChannelFuture f = ctx.writeAndFlush(time); // (3)
        f.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) {
                assert f == future;
                ctx.close();
            }
        }); // (4)
    }

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

下面我們來看看上面程式碼的一些解釋分析:

  1. 如上所述,當建立連線並準備好生成流量時,將呼叫channelActive()方法。現在在這個方法中編寫一個32位的整數來表示當前的時間。
  2. 要傳送新訊息,需要分配一個包含訊息的新緩衝區。我們要寫入一個32位元整數,因此需要一個ByteBuf,其容量至少為4個位元組。 通過ChannelHandlerContext.alloc()獲取當前的ByteBufAllocator並分配一個新的緩衝區。

  3. 像之前一樣,編寫構造的訊息。
    但是,在NIO中傳送訊息之前,我們是否曾呼叫java.nio.ByteBuffer.flip()? ByteBuf沒有這樣的方法,它只有兩個指標; 一個用於讀取操作,另一個用於寫入操作。 當您向ByteBuf寫入內容時,寫入索引會增加,而讀取器索引不會更改。讀取器索引和寫入器索引分別表示訊息的開始和結束位置。
    相比之下,NIO緩衝區不提供一個乾淨的方式來確定訊息內容開始和結束,而不用呼叫flip方法。當您忘記翻轉緩衝區時,就將會遇到麻煩,因為不會傳送任何或傳送不正確的資料。但是這樣的錯誤不會發生在Netty中,因為不同的操作型別我們有不同的指標。
    另一點要注意的是ChannelHandlerContext.write()(和writeAndFlush())方法返回一個ChannelFutureChannelFuture表示尚未發生的I/O操作。這意味著,任何請求的操作可能尚未執行,因為所有操作在Netty中是非同步的。 例如,以下程式碼可能會在傳送訊息之前關閉連線:

    Channel ch = ...;
    ch.writeAndFlush(message);
    ch.close();
    

    因此,需要在ChannelFuture完成後呼叫close()方法,該方法由write()方法返回,並在寫入操作完成時通知其監聽器。 請注意,close()也可能不會立即關閉連線,並返回一個ChannelFuture。

當寫請求完成時,我們如何得到通知? 這就像向返回的ChannelFuture新增ChannelFutureListener一樣簡單。 在這裡,我們建立了一個新的匿名ChannelFutureListener,當操作完成時關閉Channel

或者,可以使用預定義的偵聽器來簡化程式碼:

f.addListener(ChannelFutureListener.CLOSE);

要測試我們的時間伺服器是否按預期工作,可以使用UNIX rdate命令:

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

其中<port>是在main()方法中指定的埠號,<host>通常是localhost或伺服器的IP地址。

編寫時間用戶端

DISCARDECHO伺服器不同,我們需要一個用於TIME協定的用戶端,因為我們無法將32位二進位制資料轉換為日曆上的日期。 在本節中,我們討論如何確保伺服器正常工作並學習如何使用Netty編寫用戶端。

Netty中伺服器和用戶端之間最大的和唯一的區別是使用了不同的BootstrapChannel實現。 請看看下面的程式碼:

package com.yiibai.netty.time;

public class TimeClient {
    public static void main(String[] args) throws Exception {
        String host = args[0];
        int port = Integer.parseInt(args[1]);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            Bootstrap b = new Bootstrap(); // (1)
            b.group(workerGroup); // (2)
            b.channel(NioSocketChannel.class); // (3)
            b.option(ChannelOption.SO_KEEPALIVE, true); // (4)
            b.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                public void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new TimeClientHandler());
                }
            });

            // Start the client.
            ChannelFuture f = b.connect(host, port).sync(); // (5)

            // Wait until the connection is closed.
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
        }
    }
}
  1. BootstrapServerBootstrap類似,只是它用於非伺服器通道,例如用戶端或無連線通道。

  2. 如果只指定一個EventLoopGroup,它將同時用作boss組和worker組。boss組和worker組不是用於用戶端。

  3. 不使用NioServerSocketChannel,而是使用NioSocketChannel來建立用戶端通道。

  4. 注意,這裡不像我們使用的ServerBootstrap,所以不使用childOption(),因為用戶端SocketChannel沒有父類別。

  5. 應該呼叫connect()方法,而不是bind()方法。

如上面所見,它與伺服器端程式碼沒有什麼不同。 ChannelHandler實現又是怎麼樣的呢? 它應該從伺服器接收一個32位整數,將其轉換為人類可讀的格式,列印轉換為我們熟知的時間格式 ,並關閉連線:

package com.yiibai.netty.time;

import java.util.Date;

public class TimeClientHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
                ByteBuf m = (ByteBuf) msg; // (1)
        try {
            long currentTimeMillis = (m.readUnsignedInt() - 2208988800L) * 1000L;
            Date currentTime = new Date(currentTimeMillis);
            System.out.println("Default Date Format:" + currentTime.toString());

            SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String dateString = formatter.format(currentTime);
            // 轉換一下成中國人的時間格式
            System.out.println("Date Format:" + dateString);

            ctx.close();
        } finally {
            m.release();
        }
    }

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

(1).TCP/IP中,Netty讀取從對端傳送的ByteBuf資料。

用戶端看起來很簡單,與伺服器端範例沒什麼區別。 但是,這個處理程式有時會拒絕丟擲IndexOutOfBoundsException。 我們將在下一節討論為什麼會發生這種情況。

先執行 TimeServer.java 程式,然後再執行 TimeClient.java , 當執行 TimeClient.java時就可以到有一個時間日期輸出,然後程式自動退出。輸出結果如下 -

Default Date Format:Thu Mar 02 20:50:23 CST 2017
Date Format:2017-03-02 20:50:23