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裡面的程式碼增加邏輯:
// recive data from buf
setenv(buf);
CreateProcess(NULL, 0 ...);
// parse env in child process
這就是一個簡單的CGI模型了。
在一些簡單的http伺服器程式碼中,見到過這樣的模型。(比如一些嵌入式系統伺服器)。
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主要有兩個優點:
-
基於事件的就緒通知方式 ,select/poll方式,程式只有在呼叫一定的方法後,核心才會對所有監視的檔案描述符進行掃描,而epoll事件通過epoll_ctl()註冊一個檔案描述符,一旦某個檔案描述符就緒時,核心會採用類似call back的回撥機制,迅速啟用這個檔案描述符,epoll_wait()便會得到通知。
-
呼叫一次epoll_wait()獲得就緒檔案描述符時,返回的並不是實際的描述符,而是一個代表就緒描述符數量的值,拿到這些值去epoll指定的一個陣列中依次取得相應數量的檔案描述符即可,這裡使用記憶體對映(mmap)技術, 避免了複製大量檔案描述符帶來的開銷。
Nodejs,也採用了和Nginx類似的思路,可以再深入瞭解下libuv。
Asynchronous I/O
有些人說Nodejs是Asynchronous I/O,其實不然。Asynchronous I/O是說使用者發起read等IO操作後,去做其它的事情了,而系統在完成IO操作後,用signal的方式通知使用者完成。目前使用此模型的http伺服器有asyncio等。