跳转至

09-Unix域

Unix域一种进程间通信的方式,Unix域不仅支持没有继承关系的进程间进行通信,而且支持进程间传递文件描述符。Unix域是Node.js中核心的功能,它是进程间通信的底层基础,child_process和cluster模块都依赖Unix域的能力。从实现和使用上来看,Unix域类似TCP,但是因为它是基于同主机进程的,不像TCP需要面临复杂的网络的问题,所以实现也没有TCP那么复杂。Unix域和传统的socket通信一样,遵循网络编程的那一套流程,由于在同主机内,就不必要使用IP和端口的方式。Node.js中,Unix域采用的是一个文件作为标记。大致原理如下。
1 服务器首先拿到一个socket。
2 服务器bind一个文件,类似bind一个IP和端口一样,对于操作系统来说,就是新建一个文件(不一定是在硬盘中创建,可以设置抽象路径名),然后把文件路径信息存在socket中。
3 调用listen修改socket状态为监听状态。
4 客户端通过同样的文件路径调用connect去连接服务器。这时候用于表示客户端的结构体插入服务器的连接队列,等待处理。
5 服务器调用accept摘取队列的节点,然后新建一个通信socket和客户端进行通信。
Unix域通信本质还是基于内存之间的通信,客户端和服务器都维护一块内存,这块内存分为读缓冲区和写缓冲区。从而实现全双工通信,而Unix域的文件路径,只不过是为了让客户端进程可以找到服务端进程,后续就可以互相往对方维护的内存里写数据,从而实现进程间通信。

9.1 Unix域在Libuv中的使用

接下来我们看一下在Libuv中关于Unix域的实现和使用。

9.1.1 初始化

Unix域使用uv_pipe_t结构体表示,使用之前首先需要初始化uv_pipe_t。下面看一下它的实现逻辑。

1
2
3
4
5
6
7
8
    int uv_pipe_init(uv_loop_t* loop, uv_pipe_t* handle, int ipc) { 
      uv__stream_init(loop, (uv_stream_t*)handle, UV_NAMED_PIPE);  
      handle->shutdown_req = NULL;  
      handle->connect_req = NULL;  
      handle->pipe_fname = NULL;  
      handle->ipc = ipc;  
      return 0;  
    }  

uv_pipe_init逻辑很简单,就是初始化uv_pipe_t结构体的一些字段。uv_pipe_t继承于stream,uv__stream_init就是初始化stream(父类)的字段。uv_pipe_t中有一个字段ipc,该字段标记了是否允许在该Unix域通信中传递文件描述符。

9.1.2 绑定Unix域路径

开头说过,Unix域的实现类似TCP的实现。遵循网络socket编程那一套流程。服务端使用bind,listen等函数启动服务。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
    // name是unix路径名称  
    int uv_pipe_bind(uv_pipe_t* handle, const char* name) {  
      struct sockaddr_un saddr;  
      const char* pipe_fname;  
      int sockfd;  
      int err;  
      pipe_fname = NULL; 
      pipe_fname = uv__strdup(name);  
      name = NULL;  
      // 流式Unix域套接字  
      sockfd = uv__socket(AF_UNIX, SOCK_STREAM, 0);  
      memset(&saddr, 0, sizeof saddr);  
      strncpy(saddr.sun_path, pipe_fname, sizeof(saddr.sun_path) - 1);
      saddr.sun_path[sizeof(saddr.sun_path) - 1] = '\0';  
      saddr.sun_family = AF_UNIX;  
      // 绑定到路径,TCP是绑定到IP和端口  
      if (bind(sockfd, (struct sockaddr*)&saddr, sizeof saddr)) { 
       // ...  
      }  

      // 设置绑定成功标记  
      handle->flags |= UV_HANDLE_BOUND;
        // Unix域的路径  
      handle->pipe_fname = pipe_fname;   
      // 保存socket对应的fd  
      handle->io_watcher.fd = sockfd;  
      return 0;  
    }  

