如何從BIO演進到NIO,再到Netty

掃描下方二維碼或者微信搜索公衆號菜鳥飛呀飛,即可關注微信公衆號,閱讀更多Spring源碼分析Java併發編程文章。

微信公衆號

前言

年初的時候給自己定了一個目標,就是學習Netty的源碼,因此在Q2的OKR上,其中一個目標就是學習Netty源碼,並且在部門內進行一次Netty相關的學習分享。然而,出生牛犢不怕虎,當時甚至都不知道Netty怎麼用,上來就是看源碼,看了不到半天就懵了,連學下去的信心都沒了。到Q2結束,我的Netty學習進度幾乎爲0。Q3的OKR上再次定下了要看Netty源碼的目標,然後,整個Q3中,這個口號依舊只是在嘴上喊喊,從沒付出行動。到了Q4,依舊在OKR上寫上了Netty源碼學習的目標,眼瞅着Q4還剩下一個月,Netty源碼的學習產出依舊爲0。連着失信兩次,在Q2、Q3上空喊了兩個季度的口號,常言道事不過三,終於決定動筆寫寫Netty相關的源碼分析。

結合我自身學習Netty的經歷,在正式進入Netty的源碼分析,決定有必要先說說BIO、NIO的關係。在平時工作中,很少直接寫網絡編程的代碼,即使要寫也都是使用成熟的網絡框架,很少直接進行Socket編程。下面將結合一個Demo示例,來熟悉下網絡編程相關知識。在Demo示例中,客戶端每隔三秒向服務端發送一條Hello World的消息,服務端收到後進行打印。

BIO

BIO是阻塞IO,也稱之爲傳統IO,在Java的網絡編程中,指的就是ServerSocket、Socket套接字實現的網絡通信,這也是我們最開始學Java時接觸到的網絡編程相關的類。服務端在有新連接接入時或者在讀取網絡消息時,它會對主線程進行阻塞。下面是通過BIO來實現上面場景的代碼。

服務端代碼BioServer.java

/**
 * @author liujinkun
 * @Title: BioServer
 * @Description: BIO 服務端
 * @date 2019/11/24 2:54 PM
 */
public class BioServer {

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8080);
        while(true){
            // accept()方法是個阻塞方法,如果沒有客戶端來連接,線程就會一直阻塞在這兒
            Socket accept = serverSocket.accept();
            InputStream inputStream = accept.getInputStream();
            byte[] bytes = new byte[1024];
            int len;
            // read()方法是一個阻塞方法,當沒有數據可讀時,線程會一直阻塞在read()方法上
            while((len = inputStream.read(bytes)) != -1){
                System.out.println(new String(bytes,0,len));
            }
        }
    }
}

在服務端的代碼中,創建了一個ServerSocket套接字,並綁定端口8080,然後在while死循環中,調用ServerSocket的accept()方法,讓其不停的接收客戶端連接。accept()方法是一個阻塞方法,當沒有客戶端來連接時,main線程會一直阻塞在accept()方法上(現象:程序一直停在accept()方法這一行,不往下執行)。當有客戶端連接時,main線程解阻塞,程序繼續向下運行。接着調用read()方法來從客戶端讀取數據,read()方法也是一個阻塞方法,如果客戶端發送來數據,則read()能讀取到數據;如果沒有數據可讀,那麼main線程就又會一直停留在read()方法這一行。

這裏使用了兩個while循環,外層的while循環是爲了保證能不停的接收連接,內層的while循環是爲了保證不停的從客戶端中讀取數據。

客戶端代碼BioClient.java

/**
 * @author liujinkun
 * @Title: BioClient
 * @Description: BIO 客戶端
 * @date 2019/11/24 2:54 PM
 */
public class BioClient {

    public static ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();

