Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient.数组
IO(Input & Output),顾名思义,输入输出便是IO。磁盘,网络,鼠标,键盘等都算IO;而你们一般说的IO,大部分指磁盘和网络的数据操做。
对于磁盘,IO=读写;对于网络,IO=收发。服务器
学习C语言时,有个做业,大意是写一个server程序和client程序,实现TCP/UDP通讯。看起来代码以下:网络
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; }
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,都会阻塞住当前代码的执行。单靠这种模型,是没法实现一个完善的服务器的。多线程
为了让Server能服务更多的Client,基于Blocking I/O,能够采用多线程(进程)来处理,实现1对多的服务。并发
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
由于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。
有些人说Nodejs是Asynchronous I/O,其实否则。Asynchronous I/O是说用户发起read等IO操做后,去作其它的事情了,而系统在完成IO操做后,用signal的方式通知用户完成。目前使用此模型的http服务器有asyncio等。