NIO 及其在 Golang 網絡庫中的應用

【導讀】NIO 是如何讓 go 語言 web 庫速度快的?本文對 NIO 和 golang 網絡庫做了詳細介紹。

NIO(Non-blocking I/O),是一種同步非阻塞的 I/O 模型,也是 I/O 多路複用的基礎,是現今主流的大流量、高併發 IO 有效解決方案。

五種 IO 模型

在 UNIX 下,IO 模式分爲五類,分別是:阻塞式 IO(bloking IO)、非阻塞式 IO(non-blocking IO)、多路複用 IO 模型(multiplexing IO)、信號驅動 IO 模型(signal-driven IO)以及異步 IO 模型(asynchronous IO)。其中又以前三種模式最爲常見。

5 種 IO 模型對比圖

傳統的 BIO 模式

在闡述選擇 NIO 的原因之前,首先說明一下阻塞和非阻塞的概念。阻塞和非阻塞的核心區別就在於,在 IO 就緒態(讀就緒、寫就緒、有新連接)到來之前是否會阻塞等待。

在最初的網絡編程中,我們使用 BIO 模式構建編程模型,如下面的僞代碼所示,這是經典的 per thread per connection 模型。這段代碼的核心部分在於 accept()、socket.read()、socket.write() 三個函數,這三個函數在等待 IO 就緒態到來的過程中都將阻塞各自的線程。當連接數量達到一定程度之後,這樣的阻塞、對線程資源的無效佔用就變得不可容忍。後續的優化包括建立線程池,進行線程擴容,但這並沒有根本解決問題。而 NIO + 多路複用的網絡模型很好的解決了這個問題。

//BIO JAVA僞代碼示意
class Server {
    public static void main() {
        while(true){    
            socket = server.accept();
            executor.submit(new ConnectIOHandler(socket));
        }
    }
}

class ConnectIOnHandler implements Runnable{
    private Socket socket;
    public ConnectIOnHandler(Socket socket){
       this.socket = socket;
    }

    @Override
    public void run() {
        while (!Thread.currentThread.isInturruted() && socket.isClosed()) {
            //讀取數據
            String data = socket.read()....
            if (data != null) {
                //處理數據
                dosomething();
                //寫數據
                socket.write()...
            }
        }
    }

NIO + multiplexing IO

現今的高性能網絡庫基本採用了 NIO + 多路複用的模式構建,例如著名的 netty,那麼這是爲什麼呢?我們都知道,在網絡 IO 最耗時的部分就在於等待 IO 就緒的過程,而真正的 IO 操作是一個高性能的過程,而 NIO 有一個重要的特點:socket 的主要讀、寫、註冊和接收函數,在等待就緒態前都是非阻塞的,只有在進行真正的 IO 操作時是同步阻塞的。結合多路複用帶來的事件通知特性,就可以構建一套更高性能的網絡模型。

讀到這裏你可能會有疑問,爲什麼是 NIO + multiplexing IO,而不是 BIO + multiplexing IO 呢?以 Linux 下的 Epoll 舉例,我們向 Epoll 的 selector 中註冊一個 socket 並標示可讀事件,當 epoll_wait 返回可讀事件 EPOLLIN 到來,我們只知道 socket 可讀,但不會知道有多少的數據可讀,如果我們多次調用 read 函數將可能導致阻塞事件發生,所以如果是 BIO+ multiplexing IO,我們必須每次 read 過後就馬上返回 epoll_wait,這種要求是苛刻的,在某些業務場景下也是不允許的,所以在實際的應用中,BIO + multiplexing IO 的組合幾乎不會出現。

golang 的 NIO

這是一個典型的 Golang TCP Server 示例

package main

import (
    "fmt"
    "net"
)

func main() {
    listen, err := net.Listen("tcp", ":8888")
    if err != nil {
        fmt.Println("listen error: ", err)
        return
    }

    for {
        conn, err := listen.Accept()
        if err != nil {
            fmt.Println("accept error: ", err)
            break
        }

        // start a new goroutine to handle the new connection
        go HandleConn(conn)
    }
}
func HandleConn(conn net.Conn) {
    defer conn.Close()
    packet := make([]byte, 1024)
    for {
        // 如果沒有可讀數據,也就是讀 buffer 爲空,則阻塞
        _, _ = conn.Read(packet)
        // 同理,不可寫則阻塞
        _, _ = conn.Write(packet)
    }
}

這段代碼看起來和上文中的 JAVA BIO 代碼很類似。那麼這段 Go 代碼裏就緒態等待也會阻塞線程麼?答案是並不會。相比與 java,golang 應用直接調用的是更爲輕量級的協程 goroutine,當 socket 在進行就緒態等待的時候,會阻塞協程,但是並不會阻塞線程。同時,golang 的原生網絡庫底層同樣實現了一套 NIO + multiplexing IO 的網絡模型(netpoll),我們以 Linux 環境舉例,在 Linux 下,netpoll 的底層實現是 Epoll, 我們的連接套接字被創建後會被設置爲 NO-BLOCK 模式,而後加入到 Epoll 中進行監聽,當讀寫就緒態到來之後,套接字阻塞的 goroutine 會被加入到可運行隊列中,等待 golang 調度器的調度運行。

轉自:

fbelisk.github.io/

Go 開發大全

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/4VH5fFyui76vz7rIhfqKmA