    public static void main(String[] args) throws IOException {

        Socket socket = new Socket("127.0.0.1",8080);
        // 採用具有定時執行任務的線程池,來模擬客戶端每隔一段時間來向服務端發送消息
        // 這裏是每隔3秒鐘向服務端發送一條消息
        executorService.scheduleAtFixedRate(()->{
            try {
                OutputStream outputStream = socket.getOutputStream();
                // 向服務端發送消息(消息內容爲:客戶端的ip+端口+Hello World,示例:/127.0.0.1:999999 Hello World)
                String message = socket.getLocalSocketAddress().toString() + " Hello World";
                outputStream.write(message.getBytes());
                outputStream.flush();
            } catch (IOException e) {
                e.printStackTrace();
            }
        },0,3,TimeUnit.SECONDS);

    }
}

在客戶端的代碼中,創建了一個Socket套接字,並指定要連接的服務器地址和端口號。然後通過使用一個具有定時執行任務功能的線程池,讓客戶端每隔3秒鐘向服務端發送一條數據。

這裏爲了每隔3秒發送一次數據,使用了具有定時執行任務功能的線程池,也可以不使用它,採用線程的sleep()方法來模擬3秒鐘。如果有對線程池不夠了解的朋友,可以參考這兩篇文章: 線程池ThreadPoolExecutor的實現原理爲什麼《阿里巴巴Java開發手冊》上要禁止使用Executors來創建線程池

然後我們分別啓動服務端和一個客戶端,從服務端的控制檯就可以看到,每個3秒鐘就會打印一行 客戶端的ip+端口+Hello World的日誌。

/127.0.0.1:99999 Hello World
/127.0.0.1:99999 Hello World
/127.0.0.1:99999 Hello World

爲了模擬多個客戶端的接入,然後我們再啓動一個客戶端,這個時候我們期待在服務端的控制檯,會打印出兩個客戶端的ip+端口+Hello World,由於服務端和兩個客戶端都是在同一臺機器上,因此這個時候打印出來的兩個客戶端的ip是相同的,但是端口口應該是不一樣的。我們期望的日誌輸出應該是這樣的。

/127.0.0.1:99999 Hello World
/127.0.0.1:88888 Hello World
/127.0.0.1:99999 Hello World
/127.0.0.1:88888 Hello World

然而,這只是我們期望的,實際現象,並非如此,當我們再啓動一個客戶端後,發現控制檯始終只會出現一個客戶端的端口號,並非兩個。

那麼爲什麼呢?原因就在於服務端的代碼中,read()方法是一個阻塞方法,當第一個客戶端連接後,讀取完第一個客戶端的數據,由於第一個客戶端一直不釋放連接,因此服務端也不知道它還有沒有數據要發送過來,這個時候服務端的main線程就一直等在read()方法處,當有第二個客戶端接入時,由於main線程一直阻塞在read()方法處,因此它無法執行到accept()方法來處理新的連接,所以此時我們看到的現象就是,只會打印一個客戶端發送來的消息。

那麼我們該怎麼辦呢?既然知道了問題出現main線程阻塞在read()方法處,也就是在讀數據的時候出現了阻塞。而要解決阻塞的問題,最直接的方式就是利用多線程技術了,因此我們就在讀數據的時候新開啓一條線程來進行數據的讀取。升級之後的服務端代碼如下。

服務端代碼BioServerV2.java

/**
 * @author liujinkun
 * @Title: BioServer
 * @Description: BIO 服務端
 * @date 2019/11/24 2:54 PM
 */
