由Nodejs來講I/O

Nodejs定義

Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient.數組

什麼是IO

IO(Input & Output),顧名思義,輸入輸出便是IO。磁盤,網絡,鼠標,鍵盤等都算IO;而你們一般說的IO,大部分指磁盤和網絡的數據操做。
對於磁盤,IO=讀寫;對於網絡,IO=收發。服務器

Blocking I/O,從一個做業提及

學習C語言時,有個做業,大意是寫一個server程序和client程序,實現TCP/UDP通訊。看起來代碼以下:網絡

Client端

int ClientSend(SOCKET s, char* msg)
{
    char buf[BUF_SIZE] = {0};
    if (s && msg)
    {        
        int len = send(s, msg, strlen(msg), 0);
        if (len > 0)
        {
            println("Client send OK!");
            len = recv(s, buf, BUF_SIZE);
            if (len > 0)
            {
                println("Client receive: %s", buf);
            }
            // else socket recv error
        }
        // else socket send error
    }
    // else 
}

int main(char* argc, char* argv[])
{
    // 初始化socket
    SOCKET s = InitSocket();
    if (s != -1)
    {
        ClientSend(s, "Hi, I am Client");
    }
    // else socket init error
    return 0;
}

Server端

int main(char* argc, char* argv[])
{
    char buf[BUF_SIZE] = {0};
    const char* msg = "Roger that, I am Server";
     // 初始化socket,略
    SOCKET s = InitSocket();
    SOCKET cs;
    sockaddr_in addr;
    int nAddrLen = sizeof(addr);
    
    while ((cs = accept(s, &addr, &nAddrLen)) != -1)
    {
        int len = recv(cs, buf, BUF_SIZE, 0);
        if (len > 0)
        {
            len = send(cs, msg, strlen(msg), 0);
            if (len > 0)
            {
                println("Serve one client");
            }
            // else socket send error
        }
    }
    return 0;
}

在這個例子中,若是一個Client通訊沒有結束,其它的Client是沒法和Server通訊的。緣由就是代碼裏面使用的是Blocking I/O,即同步IO。由於在代碼中的recv或者send,都會阻塞住當前代碼的執行。單靠這種模型,是沒法實現一個完善的服務器的。多線程

Blocking I/O,多線程(多進程)

爲了讓Server能服務更多的Client,基於Blocking I/O,能夠採用多線程(進程)來處理,實現1對多的服務。併發

Server端

int ThreadProc(void* pParam)
{
   char buf[BUF_SIZE] = {0};
   const char* msg = "Roger that, I am Server";
   if (pParam)
   {
       int len = recv(cs, buf, BUF_SIZE, 0);
        if (len > 0)
        {
            len = send(cs, msg, strlen(msg), 0);
            if (len > 0)
            {
                println("Serve one client");
            }
            // else socket send error
        }
      // else socket recv error
   } 
   // else param error
   return 0;
}

int main(char* argc, char* argv[])
{
     // 初始化socket,略
    SOCKET s = InitSocket();
    SOCKET cs;
    sockaddr_in addr;
    int nAddrLen = sizeof(addr);
    
    while ((cs = accept(s, &addr, &nAddrLen)) != -1)
    {
        int pThread = CreateThread(NULL, 0, ThreadProc, cs);
      // serve on client
    }
    return 0;
}

這樣的方案,的確能同時處理多個Client請求,實現併發。但因爲建立線程的成本很高(須要分配內存,調度CPU等),受Server硬件條件的限制,這種方案不能服務不少Client,即服務器性能很低下。
另外,若是把ThreadProc裏面的代碼增長邏輯:socket

// recive data from buf
setenv(buf);
CreateProcess(NULL, 0 ...);
// parse env in child process

這就是一個簡單的CGI模型了。
在一些簡單的http服務器代碼中,見到過這樣的模型。(好比一些嵌入式系統服務器)。async

Non-blocking I/O,你完事兒沒有?

由於Blocking I/O的特色,因此係統提供了另外的方法,Non-blocking I/O,即調用send,recv等接口時,不會阻塞線程,但調用者須要本身去輪訓IO的狀態來斷定操做;就像一個監工不停的問工人,你完事兒沒有。性能

int main(char * argc, char * argv[])
{
    // 初始化socket,略
    SOCKET s = InitSocket();
    SOCKET cs;
    sockaddr_in addr;
    int fd;
    int nAddrLen = sizeof(addr);
    SetNonblocking(s);

    while (running) 
    {
        int ret = select(FD_SETSIZE, ...);
        if (ret == -1) break;
        if (ret == 0) continue;

        for (fd = 0; fd < FD_SETSIZE; fd++)
        {
            if (FD_ISSET(fd, ...)
            { 
                // 有新的client進來   
                if (fd == s)
                {
                    cs = accept(s, & addr, & nAddrLen, 0);
                    FD_SET(cs, ...);
                }
                else // cs中的一個裏面有變化
                {
                    ioctl(fd, FIONREAD, & nread); 
                    // 處理完畢
                    if (nread == 0)
                    {
                        close(fd);
                        FD_CLR(fd, ...);
                    }
                    else
                    {
                        // 處理Client邏輯,這裏可能會建立線程。
                        ......
                    }
                }
            }
            // serve on client
        }
    }
    return 0;
}

在這種模型中,while和for循環不停的檢查fd_set的狀態,並作相應的處理,相似Apache的解決方案。
可是,這個模型裏面還有一個block,就是select,當有fd發生變化時,select纔會返回。
還有,select中的FD_SETSIZE有限制(通常是2048),就代表單進程仍是不能支持更大量級的併發。Apache採用多進程的方式來解決這個問題。
後期有了epoll,這個限制放的更寬,不少http服務器是用epoll來實現的(Nginx)。學習

epoll主要有兩個優勢:線程

  1. 基於事件的就緒通知方式 ,select/poll方式,進程只有在調用必定的方法後,內核纔會對全部監視的文件描述符進行掃描,而epoll事件經過epoll_ctl()註冊一個文件描述符,一旦某個文件描述符就緒時,內核會採用相似call back的回調機制,迅速激活這個文件描述符,epoll_wait()便會獲得通知。

  2. 調用一次epoll_wait()得到就緒文件描述符時,返回的並非實際的描述符,而是一個表明就緒描述符數量的值,拿到這些值去epoll指定的一個數組中依次取得相應數量的文件描述符便可,這裏使用內存映射(mmap)技術, 避免了複製大量文件描述符帶來的開銷。

Nodejs,也採用了和Nginx相似的思路,能夠再深刻了解下libuv。

Asynchronous I/O

有些人說Nodejs是Asynchronous I/O,其實否則。Asynchronous I/O是說用戶發起read等IO操做後,去作其它的事情了,而系統在完成IO操做後,用signal的方式通知用戶完成。目前使用此模型的http服務器有asyncio等。

相關文章
相關標籤/搜索