Netty快速入門

目錄

1.編寫Echo服務器

1.1 ChannelHandler和業務邏輯

1.2 引導服務器

2.編寫Echo客戶端

2.1 通過ChannelHandler實現客戶端邏輯

2.2 引導客戶端

3. 編譯運行

4.小結


本章我們將展示如何構建一個基於Netty的客戶端和服務器。應用程序非常簡單,客戶端將消息發送給服務端,而服務器再將消息回送給客戶端。開發環境搭建步驟我們直接跳過了,這裏有一點需要注意的是我們下載的是JDK,而不是Java運行環境(JRE),其只可以運行Java應用程序,但是不能夠編譯它們。

下圖展示了我們要編寫的Echo客戶端和服務器應用程序:

Echo客戶端和服務器之間的交互非常簡單的,在客戶端建立一個連接之後,它會向服務器發送一個或多個消息,反過來,服務器又將每個消息回送給客戶端。

1.編寫Echo服務器

所有的Netty服務器都需要以下兩個部分:

  1. 至少一個ChannelHandler:該組件實現了服務器對從客戶端接收的數據的處理,即它的業務邏輯。
  2. 引導——這是配置服務器的啓動代碼。至少,它會將服務器綁定到它要監聽連接請求的端口上。

1.1 ChannelHandler和業務邏輯

ChannelHandler是一個接口族的父接口,它的實現負責接收並響應事件通知。在Netty應用程序中,所有的數據處理邏輯都包含在這些核心抽象的實現中。

因爲我們的Echo服務器會響應傳入的消息,所以它需要實現ChannelInboundHandler接口,用來定義響應入站事件的方法。這個簡單的應用程序只需要用到少量的這些方法,所以繼承ChannelInboundHandlerAdapter類就足夠了,它提供了ChannelInboundHandler的默認實現。

我們項目中主要用到了下面的方法:

  • channelRead():對於每個傳入的消息都要調用
  • channelReadComplete():通知ChannelInboundHandler,最後一次對channelRead調用(讀取最後一次消息)
  • exceptionCaught():在讀取操作期間,有異常拋出時會調用

該Echo服務器的ChannelHandler實現是EchoServerHandler,實現代碼如下:

package com.martin.learn.netty.server;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;

import java.nio.charset.Charset;

/**
 * 負責接收並響應時間的通知
 *
 * @author: martin
 * @date: 2018/12/21 20:38
 * @description:
 */

/**
 * 標識一個ChannelHandler可以被多個Channel安全的共享
 */
@ChannelHandler.Sharable
public class EchoServiceHandler extends ChannelInboundHandlerAdapter {

    /**
     * 對於每個傳入的消息都要調用
     *
     * @param ctx
     * @param msg
     * @throws Exception
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf in = (ByteBuf) msg;
        System.out.println("Server received:" + in.toString(CharsetUtil.UTF_8));
        //將接收到的消息寫給發送者
        ctx.write(in);
    }

    /**
     * 讀取操作完成時調用
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        //將未決的消息沖刷到遠程節點,並且關閉該Channel
        ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
    }

    /**
     * 在讀取操作期間,有異常拋出時調用
     *
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        //關閉該Channel
        ctx.close();
    }
}

ChannelInboundHandlerAdapter有一個直觀的API,並且它的每個方法都可以被重寫以掛鉤到事件生命週期的恰當點上。因爲需要處理所有接收到的數據,所以我們重寫了channelRead()方法。

重寫exceptionCaught()方法允許我們對Throwable的任何子類型做出處理,在這裏我們記錄了異常並關閉了連接。

如果我們在這裏不捕獲異常,會發生什麼呢?

每個Channel都擁有一個與之相關聯的ChannelPipeline,其持有一個ChannelHandler的實例鏈。在默認的情況下,ChannelHadler會把對它的方法的調用轉發給鏈中的下一個ChannelHandler。因此,如果exceptionCaught()方法沒有被該鏈中的某處實現,那麼所接收的異常將會被傳遞到ChannelPipeline的尾端並被記錄。爲此,我們的應用程序應該提供至少一個實現了exceptionCaught()方法的ChannelHandler。

關鍵點總結:

  • 針對不同類型的事件來調用ChannelHandler。
  • 應用程序通過實現或者擴展ChannelHandler來掛鉤到事件的生命週期,並且提供自定義的應用程序邏輯。
  • 在架構上,ChannelHandler有助於保持業務邏輯與網絡代理代碼的分離,這簡化了開發過程。

1.2 引導服務器

引導服務器涉及以下內容:

  • 綁定到服務器將在其上監聽並接受傳入連接請求的端口。
  • 配置Channel,以便將有關的入站消息通知給EchoServerHandler實例。

實現代碼如下:

package com.martin.learn.netty.server;

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

import java.net.InetSocketAddress;

/**
 * 服務器類
 *
 * @author: martin
 * @date: 2018/12/22 9:54
 * @description:
 */
public class EchoServer {
    private final int port;

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

