• <noscript id="e0iig"><kbd id="e0iig"></kbd></noscript>
  • <td id="e0iig"></td>
  • <option id="e0iig"></option>
  • <noscript id="e0iig"><source id="e0iig"></source></noscript>
  • 【Java網絡編程】Netty 網絡框架

    標簽: Java網絡編程

    Netty 網絡框架

    Netty 是一個利用 Java 的高級網絡的能力,隱藏其背后的復雜性而提供一個易于使用的 API 的客戶端/服務器框架。Netty 提供高性能和可擴展性,讓你可以自由地專注于你真正感興趣的東西,你的獨特的應用!

    Netty 是一個高性能、異步事件驅動網絡庫,它提供了對 TCP、UDP 和文件傳輸的支持使(這里首先就要搞清楚異步的 NIO 框架是什么意思)用更高效的 socket 底層,對 selector 空輪詢引起的 cpu 占用飆升在內部進行了處理,避免了直接使用 NIO 的陷阱,簡化了 NIO 的處理方式。采用多種decoder/encoder 支持,(后面我們會舉例說明)對 TCP 粘包/分包進行自動化處理(后面也會演示說明)可使用接受/處理線程池,提高連接效率,對重連、心跳檢測的簡單支持可配置 IO 線程數、TCP 參數,TCP 接收和發送緩沖區使用直接內存代替堆內存,通過內存池的方式循環利用 ByteBuf 通過引用計數器及時申請釋放不再引用的對象,降低了 GC 頻率。高效的 Reactor 線程模型,大量使用了 volitale、使用了CAS 和原子類、線程安全類的使用、讀寫鎖的使用。

    在這里插入圖片描述

    Netty 技術和方法的特點

    設計

    • 針對多種傳輸類型的統一接口 - 阻塞和非阻塞。
    • 簡單但更強大的線程模型。
    • 真正的無連接的數據報套接字支持。
    • 鏈接邏輯支持復用。

    易用性

    • 大量的 Javadoc 和 代碼實例。
    • 除了在 JDK 1.6 + 額外的限制。(一些特征是只支持在 Java 1.7 +。可選的功能可能 有額外的限制。)

    性能

    • 比核心 Java API 更好的吞吐量,較低的延時。
    • 資源消耗更少,這個得益于共享池和重用。
    • 減少內存拷貝。

    健壯性

    • 消除由于慢,快,或重載連接產生的 OutOfMemoryError。
    • 消除經常發現在 NIO 在高速網絡中的應用中的不公平的讀/寫。

    安全

    • 完整的 SSL / TLS 和 StartTLS 的支持。
    • 運行在受限的環境例如 Applet 或 OSGI。

    社區

    • 發布的更早和更頻繁。
    • 社區驅動。

    Netty 和 NIO 的關系

    處理客戶端連接的 EventLoopGroup 一般包含一個 NioEventLoop,NioEventLoop 即為一個 Selector(也是一個線程,負責 NIO),負責處理 NioServerSocketChannel 的狀態監測,當有連接到來時,執行 accept(),新建一個 NioSocketChannel 負責與 Client 端的通信,NioSocketChannel 從 WorkerEventLoopGroup(NioEventLoop 數量根據配置生成)中選擇一個 NioEventLoop register 進去,該 Channel 后續所有的 NIO 操作均由該 NioEventLoop 負責處理。

    NioEventLoop 主要包含兩部分操作:

    • processSelectedKeys() :即 selector 功能。
    • RunAllTasks():執行任務隊列中的任務,主要是定時任務和外部工作線程添加的讀寫任務。

    Netty 的優勢

    • a、對 epoll 空輪詢引起的 cpu 占用飆升在內部進行了處理,避免了直接使用 NIO 的陷阱。
    • b、簡化了 NIO 的處理方式。
    • c、采用多種 decoder/encoder 支持。
    • d、對 TCP 粘包/分包進行自動化處理可使用接受/處理線程池。
    • e、提高連接效率,對重連、心跳檢測的簡單支持。
    • f、可配置 IO 線程數、TCP 參數,TCP 接收和發送緩沖區使用直接內存代替堆內存,通過內存池的方式循環利用 ByteBuf。
    • g、通過引用計數器及時申請釋放不再引用的對象,降低了 GC 頻率。
    • h、使用單線程串行化的方式,高效的 Reactor 線程模型。
    • i、大量使用了 volitale、使用了 CAS 和原子類、線程安全類的使用、讀寫鎖的使用。

    Netty 的編程流程及編碼實踐

    服務端代碼

    public class EchoServer {
        public void run() throws Exception { // 進行服務器端的啟動處理
            // 線程池是提升服務器性能的重要技術手段,利用定長的線程池可以保證核心線程的有效數量
            // 在 Netty 之中線程池的實現分為兩類:主線程池(接收客戶端連接)、工作線程池(處理客戶端連接)
            EventLoopGroup bossGroup = new NioEventLoopGroup(10); // 創建接收線程池
            EventLoopGroup workerGroup = new NioEventLoopGroup(20); // 創建工作線程池
            System.out.println("服務器啟動成功,監聽端口為:" + HostInfo.PORT);
            try {
                // 創建一個服務器端的程序類進行 NIO 啟動,同時可以設置 Channel
                ServerBootstrap serverBootstrap = new ServerBootstrap(); // 服務器端
                // 設置要使用的線程池以及當前的 Channel 類型
                serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class);
                // 接收到信息之后需要進行處理,于是定義子處理器
                serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        socketChannel.pipeline().addLast(new EchoServerHandler()); // 追加了處理器
                    }
                });
                // 可以直接利用常亮進行 TCP 協議的相關配置
                serverBootstrap.option(ChannelOption.SO_BACKLOG, 128);
                serverBootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
                // ChannelFuture 描述的時異步回調的處理操作
                ChannelFuture future = serverBootstrap.bind(HostInfo.PORT).sync();
                future.channel().closeFuture().sync();// 等待 Socket 被關閉
            } finally {
                workerGroup.shutdownGracefully();
                bossGroup.shutdownGracefully();
            }
        }
    }
    
    public class EchoServerHandler extends ChannelInboundHandlerAdapter {
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            // 當客戶端連接成功之后會進行此方法的調用,明確可以給客戶端發送一些信息
            byte data[] = "【服務器**信息】連接通道已經創建,服務器開始進行響應交互。".getBytes();
            // NIO是基于緩存的操作,所以Netty也提供有一系列的緩存類(封裝了NIO中的Buffer)
            ByteBuf buf = Unpooled.buffer(data.length); // Netty 自己定義的緩存類
            buf.writeBytes(data); // 將數據寫入到緩存之中
            ctx.writeAndFlush(buf); // 強制性發送所有的數據
        }
    
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            try {
                // 表示要進行數據信息的讀取操作,對于讀取操作完成后也可以直接回應
                // 對于客戶端發送來的數據信息,由于沒有進行指定的數據類型,所以都統一按照 Object 進行接收
                ByteBuf buf = (ByteBuf) msg; // 默認情況下的類型就是 ByteBuf 類型
                // 在進行數據類型轉換的過程之中還可以進行編碼指定(NIO 的封裝)
                String inputData = buf.toString(CharsetUtil.UTF_8); // 將字節緩沖區的內容轉為字符串
                String echoData = "【ECHO】" + inputData; // 數據的回應處理
                // exit 是客戶端發送來的內容,可以理解為客戶端的編碼,而 quit 描述的是一個客戶端的結束
                if ("exit".equalsIgnoreCase(inputData)) { // 進行溝通的端開
                    echoData = "quit"; // 結束當前交互
                }
                byte[] data = echoData.getBytes(); // 將字符串變為字節數組
                ByteBuf echoBuf = Unpooled.buffer(data.length);
                echoBuf.writeBytes(data);// 將內容保存在緩存之中
                ctx.writeAndFlush(echoBuf); // 回應的輸出操作
            } finally {
                ReferenceCountUtil.release(msg); // 釋放緩存
            }
        }
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throwsException {
            cause.printStackTrace();
            ctx.close();
        }
    }
    

    客戶端代碼

    public class EchoClient {
        public void run() throws Exception {
            // 1、如果現在客戶端不同,那么也可以不使用多線程模式來處理;
            // 在 Netty 中考慮到代碼的統一性,也允許你在客戶端設置線程池
            EventLoopGroup group = new NioEventLoopGroup(); // 創建一個線程池
            try {
                Bootstrap client = new Bootstrap(); // 創建客戶端處理程序
                client.group(group).channel(NioSocketChannel.class)
                        .option(ChannelOption.TCP_NODELAY, true) // 允許接收大塊的返回數據
                        .handler(new ChannelInitializer<SocketChannel>() {
                            @Override
                            protected void initChannel(SocketChannel socketChannel) throwsException {
                                socketChannel.pipeline().addLast(new EchoClientHandler()); // 追加了處理器
                            }
                        });
                ChannelFuture channelFuture = client.connect(HostInfo.HOST_NAME, HostInfo.PORT).sync();
                channelFuture.channel().closeFuture().sync(); // 關閉連接
            } finally {
                group.shutdownGracefully();
            }
        }
    }
    
    public class EchoClientHandler extends ChannelInboundHandlerAdapter {
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            // 只要服務器端發送完成信息之后,都會執行此方法進行內容的輸出操作
            try {
                ByteBuf readBuf = (ByteBuf) msg;
                String readData = readBuf.toString(CharsetUtil.UTF_8).trim(); // 接收返回數據內容
                if ("quit".equalsIgnoreCase(readData)) { // 結束操作
                    System.out.println("【EXIT】拜拜,您已經結束了本次網絡傳輸,再見!");
                    ctx.close(); // 關閉通道
                } else {
                    System.out.println(readData); // 輸出服務器端的響應內容
                    String inputData = InputUtil.getString("請輸入要發送的消息:");
                    byte[] data = inputData.getBytes(); // 將輸入數據變為字節數組的形式
                    ByteBuf sendBuf = Unpooled.buffer(data.length);
                    sendBuf.writeBytes(data); // 將數據保存在緩存之中
                    ctx.writeAndFlush(sendBuf); // 數據發送
                }
            } finally {
                ReferenceCountUtil.release(msg); // 釋放緩存
            }
        }
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            cause.printStackTrace();
            ctx.close();
        }
    }
    

    Netty 重要組件

    啟動輔助類

    Bootstrap 是 Netty 提供的一個便利的工廠類,可以通過它來完成 Netty 的客戶端或服務器端的Netty 初始化。當然,Netty 的官方解釋說,可以不用這個啟動器。但是,一點點去手動創建 channel 并且完成一些的設置和啟動,會非常麻煩。還是使用這個便利的工具類,會比較好。有兩個啟動器,分別應用在服務器和客戶端。

    • 服務器:ServerBootstrap
    • 客戶端:Bootstrap

    netty 的輔助啟動器,netty 客戶端和服務器的入口,Bootstrap 是創建客戶端連接的啟動器,ServerBootstrap 是監聽服務端端口的啟動器,是程序的入口。

    在這里插入圖片描述

    Channel

    關聯 jdk 原生 socket 的組件,常用的是 NioServerSocketChannel 和 NioSocketChannel,NioServerSocketChannel 負 責 監 聽 一 個 tcp 端 口 , 有 連 接 進 來 通 過 boss reactor 創 建 一 個NioSocketChannel 將 其 綁 定 到 worker reactor( ‘ 子 線 程 ’ ) , 然 后 worker reactor 負 責 這 個NioSocketChannel 的讀寫等 io 事件。Channel 是 Netty 的核心概念之一,它是 Netty 網絡通信的主體,由它負責同對端進行網絡通信、注冊和數據操作等功能。一旦用戶端連接成功,將新建一個 channel(’accpet 到的’)同該用戶端進行綁定 channel 從 EventLoopGroup 獲得一個 EventLoop,并注冊到該 EventLoop,channel 生命周期內都和該 EventLoop 在一起(注冊時獲得 selectionKey)channel 同用戶端進行網絡連接、關閉和讀寫,生成相對應的 event(改變 selectinKey 信息),觸發 eventloop 調度線程進行執行如果是讀事件,執行線程調度 pipeline 來處理用戶業務邏輯。

    在這里插入圖片描述

    EventLoopGroup

    EventLoopGroup bossGroup = new NioEventLoopGroup(10); // 創建接收線程池
    EventLoopGroup workerGroup = new NioEventLoopGroup(20); // 創建工作線程池。
    

    netty 最核心的幾大組件之一,就是我們常說的 reactor,人為劃分為 boss reactor 和 worker reactor。通過 EventLoopGroup(Bootstrap 啟動時會設置 EventLoopGroup)生成,最常用的是 nio 的 NioEventLoop,就如同 EventLoop 的名字,EventLoop 內部有一個無限循環,維護了一個 selector,處理所有注冊到 selector 上的 io 操作,在這里實現了一個線程維護多條連接的工作。在 Netty 中,每一個 channel 綁定了一個 thread 線程。一個 thread 線程,封裝到一個 EventLoop ,多個 EventLoop ,組成一個線程組 EventLoopGroup。反過來說,EventLoop 這個相當于一個處理線程,是 Netty 接收請求和處理 IO 請求的線程。 EventLoopGroup 可以理解為將多個 EventLoop 進行分組管理的一個類,是EventLoop 的一個組。

    在這里插入圖片描述

    ChannelPipeline

    ChannelPipeline 是一系列的 ChannelHandler 實例,用于攔截 流經一個 Channel 的入站和出站事件,ChannelPipeline 允許用戶自己定義對入站/出站事件的處理邏輯,以及 pipeline 里的各個 Handler之間的交互。

    每一次創建了新的 Channel ,都會新建一個新的 ChannelPipeline 并綁定到 Channel 上。這個關聯是 永久性的;Channel 既不能附上另一個 ChannelPipeline 也不能分離 當前這個。這些都由 Netty負責完成,而無需開發人員的特別處理。

    根據它的起源,一個事件將由 ChannelInboundHandler 或 ChannelOutboundHandler 處理。隨后它將調用 ChannelHandlerContext 實現轉發到下一個相同的超類型的處理程序。

    ChannelHandlerContext

    一個 ChannelHandlerContext 使 ChannelHandler 與 ChannelPipeline 和 其他處理程序交互。一個處理程序可以通知下一個 ChannelPipeline 中的 ChannelHandler 甚至動態修改 ChannelPipeline 的歸屬。

    在這里插入圖片描述

    接口 ChannelHandlerContext 代表 ChannelHandler 和 ChannelPipeline 之間的關聯,并在ChannelHandler 添加到 ChannelPipeline 時創建一個實例。ChannelHandlerContext 的主要功能是管理通過同一個 ChannelPipeline 關聯的 ChannelHandler 之間的交互。

    ChannelHandlerContext 有許多方法,其中一些也出現在 Channel 和 ChannelPipeline 本身。然而,如果您通過 Channel 或 ChannelPipeline 的實例來調用這些方法,他們就會在整 pipeline 中傳播。相比之下,一樣的 的方法在 ChannelHandlerContext 的實例上調用,就只會從當前的 ChannelHandler 開始并傳播到相關管道中的下一個有處理事件能力的 ChannelHandler。

    ChannelInboundHandler

    在這里插入圖片描述

    ChannelOutboundHandler

    在這里插入圖片描述

    Buffer

    正如我們先前所指出的,網絡數據的基本單位永遠是 byte(字節)。Java NIO 提供 ByteBuffer 作為字節的容器,但它的作用太有限,也沒有進行優化。使用 ByteBuffer 通常是一件繁瑣而又復雜的事。

    它有以下缺陷:

    • ByteBuffer 長度固定,一旦分配完成,它的容量不能動態擴展和收縮,當需要編碼的 POJO 對象大于 ByteBuffer 的容量時,會發生索引越界異常;
    • ByteBuffer 只有一個標識位控的指針 position,讀寫的時候需要手工調用 flip()和 rewind()等,使用者必須小心謹慎地處理這些 API,否則很容易導致程序處理失敗;
    • ByteBuffer 的 API 功能有限,一些高級和實用的特性它不支持,需要使用者自己編程實現。

    幸運的是,Netty 提供了一個強大的緩沖實現類用來表示字節序列以及幫助你操作字節和自定義的 POJO。這個新的緩沖類,ByteBuf,效率與 JDK 的 ByteBuffer 相當。設計 ByteBuf 是為了在 Netty 的pipeline 中傳輸數據。并且解決 ByteBuffer 存在的一些問題以及滿足網絡程序開發者的需求,以提高他們的生產效率而被設計出來的。請注意,在本書剩下的章節中,為了幫助區分,我將使用數據容器指代 Netty 的緩沖接口及實 現,同時仍然使用 Java 的緩沖 API 指代 JDK 的緩沖實現。

    Netty 緩沖 API 提供了幾個優勢:

    • 1、可以自定義緩沖類型。

    ByteBuf 的三種類型:heapBuffer(堆緩沖區)、directBuffer(直接緩沖區)以及 CompositeBuffer(復合緩沖區)。

    1. HEAP BUFFER(堆緩沖區) 數組 底層就是封裝了數組
      最常用的模式是 ByteBuf 將數據存儲在 JVM 的堆空間,這是通過將數據存儲在數組的實現。堆緩沖區可以快速分配,當不使用時也可以快速釋放。它還提供了直接訪問數組的方法,通過 ByteBuf.array()來獲取 byte[]數據。
    2. DIRECT BUFFER(直接緩沖區) 跟 jvm 無關一塊區域
      “直接緩沖區”是另一個 ByteBuf 模式。對象的所有內存分配發生在堆,對不對?好吧,并非總是如此。在 JDK1.4 中被引入 NIO 的 ByteBuffer 類允許 JVM 通過本地方法調用分配內存, 其目的是通過免去中間交換的內存拷貝, 提升 IO 處理速度; 直接緩沖區的內容可以駐留在垃圾回收掃描的堆區以外。DirectBuffer 在 -XX:MaxDirectMemorySize=xxM 大小限制下, 使用 Heap 之外的內存, GC 對此無能為力”,也就意味著規避了在高負載下頻繁的 GC 過程對應用線程的中斷影響。
    3. COMPOSITE BUFFER(復合緩沖區)
      最后一種模式是復合緩沖區,我們可以創建多個不同的 ByteBuf,然后提供一個這些 ByteBuf 組合的視圖。復合緩沖區就像一個列表,我們可以動態的添加和刪除其中的 ByteBuf,JDK 的 ByteBuffer 沒有這樣的功能。

    在這里插入圖片描述

    • 2、通過一個內置的復合緩沖類型實現零拷貝。
    • 3、擴展性好。

    Bytebuf 底層提供自動擴容機制。容量小于 64 字節之前直接擴容到 64 字節大小,容量大于 64 字節容量小于 4M 的時候 2 倍擴容(指數),大于 4M(門限)小于最大容量時每次擴容 4M。(+4M) 整數的最大值,作為容量的上限。

    • 4、不需要調用 flip() 來切換讀/寫模式。
    • 5、讀取和寫入索引分開。

    因為讀寫索引是分開的所以不需要通過 flip()切換下標。所以再 bytebuf 中提供了很多關于這兩個下標的操作。

    • 6、方法鏈。
    • 7、引用計數。

    Netty 里四種主力的 ByteBuf,其中 UnpooledHeapByteBuf 底下的 byte[]能夠依賴 JVM GC 自然回收;而 UnpooledDirectByteBuf 底下是 DirectByteBuffer,如 Java 堆外內存掃盲貼所述 , 除 了 等 JVM GC , 最 好 也 能 主 動 進 行 回 收 ; 而 PooledHeapByteBuf 和 PooledDirectByteBuf,則必須要主動將用完的 byte[]/ByteBuffer 放回池里,如果不釋放,內存池會越來越大,直到內存溢出。所以,Netty ByteBuf 需要在 JVM 的 GC 機制之外,有自己的引用計數器和回收過程(主要是回收到 netty 申請的內存池)。

    • 8、Pooling(池)。

    池化概念類似線程池和連接池,其優勢也類似。

    ByteBuf 基本操作:

    • ByteBufAllocator

    為了減少分配和釋放內存的開銷,Netty 通過支持池類 ByteBufAllocator,可用于分配的任何ByteBuf 我們已經描述過的類型的實例。是否使用池是由應用程序決定的,下表列出了 ByteBufAllocator 提供的操作。

    在這里插入圖片描述

    通過一些方法接受整型參數允許用戶指定 ByteBuf 的初始和最大容量值。你可能還記得, ByteBuf 存儲可以擴大到其最大容量。 得到一個 ByteBufAllocator 的引用很簡單。你可以得到從 Channel (在理論上,每 Channel 可具有不同的 ByteBufAllocator ),或通過綁定到的 ChannelHandler 的 ChannelHandlerContext 得到它,用它實現了你數據處理邏輯。

    • Unpooled (非池化)緩存

    當未引用 ByteBufAllocator 時,上面的方法無法訪問到 ByteBuf。對于這個用例 Netty 提供一個實用工具類稱為 Unpooled,,它提供了靜態輔助方法來創建非池化的 ByteBuf 實例。表 5.9 列出了最重要的方法。

    在這里插入圖片描述

    在非聯網項目,該 Unpooled 類也使得它更容易使用的 ByteBuf API,獲得一個高性能的可擴展緩沖API,而不需要 Netty 的其他部分的。

    • ByteBufUtil

    ByteBufUtil 靜態輔助方法來操作 ByteBuf,因為這個 API 是通用的,與使用池無關,這些方法已經在外面的分配類實現。 ByteBuf 分配 89ByteBuf 分配。

    也許最有價值的是 hexDump() 方法,這個方法返回指定 ByteBuf 中可讀字節的十六進制字符串,可以用于調試程序時打印 ByteBuf 的內容。一個典型的用途是記錄一個 ByteBuf 的內容 進行調試。十六進制字符串相比字節而言對用戶更友好。 而且十六進制版本可以很容易地轉 換回實際字節表示。另一個有用方法是 使用 boolean equals(ByteBuf, ByteBuf),用來比較 ByteBuf 實例是否相等。在實現自己 ByteBuf 的子類時經常用到。

    • 基本操作

    在這里插入圖片描述

    1. 字節,可以被丟棄,因為它們已經被讀。
    2. 還沒有被讀的字節是:“readable bytes(可讀字節)”。
    3. 空間可加入多個字節的是:“writeable bytes(寫字節)”。

    在這里插入圖片描述

    ByteBuf.discardReadBytes() 可以用來清空 ByteBuf 中已讀取的數據,從而使 ByteBuf 有多余的空間容納新的數據,但是 discardReadBytes() 可能會涉及內存復制,因為它需要移動 ByteBuf 中可讀的字節到開始位置,這樣的操作會影響性能,一般在需要馬上釋放內存的時候使用收益會比較大。

    • 索引管理

    在 JDK 的 InputStream 定義了 mark(int readlimit) 和 reset()方法。這些是分別用來標記流中的當前位置和復位流到該位置。 同樣,您可以設置和重新定位 ByteBuf readerIndex 和 writerIndex 通過調用 markReaderIndex(), markWriterIndex(), resetReaderIndex() 和 resetWriterIndex()。這些類似 于 InputStream 的調用,所不同的是,沒有 readlimit 參數來指定當標志變為無效。您也可以通過調用 readerIndex(int) 或 writerIndex(int) 將指標移動到指定的位置。在嘗試任何無效位置上設置一個索引將導致 IndexOutOfBoundsException 異常。

    調用 clear() 可以同時設置 readerIndex 和 writerIndex 為 0。注意,這不會清除內存中的內容。讓我們看看它是如何工作的。

    Clear 之前:

    在這里插入圖片描述

    Clear 之后:邏輯刪除

    在這里插入圖片描述

    現在 整個 ByteBuf 空間都是可寫的了。

    clear() 比 discardReadBytes() 更低成本,因為他只是重置了索引,而沒有內存拷貝。

    Netty 網絡庫工作流程

    1、Netty 抽象出兩組線程組(可以理解為線程池)。BossGroup 專門負責接收客戶端的連接,workerGroup 專門負責網絡讀寫。

    2、BossGroup 和 workerGroup 類型都是 NIOEventLoopGroup。

    3、NioEventLoopGroup 相當于一個事件循環線程組, 這個組中含有多個事件循環線程 , 每一個事件 循環線程是 NioEventLoop。

    4、NioEventLoop 表示一個不斷執行的任務線程,每個 NioEventLoop 都有一個 selector , 用于監聽注冊在其上的 channel 的網絡通訊。

    5、每個 Boss NioEventLoop 線程內部循環執行的步驟有 3 步:

    • a、輪詢 accept 事件。
    • b、處理 accept 事件 , 與 client 建立連接 , 生成 NioSocketChannel 將 NioSocketChannel 注冊到某個 worker NIOEventLoop 上的 selector。
    • c、處理任務隊列的任務 , 即 runAllTasks。

    6、每個 worker NIOEventLoop 線程循環執行的步驟。

    輪詢注冊到自己 selector 上的所有 NioSocketChannel 的 read, write 事件處理 I/O 事件, 即 read , write 事件, 在對應 NioSocketChannel 處理業務 runAllTasks 處理任務隊列 TaskQueue 的任務 ,一些耗時的業務處理一般可以放入 TaskQueue 中慢慢處理,這樣不影響數據在 pipeline 中的流動處理。

    7、每個 worker NIOEventLoop 處理 NioSocketChannel 業務時,會使用 pipeline (管道),管道中維護 了很多 handler 處理器用來處理 channel 中的數據。

    Netty 零拷貝的實現

    即所謂的 Zero-copy,就是在操作數據時,不需要將數據 buffer 從一個內存區域拷貝到另一個內存區域,因為少了一次內存的拷貝, 因此 CPU 的效率就得到的提升。

    在 OS 層面上的 Zero-copy 通常指避免在 用戶態(User-space) 與 內核態(Kernel-space) 之間來回拷貝數據。例如 Linux 提供的 mmap 系統調用,它可以將一段用戶空間內存映射到內核空間,當映射成功后,用戶對這段內存區域的修改可以直接反映到內核空間;同樣地,內核空間對這段區域的修改也直接反映用戶空間。正因為有這樣的映射關系,我們就不需要在 用戶態(User-space) 與 內核態(Kernel-space) 之間拷貝數據,提高了數據傳輸的效率。

    而需要注意的是,Netty 中的 Zero-copy 與上面我們所提到到 OS 層面上的 Zero-copy 不太一樣,Netty 的 Zero-coyp 完全是在用戶態(Java 層面)的,它的 Zero-copy 的更多的是偏向于 優化數據操作 這樣的概念。

    Netty 的 Zero-copy 體現在如下幾個方面:

    • 1、Netty 提供了 CompositeByteBuf 類,它可以將多個 ByteBuf 合并為一個邏輯上的 ByteBuf,避免了各個 ByteBuf 之間的拷貝。
    • 2、通過 wrap 操作,我們可以將 byte[] 數組、ByteBuf、ByteBuffer 等包裝成一個 Netty ByteBuf 對象,進而避免了拷貝操作。
    • 3、ByteBuf 支持 slice 操作,因此可以將 ByteBuf 分解為多個共享同一個存儲區域的 ByteBuf,避免了內存的拷貝。
    • 4、通過 FileRegion 包裝的 FileChannel.tranferTo 實現文件傳輸,可以直接將文件緩沖區的數據發送到目標 Channel,避免了傳統通過循環 write 方式導致的內存拷貝問題。

    通過 CompositeByteBuf 實現零拷貝

    假設我們有一份協議數據,它由頭部和消息體組成,而頭部和消息體是分別存放在兩個 ByteBuf 中的,即:

    ByteBuf header = ...
    ByteBuf body = ...
    

    我們在代碼處理中,通常希望將 header 和 body 合并為一個 ByteBuf,方便處理,那么通常的做法是:

    ByteBuf allBuf = Unpooled.buffer(header.readableBytes() + body.readableBytes());
    
    allBuf.writeBytes(header);
    allBuf.writeBytes(body);
    

    可以看到, 我們將 header 和 body 都拷貝到了新的 allBuf 中了,這無形中增加了兩次額外的數據拷貝操作了。那么有沒有更加高效優雅的方式實現相同的目的呢?我們來看一下 CompositeByteBuf 是如何實現這樣的需求的吧。

    ByteBuf header = ...
    ByteBuf body = ...
    CompositeByteBuf compositeByteBuf = Unpooled.compositeBuffer();
    compositeByteBuf.addComponents(true, header, body);
    

    上面代碼中, 我們定義了一個 CompositeByteBuf 對象, 然后調用

    public CompositeByteBuf addComponents(boolean increaseWriterIndex, ByteBuf... buffers) {
        ...
    }
    

    方法將 header 與 body 合并為一個邏輯上的 ByteBuf,即:

    在這里插入圖片描述

    不過需要注意的是,雖然看起來 CompositeByteBuf 是由兩個 ByteBuf 組合而成的,不過在CompositeByteBuf 內部,這兩個 ByteBuf 都是單獨存在的,CompositeByteBuf 只是邏輯上是一個整體。上面 CompositeByteBuf 代碼還以一個地方值得注意的是,我們調用 addComponents(boolean increaseWriterIndex, ByteBuf… buffers) 來添加兩個 ByteBuf,其中第一個參數是 true,表示當添加新的 ByteBuf 時,自動遞增 CompositeByteBuf 的 writeIndex。

    如果我們調用的是 compositeByteBuf.addComponents(header, body);那 么其實 compositeByteBuf 的 writeIndex 仍然是 0,因此此時我們就不可從 compositeByteBuf 中讀取到數據,這一點希望大家要特別注意。

    除了上面直接使用 CompositeByteBuf 類外 , 我們還可以使用 Unpooled.wrappedBuffer 方法,它底層封裝了 CompositeByteBuf 操作, 因此使用起來更加方便:

    ByteBuf header = ...
    ByteBuf body = ...
    ByteBuf allByteBuf = Unpooled.wrappedBuffer(header, body);
    

    通過 wrap 操作實現零拷貝

    例如我們有一個 byte 數組,我們希望將它轉換為一個 ByteBuf 對象,以便于后續的操作,那么傳統的做法是將此 byte 數組拷貝到 ByteBuf 中,即:

    byte[] bytes = ...
    ByteBuf byteBuf = Unpooled.buffer();
    byteBuf.writeBytes(bytes);
    

    顯然這樣的方式也是有一個額外的拷貝操作的,我們可以使用 Unpooled 的相關方法,包裝這個 byte 數組,生成一個新的 ByteBuf 實例,而不需要進行拷貝操作。上面的代碼可以改為:

    byte[] bytes = ...
    ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes);
    

    可以看到,我們通過 Unpooled.wrappedBuffer 方法來將 bytes 包裝成為一個 UnpooledHeapByteBuf 對象,而在包裝的過程中,是不會有拷貝操作的。即最后我們生成的生成的 ByteBuf 對象是和 bytes 數組共用了同一個存儲空間,對 bytes 的修改也會反映到 ByteBuf 對象中。

    Unpooled 工具類還提供了很多重載的 wrappedBuffer 方法:

    public static ByteBuf wrappedBuffer(byte[] array)
    public static ByteBuf wrappedBuffer(byte[] array, int offset, int length)
    public static ByteBuf wrappedBuffer(ByteBuffer buffer)
    public static ByteBuf wrappedBuffer(ByteBuf buffer)
    public static ByteBuf wrappedBuffer(byte[]... arrays)
    public static ByteBuf wrappedBuffer(ByteBuf... buffers)
    public static ByteBuf wrappedBuffer(ByteBuffer... buffers)
    public static ByteBuf wrappedBuffer(int maxNumComponents, byte[]... arrays)
    public static ByteBuf wrappedBuffer(int maxNumComponents, ByteBuf... buffers)
    public static ByteBuf wrappedBuffer(int maxNumComponents, ByteBuffer... buffers)
    

    這些方法可以將一個或多個 buffer 包裝為一個 ByteBuf 對象,從而避免了拷貝操作。

    通過 slice 操作實現零拷貝

    slice 操作可以將一個 ByteBuf 切片 為多個共享一個存儲區域的 ByteBuf 對象。

    ByteBuf 提供了兩個 slice 操作方法:

    public ByteBuf slice();
    public ByteBuf slice(int index, int length);
    

    不帶參數的 slice 方法等同于 buf.slice(buf.readerIndex(), buf.readableBytes()) 調用,即返回 buf中可讀部分的切片。而 slice(int index, int length) 方法相對就比較靈活了,我們可以設置不同的參數來獲取到 buf 的不同區域的切片。

    下面的例子展示了 ByteBuf.slice 方法的簡單用法:

    ByteBuf byteBuf = ...
    ByteBuf header = byteBuf.slice(0, 5);
    ByteBuf body = byteBuf.slice(5, 10);
    

    用 slice 方法產生 header 和 body 的過程是沒有拷貝操作的,header 和 body 對象在內部其實是共享了 byteBuf 存儲空間的不同部分而已。即:

    在這里插入圖片描述

    通過 FileRegion 實現零拷貝

    Netty 中使用 FileRegion 實現文件傳輸的零拷貝,不過在底層 FileRegion 是依賴于 Java NIO FileChannel.transfer 的零拷貝功能。

    首先我們從最基礎的 Java IO 開始吧。假設我們希望實現一個文件拷貝的功能,那么使用傳統的方式,我們有如下實現:

    public static void copyFile(String srcFile,String destFile) throws Exception{
        byte[]temp=new byte[1024];
        FileInputStream in=new FileInputStream(srcFile);
        FileOutputStream out=new FileOutputStream(destFile);
        int length;
        while((length=in.read(temp))!=-1){
            out.write(temp,0,length);
        }
        in.close();
        out.close();
    }
    

    上面是一個典型的讀寫二進制文件的代碼實現了。不用我說,大家肯定都知道,上面的代碼中不斷中源文件中讀取定長數據到 temp 數組中,然后再將 temp 中的內容寫入目的文件,這樣的拷貝操作對于小文件倒是沒有太大的影響,但是如果我們需要拷貝大文件時,頻繁的內存拷貝操作就消耗大量的系統資源了。

    下面我們來看一下使用 Java NIO 的 FileChannel 是如何實現零拷貝的:

    public static void copyFileWithFileChannel(String srcFileName, String destFileName) throws Exception {
        RandomAccessFile srcFile = new RandomAccessFile(srcFileName, "r");
        FileChannel srcFileChannel = srcFile.getChannel();
    
        RandomAccessFile destFile = new RandomAccessFile(destFileName, "rw");
        FileChannel destFileChannel = destFile.getChannel();
    
        long position = 0;
        long count = srcFileChannel.size();
    
        srcFileChannel.transferTo(position, count, destFileChannel);
    }
    

    可以看到,使用了 FileChannel 后,我們就可以直接將源文件的內容直接拷貝( transferTo ) 到目的文件中,而不需要額外借助一個臨時 buffer,避免了不必要的內存操作。

    有了上面的一些理論知識,我們來看一下在 Netty 中是怎么使用 FileRegion 來實現零拷貝傳輸一個文件的:

    public class TestDemo {
        public static void main(String[] args) {
            File file = new File("D:\\Java\\a.txt");
            if (file.exists()) {
                if (!file.isFile()) {
                    //寫入換行符表示文件結束
                    ctx.writeAndFlush("Not a file: " + file);
                    return;
                }
                //換行符表示文件結尾
                System.out.println("開始發送文件");
                RandomAccessFile randomAccessFile = new RandomAccessFile("D:\\Java\\a.txt", "r");
                FileRegion region = new DefaultFileRegion(
                        randomAccessFile.getChannel(), 0, randomAccessFile.length());
                ctx.write(region);
                ctx.writeAndFlush(System.getProperty("line.separator"));
                randomAccessFile.close();
            } else {
                ctx.writeAndFlush("File not found: " + file);
            }
        }
    }
    

    可以看到,第一步是通過 RandomAccessFile 打開一個文件,然后 Netty 使用了 DefaultFileRegion 來封裝一個 FileChannel 即:

    new DefaultFileRegion(raf.getChannel(), 0 , length);
    

    當有了 FileRegion 后,我們就可以直接通過它將文件的內容直接寫入 Channel 中,而不需要像傳統的做法:拷貝文件內容到臨時 buffe,然后再將 buffer 寫入 Channel。通過這樣的零拷貝操作,無疑對傳輸大文件很有幫助。

    channel 的線程安全

    在這里插入圖片描述

    • 一個 EventLoopGroup 包含一個或者多個 EventLoop,即 EventLoopGroup:EventLoop = 1:n。
    • 一個 EventLoop 在它的生命周期內只能與有個 Thread 綁定,即 EventLoop:Thread = 1:1。
    • 一個 channel 在它的生命周期內只能注冊到一個 EventLoop 上,channel:EventLoop = n:1。

    當一個連接到達時,Netty 就會創建一個 channel,然后從 EventLoopGroup 中分配一個 EventLoop 來給 channel 綁定上,在該 channel 的整個生命周期中都是由綁定的這個 EventLoop 來服務的。

    Netty 常用參數

    Option 和 ChildOption 的區別

    Netty 中的 option 主要是設置的 ServerChannel 的一些選項,而 childOption 主要是設置的 SocketChannel 的子 Channel 的選項。

    如果是在客戶端,因為是 Bootstrap,只會有 option 而沒有 childOption,所以設置的是客戶端 Channel 的選項。

    通用參數

    • CONNECT_TIMEOUT_MILLIS

    Netty 參數,連接超時毫秒數,默認值 30000 毫秒即 30 秒。

    • MAX_MESSAGES_PER_READ

    Netty 參數,一次 Loop 讀取的最大消息數,對于 ServerChannel 或者 NioByteChannel,默認值為 16,其他 Channel 默認值為 1。默認值這樣設置,是因為:ServerChannel 需要接受足夠多的連接,保證大吞吐量,NioByteChannel 可以減少不必要的系統調用 select。

    • WRITE_SPIN_COUNT

    Netty 參數,一個 Loop 寫操作執行的最大次數,默認值為 16。也就是說,對于大數據量的寫操作至多進行 16 次,如果 16 次仍沒有全部寫完數據,此時會提交一個新的寫任務給 EventLoop,任務將在下次調度繼續執行。這樣,其他的寫請求才能被響應不會因為單個大數據量寫請求而耽誤。

    • ALLOCATOR

    Netty 參數,ByteBuf 的分配器,默認值為 ByteBufAllocator.DEFAULT,4.0 版本為 UnpooledByteBufAllocator,4.1 版本為 PooledByteBufAllocator。該值也可以使用系統參數 io.netty.allocator.type 配置,使用字符串值:“unpooled”,“pooled”。

    • RCVBUF_ALLOCATOR

    Netty 參數,用于 Channel 分配接受 Buffer 的分配器,默認值為 AdaptiveRecvByteBufAllocator.DEFAULT,是一個自適應的接受緩沖區分配器,能根據接受到的數據自動調節大小。可選值為FixedRecvByteBufAllocator,固定大小的接受緩沖區分配器。

    • AUTO_READ

    Netty 參數,自動讀取,默認值為 True。Netty 只在必要的時候才設置關心相應的 I/O 事件。對于讀操作,需要調用 channel.read()設置關心的 I/O 事件為 OP_READ,這樣若有數據到達才能讀取以供用戶處理。該值為 True 時,每次讀操作完畢后會自動調用 channel.read(),從而有數據到達便能讀取;否則,需要用戶手動調用 channel.read()。需要注意的是:當調用 config.setAutoRead(boolean)方法時,如果狀態由 false 變為 true ,將會調用 channel.read()方法讀取數據;由 true 變為 false,將調用 config.autoReadCleared()方法終止數據讀取。

    • WRITE_BUFFER_HIGH_WATER_MARK

    Netty 參數,寫高水位標記,默認值 64KB。如果 Netty 的寫緩沖區中的字節超過該值,Channel 的 isWritable() 返回 False。

    • WRITE_BUFFER_LOW_WATER_MARK

    Netty 參數,寫低水位標記,默認值 32KB。當 Netty 的寫緩沖區中的字節超過高水位之后若下降到低水位,則 Channel 的 isWritable()返回 True。寫高低水位標記使用戶可以控制寫入數據速度,從而實現流量控制。推薦做法是:每次調用 channl.write(msg)方法首先調用 channel.isWritable()判斷是否可寫。

    • MESSAGE_SIZE_ESTIMATOR

    Netty 參數,消息大小估算器,默認為 DefaultMessageSizeEstimator.DEFAULT。估算 ByteBuf、ByteBufHolder 和 FileRegion 的大小,其中 ByteBuf 和 ByteBufHolder 為實際大小,FileRegion 估算值為 0。該值估算的字節數在計算水位時使用,FileRegion 為 0 可知 FileRegion 不影響高低水位。

    • SINGLE_EVENTEXECUTOR_PER_GROUP

    Netty 參數,單線程執行 ChannelPipeline 中的事件,默認值為 True。該值控制執行 ChannelPipeline中執行 ChannelHandler 的線程。如果為 true,整個 pipeline 由一個線程執行,這樣不需要進行線程切換以及線程同步,是 Netty4 的推薦做法;如果為 False,ChannelHandler 中的處理過程會由 Group 中的不同線程執行。

    SocketChannel 參數

    • SO_RCVBUF

    Socket 參數,TCP 數據接收緩沖區大小。該緩沖區即 TCP 接收滑動窗口,linux 操作系統可使用命令:cat /proc/sys/net/ipv4/tcp_rmem 查詢其大小。一般情況下,該值可由用戶在任意時刻設置,但當設置值超過 64KB 時,需要在連接到遠端之前設置。

    使用建議: 緩沖區大小設為網絡吞吐量達到帶寬上限時的值,即緩沖區大小 = 網絡帶寬 * 網絡時延。以千兆網卡為例進行計算,假設網絡時延為 1ms,緩沖區大小 = 1000Mb/s * 1ms = 128KB。

    • SO_SNDBUF

    Socket 參數,TCP 數據發送緩沖區大小。該緩沖區即 TCP 發送滑動窗口,linux 操作系統可使用命令:cat /proc/sys/net/ipv4/tcp_smem 查詢其大小。

    使用建議: 緩沖區大小設為網絡吞吐量達到帶寬上限時的值,即緩沖區大小 = 網絡帶寬 * 網絡時延。以千兆網卡為例進行計算,假設網絡時延為 1ms,緩沖區大小 = 1000Mb/s * 1ms = 128KB。

    • TCP_NODELAY

    TCP 參數,立即發送數據,默認值為 Ture(Netty 默認為 True 而操作系統默認為 False)。該值設置 Nagle 算法的啟用,改算法將小的碎片數據連接成更大的報文來最小化所發送的報文的數量,如果需要發送一些較小的報文,則需要禁用該算法。Netty 默認禁用該算法,從而最小化報文傳輸延時。

    使用建議:如果需要發送一些較小的報文,則需要禁用該算法,從而最小化報文傳輸延時。只有在網絡通信非常大時(通常指已經到 100k+/秒了),設置為 false 會有些許優勢,因此建議大部分情況下均應設置為 true。

    • SO_KEEPALIVE 打開了保活計時器

    Socket 參數,連接保活,默認值為 False。啟用該功能時,TCP 會主動探測空閑連接的有效性。可以將此功能視為 TCP 的心跳機制,需要注意的是:默認的心跳間隔是 7200s 即 2 小時。Netty 默認關閉該功能。

    使用建議: 心跳機制由應用層自己實現;由應用程序自己發送心跳包來檢測連接是否正常,服務器每隔一定時間向客戶端發送一個短小的數據包,然后啟動一個線程,在線程中不斷檢測客戶端的回應, 如果在一定時間內沒有收到客戶端的回應,即認為客戶端已經掉線;同樣,如果客戶端在一定時間內沒有收到服務器的心跳包,則認為連接不可用。

    • SO_REUSEADDR

    Socket 參數,地址復用,默認值 False。有四種情況可以使用:

    1. 當有一個有相同本地地址和端口的 socket1 處于 TIME_WAIT 狀態時,而你希望啟動的程序的 socket2 要占用該地址和端口,比如重啟服務且保持先前端口。
    2. 有多塊網卡或用 IP Alias 技術的機器在同一端口啟動多個進程,但每個進程綁定的本地 IP 地址不能相同。
    3. 單個進程綁定相同的端口到多個 socket 上,但每個 socket 綁定的 ip 地址不同。
    4. 完全相同的地址和端口的重復綁定。但這只用于 UDP 的多播,不用于 TCP。
    • SO_LINGER

    Socket 參數,關閉 Socket 的延遲時間,默認值為-1,表示禁用該功能。-1 表示 socket.close()方法立即返回,但 OS 底層會將發送緩沖區全部發送到對端。0 表示 socket.close()方法立即返回,OS 放棄發送緩沖區的數據直接向對端發送 RST 包,對端收到復位錯誤。非 0 整數值表示調用 socket.close()方法的線程被阻塞直到延遲時間到或發送緩沖區中的數據發送完畢,若超時,則對端會收到復位錯誤。

    使用建議: 使用默認值,不做設置。

    • IP_TOS

    IP 參數,設置 IP 頭部的 Type-of-Service 字段,用于描述 IP 包的優先級和 QoS 選項。

    • ALLOW_HALF_CLOSURENetty

    一個連接的遠端關閉時本地端是否關閉,默認值為 False。值為 False時,連接自動關閉;為 True 時,觸發 ChannelInboundHandler 的 userEventTriggered()方法,事件為 ChannelInputShutdownEvent。

    ServerSocketChannel 參數

    • SO_RCVBUF

    前面已說明。但是需要注意的是:當設置值超過 64KB 時,需要在綁定到本地端口前設置。該值設置的是由 ServerSocketChannel 使用 accept 接受的 SocketChannel 的接收緩沖區。

    • SO_REUSEADDR

    Socket 參數,地址復用,默認值 False。有四種情況可以使用:

    1. 當有一個有相同本地地址和端口的 socket1 處于 TIME_WAIT 狀態時,而你希望啟動的程序的 socket2 要占用該地址和端口,比如重啟服務且保持先前端口。
    2. 有多塊網卡或用 IP Alias 技術的機器在同一端口啟動多個進程,但每個進程綁定的本地 IP 地址不能相同。
    3. 單個進程綁定相同的端口到多個 socket 上,但每個 socket 綁定的 ip 地址不同。
    4. 完全相同的地址和端口的重復綁定。但這只用于 UDP 的多播,不用于 TCP。
    • SO_BACKLOG

    隊列存儲.
    存儲已經完成 TCP 連接的但是還沒有進行 accpet socketChannel.
    大小和服務器處理性能有關。
    Socket 參數,用于臨時存放已完成三次握手的請求的隊列服務端接受連接的隊列長度,如果隊列已滿,客戶端連接將被拒絕。默認值,Windows 為 200,其他為 128。
    越大肯定服務器越有問題。

    DatagramChannel 參數 (UDP)

    • SO_BROADCAST

    Socket 參數,設置廣播模式。

    • SO_RCVBUF

    前面已說明。

    • SO_SNDBUF

    前面已說明。

    • SO_REUSEADDR

    前面已說明。

    • IP_MULTICAST_LOOP_DISABLED

    對應 IP 參數 IP_MULTICAST_LOOP,設置本地回環接口的多播功能。由于 IP_MULTICAST_LOOP 返回 True 表示關閉,所以 Netty 加上后綴_DISABLED 防止歧義。

    • IP_MULTICAST_ADDR

    對應 IP 參數 IP_MULTICAST_IF,設置對應地址的網卡為多播模式。

    • IP_MULTICAST_IF

    對應 IP 參數 IP_MULTICAST_IF2,同上但支持 IPV6。

    • IP_MULTICAST_TTL

    IP 參數,多播數據報的 time-to-live 即存活跳數。

    • IP_TOS

    前面已說明。

    • DATAGRAM_CHANNEL_ACTIVE_ON_REGISTRATION

    Netty 參數,DatagramChannel 注冊的 EventLoop 即表示已**。

    版權聲明:本文為Sampson_S原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。
    本文鏈接:https://blog.csdn.net/Sampson_S/article/details/109366278

    智能推薦

    Netty學習筆記(零)—— Java網絡編程的演進之路

    最近在學習Netty,借此平臺記錄學習心得,方便今后的復習回顧,若在下對知識有理解錯誤的地方望各位大佬批評指正,不多B了。 本文目錄: 1、 Linux網絡I/O 2、Java的I/O演進 3、Java IO編程 3.1、 傳統BIO 3.2、偽異步IO 3.3、NIO 3.4、AIO 1、 Linux網絡I/O 我們先了解一下Linux系統的I/O,Linux系統的I/O分為兩個階段:1)內核準...

    HTML中常用操作關于:頁面跳轉,空格

    1.頁面跳轉 2.空格的代替符...

    freemarker + ItextRender 根據模板生成PDF文件

    1. 制作模板 2. 獲取模板,并將所獲取的數據加載生成html文件 2. 生成PDF文件 其中由兩個地方需要注意,都是關于獲取文件路徑的問題,由于項目部署的時候是打包成jar包形式,所以在開發過程中時直接安照傳統的獲取方法沒有一點文件,但是當打包后部署,總是出錯。于是參考網上文章,先將文件讀出來到項目的臨時目錄下,然后再按正常方式加載該臨時文件; 還有一個問題至今沒有解決,就是關于生成PDF文件...

    電腦空間不夠了?教你一個小秒招快速清理 Docker 占用的磁盤空間!

    Docker 很占用空間,每當我們運行容器、拉取鏡像、部署應用、構建自己的鏡像時,我們的磁盤空間會被大量占用。 如果你也被這個問題所困擾,咱們就一起看一下 Docker 是如何使用磁盤空間的,以及如何回收。 docker 占用的空間可以通過下面的命令查看: TYPE 列出了docker 使用磁盤的 4 種類型: Images:所有鏡像占用的空間,包括拉取下來的鏡像,和本地構建的。 Con...

    requests實現全自動PPT模板

    http://www.1ppt.com/moban/ 可以免費的下載PPT模板,當然如果要人工一個個下,還是挺麻煩的,我們可以利用requests輕松下載 訪問這個主頁,我們可以看到下面的樣式 點每一個PPT模板的圖片,我們可以進入到詳細的信息頁面,翻到下面,我們可以看到對應的下載地址 點擊這個下載的按鈕,我們便可以下載對應的PPT壓縮包 那我們就開始做吧 首先,查看網頁的源代碼,我們可以看到每一...

    猜你喜歡

    Linux C系統編程-線程互斥鎖(四)

    互斥鎖 互斥鎖也是屬于線程之間處理同步互斥方式,有上鎖/解鎖兩種狀態。 互斥鎖函數接口 1)初始化互斥鎖 pthread_mutex_init() man 3 pthread_mutex_init (找不到的情況下首先 sudo apt-get install glibc-doc sudo apt-get install manpages-posix-dev) 動態初始化 int pthread_...

    統計學習方法 - 樸素貝葉斯

    引入問題:一機器在良好狀態生產合格產品幾率是 90%,在故障狀態生產合格產品幾率是 30%,機器良好的概率是 75%。若一日第一件產品是合格品,那么此日機器良好的概率是多少。 貝葉斯模型 生成模型與判別模型 判別模型,即要判斷這個東西到底是哪一類,也就是要求y,那就用給定的x去預測。 生成模型,是要生成一個模型,那就是誰根據什么生成了模型,誰就是類別y,根據的內容就是x 以上述例子,判斷一個生產出...

    styled-components —— React 中的 CSS 最佳實踐

    https://zhuanlan.zhihu.com/p/29344146 Styled-components 是目前 React 樣式方案中最受關注的一種,它既具備了 css-in-js 的模塊化與參數化優點,又完全使用CSS的書寫習慣,不會引起額外的學習成本。本文是 styled-components 作者之一 Max Stoiber 所寫,首先總結了前端組件化樣式中的最佳實踐原則,然后在此基...

    基于TCP/IP的網絡聊天室用Java來實現

    基于TCP/IP的網絡聊天室實現 開發工具:eclipse 開發環境:jdk1.8 發送端 接收端 工具類 運行截圖...

    精品国产乱码久久久久久蜜桃不卡