<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    從 IO、NIO聊到Netty,最后帶你實戰即時聊天系統

    一顆小胡椒2022-03-17 06:47:57

    在開始了解Netty是什么之前,我們先來回顧一下,如果需要實現一個客戶端與服務端通信的程序,使用傳統的IO編程,應該如何來實現?

    IO編程

    我們簡化一下場景:客戶端每隔兩秒發送一個帶有時間戳的“hello world”給服務端,服務端收到之后打印它。

    在傳統的IO模型中,每個連接創建成功之后都需要由一個線程來維護,每個線程都包含一個while死循環,那么1萬個連接對應1萬個線程,繼而有1萬個while死循環,這就帶來如下幾個問題。

    • 線程資源受限:線程是操作系統中非常寶貴的資源,同一時刻有大量的線程處于阻塞狀態,是非常嚴重的資源浪費,操作系統耗不起。
    • 線程切換效率低下:單機CPU核數固定,線程爆炸之后操作系統頻繁進行線程切換,應用性能急劇下降。
    • 除了以上兩個問題,在IO編程中,我們看到數據讀寫是以字節流為單位的。

    為了解決這3個問題,JDK在1.4版本之后提出了NIO。

    NIO編程

    在NIO編程模型中,新來一個連接不再創建一個新線程,而是可以把這個連接直接綁定到某個固定的線程,然后這個連接所有的讀寫都由這個線程來負責,那么它是怎么做到的?我們用下圖來對比一下IO與NIO。

    如上圖所示,在IO模型中,一個連接來了,會創建一個線程,對應一個while死循環,死循環的目的就是不斷監測這個連接上是否有數據可以讀。在大多數情況下,1萬個連接里面同一時刻只有少量的連接有數據可讀,因此,很多while死循環都白白浪費掉了,因為讀不出數據。

    而在NIO模型中,這么多while死循環轉換為一個死循環,這個死循環由一個線程控制,那么NIO又是如何做到一個線程一個while死循環就能監測1萬個連接是否有數據可讀的呢?

    這就是NIO模型中Selector的作用,一個連接來了之后,不會創建一個while死循環去監聽是否有數據可讀,而是直接把這條連接注冊到Selector上。然后,通過檢查這個Selector,就可以批量監測出有數據可讀的連接,進而讀取數據。下面我們舉一個生活中非常簡單的例子來說明IO與NIO的區別。

    在一家幼兒園里,小朋友有上廁所的需求,小朋友都太小以至于你要問他要不要上廁所,他才會告訴你。幼兒園一共有100個小朋友,有兩種方案可以解決小朋友上廁所的問題。

    1.每個小朋友都配一個老師。每個老師都隔段時間詢問小朋友是否要上廁所。如果要上,就領他去廁所,100個小朋友就需要100個老師來詢問,并且每個小朋友上廁所的時候都需要一個老師領著他去,這就是IO模型,一個連接對應一個線程。

    2.所有的小朋友都配同一個老師。這個老師隔段時間詢問所有的小朋友是否有人要上廁所,然后每一時刻把所有要上廁所的小朋友批量領到廁所,這就是NIO模型。所有小朋友都注冊到同一個老師,對應的就是所有的連接都注冊到同一個線程,然后批量輪詢。

    這就是NIO模型解決線程資源受限問題的方案。在實際開發過程中,我們會開多個線程,每個線程都管理著一批連接,相對于IO模型中一個線程管理一個連接,消耗的線程資源大幅減少。

    由于NIO模型中線程數量大大降低,因此線程切換效率也大幅度提高。

    IO讀寫是面向流的,一次性只能從流中讀取一字節或者多字節,并且讀完之后流無法再讀取,需要自己緩存數據。而NIO的讀寫是面向Buffer的,可以隨意讀取里面任何字節數據,不需要自己緩存數據,只需要移動讀寫指針即可。

    簡單講完了JDK NIO的解決方案之后,接下來我們使用NIO方案替換掉IO方案。先來看看,如果用JDK原生的NIO來實現服務端,該怎么做。

    前方高能預警:以下代碼可能會讓你感覺極度不適,如有不適,請跳過。

    NIOServer.java

    /** * @author 閃電俠 */public class NIOServer {    public static void main(String[] args) throws IOException {        Selector serverSelector = Selector.open();        Selector clientSelector = Selector.open();
            new Thread(() -> {            try {                // 對應IO編程中的服務端啟動                ServerSocketChannel listenerChannel = ServerSocketChannel.open();                listenerChannel.socket().bind(new InetSocketAddress(8000));                listenerChannel.configureBlocking(false);                listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);
                    while (true) {                    // 監測是否有新連接,這里的1指阻塞的時間為 1ms                    if (serverSelector.select(1) > 0) {                        Set set = serverSelector.selectedKeys();                        Iterator keyIterator = set.iterator();
                            while (keyIterator.hasNext()) {                            SelectionKey key = keyIterator.next();
                                if (key.isAcceptable()) {                                try {                                    // (1)每來一個新連接,不需要創建一個線程,而是直接注冊到clientSelector                                    SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();                                    clientChannel.configureBlocking(false);                                    clientChannel.register(clientSelector, SelectionKey.OP_READ);                                } finally {                                    keyIterator.remove();                                }                            }
                            }                    }                }            } catch (IOException ignored) {            }
            }).start();
            new Thread(() -> {            try {                while (true) {                    // (2)批量輪詢哪些連接有數據可讀,這里的1指阻塞的時間為 1ms                    if (clientSelector.select(1) > 0) {                        Set set = clientSelector.selectedKeys();                        Iterator keyIterator = set.iterator();
                            while (keyIterator.hasNext()) {                            SelectionKey key = keyIterator.next();
                                if (key.isReadable()) {                                try {                                    SocketChannel clientChannel = (SocketChannel) key.channel();                                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);                                    // (3)面向Buffer                                    clientChannel.read(byteBuffer);                                    byteBuffer.flip();                                    System.out.println(Charset.defaultCharset().newDecoder(). decode(byteBuffer)                                            .toString());                                } finally {                                    keyIterator.remove();                                    key.interestOps(SelectionKey.OP_READ);                                }                            }
                            }                    }                }            } catch (IOException ignored) {            }        }).start();
        }}
    

    相信大部分沒有接觸過NIO的讀者應該會直接跳過代碼來到這一行:原來使用JDK原生NIO的API實現一個簡單的服務端通信程序如此復雜!

    我們還是先對照NIO來解釋一下核心思路。

    • NIO模型中通常會有兩個線程,每個線程都綁定一個輪詢器Selector。在這個例子中,serverSelector負責輪詢是否有新連接,clientSelector負責輪詢連接是否有數據可讀。
    • 服務端監測到新連接之后,不再創建一個新線程,而是直接將新連接綁定到clientSelector上,這樣就不用IO模型中的1萬個while循環死等,參見(1)。
    • clientSelector被一個while死循環包裹著,如果在某一時刻有多個連接有數據可讀,那么通過clientSelector.select(1)方法可以輪詢出來,進而批量處理。
    • 數據的讀寫面向Buffer。

    其他細節部分,因為實在是太復雜,所以筆者不再多講,讀者也不用對代碼的細節深究到底。總之,強烈不建議直接基于JDK原生NIO來進行網絡開發,下面是筆者總結的原因。

    • JDK的NIO編程需要了解很多概念,編程復雜,對NIO入門非常不友好,編程模型不友好,ByteBuffer的API簡直“反人類”。
    • 對NIO編程來說,一個比較合適的線程模型能充分發揮它的優勢,而JDK沒有實現,需要自己實現,就連簡單的自定義協議拆包都要自己實現。
    • JDK的NIO底層由Epoll實現,該實現飽受詬病的空輪詢Bug會導致CPU占用率飆升至100%。
    • 項目龐大之后,自行實現的NIO很容易出現各類Bug,維護成本較高,上面這些代碼筆者都不能保證沒有Bug。

    正因為如此,客戶端代碼這里就省略了,讀者可以直接使用IOClient.java與NIOServer.java通信。

    JDK的NIO猶如帶刺的玫瑰,雖然美好,讓人向往,但是使用不當會讓你抓耳撓腮,痛不欲生,正因為如此,Netty橫空出世!

    Netty編程

    Netty到底是何方神圣?

    用一句簡單的話來說就是:Netty封裝了JDK的NIO,讓你用得更方便,不用再寫一大堆復雜的代碼了。

    用官方正式的話來說就是:Netty是一個異步事件驅動的網絡應用框架,用于快速開發可維護的高性能服務端和客戶端。

    下面是筆者總結的使用Netty而不使用JDK原生NIO的原因。

    • 使用JDK原生NIO需要了解太多概念,編程復雜,一不小心就Bug橫飛。
    • Netty底層IO模型隨意切換,而這一切只需要做微小的改動,改改參數,Netty可以直接從NIO模型變身為IO模型。
    • Netty自帶的拆包/粘包、異常檢測等機制讓你從NIO的繁重細節中脫離出來,只需要關心業務邏輯即可。
    • Netty解決了JDK很多包括空輪詢在內的Bug。
    • Netty底層對線程、Selector做了很多細小的優化,精心設計的Reactor線程模型可以做到非常高效的并發處理。
    • 自帶各種協議棧,讓你處理任何一種通用協議都幾乎不用親自動手。
    • Netty社區活躍,遇到問題隨時郵件列表或者Issue。
    • Netty已經歷各大RPC框架、消息中間件、分布式通信中間件線上的廣泛驗證,健壯性無比強大。

    這些原因看不懂沒有關系,在后續的章節中我們都可以學到。接下來我們用Netty來重新實現一下本章開篇的功能吧!

    首先引入Maven依賴,案例后續Netty都基于4.1.6.Final版本。

      <dependency>        <groupId>io.nettygroupId>        <artifactId>netty-allartifactId>        <version>4.1.6.Finalversion>    dependency>
    

    然后是服務端實現部分。

    NettyServer.java

    /** * @author 閃電俠 */public class NettyServer {    public static void main(String[] args) {        ServerBootstrap serverBootstrap = new ServerBootstrap();
            NioEventLoopGroup boss = new NioEventLoopGroup();        NioEventLoopGroup worker = new NioEventLoopGroup();        serverBootstrap                .group(boss, worker)                .channel(NioServerSocketChannel.class)                .childHandler(new ChannelInitializer() {                    protected void initChannel(NioSocketChannel ch) {                        ch.pipeline().addLast(new StringDecoder());                        ch.pipeline().addLast(new SimpleChannelInboundHandler() {                            @Override                            protected void channelRead0(ChannelHandlerContext ctx, String msg) {                                System.out.println(msg);                            }                        });                    }                })                .bind(8000);    }}
    

    這么一小段代碼就實現了我們前面NIO編程中的所有功能,包括服務端啟動、接收新連接、打印客戶端傳來的數據,怎么樣?是不是比JDK原生NIO編程簡潔許多?

    初學Netty的時候,由于大部分人對NIO編程缺乏經驗,因此,將Netty里的概念與IO模型結合起來可能更好理解。

    • boss對應IOServer.java中的負責接收新連接的線程,主要負責創建新連接。
    • worker對應IOServer.java中的負責讀取數據的線程,主要用于讀取數據及業務邏輯處理。

    剩下的邏輯筆者在后面的內容中會詳細分析,讀者可以先把這段代碼復制到自己的IDE里,然后運行main函數。

    下面是客戶端NIO的實現部分。

    NettyClient.java

    /** * @author 閃電俠 */public class NettyClient {    public static void main(String[] args) throws InterruptedException {        Bootstrap bootstrap = new Bootstrap();        NioEventLoopGroup group = new NioEventLoopGroup();
            bootstrap.group(group)                .channel(NioSocketChannel.class)                .handler(new ChannelInitializer() {                    @Override                    protected void initChannel(Channel ch) {                        ch.pipeline().addLast(new StringEncoder());                    }                });
            Channel channel = bootstrap.connect("127.0.0.1", 8000).channel();
            while (true) {            channel.writeAndFlush(new Date() + ": hello world!");            Thread.sleep(2000);        }    }}
    

    在客戶端程序中,group對應了IOClient.java中main函數起的線程,剩下的邏輯在后面的內容中會詳細分析,現在你要做的事情就是把這段代碼復制到你的IDE里,然后運行main函數,最后回到NettyServer.java的控制臺,你會看到效果。

    使用Netty之后是不是覺得整個世界都變美好了?一方面,Netty對NIO封裝得如此完美,寫出來的代碼非常優雅;另一方面,使用Netty之后,網絡通信的性能問題幾乎不用操心,盡情地讓Netty“榨干”你的CPU吧。

    nionetty
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    線程切換效率低下:單機CPU核數固定,線程爆炸之后操作系統頻繁進行線程切換,應用性能急劇下降。
    Dubbo Kryo & FST RCE
    2022-11-25 15:31:47
    影響版本Dubbo 2.7.0 to 2.7.8Dubbo 2.6.0 to 2.6.9Dubbo all 2.5.x versions 環境復現 安裝zookeeper和dubbo-samples,用idea打開dubbo-samples-api,然后修改其中的pom.xml如下: 注意,dubbo-common必須 ≤2.7.3版本。在Dubbo<=2.7.3中fastjson的版本≤1.2.46 ,這也是我們這個洞的利用點,不過這里復現使用的更高版本所以需要添加依賴, com.alibabagroupId> fastjsonartifactId> 1.2.46version>dependency>. 案例漏洞分析 FTS反序列化FTS反序列化發生在RPC協議反序列化。
    同時例如 jstack、jmap 等工具也是不囿于一個方面的問題的,基本上出問題就是df、free、top 三連,然后依次jstack、jmap伺候,具體問題具體分析即可。CPU 異常往往還是比較好定位的。
    java版本: java version "1.8.0_131" Java(TM) SE Runtime Environment (build 1.8.0_131-b11) Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)
    它指的是一個有用的工具庫,幫助處理和操作XML格式的數據。ROME庫允許我們把XML數據轉換成Java中的對象,這樣我們可以更方便地在程序中操作數據。另外,它也支持將Java對象轉換成XML數據,這樣我們就可以把數據保存成XML文件或者發送給其他系統。
    調用了mbsInterceptor屬性的registerMBean方法進行注冊,這里的mbsInterceptor屬性即是DefaultMBeanServerInterceptor對象,跟進一下。最后調用了registerWithRepository進行進一步的注冊。在這個方法中,調用了該類的repository屬性的addMBean方法進行MBean的添加。
    通過common-collection相關gadget,想辦法調用org.mozilla.classfile.DefiningClassLoader這個類去加載字節碼。然后通過T3協議的反序列化漏洞發送給待攻擊weblogic服務器。
    隨機讀寫會導致尋址時間延長,從而影響磁盤的讀寫速度。而Kafka在將數據持久化到磁盤時,采用只追加的順序寫,有效降低了尋址時間,提高效率。當讀操作發生時,先從PageCache中查找,如果發生缺頁才進行磁盤調度,最終返回需要的數據。對應到Kafka生產和消費消息中:producer把消息發到broker后,數據并不是直接落入磁盤的,而是先進入PageCache。
    記得他當時是在本地模擬的一個實戰場景來做的這個測試實驗(繞過安全防護進行端口轉發)。
    強網擬態線上mobile的兩道wp
    一顆小胡椒
    暫無描述
      亚洲 欧美 自拍 唯美 另类