由Nodejs来说I/O

java 4 2016-02-29 13:03
女装

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主要有两个优点:

  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等。

女装
文章评论