uv_pipe_bind函数首先申请一个socket,然后调用操作系统的bind函数把Unix域路径保存到socket中。最后标记已经绑定标记,并且保存Unix域的路径和socket对应的fd到handle中,后续需要使用。我们看到Node.js中Unix域的类型是SOCK_STREAM。Unix域支持两种数据模式。
1 流式( SOCK_STREAM),类似TCP,数据为字节流,需要应用层处理粘包问题。
2 数据报模式( SOCK_DGRAM ),类似UDP,不需要处理粘包问题。
通过Unix域虽然可以实现进程间的通信,但是我们拿到的数据可能是"乱的",这是为什么呢?一般情况下,客户端给服务器发送1个字节,然后服务器处理,如果是基于这种场景,那么数据就不会是乱的。因为每次就是一个需要处理的数据单位。但是如果客户端给服务器发送1个字节,服务器还没来得及处理,客户端又发送了一个字节,那么这时候服务器再处理的时候,就会有问题。因为两个字节混一起了。就好比在一个TCP连接上先后发送两个HTTP请求一样,如果服务器没有办法判断两个请求的数据边界,那么处理就会有问题。所以这时候,我们需要定义一个应用层协议,并且实现封包解包的逻辑,才能真正完成进程间通信。

9.1.3 启动服务

绑定了路径后,就可以调用listen函数使得socket处于监听状态。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
    int uv_pipe_listen(uv_pipe_t* handle, int backlog, uv_connection_cb cb) {  
      // uv__stream_fd(handle)得到bind函数中获取的socket  
      if (listen(uv__stream_fd(handle), backlog))  
        return UV__ERR(errno);  
      // 保存回调,有进程调用connect的时候时触发,由uv__server_io函数触发  
      handle->connection_cb = cb;  
      // IO观察者的回调  
      handle->io_watcher.cb = uv__server_io;  
      // 注册IO观察者到Libuv,等待连接,即读事件到来  
      uv__io_start(handle->loop, &handle->io_watcher, POLLIN);  
      return 0;  
    }  

uv_pipe_listen执行操作系统的listen函数使得socket成为监听型的套接字。然后把socket对应的文件描述符和回调封装成IO观察者。注册到Libuv中。等到有读事件到来(有连接到来)。就会执行uv__server_io函数,摘下对应的客户端节点。最后执行connection_cb回调。

9.1.4 发起连接

这时候,我们已经成功启动了一个Unix域服务。接下来就是看客户端的逻辑。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
    void uv_pipe_connect(uv_connect_t* req, 
                          uv_pipe_t* handle, 
                          const char* name, 
                          uv_connect_cb cb) {  
      struct sockaddr_un saddr;  
      int new_sock;  
      int err;  
      int r;  
      // 判断是否已经有socket了,没有的话需要申请一个,见下面  
      new_sock = (uv__stream_fd(handle) == -1);  
      // 客户端还没有对应的socket fd  
      if (new_sock) {  
        handle->io_watcher.fd= uv__socket(AF_UNIX, 
                                               SOCK_STREAM, 
                                               0);  
      }  
      // 需要连接的服务器信息。主要是Unix域路径信息  
      memset(&saddr, 0, sizeof saddr);  
      strncpy(saddr.sun_path, name, sizeof(saddr.sun_path) - 1);  
      saddr.sun_path[sizeof(saddr.sun_path) - 1] = '\0';  
      saddr.sun_family = AF_UNIX;  
      // 非阻塞式连接服务器,Unix域路径是name  
      do {  
        r = connect(uv__stream_fd(handle),
                          (struct sockaddr*)&saddr, sizeof saddr);  
      }  
      while (r == -1 && errno == EINTR);  
      // 忽略错误处理逻辑  
      err = 0;  
      // 设置socket的可读写属性  
      if (new_sock) {  
        err = uv__stream_open((uv_stream_t*)handle,  
                      uv__stream_fd(handle),  
                     UV_HANDLE_READABLE | UV_HANDLE_WRITABLE);  
      }  
      // 把IO观察者注册到Libuv,等到连接成功或者可以发送请求  
      if (err == 0)  
        uv__io_start(handle->loop, 
                         &handle->io_watcher, 
                         POLLIN | POLLOUT);  

    out:  
      // 记录错误码,如果有的话  
      handle->delayed_error = err;  
      // 保存调用者信息  
      handle->connect_req = req; 
      uv__req_init(handle->loop, req, UV_CONNECT);  
      req->handle = (uv_stream_t*)handle;  
      req->cb = cb;  
      QUEUE_INIT(&req->queue);  
      /*
         如果连接出错,在pending阶段会执行uv__stream_io,
          从而执行req对应的回调。错误码是delayed_error 
        */ 
      if (err)  
        uv__io_feed(handle->loop, &handle->io_watcher);  
    }  