public class BioServerV2 {

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8080);
        while (true) {
            // accept()方法是個阻塞方法,如果沒有客戶端來連接,線程就會一直阻塞在這兒
            Socket accept = serverSocket.accept();
            // 用另外一個線程來讀寫數據
            handleMessage(accept);
        }

    }

    private static void handleMessage(Socket socket) {
        // 新創建一個線程來讀取數據
        new Thread(() -> {
            try {
                InputStream inputStream = socket.getInputStream();
                byte[] bytes = new byte[1024];
                int len;
                while ((len = inputStream.read(bytes)) != -1) {
                    System.out.println(new String(bytes, 0, len));
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();

    }
}

客戶端的代碼不變,依然使用BioClient。在服務端的代碼BioServerV2中,讀數據的操作我們提取到了handleMessage()方法中進行處理,在該方法中,會新創建一個線程來讀取數據,這樣就不會造成main線程阻塞在read()方法上了。啓動服務端和兩個客戶端進行驗證。控制檯打印結果如下。

/127.0.0.1:99999 Hello World
/127.0.0.1:88888 Hello World
/127.0.0.1:99999 Hello World
/127.0.0.1:88888 Hello World

雖然解決了多個客戶端同時接入的問題,但是其中的缺點我們也很容易發現:每當有一個新的客戶端來連接服務端時,我們都需要爲這個客戶端創建一個線程來處理讀數據的操作,當併發度很高時,我們就需要創建很多的線程,這顯然這是不可取的。線程是服務器的寶貴資源,創建和銷燬都需要花費很長時間,當線程過多時,CPU的上線文切換也更加頻繁,這樣就會造成服務響應緩慢。當線程過多時,甚至還會出現句柄溢出、OOM等異常,最終導致服務宕機。另外,我們在讀取數據時,是基於IO流來讀取數據的,每次只能讀取一個或者多個字節,性能較差。

因此BIO的服務端,適用於併發度不高的應用場景,但是對於高併發,服務負載較重的場景,使用BIO顯然是不適合的。

NIO

爲了解決BIO無法應對高併發的問題,JDK從1.4開始,提供了一種新的網絡IO,即NIO,通常稱它爲非阻塞IO。然而NIO的代碼極爲複雜和難懂,下面簡單介紹寫NIO中相關的類。

NIO相關的類均在java.nio包下。與BIO中ServerSocket、Socket對應,NIO中提供了ServerSocketChannle、SocketChannel分別表示服務端channel和客戶端channel。與BIO中不同的是,NIO中出現了Selector輪詢器的概念,不同的操作系統有不同的實現方式(在windows平臺底層實現是select,在linux內核中採用epoll實現,在MacOS中採用poll實現)。另外NIO是基於ByteBuffer來讀寫數據的。

介紹了這麼多關於NIO的類,估計你也懵逼了,下面先看看如何用NIO來實現上面的場景。

服務端代碼NioServer.java

/**
 * @author liujinkun
 * @Title: NioServer
 * @Description: NIO 服務端
 * @date 2019/11/24 2:55 PM
 */
public class NioServer {

    public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 輪詢器,不同的操作系統對應不同的實現類
        Selector selector = Selector.open();
        // 綁定端口
        serverSocketChannel.bind(new InetSocketAddress(8080));
        serverSocketChannel.configureBlocking(false);
        // 將服務端channel註冊到輪詢器上,並告訴輪詢器,自己感興趣的事件是ACCEPT事件
        serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);

        while(true){
            // 調用輪詢器的select()方法,是讓輪詢器從操作系統上獲取所有的事件(例如:新客戶端的接入、數據的寫入、數據的寫出等事件)
            selector.select(200);
            // 調用select()方法後,輪詢器將查詢到的事件全部放入到了selectedKeys中
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            // 遍歷所有事件
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while(iterator.hasNext()){
                SelectionKey key = iterator.next();

                // 如果是新連接接入
                if(key.isAcceptable()){
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    System.out.println("有新客戶端來連接");
                    socketChannel.configureBlocking(false);
                    // 有新的客戶端接入後,就樣將客戶端對應的channel所感興趣的時間是可讀事件
                    socketChannel.register(selector,SelectionKey.OP_READ);
                }
                // 如果是可讀事件
                if(key.isReadable()){
                    // 從channel中讀取數據
                    SocketChannel channel = (SocketChannel) key.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    channel.read(byteBuffer);
                    byteBuffer.flip();
                    System.out.println(Charset.defaultCharset().decode(byteBuffer));
                    // 讀完了以後,再次將channel所感興趣的時間設置爲讀事件,方便下次繼續讀。當如果後面要想往客戶端寫數據,那就註冊寫時間:SelectionKey.OP_WRITE
                    channel.register(selector,SelectionKey.OP_READ);
                }
                // 將SelectionKey從集合中移除,
                // 這一步很重要,如果不移除,那麼下次調用selectKeys()方法時,又會遍歷到該SelectionKey,這就造成重複處理了,而且最終selectionKeys這個集合的大小會越來越大。
                iterator.remove();
            }


        }
    }
}

看完代碼實現,你可能更加懵逼了。這麼簡單的一個Hello World,居然要寫這麼多的代碼,而且還全都看不懂。(我第一次接觸這些代碼和API時,就是這種想法,以至於我Q2、Q3季度都不想去學和NIO相關的東西)。下面簡答解釋下這段代碼。

    1. 首先通過ServerSocketChannel.open()這行代碼創建了一個服務端的channel,也就是服務端的Socket。(在計算機網路的7層模型或者TCP/IP模型中,上層的應用程序通過Socket來和底層溝通)
    1. 通過Selector.open()創建了一個輪詢器,這個輪詢器就是後續用來從操作系統中,遍歷有哪些socket準備好了。這麼說有點抽象,舉個栗子。坐火車時,通常都會有補票環節。每過一站後,乘務員就會在車廂中吼一嗓子,哪些人需要補票的?需要補票的人去幾號車廂辦理。這個乘務員就對應NIO中輪詢器,定時去車廂中吼一嗓子(也就是去操作系統中“吼一嗓子”),這個時候如果有人需要補票(有新的客戶端接入或者讀寫事件發生),那麼它就會去對應的車廂辦理(這些接入事件或者讀寫事件就會跑到selectKeys集合中)。而對應BIO,每對應一個新客戶端,都需要新建一個線程,也就是說每出現一個乘客,我們都要爲它配備一個乘務員,這顯然是不合理的,不可能有那麼多的乘務員。因此這就是NIO對BIO的一個巨大優勢。
    1. 然後爲服務端綁定端口號,並設置爲非阻塞模式。我們使用NIO的目的就是爲了使用它的非阻塞特性,因此這裏需要調用 serverSocketChannel.configureBlocking(false)設置爲非阻塞
    1. 然後將ServerSocketChannel註冊到輪詢器selector上,並告訴輪詢器,它感興趣的事件是ACCEPT事件(服務端的Channel就是用來處理客戶端接入的,因此它感興趣的事件就是ACCEPT事件。爲什麼要把它註冊到輪詢器上呢?前面已經說到了,輪詢器會定期去操作系統中“吼一嗓子,誰要補票”,如果不註冊到輪詢器上(不上火車),輪詢器吼一嗓子的時候,你怎麼聽得見呢?)
    1. 接着就是在一個while循環中,每過一段時間讓輪詢器去操作系統中輪詢有哪些事件發生。select()方法就是去操作系統中輪詢(吼一嗓子),它可以傳入一個參數,表示在操作系統中等多少毫秒,如果在這段時間中沒有事件發生(沒有人要補票),那麼就從操作系統中返回。如果有事件發生,那麼就將這些事件方法放到輪詢器的publicSelectedKeys屬性中,當調用selector.selectedKeys()方法時,就將這些事件返回。
    1. 接下來就是判斷事件是哪種事件,是接收事件還是讀事件,亦或是寫事件,然後針對每種不同的事件做不同的處理。
    1. 最後將key從集合中移除。爲什麼移除,見代碼註釋。
      好了,解釋了那麼多,裏面的細節還有很多,此時估計你還是佷懵。很懵就對了,這就是JDK中NIO的特點,操作繁瑣,涉及到的類很多,API也很多,開發者想要掌握這些,難度很大。關鍵是掌握了,不一定代碼寫得好;寫得好,不一定BUG少;BUG少,不一定性能高。JDK的NIO寫法,一不小心就容易BUG滿天飛。
      上面是服務端NIO的寫法,這個時候,可以直接利用BIO的客戶端去進行測試。當然NIO也有客戶端寫法。雖然NIO的寫法很複雜,但一回生,二回熟,多見幾回就習慣了,所以下面還是貼出了NIO客戶端的寫法。

NIO客戶端NioClient.java

/**
 * @author liujinkun
 * @Title: NioClient
 * @Description: NIO客戶端
 * @date 2019/11/24 2:55 PM
 */
public class NioClient {

    private static ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();

    public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);
        boolean connect = socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));
        // 因爲連接是一個異步操作,所以需要在下面一行判斷連接有沒有完成。如果連接還沒有完成,就進行後面的操作,會出現異常
        if(!connect){
            // 如果連接未完成,就等待連接完成
            socketChannel.finishConnect();
        }
        // 每個3秒向服務端發送一條消息
        executorService.scheduleAtFixedRate(() -> {
            try {
                String message = socketChannel.getLocalAddress().toString() + " Hello World";
                // 使用ByteBuffer進行數據發送
                ByteBuffer byteBuffer = ByteBuffer.wrap(message.getBytes());
                socketChannel.write(byteBuffer);
            } catch (IOException e) {
                e.printStackTrace();
            }

        }, 0, 3, TimeUnit.SECONDS);
    }
}

