Netty快速開始

前言

Github:https://github.com/yihonglei/thinking-in-netty

一 Netty概述

Netty 提供異步的、事件驅動的網絡應用程序框架和工具,用以快速簡單開發高性能、高可靠性的網絡服務器和客戶端程序。

它大大簡化了網絡編程,如TCP和UDP套接字服務器。“快速簡單”並不意味着生成的應用程序將受到可維護性或

性能問題的影響。Netty經過精心設計,並積累了許多協議(如ftp、smtp、http)的實施經驗,以及各種二進制和

基於文本的遺留協議。因此,Netty成功地找到了一種方法,可以在不妥協的情況下實現輕鬆的開發、性能、穩定性和靈活性。

二 Netty特性

Netty官方架構圖。

設計

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

性能

  • 更好的吞吐量,低延遲
  • 更省資源
  • 儘量減少必要的內存拷貝

安全

  • 完整的SSL/ TLS和STARTTLS的支持

易用

  • 完善的Java doc,用戶指南和樣例
  • 僅依賴於JDK1.6(netty 4.x)

三 Netty核心組件

1、Tansport Channel

對應NIO中的Channel,是NIO的基本構造,可以把Channel看成是傳入(入站)或者傳出(出站)數據的載體。

因此,它可以被打開或者關閉,連接或者斷開連接。

2、EventLoop、ChannelHandler、EventLoopGroup

EventLoop是NIO中selector程序的抽象,消除了在NIO中手動的編寫派發代碼。在Netty內部,將會爲每個Channel

分配一個EventLoop,用以處理所有I/O事件:

  • 註冊感興趣的事件;
  • 將事件派發給ChannelHandler;
  • 安排進一步的處理;

每一個事件觸發後將直接交由ChannelHandler接口實現處理。

EventLoop本身只由一個線程驅動,處理一個Channel的所有I/O事件,並且在該EventLoop的整個生命週期內部不會改變。

一個EventLoopGroup可以包含多個EventLoop。

3、ChannelFutue

Netty提供了另外一種在操作完成時通知應用程序的方式。在未來的某個時刻完成時提供對其結果的訪問。

JDK內置包裏面java.util.concurrent.Future接口提供了實現,但是操作比較麻煩,Netty提供了自己的實現,

ChannelFuture用於在執行異步操作的時候使用。ChannelFutue不會阻塞,完全是異步和事件驅動的。

4、ByteBuf

對應NIO中的ByteBuffer。

5、Bootstrap 和 ServerBootstrap 

對應NIO中的Selector、ServerSocketChannel等的創建、配置、啓動等。

Netty裏面有很多的概念,概念說太多就迷糊了,先來個實例,體驗下Netty入門編程。

四 Netty入門實戰

jar包

<dependency>
   <groupId>io.netty</groupId>
   <artifactId>netty-all</artifactId>
   <version>4.1.44.Final</version>
</dependency>

1、服務端

1.1 EchoServer

啓動服務端,等待客戶端連接,將連接事件交給EchoServerHandler處理。

package com.jpeony.netty.echo;

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;

/**
 * 一 Netty Server啓動流程
 * 1、設置服務端ServerBootStrap啓動參數
 * <p>
 * group()方法分配一個NioEventLoopGroup實例以進行事件處理,如接受新連接以及讀/寫數據;
 * <p>
 * channel()設置通道類型;
 * <p>
 * localAddress()指定服務器綁定的本地的InetSocketAddress;
 * <p>
 * childHandler()添加一個EchoServerHandler到Channel的ChannelPipeline;
 * 2、調用ServerBootStrap.bind()方法啓動服務端
 * 調用ServerBootStrap.bind()方法啓動服務端,bind方法會在group中註冊NioServerScoketChannel,
 * 監聽客戶端的連接請求會創建一個NioServerSocketChannel實例,並將其在group中進行註冊;
 *
 * @author yihonglei
 */
public class EchoServer {
    private final int port;

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

    /**
     * 服務器端啓動方法
     *
     * @author yihonglei
     */
    private void start() throws Exception {
        // 創建EventLoopGroup
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            // 創建ServerBootstrap
            ServerBootstrap b = new ServerBootstrap();

            // 指定EventLoopGroup以處理服務端事件,需要適用於NIO的實現。
            b.group(bossGroup, workerGroup)
                    // 指定所使用的NIO傳輸Channel
                    .channel(NioServerSocketChannel.class)
                    // 使用指定的端口設置套接字地址
                    .localAddress(new InetSocketAddress(port))
                    // 添加一個EchoServerHandler到Channel的ChannelPipeline
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            // EchoServerHandler被標註爲@Shareable,所以我們可以總是使用同樣的實例
                            ch.pipeline().addLast(new EchoServerHandler());
                        }
                    });

            // 異步地綁定服務器,調用sync()方法阻塞直到綁定完成
            ChannelFuture f = b.bind().sync();

            // 綁定Channel的CloseFuture,並且阻塞當前線程直到它完成
            f.channel().closeFuture().sync();
        } finally {
            // 關閉EventLoopGroup並且釋放所有的資源
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception {
        // 設置端口值
        new EchoServer(9999).start();
    }
}