uv_pipe_connect函数首先以非阻塞的方式调用操作系统的connect函数,调用connect后操作系统把客户端对应的socket直接插入服务器socket的待处理socket队列中,等待服务器处理。这时候socket是处于连接中的状态,当服务器调用accept函数处理连接时,会修改连接状态为已连接(这和TCP不一样,TCP是完成三次握手后就会修改为连接状态,而不是accept的时候),并且会触发客户端socket的可写事件。事件驱动模块就会执行相应的回调(uv__stream_io),从而执行C++和JS的回调。

9.1.5 关闭Unix域

我们可以通过uv_close关闭一个Unix域handle。uv_close中会调用uv__pipe_close。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    void uv__pipe_close(uv_pipe_t* handle) {  
      // 如果是Unix域服务器则需要删除Unix域路径并删除指向的堆内存  
      if (handle->pipe_fname) {  
        unlink(handle->pipe_fname);  
        uv__free((void*)handle->pipe_fname);  
        handle->pipe_fname = NULL;  
      }  
      // 关闭流相关的内容  
      uv__stream_close((uv_stream_t*)handle);  
    }  

关闭Unix域handle时,Libuv会自动删除Unix域路径对应的文件。但是如果进程异常退出时,该文件可能不会被删除,这样会导致下次监听的时候报错listen EADDRINUSE,所以安全起见,我们可以在进程退出或者监听之前判断该文件是否存在,存在的话则删除。另外还有一个问题是,如果两个不相关的进程使用了同一个文件则会导致误删,所以Unix域对应的文件,我们需要小心处理,最好能保证唯一性。

Unix域大致的流程和网络编程一样。分为服务端和客户端两面。Libuv在操作系统提供的API的基础上。和Libuv的异步非阻塞结合。在Libuv中为进程间提供了一种通信方式。下面看一下在Node.js中是如何使用Libuv提供的功能的。

9.2 Unix域在Node.js中的使用

9.2.1 Unix域服务器

在Node.js中,我们可以通过以下代码创建一个Unix域服务器

1
2
3
4
5
6
    const server = net.createServer((client) => {  
      // 处理client  
    });  
    server.listen('/tmp/test.sock', () => {  
      console.log(`bind uinx domain success`);  
    });  

我们从listen函数开始分析这个过程。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
    Server.prototype.listen = function(...args) {  
      const normalized = normalizeArgs(args);  
      let options = normalized[0];  
      const cb = normalized[1];  
      // 调用底层的listen函数成功后执行的回调  
      if (cb !== null) {  
        this.once('listening', cb);  
      }  
      if (options.path && isPipeName(options.path)) {  
        const pipeName = this._pipeName = options.path;  
        backlog = options.backlog || backlogFromArgs;  
        listenIncluster(this, pipeName, -1, -1, backlog, undefined, 
                          options.exclusive);  
        /*
          Unix域使用文件实现的,客户端需要访问该文件的权限才能通信,
          这里做权限控制 
         */ 
        let mode = 0;  
        if (options.readableAll === true)  
          mode |= PipeConstants.UV_READABLE;  
        if (options.writableAll === true)  
          mode |= PipeConstants.UV_WRITABLE;  
        if (mode !== 0) {  
          // 修改文件的访问属性  
          const err = this._handle.fchmod(mode);  
          if (err) {  
            this._handle.close();  
            this._handle = null;  
            throw errnoException(err, 'uv_pipe_chmod');  
          }  
        }  
        return this;  
      }  
    }  

这段代码中最主要的是listenIncluster函数。我们看一下该函数的逻辑。

