<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??一站式輕松地調用各大LLM模型接口,支持GPT4、智譜、豆包、星火、月之暗面及文生圖、文生視頻 廣告
                ## IO模型 在網絡通信的過程中,應用程序可能接收數據包,也有可能發送數據包。這個網絡的IO模型正式基于應用程序的數據包的讀寫而進行演變的。 1. 應用程序發送數據包 :-: ![](https://img.kancloud.cn/86/30/8630bc1e6091b2a44c4a53614d1b16df_729x787.png) - 調用系統調用send方法的時候,用戶線程切換到內核態,在內核中根據fd找到對應的Socket對象,根據這個Socket對象構造出**msghdr**結構體對象。將用戶需要發送的全部數據封裝在這個**msghdr**結構體中。 - 之后調用內核網絡協議棧的`inet_sendmsg`方法,進入內核協議棧的處理流程,根據具體的協議調用對應傳輸層方法(tcp_sendmsg或者upd_sendmsg)。 - 以tcp_sendmsg為例,將msghdr拷貝到sk_buffer,將新創建的sk_buffer添加到Socket發送隊列的尾部。 - 如果符合tcp協議的發送條件,則會調用`tcp_write_xmit`方法,循環獲取發送隊列的內容,然后進行擁塞控制和流量控制。 - 將發送隊列中的sk_buffer重新拷貝一份設置TCP頭部后交給網絡層(拷貝的目的在于可以超時重傳)。 - 之后填充IP頭,查找MAC地址,封裝成幀。將數據放入RingBuffer中,調用網卡驅動程序來發送數據。 2. 應用程序接收數據包 :-: ![](https://img.kancloud.cn/60/e0/60e01c51e4388439937c79100514aef3_721x695.png) - 當網絡包到達網卡的時候,操作系統通過**DMA**的方式將網絡包放到環形緩沖區(RingBuffer)。當RingBuffer滿的時候新來的數據幀將會被丟棄。 - DMA操作完成的時候,網卡向CPU發送一個**硬中斷**,CPU調用對應的`硬中斷響應程序`,該響應程序會將RingBuffer中的內容拷貝到sk_buffer中。 - 當數據拷貝到sk_buffer中后會觸發**軟中斷**,內核線程ksoftirqd發現有軟中斷請求時,會調用網卡驅動注冊程序的poll函數,該函數將sk_buffer中的網絡包發送到`內核協議棧`中的`ip_rcv`函數中。 - 在`ip_rcv`函數中(網絡層),取出數據包的IP頭,判斷該數據包的走向,如果是ip與本主機的ip相同,則取出傳輸層的協議類型,去掉ip頭,調用傳輸層的處理函數(tcp_rcv或者upd_rcv)。 - 如果采用的是tcp協議,則會去掉tcp頭,找到對應socket四元組,將數據拷貝到Socket的接受緩沖區中。如果沒有找到對應的Socket四元組,則會發送一個`目標不可達`的ICMP包。 - 當應用程序調用read讀取Socket緩沖區的數據時,如果沒有數據,**應用程序就會在系統上阻塞(進入阻塞狀態)**直到Socket緩沖區有數據,然后CPU將Socket緩沖區的內容拷貝到用戶空間,最后read方法返回,應用程序讀取數據。 > 硬中斷:由硬件產生,例如磁盤、網卡、鍵盤、時鐘等,每個設備都有自己的硬件請求,基于硬件請求CPU可以將相應的請求發送到對應的硬件驅動程序上。硬中斷可以直接中斷CPU。 > 軟中斷:處理方式非常像硬中斷,但是是由當前正在運行的進程產生的。不會直接中斷CPU。 > ksoftirqd線程:每個CPU都會綁定一個ksoftirqd線程專門處理軟中斷響應。 > sk_buffer緩沖區:維護網絡幀結構的雙向鏈表,鏈表中的每個元素都是一個數據幀。 &nbsp; 對上面的應用程序接收和發送數據的過程進一步抽象,可以劃分為如下的兩個部分: :-: ![](https://img.kancloud.cn/41/1a/411ac25ec1d4eeff78bc7966190c3319_940x570.png) 對于接收數據的過程中,其可以分為兩個主要的階段: 1. 數據準備階段:網卡中的數據經過DMA、硬中斷、軟中斷、網絡協議棧處理最終到達Socket緩沖區(ksoftirqd線程處理)。 2. 數據拷貝階段:將內核Socket緩沖區中的數據拷貝到應用層,應用程序才能讀取數據。 &nbsp; ## 阻塞與非阻塞、同步與異步 有了上面應用程序讀取網卡數據抽象出來的兩個過程,也就可以更好的理解阻塞和非阻塞究竟是發生在哪一個過程了。 1. 阻塞和非阻塞 - 阻塞:用戶線程發生系統調用會去讀取Socket緩沖區的內容,如果此時Socket緩沖區沒有數據,則**會對阻塞用戶線程**,直到Socket緩沖區有數據之后喚醒用戶線程,**用戶線程負責**將Socket緩沖區的內容拷貝到用戶空間中,這個過程用戶線程也是阻塞的。`阻塞發生在第一第二階段` - 非阻塞:與阻塞相比發生在第一階段,如果此時Socket緩沖區沒有數據,則會直接返回。如果Socket緩沖區有數據,則用戶線程負責將Socket緩沖區的內容拷貝到用戶空間。`非阻塞的特點是在第一階段不會等待,但是第二階段仍然會等待。` &nbsp; 2. 同步和異步 - 同步:在數據拷貝階段,是由**用戶線程負責將Socket緩沖區的內容拷貝到用戶空間**。 - 異步:在數據拷貝階段,是由**內核來負責將Socket緩沖區的內容拷貝到用戶空間,接著通知用戶線程IO操作已經完成。** > 異步需要操作系統內核的支持,做的比較好的是Windows系統,Linux系統還不是很完善。 因此可以這么理解成阻塞與非阻塞主要關注的是**數據準備**階段,而同步與異步主要關注的是**數據拷貝**階段。 > 是不是可以理解為什么沒有`異步阻塞`這種模型了呢? > 阻塞的意思意味著**用戶線程**要去等待Socket緩沖區有數據,而異步的意思意味著數據從用戶態拷貝到內核態不是由用戶線程執行的。那么用戶線程的等待就沒有意義了。 &nbsp; ## IO模型 在《UNIX網絡編程》中一共介紹了5種IO模型:`阻塞IO、非阻塞IO、IO多路復用、信號驅動IO、異步IO`。其演變過程的原則是:**如何用盡可能少的線程去處理更多的連接。** ### 阻塞IO 由上面可知,阻塞IO模型會在數據準備階段和數據拷貝階段用戶線程都會進行等待。 - 阻塞讀:用戶線程發生系統調用,查看Socket緩沖區是否有數據到來,如果有則用戶線程拷貝將Socket緩沖區的數據拷貝到用戶空間。如果沒有則阻塞線程(等待事件,進入阻塞狀態),直到Socket緩沖區有數據了(事件發生),則喚醒線程進入就緒狀態,等待CPU的調度將數據拷貝回去用戶空間。 - 阻塞寫:當Socket緩沖區能夠容納下發送數據時,用戶線程會將全部的發送數據寫入Socket緩沖區中;Socket緩沖區不夠的時候用戶線程會進入阻塞狀態,直到Socket緩沖區有足夠的空間能夠容納下全部發送數據時,內核喚醒用戶線程,繼續發送數據。 在阻塞模型下:每個請求都需要被一個獨立的線程處理,一個線程在同一時刻只能與一個連接綁定。同時網絡連接并不是總是有事件發生的,因此也就會導致有大量的線程處于阻塞狀態了。 適用場景:連接少、并發度低的內部管理系統。 &nbsp; ### 非阻塞IO 先來看看非阻塞IO的讀寫過程: - 非阻塞讀:用戶線程判斷Socket緩沖區是否有內容,沒有內容則直接返回不等待。有數據則將數據拷貝會用戶空間,這個過程是等待的。 - 非阻塞寫:用戶線程直接將數據寫入Socket緩沖區中,如果緩沖區已滿了則直接返回;當Socket緩沖區有空間的時候將數據拷貝到Socket緩沖區中。 根據這非阻塞IO的特點,也就可以在線程數量這塊做出優化了: 一個或者少數幾個線程(假設稱為**輪詢線程**)遍歷每個緩沖區是否有數據到達,如果沒有數據則繼續遍歷下一個,如果有數據則將讀寫數據的業務處理交給**業務線程或業務線程池**進行處理,輪詢線程繼續輪詢。下面是java中使用非阻塞IO的編程方式: ~~~ public static void main(String[] args) throws Exception { // 保留所有的Socket連接 LinkedList<SocketChannel> clients = new LinkedList<>(); ServerSocketChannel ss = ServerSocketChannel.open(); ss.bind(new InetSocketAddress(9090)); ss.configureBlocking(false); //設置為非阻塞IO模式 while (true) { SocketChannel client = ss.accept(); //沒有連接來則不會阻塞 if (client == null) { System.out.println("null....."); } else { // 有連接了則將連接添加進集合里面 client.configureBlocking(false); int port = client.socket().getPort(); System.out.println("client...port: " + port); clients.add(client); } for (SocketChannel c : clients) { // 遍歷每個Socket連接,處理具體的讀寫事件 } } } } ~~~ 由于是非阻塞的,在上面的輪詢各個Socket連接可以交由一個線程來做,處理具體的IO事件和業務的讀寫邏輯的時候可以交由線程池去處理。這樣就可以使用少量的線程來處理較多的連接了。 **非阻塞與阻塞已經在線程的數量的優化邁出巨大的一步了。但是在高并發的場景下任然有個致命的問題就是:每次輪詢判斷是否有數據時都要發生一次系統調用,進行上下文的切換,隨著并發量的增加,這個開銷也是非常巨大的。** > 應用場景:C10K以下的場景。 &nbsp; ### IO多路復用 再強調一遍,**IO模型的核心是應用如何使用少量的線程來處理大量的連接。** 上面的非阻塞IO提供了解決阻塞IO需要創建大量線程的問題,那么在高并發場景下存在大量系統調用的問題又該如何解決呢?于是就演變出IO多路復用這個IO模型。 - 多路:指的是要處理的眾多連接。 - 復用:可以理解為多次系統調用復用為一次系統調用,實現了復用就解決了非阻塞IO模型下存在的大量系統調用而導致上下文切換開銷的問題。 > 如何去實現復用?在非阻塞IO中需要在用戶空間中輪詢是否有數據,這樣的每次輪詢就會發生一次系統調用,也就是說能不能將這個輪詢的過程交由內核空間來完成,這樣的話只需要一次系統調用(這個系統調用就是多路復用器)就能找到所有有狀態的連接了。 Linux提供了三種多路復用器: #### Select Select多路復用器的工作方式如下: :-: ![](https://img.kancloud.cn/f2/14/f214fada261e0874edb5c995f59156f8_884x502.png) 1. 調用select方法,用戶代碼的執行流程會阻塞在select系統調用上,用戶線程從用戶態切換到內核態。 2. 用戶線程將需要監聽的Socket對應的文件描述符fd數組通過select系統調用傳遞給內核,這個fd數組的索引表示進程對應的文件描述符,值表示文件描述符的狀態。 3. 用戶線程在內核空間開始輪詢文件描述符數組。 4. 修改有讀寫狀態的文件描述符,設置為1。 5. 內核將修改后的fd數組拷貝會用戶空間,此時用戶代碼的執行流程從select中恢復,阻塞解除。 6. 用戶線程在用戶空間遍歷fd數組,找出值為1的fd進行對應的處理。 > 這里的fd數組其實是一個Bitmap結構,該結構最多支持1024個位(FD_SETSIZE),因此select只支持最多處理1024個Socket連接。每一位對應的索引就是一個文件描述符fd。 > 在Linux中Socket也是一個文件,在PCB控制塊(task_struct結構體)中有一個屬性files_struct *files的結構體屬性,它最終指向了進程的所有打開文件表數組中,該數組的元素是一個封裝了文件信息的file結構體,**打開文件表數組中的下標也就是常說的文件描述符fd。** > 注意:由于內核返回的文件描述符數組會修改到原來的狀態,因此用戶線程在每個處理完之后需要重新設置文件描述符的狀態。 性能開銷: 1. 一次系統調用,兩次上下文的切換,這是不可避免的。 2. 兩次fd數組的全量拷貝(但是其實最多只能有128個字節大小的bitmap)。 3. 兩次fd數組的全量遍歷,一次在內核中遍歷是否有事件,另外一次在用戶空間中遍歷哪些有事件。 > 注意:select系統調用不是線程安全的,因為進程fd數組是共享的。 &nbsp; #### poll poll的工作原理和select沒有太大的區別,主要是在文件描述符數組的結構和文件描述符大小的限制上。其將Bitmap換成一個沒有固定長度的鏈表,鏈表中的元素如下: ``` struct pollfd { int fd; // 文件描述符 short events; // 需要監聽的事件 short revents; // 實際發生的事件,由內核修改 } ``` 性能開銷方面和select是一樣的。 兩者的問題: > 1. 每次新增、刪除要監聽的Socket時,都需要進行全量的拷貝。 > 2. 遍歷的開銷會隨著文件描述符數量的增大而增大。 性能瓶頸產生的原因: > 1. 內核空間不會保存要監聽的socket集合,所以需要全量拷貝。 > 2. 內核不會通知具體IO就緒的Socket,所以需要全量遍歷并對IO就緒的Socket打上標記。 &nbsp; #### epoll epoll提供了select和poll性能瓶頸的解決方案,即在**內核保存要監聽的socket集合和通知具體就緒的IO。** 其主要包括三個系統調用: **一、 epoll_create** 內核提供的一個創建epoll對象的系統調用,在用戶進程調用epoll_create時,內核會創建一個**eventpoll**的結構體,并且創建相對應的file對象與之相關聯(也就是說epoll對象也是個文件,“一切皆文件”),同時將這個文件放入進程的打開文件表中。eventpoll的定義如下: ``` struct eventpoll { // 等待隊列,阻塞在epoll上的進程會放在這里 wait_queue_head_t wq; // 就緒隊列,IO就緒的Socket連接會放在這里 struct list_head rdllist; // 紅黑樹,用來監聽所有的socket連接 struct rb_root rbr; // 關聯的文件對象 struct file *file; } ``` 1. `wait_queue_head_t`,epoll中的等待隊列,存放阻塞在epoll上的用戶線程,在IO就緒的時候epoll通過這個隊列中的線程并喚醒。 2. `list_head rdllist`,存放IO就緒的Socket連接,阻塞在epoll上的線程被喚醒時可以直接讀取這個隊列獲取有事件發生的Socket,不用再次遍歷整個集合(**避免全量遍歷**)。 3. `rb_root rbr`,紅黑樹在查找、插入、刪除等方面的綜合性能比較優,epoll內部使用一顆紅黑手來管理大量的Socket連接。 > select用數組來管理,poll用鏈表來管理。 **二、 epoll_ctl** 當創建出來epoll對象eventpoll后,可以利用epoll_ctl向epoll對象中添加要管理的Socket連接。這個過程如下: 1. 首先會在內核中創建一個表示Socket連接的數據結構epitem,這個epitem就是紅黑樹的一個節點。 ``` strict epitem { // 指向所屬的epoll對象 struct eventpoll *ep; // 注冊感興趣的事件,也就是用戶空間的epoll_event struct epoll_event event; // 指向epoll中的就緒隊列 struct list_head rdllink; // 指向epoll中的紅黑樹節點 struct rb_node rbn; // 指向epitem所表示的Socket文件 struct epoll_filefd ffd; } ``` 2. 內核在創建完epitem結構后,需要在Socket中的等待隊列上創建等待項wait_queue_t,并且注冊epoll的回調函數ep_poll_callback。這個回調函數是epoll同步IO事件通知機制的核心所在,也是區別于select、poll采用輪詢方式性能差異所在。ep_poll_callback會找到epitem,將IO就緒的epitem放入到epoll的就緒隊列中。(通過一個epoll_entry的結構關聯Socket等待隊列上的wait_queue_t和epitem) 3. 在Socket等待隊列創建好等待項,注冊回調函數并關聯好epitem后,就可以將epitem插入紅黑樹中了。 > epoll紅黑樹優化點之一:插入刪除Socket連接不需要像select、poll那樣全量復制。 **三、epoll_wait** epoll_wait用于同步阻塞獲取IO就緒的Socket。 1. 用戶程序調用epoll_wait后,進入內核首先會找到epoll中的就緒隊列eventpoll->rdllit是否有就緒的epitem。如果有的話將epitm中封裝的socket信息封裝到epoll_event返回。 2. 如果就緒隊列中沒有IO就緒的epitem,則會創建等待項,將用戶線程的fd關聯到wait_queue_t->private上,并注冊回調函數`default_wake_function`。最后將等待項添加到epoll中的等待隊列(eventpoll->wq)中。用戶線程讓出CPU,進入阻塞狀態。 epoll有事件發生時的工作流程: 1. 當數據包通過軟中斷經過內核協議棧到達Socket的接受緩沖區的時候,調用數據就緒回調函數。在socket的等待隊列中找到等待項,該等待項注冊的回調函數為ep_poll_callback。 2. 在回調函數ep_poll_callback中找到關聯的epitem對象,并將它放到epoll的就緒隊列(eventpoll->rdllist)中。 3. 接著查看epoll中的等待隊列中是否有等待項,如果有的話喚醒線程,將socket信息封裝到epoll_event中返回。 4. 用戶線程拿到epoll_event獲取IO就緒的socket,就可以對該socket發起系統調用讀取數據了。 :-: ![](https://img.kancloud.cn/b2/ea/b2eaecba9f13c70282a02247c94a79cb_973x487.png) &nbsp; **邊緣觸發和水平觸發** 從上面的執行流程來看,邊緣觸發和水平觸發最關鍵的區別在于**當socket接收緩沖區還有數據可讀時,epoll_wait是否會清空rdllist**。 1. 邊緣觸發:epoll_wait獲取到IO就緒的Socket后會直接清空rdllist,不管socket上是否還有數據可讀。因此使用邊緣觸發模式需要一次性盡可能的將socket上的數據讀取完畢,否則用戶程序無法再次獲得這個socket,直到該socket下次有數據到達被重新放入rdllist。因此邊緣觸發只會從epoll_wait中蘇醒一次。 2. 水平觸發:epoll_wait獲取到IO就緒的Socket后不會清空rdllist,假如Socket中的數據只讀取了一部分沒有讀取完畢,再次調用epoll_wait(用戶線程調用)會檢查這些Socket中的接受緩沖區是否還有數據可讀,如果有數據可讀,則會將這些socket重新放回rdllist中。下次再調用epoll_wait時仍然可以獲得這些沒有讀取完數據的Socket。 總之:邊緣觸發模式的epoll_wait會清空就緒隊列,水平觸發模式的epoll_wait不會清空rdllist。 > Netty中的EpollSocketChannel默認的是`邊緣觸發`模式。 > JDK的NIO默認的是`水平觸發`模式。 **總結epoll的優化點** 1. 內核中通過紅黑樹維護海量連接,在調用epoll_wait時不需要出入監聽的Socket集合,內核只需要將就緒隊列中的Socket集合返回即可。 2. epoll通過同步IO事件的機制將IO就緒的Socket放入就緒隊列中。不用去遍歷監聽的所有Socket集合。 應用場景:各大主流網絡框架用到的網絡IO模型,可以解決C10K、C100K甚至是C1000K的問題。 &nbsp; ## 信號驅動IO 用戶線程通過系統調用`sigaction函數`發起一個IO請求,在對應的Socket注冊一個`信號回調`,不阻塞用戶線程,用戶線程繼續工作。當數據就緒的時候(放到Socket緩沖區),就會為該線程產生一個`SIGIO信號`,通過信號回調通知線程進行相關的IO操作。 **信號驅動仍然是同步IO,引用在數據拷貝階段是需要用戶線程自己拷貝的。** > TCP很少回去使用信號驅動IO,因為其信號事件比較多;UDP只有一種信號事件,可以采用信號驅動IO。 &nbsp; ## 異步IO(AIO) 如果信號驅動IO是通知用戶線程數據已經到達了可以去拷貝了,那么異步IO就是同時數據已經拷貝好了,可以去使用了。 異步IO的系統調用需要內核的支持,目前只有Window中的IOCP實現了非常成熟的異步IO機制,Linux系統對異步IO機制的實現不夠成熟,但是在5.1版本后引入了io_uring異步IO庫,改善了一些性能。 &nbsp; 上面的IO模型是從內核空間的角度進行的,從用戶空間角度的話就可以引出`IO線程模型`,這些不同的IO線程模型都是在討論如何在多線程中分配工作,誰負責接收連接、誰負責響應讀寫、誰負責計算、誰負責發送和接收等等。`Reactor模型就對這些分工做出了具體的劃分`。 > IO線程模型有Reactor和Proactor。 &nbsp; ## Reactor 利用NIO對IO線程進行不同的分工: - 利用IO多路復器進行IO事件的注冊和監聽。 - 將監聽到的就緒IO事件分發到各個具體的Handler進行處理。 通過IO多路復用技術就可以不斷的監聽IO事件,不斷的分發,就想一個反應堆一樣,于是就將這種模型稱為Reactor模型。可以分為如下的幾種: 1. 單Reactor單線程 例如使用epoll來進行IO多路復用、監聽IO就緒事件。 - 單Reactor:意味著**只有一個epoll對象**,用來監聽所有的事件,連接事件、讀寫事件等。 - 單線程:意味著只有一個線程來執行epoll_wait獲取IO就緒的Socket,然后對這些就緒的Socket執行讀寫操作、業務邏輯操作。 **其實就只有一個線程。** &nbsp; 2. 單Reactor多線程 - 單Reactor:只有一個epoll對象來監聽所有的IO事件,一個線程來調用epoll_wait獲取IO就緒的Socket。 - 多線程:當獲取到IO就緒的Socket時,通過線程池來處理具體的IO事件及業務邏輯。 &nbsp; 3. 主從Reactor多線程 - 主從Reactor:將原來的單Reactor變為了多Reactor,主Reactor負責監聽Socket連接事件。將要監聽的read事件注冊到從Reactor中,由從Reactor監聽Socket的讀寫事件。。 - 多線程:將讀寫的業務邏輯交由線程池處理。 &nbsp; ## Netty中的IO模型 Netty支持三種Reactor模型,但是常用的是`主從Reactor多線程模型`。注意三種Reactor只是一種設計思想,具體實現不一定嚴格按照其來實現。Netty中的主從Reactor多線程模型如下: :-: ![](https://img.kancloud.cn/72/d8/72d88c16cac52f3da47431d3f128d341_1340x638.png) - Reactor在netty中以group的形式出現,Netty中將Reactor分為兩組,一組是MainReactorGroup,也就是EventLoopGroup=BossGroup。另外一組是SubReactorGroup,也就是EventLoopGroup=workerGoup。主Reactor負責監聽,將產生的NIOSocketChannel對象通過負載均衡的方式注冊到從Reactor中。(單個Reactor的原因是一般只監聽一個端口)。從Reactor一般有多個,默認的Reactor個數為CPU核心數*2,主要負責Socket上的讀寫事件。 - 一個Reactor分配一個IO線程,這個IO線程負責從Reactor中獲取IO就緒事件,執行IO調用獲取IO數據,執行Pipeline。同時每個Socket會被綁定到固定的Reactor中,實現無鎖串行化,避免線程安全問題。 - 當`IO請求`在業務線程中完成相應的業務邏輯處理后,在業務線程中利用持有的`ChannelHandlerContext`引用將響應數據在`PipeLine`中反向傳播,最終寫回給客戶端。 **配置各種模型** 1. 配置單Reactor單線程 ``` EventLoopGroup?eventGroup?=?new?NioEventLoopGroup(1); ServerBootstrap?serverBootstrap?=?new?ServerBootstrap();? serverBootstrap.group(eventGroup); ``` 指定線程數為1。 2. 配置單Reactor多線程 ``` EventLoopGroup?eventGroup?=?new?NioEventLoopGroup(); ServerBootstrap?serverBootstrap?=?new?ServerBootstrap();? serverBootstrap.group(eventGroup); ``` 默認線程數為CPU*2。 3. 配置主從Reactor多線程 ``` EventLoopGroup?bossGroup?=?new?NioEventLoopGroup(1);? EventLoopGroup?workerGroup?=?new?NioEventLoopGroup(); ServerBootstrap?serverBootstrap?=?new?ServerBootstrap();? serverBootstrap.group(bossGroup,?workerGroup); ``` :-: ![](https://img.kancloud.cn/dc/f9/dcf91e88bcae47ce0a635339c74c6432_1554x775.png) &nbsp; 【參考】 https://mp.weixin.qq.com/s/zAh1yD5IfwuoYdrZ1tGf5Q
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看