1.2 EchoServerHandler

EchoServerHandler負責服務端具體邏輯處理。

package com.jpeony.netty.echo;

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;


/**
 * 服務端業務邏輯處理。
 *
 * @author yihonglei
 */
@ChannelHandler.Sharable //註解@ChannelHandler.Sharable表示一個ChannelHandler可以被多個Channel安全地共享。
public class EchoServerHandler extends ChannelInboundHandlerAdapter {

    /**
     * 每個傳入的消息都要調用該方法。
     *
     * @author yihonglei
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        // 將客戶端發送過來的消息打印到控制檯
        ByteBuf in = (ByteBuf) msg;
        System.out.println("server received msg from client:" + in.toString(CharsetUtil.UTF_8));

        // 寫一條消息響應給客戶端
        ByteBuf responseMsg = Unpooled.wrappedBuffer(new String("Hello Client!").getBytes());
        ctx.write(responseMsg);
    }

    /**
     * 通知ChannelInboundHandler最後一次對channelRead()的調用時當前批量讀取中的最後一條消息。
     *
     * @author yihonglei
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        // 將未決消息沖刷到遠程節點,並且關閉該Channel
        ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
                .addListener(ChannelFutureListener.CLOSE);
    }

    /**
     * 在讀取操作期間,有異常拋出時會調用。
     *
     * @author yihonglei
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        // 打印異常棧跟蹤信息
        cause.printStackTrace();

        // 關閉該Channel
        ctx.close();
    }
}

2、客戶端

2.1 EchoClient

1)連接到服務器;

2)發送一個或者多個消息;

3)對於每個消息,等待並接收從服務器發回的消息;

4)關閉連接;

客戶端邏輯交由EchoClientHandler處理。

package com.jpeony.netty.echo;

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;

/**
 * 1、初始化客戶端,將創建一個Bootstrap實例;
 * 2、爲事件處理分配一個NioEventLoopGroup實例,其中事件處理包括創建新的連接以及處理入站和出站數據;
 * 3、爲服務器連接創建一個InetSocketAddress實例;
 * 4、當連接被建立時,一個EchoClientHandler實例會被安裝到ChannelPipeline中;
 * 5、在一切都設置完成後,調用Bootstrap.connect()方法連接到遠程節點;
 *
 * @author yihonglei
 */
public class EchoClient {
    private final String host;
    private final int port;

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

    /**
     * 客戶端啓動方法
     *
     * @author yihonglei
     */
    private void start() throws InterruptedException {
        // 創建EventLoopGroup
        EventLoopGroup group = new NioEventLoopGroup();

        try {
            // 創建Bootstrap
            Bootstrap b = new Bootstrap();

            // 指定EventLoopGroup以處理客戶端事件,需要適用於NIO的實現。
            b.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 ch) throws Exception {
                            ch.pipeline().addLast(new EchoClientHandler());
                        }
                    });

            // 連接到遠程節點,阻塞等待知道連接完成
            ChannelFuture f = b.connect().sync();

            // 阻塞,直到Channel關閉
            f.channel().closeFuture().sync();
        } finally {
            // 關閉EventLoopGroup並且釋放所有的資源
            group.shutdownGracefully().sync();
        }
    }

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

2.2 EchoClientHandler

負責客戶端邏輯處理。

package com.jpeony.netty.echo;

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 yihonglei
 */
@ChannelHandler.Sharable // 標記該類的實例可以被多個Channel共享
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {

    /**
     * 連接服務器成功之後被調用
     *
     * @author yihonglei
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        // 當被通知Channel是活躍的時候,發送一條消息(向服務端發送一條消息)
        String data = "Hello Server!";
        ctx.writeAndFlush(Unpooled.copiedBuffer(data, CharsetUtil.UTF_8));
    }

    /**
     * 從服務器接收到消息時被調用
     *
     * @author yihonglei
     * @date 2019/4/20 16:27
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf in) {
        // 將服務端響應的消息打印出來
        System.out.println("client received msg from client:" + in.toString(CharsetUtil.UTF_8));
    }

    /**
     * 在處理過程中拋異常時調用
     *
     * @author yihonglei
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        // 打印異常棧跟蹤信息
        cause.printStackTrace();

        // 關閉該Channel
        ctx.close();
    }
}

3、程序啓動

先啓動服務端EchoServer,然後再啓動客戶端EchoClient。

服務端接收到客戶端發送過來的消息Hello Server!

客戶端收到服務端響應的消息Hello Client!

 

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