NIO客戶端的寫法相對比較簡單,利用SocketChannel進行IP和端口的綁定,然後調用connect()方法進行連接到服務端。最後利用ByteBuffer裝載數據,通過SocketChannel將數據寫出去。

相比BIO而言,NIO不需要爲每個連接去創建一個線程,它是通過輪詢器,定期的從操作系統中獲取到準備好的事件,然後進行批量處理。同時NIO是通過ByteBuffer來讀寫數據,相比於BIO中通過流來一個字節或多個字節的對數據,NIO的效率更高。但是ByteBuffer的數據結構設計,有點反人類,一不小心就會出BUG。關於ByteBuffer詳細的API操作,有興趣的朋友可以自己去嘗試寫寫。關於BIO和NIO的區別,可以用下面一張圖表示。

BIO與NIO

Netty

BIO在高併發下不適合用,而NIO雖然可以應對高併發的場景,但是它一方面因爲寫法複雜,掌握難度大,更重要的是還存在空輪詢的BUG(產生空輪詢的原因是操作系統的緣故),因此Netty出現了。Netty是目前應該使用最廣泛的一款網絡框架,用官方術語講就是:它是一款基於事件驅動的高性能的網絡框架。實際上它是一款將NIO包裝了的框架,同時它規避了JDK中空輪訓的BUG。雖然它是對NIO的包裝,但是它對很多操作進行了優化,其性能更好。目前在很多Java流行框架中,底層都採用了Netty進行網絡通信,比如RPC框架中Dubbo、Motan,Spring5的異步編程,消息隊列RocketMQ等等都使用了Netty進行網絡通信。

