網路應用框架Netty快速入門

SnailClimb發表於2018-03-30

一 初遇Netty

Netty是什麼?

Netty 是一個提供 asynchronous event-driven (非同步事件驅動)的網路應用框架,是一個用以快速開發高效能、可擴充套件協議的伺服器和客戶端。

Netty能做什麼?

Netty 是一個 NIO 客戶端伺服器框架,使用它可以快速簡單地開發網路應用程式,比如伺服器(HTTP伺服器,FTP伺服器,WebSocket伺服器,Redis的Proxy伺服器等等)和客戶端的協議。Netty 大大簡化了網路程式的開發過程比如 TCP 和 UDP 的 socket 服務的開發。

Netty為什麼好?

Netty是建立在NIO基礎之上,Netty在NIO之上又提供了更高層次的抽象,使用它你可以更容易利用Java NIO提高服務端和客戶端的效能。

Netty的特性

1. 設計

1.1 統一的API,適用於不同的協議(阻塞和非阻塞)

1.2 基於可擴充套件和靈活的事件驅動模型

1.3高度可定製的執行緒模型 - 單執行緒,一個或多個執行緒池,如SEDA

1.4真正的無連線資料包套接字支援(自3.1以來)
複製程式碼

2. 效能

 2.1更好的吞吐量,低延遲

 2.2更省資源

 2.3儘量減少不必要的記憶體拷貝
複製程式碼

3. 安全

 完整的SSL / TLS和StartTLS協議的支援
複製程式碼

4. 易用性

 4.1 官方有詳細的使用指南

 4.2 對環境要求很低
複製程式碼

NIO和IO的區別是什麼?

1. 一個面向位元組一個面向緩衝;

IO面向流意味著每次從流中讀一個或多個位元組,直至讀取所有位元組,它們沒有被快取在任何地方。此外,它不能前後移動流中的資料。如果需要前後移動從流中讀取的資料,需要先將它快取到一個緩衝區。 Java NIO的緩衝導向方法略有不同。資料讀取到一個它稍後處理的緩衝區,需要時可在緩衝區中前後移動。這就增加了處理過程中的靈活性。但是,還需要檢查是否該緩衝區中包含所有您需要處理的資料。而且,需確保當更多的資料讀入緩衝區時,不要覆蓋緩衝區裡尚未處理的資料。

2. NIO是非阻塞IO,IO是阻塞IO

阻塞意味著當一個執行緒呼叫read() 或 write()時,該執行緒被阻塞,直到有一些資料被讀取,或資料完全寫入,該執行緒在此期間不能再幹任何事情了。而非阻塞不會這樣。

二 Netty使用

環境要求:

  • JDK 7+
  • Maven 3.2.x
  • Netty 4.x

Maven依賴:

		<dependency>
			<groupId>io.netty</groupId>
			<artifactId>netty-all</artifactId>
			<version>4.0.32.Final</version>
		</dependency>
複製程式碼

以下Netty examples來源: 官方文件

2.1 寫個拋棄伺服器

DiscardServerHandler.java

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

/**
 * handler 是由 Netty 生成用來處理 I/O 事件的。
 */
public class DiscardServerHandler extends ChannelInboundHandlerAdapter { // (1)
    /**
     * 這裡我們覆蓋了 chanelRead() 事件處理方法。
     * 每當從客戶端收到新的資料時,這個方法會在收到訊息時被呼叫。
     *((ByteBuf) msg).release():丟棄資料
     */
    @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();
    }
}
複製程式碼

目前我們已經實現了 DISCARD 伺服器的一半功能,剩下的需要編寫一個 main() 方法來啟動服務端的 DiscardServerHandler。

DiscardServer.java

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;

/**
 * 啟動服務端的 DiscardServerHandler
 */
public class DiscardServer {

    private int port;

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

    public void run() throws Exception {
    	//在這個例子中我們實現了一個服務端的應用,因此會有2個 NioEventLoopGroup 會被使用。
        //第一個經常被叫做‘boss’,用來接收進來的連線。
    	//第二個經常被叫做‘worker’,用來處理已經被接收的連線,一旦‘boss’接收到連線,就會把連線資訊註冊到‘worker’上。
    	EventLoopGroup bossGroup = new NioEventLoopGroup(); 
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
        	//啟動 NIO 服務的輔助啟動類
            ServerBootstrap serverBootstrap = new ServerBootstrap(); 
            //用於處理ServerChannel和Channel的所有事件和IO。
            serverBootstrap.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)

            // 繫結埠,開始接收進來的連線
            ChannelFuture f = serverBootstrap.bind(port).sync(); // (7)

            // 等待伺服器  socket 關閉 。
            // 在這個例子中,這不會發生,但你可以優雅地關閉你的伺服器。
            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();
    }
}
複製程式碼

2.2 檢視收到的資料

我們剛剛已經編寫出我們第一個服務端,我們需要測試一下他是否真的可以執行。最簡單的測試方法是用 telnet 命令。例如,你可以在命令列上輸入telnet localhost 8080 或者其他型別引數。

然而我們能說這個服務端是正常執行了嗎?事實上我們也不知道,因為他是一個 discard 服務,你根本不可能得到任何的響應。為了證明他仍然是在正常工作的,讓我們修改服務端的程式來列印出他到底接收到了什麼。

我們已經知道 channelRead() 方法是在資料被接收的時候呼叫。讓我們放一些程式碼到 DiscardServerHandler 類的 channelRead() 方法。

修改DiscardServerHandler類的channelRead(ChannelHandlerContext ctx, Object msg)方法如下:

@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)
    }
}
複製程式碼

再次驗證,cmd下輸入:telnet localhost 8080。你將會看到服務端列印出了他所接收到的訊息。

如下:

你在dos介面輸入的訊息會被顯示出來

效果圖

2.3 寫個應答伺服器

到目前為止,我們雖然接收到了資料,但沒有做任何的響應。然而一個服務端通常會對一個請求作出響應。讓我們學習怎樣在 ECHO 協議的實現下編寫一個響應訊息給客戶端,這個協議針對任何接收的資料都會返回一個響應。

和 discard server 唯一不同的是把在此之前我們實現的 channelRead() 方法,返回所有的資料替代列印接收資料到控制檯上的邏輯。因此,需要把 channelRead() 方法修改如下:

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ctx.write(msg); 
        ctx.flush(); 
    }
複製程式碼

再次驗證,cmd下輸入:telnet localhost 8080。你會看到服務端會發回一個你已經傳送的訊息。 如下:

網路應用框架Netty快速入門

下一篇我們會學習如何用Netty實現聊天功能。

歡迎關注我的微信公眾號(分享各種Java學習資源,面試題,以及企業級Java實戰專案回覆關鍵字免費領取):

微信公眾號

相關文章