網路程式設計包 - Magician 的原理 與 使用

推廣 Joker123456789 • at 2021-04-20 10:08:10 • 10 Views

Magician 是一個非同步非阻塞的網路程式設計包,用 java 語言實現,支援 Http, WebSocket, UDP 等協議

執行環境

jdk11+

簡單的原理介紹

Magician 的底層使用的是 NIO,但是沒有用 Selector,因為 Selector 的設計初衷就是為了用單執行緒來處理高併發,從而減少 因為連線太多而造成執行緒太多,佔用過多的伺服器資源。 但是這樣做的壞處也很明顯,就是無法充分利用 cpu 的多核效能,還有一個就是 如果業務比較耗時,會造成整個迴圈被堵住。

所以,考慮到這一點,Magician 決定使用 accept,程式碼如下:

while (true){
    SocketChannel channel = null;
    try {
        /* 這個方法會阻塞,直到有新連線進來 */
        channel = serverSocketChannel.accept();
        channel.configureBlocking(false);

        /* 將任務新增到佇列裡執行 */
        ParsingThreadManager.addTaskToParsingThread(channel);
    } catch (Exception e){
        logger.error("處理請求出現異常", e);
        ChannelUtil.close(channel);
    }
}

有一個 while 不斷的監聽 accept,當沒有新請求進來的時候 accept 是阻塞的,所以不會有空輪詢的問題,當有了新的請求進來,就會把 channel 丟到佇列裡面去,然後繼續監聽 accept 。

那麼這個佇列是什麼結構的?他又是如何來執行的呢?

首先,佇列的結構是這樣的:他是一個 LinkedBlockingDeque,有序 且 長度無限(除非記憶體爆了),然後這個佇列 放在了一個執行緒中, 執行緒開啟後就會有一個 while 不斷的從這個佇列裡 take 元素,如果佇列為空,take 就會阻塞,佇列一旦有資料 take 就會按順序返回裡面的元素。

所以,只要這個執行緒在執行,我們就可以不斷的往佇列裡丟任務,讓這個執行緒來慢慢消化。

如果這樣的執行緒+佇列有多個, 我們把收到的請求 通過輪詢演算法 分配到這些佇列,讓執行緒各自消化,是不是就可以實現一個,執行緒數量可控,同時又具有非同步特性的模型?

這個模型就是 Magician 現在所使用的,如下圖所示:

avatar

在使用 Magician 的時候,可以自己配置 需要幾個執行緒來同時執行。

除了 http,udp 也是採用的這個模型。只不過 udp 是以同步的模式讀資料,資料讀完了 再丟到佇列裡 讓佇列去執行業務邏輯。

如何使用

說了這麼多原理,我們接下來說說 如何使用 Magician 來開發各種服務。

首先我們看一下 http 的實現

Magician.createHttpServer().httpHandler("/", req -> {

                        req.getResponse()
                           .sendJson(200, "{'status':'ok'}");

                    }).bind(8080).start();

如果不想把 handler 跟這段程式碼窩在一起,可以單獨建立 handler,在這裡新增進去即可

WebSocket 實現

Magician.createHttpServer().bind(8080)
                    .httpHandler("/", new DemoHandler())
                    .webSocketHandler("/websocket", new DemoSocketHandler())
                    .start();

只需要在建立 http 服務的時候,新增一個 WebSocketHandler 即可。

UDP 實現

Magician.createUdpServer()
                .handler(outputStream -> {
                    // outputStream 是 ByteArrayOutputStream 型別的
                    // 它是客戶端發過來的資料,自行解析即可
                }).bind(8088).start();

同樣的,也可以單獨建立 handler,在這裡新增進去

瞭解更多

想了解更多的話,歡迎訪問 Magician 官網:http://magician-io.com