    public void start() throws InterruptedException {
        final EchoServiceHandler serviceHandler = new EchoServiceHandler();
        //創建並分配一個NioEventLoopGroup實例以進行事件的處理,如接受新連接以及讀寫數據
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(group)
                    //指定所使用的NIO傳輸Channel
                    .channel(NioServerSocketChannel.class)
                    //使用指定的端口設置套接字的地址
                    .localAddress(new InetSocketAddress(port))
                    //添加一個EchoServerHandler到子Channel的ChannelPipeline
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            //EchoServerHandler被標註爲@Shareable,所以我們總是使用相同的實例
                            socketChannel.pipeline().addLast(serviceHandler);
                        }
                    });
            //異步地綁定服務器,調用sync()方法阻塞等待直到綁定完成
            ChannelFuture future = bootstrap.bind().sync();
            //獲取Channel的CloseFuture,並且阻塞當前線程直到它完成
            future.channel().closeFuture().sync();
        } finally {
            //關閉EventLoopGroup,釋放所有的資源
            group.shutdownGracefully().sync();
        }


    }

    public static void main(String[] args) throws Exception {
        if (args.length != 1) {
            System.err.println("必須指定服務器端口");
        }

        int port = Integer.parseInt(args[0]);
        new EchoServer(port).start();
    }
}

這個實力使用了NIO,因爲得益於它的可擴展性和徹底的異步性,它是目前使用最廣泛的傳輸。但是也可以使用一個不同的傳輸實現,比如OIO傳輸,將需要指定OioServerSocketChannel和OioEventLoopGroup。引導的步驟如下:

  • 創建一個ServerBootstrap的實例以引導和綁定服務器
  • 創建並分配一個NioEventLoopGroup實例以進行事件的處理,如接受新連接以及讀/寫數據。
  • 指定服務器綁定的本地的InetSocketAddress
  • 使用一個EchoServerHandler的實例初始化每一個新的Channel
  • 調用ServerBootstrap.bind()方法以綁定服務器

這個時候,服務器已經初始化,並且就緒已經能被使用了。

2.編寫Echo客戶端

Echo客戶端將會:

  1. 連接到服務器
  2. 發送一個或者多個消息
  3. 對於每個消息,等待並接收從服務器發回的相同的消息
  4. 關閉連接

2.1 通過ChannelHandler實現客戶端邏輯

客戶端將擁有一個用來處理數據的ChannelInboundHandler,在這裏我們擴展SimpleChannelInboundHandler類以處理所有必須的任務。這要求重寫下面的方法:

  • channelActive():在到服務器的連接已經建立之後將被調用
  • channelRead():當從服務器接收到一條消息時被調用
  • exceptionCaught():在處理過程中引發異常時被調用

實例代碼如下:

package com.martin.learn.netty.client;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.CharsetUtil;

/**
 * @author: martin
 * @date: 2018/12/22 23:00
 * @description:
 */