1
2
3
4
5
6
7
8
9
    function listenIncluster(server, address, port, addressType,  
                             backlog, fd, exclusive, flags) {  
      exclusive = !!exclusive; 
      if (cluster === undefined) cluster = require('cluster');  
      if (cluster.isMaster || exclusive) {  
        server._listen2(address, port, addressType, backlog, fd, flags);  
        return;  
      }  
    }  

直接调用_listen2(isMaster只有在cluster.fork创建的进程中才是false,其余情况都是true,包括child_process模块创建的子进程)。我们继续看listen函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    Server.prototype._listen2 = setupListenHandle;

    function setupListenHandle(address, 
                                  port, 
                                  addressType, 
                                  backlog, 
                                  fd, 
                                  flags) {  
      this._handle = createServerHandle(address, 
                                           port, 
                                           addressType, 
                                           fd, 
                                           flags);  
      // 有完成连接完成时触发  
      this._handle.onconnection = onconnection;  
      const err = this._handle.listen(backlog || 511);  
      if (err) {  
        // 触发error事件
      }  
      // 下一个tick触发listen回调  
      defaultTriggerAsyncIdScope(this[async_id_symbol],  
                                 process.nextTick,  
                                 emitListeningNT,  
                                 this);  
    }
首先调用createServerHandle创建一个handle,然后执行listen函数。我们首先看一下createServerHandle。
1
2
3
4
5
6
7
8
9
    function createServerHandle(address, 
                                   port, 
                                   addressType, 
                                   fd, 
                                   flags) {  
      let handle = new Pipe(PipeConstants.SERVER);  
      handle.bind(address, port);  
      return handle;  
    }  

创建了一个Pipe对象,然后调用它的bind和listen函数,我们看new Pipe的逻辑,从pipe_wrap.cc的导出逻辑,我们知道,这时候会新建一个C++对象,然后执行New函数,并且把新建的C++对象等信息作为入参。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    void PipeWrap::New(const FunctionCallbackInfo<Value>& args) {  
      Environment* env = Environment::GetCurrent(args);  
      // 类型  
      int type_value = args[0].As<Int32>()->Value();  
      PipeWrap::SocketType type = static_cast<PipeWrap::SocketType>(type_value);  
      // 是否是用于IPC
      bool ipc;  
      ProviderType provider;  
      switch (type) {  
        case SOCKET:  
          provider = PROVIDER_PIPEWRAP;  
          ipc = false;  
          break;  
        case SERVER:  
          provider = PROVIDER_PIPESERVERWRAP;  
          ipc = false;  
          break;  
        case IPC:  
          provider = PROVIDER_PIPEWRAP;  
          ipc = true;  
          break;  
        default:  
          UNREACHABLE();  
      }  

      new PipeWrap(env, args.This(), provider, ipc);  
    }  

New函数处理了参数,然后执行了new PipeWrap创建一个对象。

1
2
3
4
5
6
7
    PipeWrap::PipeWrap(Environment* env,  
                       Local<Object> object,  
                       ProviderType provider,  
                       bool ipc)  
        : ConnectionWrap(env, object, provider) {  
      int r = uv_pipe_init(env->event_loop(), &handle_, ipc);  
    }
new Pipe执行完后,就会通过该C++对象调用Libuv的bind和listen完成服务器的启动,就不再展开分析。

9.2.2 Unix域客户端

接着我们看一下Unix域作为客户端使用时的过程。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
    Socket.prototype.connect = function(...args) {  
      const path = options.path;  
      // Unix域路径  
      var pipe = !!path;  
      if (!this._handle) {  
        // 创建一个C++层handle,即pipe_wrap.cc导出的Pipe类  
        this._handle = pipe ?  
          new Pipe(PipeConstants.SOCKET) :  
          new TCP(TCPConstants.SOCKET);  
        // 挂载onread方法到this中  
        initSocketHandle(this);  
      }  

      if (cb !== null) {  
        this.once('connect', cb);  
      }  
      // 执行internalConnect  
      defaultTriggerAsyncIdScope(  
          this[async_id_symbol], internalConnect, this, path  
      );  
      return this;  
    };  