Total: 51
  • zifangsky 2021-04-21 10:08:10
    看起來不錯,後面有機會可以試用一下
  • guyeu 2021-04-21 10:08:10
    1. 推廣就發推廣;
    2. 看來看去看不出來比 netty 高在哪兒
  • Joker123456789 2021-04-21 10:08:10
    @guyeu

    1. 推廣當然要發在人多的地方,推廣節點有人看嗎? 你會去一個全是廣告的節點翻帖子??? 如果你有意見可以舉報一波,讓管理員來刪帖。輪不到你噴我。
    2. 我好像全文都沒跟 netty 對比吧?你突然冒出一句 看不出來比 netty 高階在哪,我很懵逼啊。
  • Joker123456789 2021-04-21 10:08:10
    @guyeu 而且,這個節點是 java,我發的也是 java 專案,而且是開源非商業, 我不覺得有啥大問題。 如果真的冒犯到了這裡的規矩,我接受管理員的刪帖 甚至封號。
  • GuuJiang 2021-04-21 10:08:10
    折騰的精神可嘉,但是。。。容我給估計還沉浸在造完輪子的喜悅中的你潑點冷水,相信我,再過幾年,你會希望自己沒有留下過這段黑歷史的
    首先,你所謂的 Selector 的“缺點”,證明你並沒有真正理解 Selector 的意義,並不是說 configureBlocking(false)就可以稱為 NIO 了,恰恰相反,Selector 才是 NIO 的靈魂,讓我們來看下你是怎麼做的吧,用一個每 100ms 執行的定時任務,迴圈對每一個 channel 進行 read,這點實在是槽點太多無從吐起,建議你自己先去看看 select 、epoll 、iocp 等的基本原理,如果看完還有疑問的話歡迎回來討論
    至於“如果業務比較耗時,會造成整個迴圈被堵住”,這個跟是否用 Selector 根本沒關係,而是使用者自己應該保證不在 IO 執行緒裡處理耗時業務,再看看你是怎麼解決這個所謂的“問題”的,我猜這個 BlockingQueue 應該是你最引以為傲的一部分了吧,恭喜你,你重新發明了執行緒池……的雛形版
    總的來說,這個東西作為一個關於 NIO 以及執行緒池的概念驗證的課後實驗還是可以的,但是實際應用的話,價值為零
  • D3EP 2021-04-21 10:08:10
    同意樓上。看上去解決了一個不存在的問題。
  • learningman 2021-04-21 10:08:10
    @livid 推廣
  • learningman 2021-04-21 10:08:10
    Http, WebSocket, UDP
    這三個玩意兒是應該並列的嗎?
  • Livid 2021-04-21 10:08:10
    @learningman 謝謝。這個主題已經已經被移動。

    @Joker123456789 請閱讀 V2EX 的節點使用說明:

    https://www.v2ex.com/help/node
  • Joker123456789 2021-04-21 10:08:10
    @GuuJiang

    100 毫秒的 那個你是不是看錯了? 那個是 websocket 。 連線上了以後 需要實時監控 channel 裡有沒有訊息。
    而且這個一看就知道 不是 http 了啊,跟我文章是描述的原理完全不搭噶,你是怎麼理解到一起的?

    建議你重新看一下 http 的實現部分。

    還有這句: [至於“如果業務比較耗時,會造成整個迴圈被堵住”,這個跟是否用 Selector 根本沒關係,而是使用者自己應該保證不在 IO 執行緒裡處理耗時業務]

    你可以試一下 在 select 的 while 裡開執行緒試試,你去試一下就知道了。

    第三執行緒池確實是這個原理,但是這有什麼問題?
  • Joker123456789 2021-04-21 10:08:10
    @learningman

    來,說出你的觀點。 不要直接一句反問,因為我不懂你的意思。
  • Joker123456789 2021-04-21 10:08:10
    @GuuJiang
    哦對了,還有一點,我分享原理 是想讓大家對這個東西瞭解多一些,從而決定自己要不要使用。並不是拿來炫耀的。

    你所謂的沒價值,只是你認為,有沒有價值取決於 每個人的 實際需求。

    最後呢,建議你看程式碼的時候仔細一點,你點的那個 package 明明就是 websocket 而不是 http,這都沒看出來嗎?
  • Joker123456789 2021-04-21 10:08:10
    @GuuJiang selector 確實是精髓,但是精髓 就在於 併發高了不會造成執行緒太多啊,因為他是用一個執行緒在消費 selectionkey,我不是在文章裡說過了嗎?

    如果你覺得可以在 while 裡開執行緒,那你就去試試,試完了再說話。

    這句程式碼會把當前的連線 放到 selectionkeys 裡
    ```java
    int select = selector.select();
    ```

    而 http 需要將連線保持到響應結束,如果你開執行緒去做別的事,這件事做完之前 連線是不會關閉的,會導致這裡將 channel 再一次的取出來,造成重複消費。不信可以自己嘗試。

    不過話說回來,while 裡開執行緒,和我這個模型有啥區別呢? 不都是把 channel 丟給執行緒去處理,有區別嗎?
  • GuuJiang 2021-04-21 10:08:10
    @Joker123456789
    這個包結構本身就是另一個槽點了,證明了你對 tcp/http/websocket 三者關係的理解都是混亂的,這個暫且不表
    你只需要回答一個問題,“在一個迴圈裡依次對一堆 channel 進行 read”這個做法,相比起被你否定掉的 NIO,優勢在哪裡?
  • Joker123456789 2021-04-21 10:08:10
    @GuuJiang 優勢在於可以控制執行緒數量啊。 使用者可以自己配置 要幾個執行緒來進行消費。 而不是隻有一個 while 在那消費 selectionskey 。

    還有,http,websocket 都是給予 tcp 的吧,websocket 甚至需要先發一個 http 來建立連線,所以 http 和 websocket 都放在 tcp 的包裡 沒什麼問題吧?

    upd 是另一種協議,我也是放到另一個單獨的包裡了。而且 udp 我就是用的 selector,因為他不需要保持連線到響應結束,因為他不需要響應,所以我可以在資料讀完以後 再開執行緒去消費,這樣就不存在重複操作 channel 的問題了。
  • GuuJiang 2021-04-21 10:08:10
    你開心就好
  • Joker123456789 2021-04-21 10:08:10
    @GuuJiang 找了個 槽點出來,吐槽了一番,後來發現自己看錯了程式碼, 然後 就吐槽我對協議的理解有問題。

    當我解釋了優勢 以及協議的關係後, 來一句“你開心就好”。

    哎~,真的是。。。一言難盡。
  • Joker123456789 2021-04-21 10:08:10
    @Livid 如果真的要按規矩來,這個帖子應該在 分享創造 節點。 謝謝。
  • D3EP 2021-04-21 10:08:11
    一般非同步非阻塞的網路程式設計框架起碼能 C10K 。但當前框架一個執行緒同時只能處理一個 TCP 連線,而且在沒有資料時一直空轉,非阻塞 IO 不和多路複用一起使用,CPU 打滿的機率非常高。設計上就存在問題,只能說自娛自樂了...
  • D3EP 2021-04-21 10:08:11
    此外,IO 執行緒和任務處理執行緒不做隔離,那更容易出問題了。可以參考 Tomcat 、Netty 的設計。
  • guyeu 2021-04-21 10:08:11
    @Joker123456789 #3 推廣的事就不提了。其實上午我懷著相當大的期待閱讀了你的部分原始碼,因為是把它當作 Netty 的替代去看待的,所以不自覺地把它和 Netty 做了一些對比,如果有冒犯的地方向你道歉。emmmmm,那就不提 Netty 說幾個有可能提升你這個專案質量的點

    1. 對協議的抽象不夠,假如某個業務想把 http 換成 udp,看不出有什麼平滑切換的可能性;
    2. 和執行緒模型綁得太死,市面上鮮少有這種自帶執行緒模型的網路庫;
    3. 沒有效能測試資料,也無從得知你這個網路庫的效能怎麼樣;
    4. 程式碼風格值得優化,起碼把 JavaDoc 按規範寫了吧;
    5. 缺乏物件導向設計,似乎從未考慮過一個服務的兩個元件同時使用你這個網路庫的任何可能性;
  • learningman 2021-04-21 10:08:11
    @Joker123456789 自己去看看 7 層網路模型,這三個分別應該在哪一層
  • Joker123456789 2021-04-21 10:08:11
    @D3EP

    1. 佇列的 take 方法 會自動檢查佇列是否為空,如果為空是不返回的 ,直接阻塞在那,所以不存在空轉。

    2. 多路複用,那是系統層面的,到了應用層還是一個執行緒在消費。Selector 確實是一個執行緒 可以處理多個 TCP 連線,但是他是在 while 裡排隊一個個處理的。 這跟我的模型區別不大吧? 我也是每一個執行緒 都在處理排著隊的多個 tcp 。

    3. 配置幾個執行緒 就是幾個執行緒同時跑,不會因為請求多了就執行緒暴增,怎麼會打滿 CPU ?

    4. 你說的執行緒沒有隔離 可否詳細一點? 對於這一條,我是真的想虛心請教的。
  • Joker123456789 2021-04-21 10:08:11
    @learningman 我只是做了一個支援 這三個協議的包,你跟我扯 網路模型幹嘛? 沒東西噴了 就來吐槽我的目錄結構嗎?
  • Joker123456789 2021-04-21 10:08:11
    @guyeu

    說實話,你這段話有點感動到我了。真的。

    我會認真考慮你的建議,並在後面儘可能優化上。
  • Joker123456789 2021-04-21 10:08:11
    @guyeu 不過,第 5 點 是支援的, 監聽兩個埠即可。
  • huang119412 2021-04-21 10:08:11
    感覺這個執行緒模型很像 tomcat 的 nio,tomcat 的 nio 的 ServerSocketChannel 就是用的 accept 。netty boss 執行緒只處理連線,worker 處理其他事件應該算是經典了。netty 相容性,穩定性,安全性,便捷性久經考驗。效能早就不少 netty 強項。是個輕量級的 nio,aio 都能和 netty 效能媲美。但是生產中敢隨便用嗎?
  • blackboom 2021-04-21 10:08:11
    建議換個頭像
  • GuuJiang 2021-04-21 10:08:11
    估計你今天情緒上很難接受,為此還專門另開一個貼抱怨下,以你現在對網路 IO 的認知,我很難跟你一一講明白你的錯誤在哪裡,但是明眼人都看得出來,真心建議你靜下心來好好找點資料看看網路基礎,各種多路複用方案到底在解決什麼問題,等你看完了如果還是不服氣,歡迎你回來對線
    你可以先帶著這個問題去看
    “當客戶端沒有發資料時,你的程式在幹什麼,而其他正確實現了的網路框架,包括( BIO 、NIO 、netty 等)又在幹什麼”

    到時候你就會明白,你這個東西稱之為“一個非同步非阻塞的網路程式設計包”,是多麼的荒謬
  • Joker123456789 2021-04-21 10:08:11
    @GuuJiang 好的, 我會去看的。

    然後回答一下你的問題, 當沒有資料進來的時候,我這個程式是等待狀態,accept 自動阻塞住了 不會返回任何東西 就停在那。 佇列的 take 方法也阻塞住了,就停在那。 整個程式除了主執行緒掛在那,就沒任何動作了。

    然後我問你一個問題, 消費 selectionkey 的那個 while 是不是有被業務阻塞的可能? 你可能會說 在 while 裡開執行緒 讓業務執行緒去處理。 但問題是 在業務執行緒跑完之前 channel 是不能關的。 因為 http 客戶端需要等待響應, 必須等業務跑完 並把響應寫入 channel 才能關吧。 而 channel 不關的話,while 會進入下一次迴圈,select 方法會再次為這個連線生成 selectkey, 這樣一來就出現重複處理了。 這個問題是我真實遇到的問題。 如果你有辦法解決, 那你可以說出來。

    多路複用器 是系統層面的, 到了應用層 就是單執行緒在一個個消費 selectkey, 如果這個你覺得不對也歡迎 指正。並說出具體哪裡不對。

    最後你可以去看一下 nio 的 Reator 的分發模型, 是不是跟我這個有點像。
Add a reply
For Commenting you need to Login. If you dont have a Account you need to Register.