既然Netty這麼好,怎麼用呢?那麼接下來就用Netty實現上面的場景。
服務端NettyServer.java

/**
 * @author liujinkun
 * @Title: NettyServer
 * @Description: Netty服務端
 * @date 2019/11/24 2:56 PM
 */
public class NettyServer {

    public static void main(String[] args) {
        // 負責處理連接的線程組
        NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
        // 負責處理IO和業務邏輯的線程組
        NioEventLoopGroup workerGroup = new NioEventLoopGroup(8);
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    // 添加日誌打印,用來觀察Netty的啓動日誌
                    .handler(new LoggingHandler(LogLevel.INFO))
                    // 添加用來處理客戶端channel的處理器handler
                    .childHandler(new ChannelInitializer<NioSocketChannel>() {
                        @Override
                        protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                            ChannelPipeline pipeline = nioSocketChannel.pipeline();
                            // 字符串解碼器
                            pipeline.addLast(new StringDecoder())
                                    // 自定義的handler,用來打印接收到的消息
                                    .addLast(new SimpleChannelInboundHandler<String>() {
                                        @Override
                                        protected void channelRead0(ChannelHandlerContext channelHandlerContext, String message) throws Exception {
                                            System.out.println(message);
                                        }

                                        @Override
                                        public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
                                            super.channelRegistered(ctx);
                                            System.out.println("有新客戶端連接");
                                        }
                                    });
                        }
                    });
            // 綁定端口,並啓動
            ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            // 關閉線程組
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

上面的代碼雖然看起來也很長,但是這段代碼幾乎是不變的,它幾乎適用於所有場景,我們只需要修改childHandler()這一行相關的方法即可。這裏面的代碼纔是處理我們自定義的業務邏輯的。具體代碼細節,就不再詳細解釋了,後面會單獨寫文章來分別從源碼的解讀來分析Netty服務端的啓動、新連接的接入、數據的處理、半包拆包等問題。

