Netty入門系列(1) --使用Netty搭建服務端和客戶端

正號先生發表於2019-05-08

引言

前面我們介紹了網路一些基本的概念,雖然說這些很難吧,但是至少要做到理解吧。有了之前的基礎,我們來正式揭開Netty這神祕的面紗就會簡單很多。

服務端

public class PrintServer {

    public void bind(int port) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup();                     //1
        EventLoopGroup workerGroup = new NioEventLoopGroup();                   //2
        try {
            ServerBootstrap b = new ServerBootstrap();                          //3
            b.group(bossGroup, workerGroup)                                     //4                                         
                    .channel(NioServerSocketChannel.class)                      //5
                    .option(ChannelOption.SO_BACKLOG, 1024)                     //6
                    .childHandler(new ChannelInitializer<SocketChannel>() {     //7
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new PrintServerHandler());
                        }
                    });

            ChannelFuture f = b.bind(port).sync();              //8
            
            f.channel().closeFuture().sync();                   //9
        } finally {
            // 優雅退出,釋放執行緒池資源
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }


    /**
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
        int port = 8080;
        new TimeServer().bind(port);
    }
}

我們來分析一下上面的這段程式碼(下面的每一點對應上面的註釋)

1~2:首先我們建立了兩個NioEventLoopGroup例項,它是一個由Netty封裝好的包含NIO的執行緒組。為什麼建立兩個?我想經過前面的學習大家應該都清楚了。對,因為Netty的底層是IO多路複用,bossGroup 是用於接收客戶端的連線,原理就是一個實現的Selector的Reactor執行緒。而workerGroup用於進行SocketChannel的網路讀寫。

3:建立一個ServerBootstrap物件,可以把它想象成Netty的入口,通過這類來啟動Netty,將所需要的引數傳遞到該類當中,大大降低了的開發難度。

4:將兩個NioEventLoopGroup例項繫結到ServerBootstrap物件中。

5:建立Channel(典型的channel有NioSocketChannel,NioServerSocketChannel,OioSocketChannel,OioServerSocketChannel,EpollSocketChannel,EpollServerSocketChannel),這裡建立的是NIOserverSocketChannel,它的功能可以理解為當接受到客戶端的連線請求的時候,完成TCP三次握手,TCP物理鏈路建立成功。並將該“通道”與workerGroup執行緒組的某個執行緒相關聯。

6:設定引數,這裡設定的SO_BACKLOG,意思是客戶端連線等待佇列的長度為1024.

7:建立連線後的具體Handler。就是我們接受資料後的具體操作,例如:記錄日誌,對資訊解碼編碼等。

8:繫結埠,同步等待成功

9:等待服務端監聽埠關閉

繫結該服務端的Handler

public class PrintServerHandler extends ChannelHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg)
        throws Exception {
    ByteBuf buf = (ByteBuf) msg;                                        //1
    byte[] req = new byte[buf.readableBytes()]; 
    buf.readBytes(req); //將快取區的位元組陣列複製到新建的req陣列中
    String body = new String(req, "UTF-8");
    System.out.println(body);
    String response= "列印成功";
    ByteBuf resp = Unpooled.copiedBuffer(response.getBytes());                      
    ctx.write(resp);                                                    //2
    }   

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
    ctx.flush();                                                        //3
    }


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

PrintServerHandler 繼承 ChannelHandlerAdapter ,在這裡它的功能為 列印客戶端發來的資料並且返回客戶端列印成功。

我們只需要實現channelRead,exceptionCaught,前一個為接受訊息具體邏輯的實現,後一個為發生異常後的具體邏輯實現。

1:我們可以看到,接受的訊息被封裝為了Object ,我們將其轉換為ByteBuf ,前一章的講解中也說明了該類的作用。我們需要讀取的資料就在該快取類中。

2~3:我們將寫好的資料封裝到ByteBuf中,然後通過write方法寫回到客戶端,這裡的3呼叫flush方法的作用為,防止頻繁的傳送資料,write方法並不直接將資料寫入SocketChannel中,而是把待傳送的資料放到傳送快取陣列中,再呼叫flush方法傳送資料。

客戶端

public class PrintClient {

    public void connect(int port, String host) throws Exception {
    EventLoopGroup group = new NioEventLoopGroup();                 //1
    try {
        Bootstrap b = new Bootstrap();                              //2
         b.group(group)                                             //3
            .channel(NioSocketChannel.class)                        //4
            .option(ChannelOption.TCP_NODELAY, true)                //5
            .handler(new ChannelInitializer<SocketChannel>() {      //6
            @Override
            public void initChannel(SocketChannel ch)               
                throws Exception {
                ch.pipeline().addLast(new TimeClientHandler());
            }
            });

        ChannelFuture f = b.connect(host, port).sync();             //7
        f.channel().closeFuture().sync();                           //8
    } finally {
        // 優雅退出,釋放NIO執行緒組
        group.shutdownGracefully();
    }
    }

    /**
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
    int port = 8080;
    new TimeClient().connect(port, "127.0.0.1");
    }
}

我們繼續來分析一下上面的這段程式碼(下面的每一點對應上面的註釋)

1:區別於服務端,我們在客戶端只建立了一個NioEventLoopGroup例項,因為客戶端你並不需要使用I/O多路複用模型,需要有一個Reactor來接受請求。只需要單純的讀寫資料即可

2:區別於服務端,我們在客戶端只需要建立一個Bootstrap物件,它是客戶端輔助啟動類,功能類似於ServerBootstrap。

3:將NioEventLoopGroup例項繫結到Bootstrap物件中。

4:建立Channel(典型的channel有NioSocketChannel,NioServerSocketChannel,OioSocketChannel,OioServerSocketChannel,EpollSocketChannel,EpollServerSocketChannel),區別與服務端,這裡建立的是NIOSocketChannel.

5:設定引數,這裡設定的TCP_NODELAY為true,意思是關閉延遲傳送,一有訊息就立即傳送,預設為false。

6:建立連線後的具體Handler。注意這裡區別與服務端,使用的是handler()而不是childHandler()。handler和childHandler的區別在於,handler是接受或傳送之前的執行器;childHandler為建立連線之後的執行器。

7:發起非同步連線操作

8:當代客戶端鏈路關閉

繫結該客戶端的Handler

public class PrintClientHandler extends ChannelHandlerAdapter {

    private static final Logger logger = Logger
        .getLogger(TimeClientHandler.class.getName());

    private final ByteBuf firstMessage;

    /**
     * Creates a client-side handler.
     */
    public TimeClientHandler() {
    byte[] req = "你好服務端".getBytes();
    firstMessage = Unpooled.buffer(req.length);                                 //1
    firstMessage.writeBytes(req);

    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
    ctx.writeAndFlush(firstMessage);                                            //2             
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg)              //3
        throws Exception {
    ByteBuf buf = (ByteBuf) msg;    
    byte[] req = new byte[buf.readableBytes()];
    buf.readBytes(req);
    String body = new String(req, "UTF-8");
    System.out.println("服務端回應訊息 : " + body);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {   //4
    // 釋放資源
    System.out.println("Unexpected exception from downstream : "
        + cause.getMessage());
    ctx.close();
    }
}

PrintClientHandler 繼承 ChannelHandlerAdapter ,在這裡它的功能為 傳送資料並列印服務端發來的資料。

我們只需要實現channelActive,channelRead,exceptionCaught,第一個為建立連線後立即執行,後兩個與一個為接受訊息具體邏輯的實現,另一個為發生異常後的具體邏輯實現。

1:將傳送的資訊封裝到ByteBuf中。

2:傳送訊息。

3:接受客戶端的訊息並列印

4:發生異常時,列印異常資訊,釋放客戶端資源

總結

這是一個入門程式,對應前面所講的I/O多路複用模型以及NIO的特性,能很有效的理解該模式的程式設計方式。如果這幾段程式碼看著很費勁,那麼可以看看之前博主的Netty基礎系列。

如果博主哪裡說得有問題,希望大家提出來,一起進步~

相關文章