Netty實踐入門-編寫簡單伺服器


按照以前的套路,一般學習一項新的IT技術,首先得來個 ‘Hello,World!‘之類的,以向世界宣告你要學習某項技術了。但在世界上最簡單的協定不是’Hello,World!‘而是 DISCARD。它是一種丟棄任何接收到的資料而沒有任何響應的協定(暫時就叫它」裝死協定」吧)。

要實現DISCARD協定,只需要忽略所有接收到的資料。讓我們從處理程式實現直接開始,這個處理程式實現處理Netty生成的I/O事件。先擼下面幾串程式碼吧 -

package com.yiibai.netty;

import io.netty.buffer.ByteBuf;

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

/**
 * 處理伺服器端通道
 */
public class DiscardServerHandler extends ChannelInboundHandlerAdapter { // (1)

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) { // (2)
        // 以靜默方式丟棄接收的資料
        ((ByteBuf) msg).release(); // (3)
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (4)
        // 出現異常時關閉連線。
        cause.printStackTrace();
        ctx.close();
    }
}

一些重要的解釋:

  1. DiscardServerHandler擴充套件了ChannelInboundHandlerAdapter,它是ChannelInboundHandler的一個實現。 ChannelInboundHandler提供了可以覆蓋的各種事件處理程式方法。 現在,它只是擴充套件了ChannelInboundHandlerAdapter,而不是自己實現處理程式介面。

  2. 我們在這裡覆蓋channelRead()事件處理程式方法。每當從用戶端接收到新資料時,使用該方法來接收用戶端的訊息。 在此範例中,接收到的訊息的型別為ByteBuf

  3. 要實現DISCARD協定,處理程式必須忽略接收到的訊息。ByteBuf是參照計數的物件,必須通過release()方法顯式釋放。請記住,處理程式負責釋放傳遞給處理程式的參照計數物件。 通常,channelRead()處理程式方法實現如下:

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
     try {
         // Do something with msg
     } finally {
         ReferenceCountUtil.release(msg);
     }
    }
    

    當Netty由於I/O錯誤或由處理事件時丟擲的異常而導致的處理程式實現引發異常時,使用Throwable呼叫exceptionCaught()事件處理程式方法。 在大多數情況下,捕獲的異常會被記錄,並且其相關的通道應該在這裡關閉,這種方法的實現可以根據想要什麼樣的方式來處理異常情況而有所不同。 例如,您可能希望在關閉連線之前傳送帶有錯誤程式碼的響應訊息。

到現在如果沒有問題,我們已經實現了DISCARD伺服器的前半部分。 現在剩下的就是編寫main()方法,並使用DiscardServerHandler啟動伺服器。

package com.yiibai.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;

/**
 * Discards any incoming data.
 */
public class DiscardServer {

    private int port;

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

    public void run() throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1)
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap(); // (2)
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class) // (3)
             .childHandler(new ChannelInitializer<SocketChannel>() { // (4)
                 @Override
                 public void initChannel(SocketChannel ch) throws Exception {
                     ch.pipeline().addLast(new DiscardServerHandler());
                 }
             })
             .option(ChannelOption.SO_BACKLOG, 128)          // (5)
             .childOption(ChannelOption.SO_KEEPALIVE, true); // (6)

            // Bind and start to accept incoming connections.
            ChannelFuture f = b.bind(port).sync(); // (7)

            // Wait until the server socket is closed.
            // In this example, this does not happen, but you can do that to gracefully
            // shut down your server.
            f.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實現。 在此範例中,實現的是伺服器端應用程式,因此將使用兩個NioEventLoopGroup。 第一個通常稱為「boss」,接受傳入連線。 第二個通常稱為「worker」,當「boss」接受連線並且向「worker」註冊接受連線,則「worker」處理所接受連線的流量。 使用多少個執行緒以及如何將它們對映到建立的通道取決於EventLoopGroup實現,甚至可以通過建構函式進行組態。

  2. ServerBootstrap是一個用於設定伺服器的助手類。 您可以直接使用通道設定伺服器。 但是,請注意,這是一個冗長的過程,在大多數情況下不需要這樣做。

  3. 在這裡,我們指定使用NioServerSocketChannel類,該類用於範例化新的通道以接受傳入連線。

  4. 此處指定的處理程式將始終由新接受的通道計算。 ChannelInitializer是一個特殊的處理程式,用於幫助使用者組態新的通道。 很可能要通過新增一些處理程式(例如DiscardServerHandler)來組態新通道的ChannelPipeline來實現您的網路應用程式。 隨著應用程式變得複雜,可能會向管道中新增更多處理程式,並最終將此匿名類提取到頂級類中。

  5. 還可以設定指定Channel實現的引數。這裡編寫的是一個TCP/IP伺服器,所以我們允許設定通訊端選項,如tcpNoDelaykeepAlive。 請參閱ChannelOption的apidocs和指定的ChannelConfig實現,以了解關於ChannelOptions

  6. 你注意到option()childOption()沒有? option()用於接受傳入連線的NioServerSocketChannelchildOption()用於由父ServerChannel接受的通道,在這個範例中為NioServerSocketChannel

  7. 現在準備好了。剩下的是系結到埠和啟動伺服器。 這裡,我們系結到機器中所有NIC(網路介面卡)的埠:8080。 現在可以根據需要多次呼叫bind()方法(使用不同的系結地址)。
    恭喜!這就完成了一個基於 Netty 的第一個伺服器。