啓動NettyServer,可以直接使用BioClient或者NioClient來測試NettyServer。當然,Netty也有客戶端的寫法。代碼如下。

Netty客戶端NettyClient

/**
 * @author liujinkun
 * @Title: NettyClient
 * @Description: Netty客戶端
 * @date 2019/11/24 2:57 PM
 */
public class NettyClient {

    private static ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();

    public static void main(String[] args) {
        // 客戶端只需要一個線程組即可
        NioEventLoopGroup nioEventLoopGroup = new NioEventLoopGroup();
        try {
            // 採用Bootstrap而不是ServerBootstrap
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(nioEventLoopGroup)
                    // 設置客戶端的SocketChannel
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<NioSocketChannel>() {
                        @Override
                        protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
                            ChannelPipeline pipeline = nioSocketChannel.pipeline();
                            // 添加一個字符串編碼器
                            pipeline.addLast(new StringEncoder());
                        }
                    });
            ChannelFuture channelFuture = bootstrap.connect("", 8080).sync();

            Channel channel = channelFuture.channel();

            executorService.scheduleAtFixedRate(()->{
                String message = channel.localAddress().toString() + " Hello World";
                channel.writeAndFlush(message);
            },0,3,TimeUnit.SECONDS);

            channel.closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            nioEventLoopGroup.shutdownGracefully();
        }

    }
}

實際上客戶端的代碼也幾乎是固定的,所有場景都可以複用這段代碼,唯一需要修改的就是handler()方法這一塊,需要針對自己的業務邏輯去添加不同的處理器。

相比於NIO的寫法,Netty的寫法更加簡潔,代碼量相對更少,幾行簡單的代碼就搞定了服務的啓動,新連接接入,數據讀寫,編解碼等問題。這也是爲什麼Netty使用這麼廣泛的原因。相比於NIO,Netty有如下幾點優點。

    1. JDK的NIO存在空輪詢的BUG,而Netty則巧妙的規避了這一點;
    1. JDK的API複雜,開發人員使用起來比較困難,更重要的是,很容易寫出BUG;而Netty的API簡單,容易上手。
    1. Netty的性能更高,它在JDK的基礎上做了很多性能優化,例如將selector中的publicSelectedKeys屬性的數據結構由Set集合改成了數組。
    1. Netty底層對IO模型可以隨意切換,針對Reactor三種線程模型,只需要通過修改參數就可以實現IO模型的切換。
    1. Netty經過了衆多高併發場景的考驗,如Dubbo等RPC框架的驗證。
    1. Netty幫助我們解決了TCP的粘包拆包等問題,開發人員不用去關心這些問題,只需專注於業務邏輯開發即可。
    1. Netty支持很多協議棧。JDK自帶的對象序列化性能很差,序列化後碼流較大,而是用其他方式的序列化則性能較高,例如protobuf等。
    1. 優點還有很多…

總結

  • 本文通過一個示例場景,從BIO的實現到NIO實現,最後到Netty的實現,分別對比了它們的優缺點,毫無疑問,Netty是網絡編程的首選框架,至於有點很多。

  • 最後總結一下,Netty的學習很難,涉及到的知識點、名詞很多,需要耐下性子,仔細琢磨,多寫示例代碼。筆者看了兩套視頻,一本《Netty權威指南》,以及《Netty權威指南》作者李林鋒在InfoQ中發表的所有netty相關的文章,甚至還去學了學計算機網絡與操作系統中關於網絡這一塊的知識,最後才感覺自己剛入門。當然了,主要是筆者比較菜。

  • 如果想學好Netty,還是建議多看看JDK中NIO相關的API,雖然這些API的寫法很複雜,但是對於學習Netty會有很大幫助。然後難用,學會了可以不用,但不可以不學。

  • 如果有需要關於Netty學習教程的朋友,可以關注公衆號,聯繫作者領取免費視頻。

微信公衆號

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