Netty 入門初體驗

前言

這篇主要介紹一個Netty 客戶端與服務端的示例代碼,對Netty有一個直觀感受,看看如何使用Netty,後續文章會對Netty的各個組件進行詳細分析

Netty簡介

Netty是一款異步的事件驅動的網絡應用程序框架,支持快速開發可維護的高性能的面向協議的服務器和客戶端。Netty主要是對java 的 nio包進行的封裝

爲什麼要使用 Netty

上面介紹到 Netty是一款 高性能的網絡通訊框架,那麼我們爲什麼要使用Netty,換句話說,Netty有哪些優點讓我們值得使用它,爲什麼不使用原生的 Java Socket編程,或者使用 Java 1.4引入的 Java NIO。接下來分析分析 Java Socket編程和 Java NIO。

Java 網絡編程

首先來看一個Java 網絡編程的例子:

public static void main(String[] args) {
        try {
            ServerSocket serverSocket = new ServerSocket(8888);
            Socket socket = serverSocket.accept();
            BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String line = "";
            while ((line = reader.readLine()) != null) {
                System.out.println("received: " + line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
}

上面展示了一個簡單的Socket服務端例子,該代碼只能同時處理一個連接,要管理多個併發客戶端,需要爲每個新的客戶端Socket創建一個 新的Thread。這種併發方案對於中小數量的客戶端來說還可以接受,如果是針對高併發,超過100000的併發連接來說該方案並不可取,它所需要的線程資源太多,而且任何時候都可能存在大量線程處於阻塞狀態,等待輸入或者輸出數據就緒,整個方案性能太差。所以,高併發的場景,一般的Java 網絡編程方案是不可取的。

Java NIO

還是先來看一個 Java NIO的例子:

public class ServerSocketChannelDemo {
    private ServerSocketChannel serverSocketChannel;
    private Selector selector;

    public ServerSocketChannelDemo(int port) throws IOException {
        serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.socket().bind(new InetSocketAddress(port));
        selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    }

    public void listener() throws IOException {
        while (true) {
            int n = selector.select();
            if (n == 0) {
                continue;
            }
            Iterator iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey = (SelectionKey) iterator.next();
                if (selectionKey.isAcceptable()) {
                    ServerSocketChannel server_channel = (ServerSocketChannel) selectionKey.channel();
                    SocketChannel socketChannel = server_channel.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);
                }
                if (selectionKey.isReadable()) {
                    //如果通道處於讀就緒的狀態
                    //讀操作
                    //TODO
                }
            }
        }
    }
}

NIO的核心部分主要有:

  • 通道 Channel
  • 緩衝區 Buffer
  • 多路複用器 Selector

選擇器 Selector 是 Java 非阻塞 I/O實現的關鍵,將通道Channel註冊在 Selector上,如果某個通道 Channel發送 讀或寫事件,這個Channel處於就緒狀態,會被Selector輪詢出來,進而進行後續I/O操作。這種I/O多路複用的方式相比上面的阻塞 I/O模型,提供了更好的資源管理:

  • 使用較少的線程便可以處理很多連接,因此也減少了內存管理和上下文切換所帶來的開銷
  • 當沒有I/O操作需要處理的時候,線程也可以被用於其他任務。

儘管使用 Java NIO可以讓我們使用較少的線程處理很多連接,但是在高負載下可靠和高效地處理和調度I/O操作是一項繁瑣而且容易出錯的任務,所以才引出了高性能網絡編程專家——Netty

Netty特性

Netty有很多優秀的特性值得讓我們去使用它(摘自《Netty實戰》):

設計

  • 統一的API,適用於不同的協議(阻塞和非阻塞)
  • 基於靈活、可擴展的事件驅動模型
  • 高度可定製的線程模型
  • 可靠的無連接數據Socket支持(UDP)

性能

  • 更好的吞吐量,低延遲
  • 更低的資源消耗
  • 最少的內存複製

健壯性

  • 不再因過快、過慢或超負載連接導致OutOfMemoryError
  • 不再有在高速網絡環境下NIO讀寫頻率不一致的問題

安全性

  • 完整的SSL/TLS和STARTTLS的支持
  • 可用於受限環境下,如 Applet 和OSGI

易用

  • 詳實的Javadoc和大量的示例集
  • 不需要超過 JDK 1.6+的依賴

Netty示例代碼

下面是server 和client的示例代碼,先來看看Netty代碼是怎麼樣的,後續文章會詳細分析各個模塊。

Server代碼

public class EchoServer {
    private final int port;

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

    public static void main(String[] args) throws InterruptedException {
        new EchoServer(8888).start();
    }

    public void start() throws InterruptedException {
        final EchoServerHandler serverHandler = new EchoServerHandler();
        //創建EventLoopGroup,處理事件
        EventLoopGroup boss = new NioEventLoopGroup();
        EventLoopGroup worker = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(boss,worker)
                    //指定所使用的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(serverHandler);
                        }
                    });
            //異步的綁定服務器,調用sync()方法阻塞等待直到綁定完成
            ChannelFuture future = b.bind().sync();
            future.channel().closeFuture().sync();
        } finally {
            //關閉EventLoopGroup,釋放所有的資源
            group.shutdownGracefully().sync();
            worker.shutdownGracefully().sync();
        }
    }
}

EchoServerHandler

@ChannelHandler.Sharable //標識一個 ChannelHandler可以被多個Channel安全地共享
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buffer = (ByteBuf) msg;
        //將消息記錄到控制檯
        System.out.println("Server received: " + buffer.toString(CharsetUtil.UTF_8));
        //將接受到消息回寫給發送者
        ctx.write(buffer);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        //將未消息沖刷到遠程節點,並且關閉該 Channel
        ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
                .addListener(ChannelFutureListener.CLOSE);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        //打印異常棧跟蹤
        cause.printStackTrace();
        //關閉該Channel
        ctx.close();
    }
}

代碼要點解讀:

  • ServerBootStrap是引導類,幫助服務啓動的輔助類,可以設置 Socket參數
  • EventLoopGroup是處理I/O操作的線程池,用來分配 服務於Channel的I/O和事件的 EventLoop,而NioEventLoopGroupEventLoopGroup的一個實現類。這裏實例化了兩個 NioEventLoopGroup,一個 boss,主要用於處理客戶端連接,一個 worker用於處理客戶端的數據讀寫工作
  • EchoServerHandler實現了業務邏輯
  • 通過調用ServerBootStrap.bind()方法以綁定服務器

Client 代碼

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 InterruptedException {
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(group)
                    .channel(NioSocketChannel.class)
                    .remoteAddress(new InetSocketAddress(host, port))
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast(new EchoClientHandler());
                        }
                    });
            ChannelFuture channelFuture = b.connect().sync();
            channelFuture.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully().sync();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new EchoClient("127.0.0.1", 8888).start();
    }
}

EchoClientHandler

@ChannelHandler.Sharable
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {
        System.out.println("Client received: "+byteBuf.toString());
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks",CharsetUtil.UTF_8));
    }

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

代碼要點解讀:

  • 爲初始化客戶端,創建了一個BootStrap實例,與ServerBootStrap一樣,也是一個引導類,主要輔助客戶端
  • 分配了一個 NioEventLoopGroup實例,裏面的 EventLoop,處理連接的生命週期中所發生的事件
  • EchoClientHandler類負責處理業務邏輯,與服務端的EchoSeverHandler作用相似。

參考資料 & 鳴謝

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