檢視接收的資料

現在我們已經編寫了第一個伺服器,還需要測試它是否真的有效地執行工作。測試它的最簡單的方法是使用telnet命令。 例如,可以在命令列中輸入telnet localhost 8080並鍵入內容。

但是,能驗證伺服器工作正常嗎? 其實不能真正知道,因為它是一個」丟棄「(什麼也不處理)伺服器。所以傳送什麼請求根本不會得到任何反應。 為了證明它是真的在執行工作,我們還修改一點伺服器端上的程式碼 - 列印它收到了什麼東西。

前面我們已經知道,只要接收到資料,就呼叫channelRead()方法。現在把一些程式碼放到DiscardServerHandlerchannelRead()方法中,如下所示:

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    ByteBuf in = (ByteBuf) msg;
    try {
        while (in.isReadable()) { // (1)
            System.out.print((char) in.readByte());
            System.out.flush();
        }
    } finally {
        ReferenceCountUtil.release(msg); // (2)
    }
    // 或者直接列印
    System.out.println("Yes, A new client in = " + ctx.name());
}

這個低效的迴圈實際上可以簡化為:

System.out.println(in.toString(io.netty.util.CharsetUtil.US_ASCII))

或者,可以在這裡寫上:in.release()

最後看一看專案的檔案結構,如下所示 -

執行 DiscardServer ,輸出結果如下 -

三月 01, 2017 00:02:07 上午 io.netty.handler.logging.LoggingHandler channelRegistered
資訊: [id: 0x4f99602f] REGISTERED
三月 01, 2017 00:02:07 上午 io.netty.handler.logging.LoggingHandler bind
資訊: [id: 0x4f99602f] BIND: 0.0.0.0/0.0.0.0:8080
三月 01, 2017 00:02:07 上午 io.netty.handler.logging.LoggingHandler channelActive
資訊: [id: 0x4f99602f, L:/0:0:0:0:0:0:0:0:8080] ACTIVE

如果再次執行telnet localhost 8080命令,將會看到伺服器列印接收到的內容。

三月 01, 2017 2:14:14 上午 io.netty.handler.logging.LoggingHandler channelRegistered
資訊: [id: 0xd63646da] REGISTERED
三月 01, 2017 2:14:14 上午 io.netty.handler.logging.LoggingHandler bind
資訊: [id: 0xd63646da] BIND: 0.0.0.0/0.0.0.0:8080
三月 01, 2017 2:14:14 上午 io.netty.handler.logging.LoggingHandler channelActive
資訊: [id: 0xd63646da, L:/0:0:0:0:0:0:0:0:8080] ACTIVE
三月 01, 2017 2:14:32 上午 io.netty.handler.logging.LoggingHandler channelRead
資訊: [id: 0xd63646da, L:/0:0:0:0:0:0:0:0:8080] RECEIVED: [id: 0x452d2ebd, L:/0:0:0:0:0:0:0:1:8080 - R:/0:0:0:0:0:0:0:1:58248]
Yes, A new client in = DiscardServerHandler#0
Yes, A new client in = DiscardServerHandler#0

上面的輸出證明,這個伺服器程式是可以正常執行工作的。