首先新建一个handle,值是new Pipe。接着执行了internalConnect,internalConnect函数的主要逻辑如下

1
2
3
4
5
6
    const req = new PipeConnectWrap();  
    // address为Unix域路径
    req.address = address;  
    req.oncomplete = afterConnect;  
    // 调用C++层connect
    err = self._handle.connect(req, address, afterConnect);  
我们看C++层的connect函数,
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
    void PipeWrap::Connect(const FunctionCallbackInfo<Value>& args) {  
      Environment* env = Environment::GetCurrent(args);  

      PipeWrap* wrap;  
      ASSIGN_OR_RETURN_UNWRAP(&wrap, args.Holder());  
      // PipeConnectWrap对象  
      Local<Object> req_wrap_obj = args[0].As<Object>();  
      // Unix域路径  
      node::Utf8Value name(env->isolate(), args[1]);  
      /*
          新建一个ConnectWrap对象,ConnectWrap是对handle进行一次连接请求
           的封装,内部维护一个uv_connect_t结构体, req_wrap_obj的一个字段
           指向ConnectWrap对象,用于保存对应的请求上下文  
        */
      ConnectWrap* req_wrap =  
          new ConnectWrap(env, 
                                 req_wrap_obj, 
                                 AsyncWrap::PROVIDER_PIPECONNECTWRAP);  
      // 调用Libuv的connect函数  
      uv_pipe_connect(req_wrap->req(),  
                      &wrap->handle_,  
                      *name,  
                      AfterConnect);  
        // req_wrap->req_.data = req_wrap;关联起来
      req_wrap->Dispatched();  
      // uv_pipe_connect() doesn't return errors.  
      args.GetReturnValue().Set(0);  
    }  

uv_pipe_connect函数,第一个参数是uv_connect_t结构体(request),第二个是一个uv_pipe_t结构体(handle),handle是对Unix域客户端的封装,request是请求的封装,它表示基于handle发起一次连接请求。连接成功后会执行AfterConnect。由前面分析我们知道,当连接成功时,首先会执行回调Libuv的uv__stream_io,然后执行C++层的AfterConnect。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
    // 主动发起连接,成功/失败后的回调  
    template <typename WrapType,typename UVType> = PipeWrap, uv_pipe_t
    void ConnectionWrap<WrapType, UVType>::AfterConnect(uv_connect_t* req        
                                                          ,int status) { 
      // 在Connect函数里关联起来的  
      ConnectWrap* req_wrap = static_cast<ConnectWrap*>(req->data);  
      // 在uv_pipe_connect中完成关联的  
      WrapType* wrap = static_cast<WrapType*>(req->handle->data);  
      Environment* env = wrap->env();  

      HandleScope handle_scope(env->isolate());  
      Context::Scope context_scope(env->context());  

      bool readable, writable;  
      // 是否连接成功  
      if (status) {  
        readable = writable = 0;  
      } else {  
        readable = uv_is_readable(req->handle) != 0;  
        writable = uv_is_writable(req->handle) != 0;  
      }  

      Local<Value> argv[5] = {  
        Integer::New(env->isolate(), status),  
        wrap->object(),  
        req_wrap->object(),  
        Boolean::New(env->isolate(), readable),  
        Boolean::New(env->isolate(), writable)  
      };  
      // 执行JS层的oncomplete回调  
      req_wrap->MakeCallback(env->oncomplete_string(), 
                               arraysize(argv), 
                               argv);  

      delete req_wrap;  
    }  

我们再回到JS层的afterConnect

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
    function afterConnect(status, handle, req, readable, writable) { 
      var self = handle.owner;  
      handle = self._handle;  
      if (status === 0) {  
        self.readable = readable;  
        self.writable = writable;  
        self._unrefTimer();  
        // 触发connect事件  
        self.emit('connect');  
        // 可读并且没有处于暂停模式,则注册等待可读事件  
        if (readable && !self.isPaused())  
          self.read(0);  
      }  
    }  

至此,作为客户端对服务器的连接就完成了。后续就可以进行通信。