@ChannelHandler.Sharable
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {

    /**
     * 在到服務器的連接已經建立之後將被調用
     *
     * @param ctx
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        //當被通知Channel是活躍的時候,發送一條消息
        ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!", CharsetUtil.UTF_8));
    }

    /**
     * 在處理過程中引發異常的時候被調用
     *
     * @param ctx
     * @param cause
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        //在發生異常的時候,記錄錯誤並關閉Channel
        cause.printStackTrace();
        ctx.close();
    }

    /**
     * 每當從服務器接收到一條消息時被調用
     *
     * @param channelHandlerContext
     * @param byteBuf
     */
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) {
        //記錄已接收消息的轉儲
        System.out.println("Client received:" + byteBuf.toString(CharsetUtil.UTF_8));
    }
}

首先,我們重寫了channelActive()方法,其將在一個連接建立時被調用,這確保了數據將會被儘可能快地寫入服務器。

接下來,我們重寫了channelRead()方法,每當有數據接收時,都會調用這個方法。需要注意的是,由服務器發送的消息可能會被分塊接收。也就是說,如果服務器發送了5字節,那麼並不能保證這5個字節會被一次性接收。即使是這麼少的數據,channelRead()方法也有可能會被調用兩次,第一次使用一個持有3字節的ByteBuf,第二次使用一個持有2字節的ByteBuf。作爲一個面向流的協議,TCP保證了字節數組將會按照服務器發送它們的順序被接收。

最後,我們重寫了exceptionCaught(),記錄Throwable,關閉Channel,終止到服務器的連接。

2.2 引導客戶端

引導客戶端類似於引導服務器,使用代碼如下:

package com.martin.learn.netty.client;

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

import java.net.InetSocketAddress;

/**
 * 客戶端連接
 *
 * @author: martin
 * @date: 2018/12/22 23:36
 * @description:
 */
public class EchoClient {
    private final String host;
    private final int port;

    public EchoClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    public void start() throws Exception {
        //指定EventLoopGroup以處理客戶端的事件;適用於NIO的實現
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            //創建Bootstrap
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    //適用於NIO傳輸的Channel類型
                    .channel(NioSocketChannel.class)
                    //設置連接服務器的InetSocketAddress
                    .remoteAddress(new InetSocketAddress(host, port))
                    //在創建Channel時,向ChannelPipeline中添加一個EchoClientHandler實例
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast(new EchoClientHandler());
                        }
                    });
            //連接到遠程節點,阻塞等待直到連接完成
            ChannelFuture channelFuture = bootstrap.connect().sync();
            //阻塞直到Channel關閉
            channelFuture.channel().closeFuture().sync();
        } finally {
            //關閉線程池並且釋放所有的資源
            group.shutdownGracefully().sync();
        }
    }

    public static void main(String[] args) throws Exception {
        if (args.length != 2) {
            System.err.println("Usage:" + EchoClient.class.getSimpleName() + "<host><port>");
            return;
        }

        String host = args[0];
        int port = Integer.parseInt(args[1]);
        new EchoClient(host, port).start();
    }
}

和服務端一樣,使用了NIO傳輸。注意,我們可以在客戶端和服務端上分別使用不同的傳輸。例如,在服務器端使用NIO傳輸,而在客戶端使用OIO傳輸。實現客戶端的關鍵點如下:

  • 爲初始化客戶端,創建了一個Bootstrap實例。
  • 爲進行事件處理分配了一個NioEventLoopGroup實例,其中事件處理包括創建新的連接以及處理入站和出站數據。
  • 爲連接服務器創建了一個InetSocketAddress實例。
  • 當連接被建立時,一個EchoClientHandler實例會被安裝到ChannelPipeline中。
  • 在一切都設置完成之後,調用Bootsrap.connect()方法連接到遠程節點。

3. 編譯運行

完成客戶端和服務端的編寫之後,先運行服務端,再運行客戶端,輸出結果如下:

Server received:Netty rocks!

Client received:Netty rocks!

4.小結

在本章中,我們開發了一個簡單的Netty客戶端和服務端。雖然這只是一個簡單的應用程序,但是它可以伸縮到支持數千個併發連接,每秒可以比普通的基於套接字的Java應用程序處理多得多的消息。

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