libuv源碼分析之unix域

unix域是一種基於單主機的進程間通信方式。實現模式類似tcp通信。今天先分析他的實現,後續會分析他的使用。在libuv中,unix域用uv_pipe_t表示。

struct uv_stream_s {
  // uv_handle_s的字段
  void* data;        
  // 所屬事件循環   
  uv_loop_t* loop;  
  // handle類型    
  uv_handle_type type;  
  // 關閉handle時的回調
  uv_close_cb close_cb; 
  // 用於插入事件循環的handle隊列
  void* handle_queue[2];
  union {               
    int fd;             
    void* reserved[4];  
  } u;      
  // 用於插入事件循環的closing階段對應的隊列
  uv_handle_t* next_closing; 
  // 各種標記 
  unsigned int flags;
  // 流拓展的字段
  // 用戶寫入流的字節大小,流緩存用戶的輸入,然後等到可寫的時候才做真正的寫
  size_t write_queue_size; 
  // 分配內存的函數,內存由用戶定義,主要用來保存讀取的數據               
  uv_alloc_cb alloc_cb;  
  // 讀取數據的回調                 
  uv_read_cb read_cb; 
  // 連接成功後,執行connect_req的回調(connect_req在uv__xxx_connect中賦值)
  uv_connect_t *connect_req; 
  // 關閉寫端的時候,發送完緩存的數據,執行shutdown_req的回調(shutdown_req在uv_shutdown的時候賦值)    
  uv_shutdown_t *shutdown_req;
  // 流對應的io觀察者,即文件描述符+一個文件描述符事件觸發時執行的回調   
  uv__io_t io_watcher;  
  // 流緩存下來的,待寫的數據         
  void* write_queue[2];       
  // 已經完成了數據寫入的隊列   
  void* write_completed_queue[2];
  // 完成三次握手後,執行的回調
  uv_connection_cb connection_cb;
  // 操作流時出錯碼
  int delayed_error;  
  // accept返回的通信socket對應的文件描述符           
  int accepted_fd;    
  // 同上,用於緩存更多的通信socket對應的文件描述符           
  void* queued_fds;
  // 標記管道是否能在進程間傳遞
  int ipc; 
  // 用於unix域通信的文件路徑
  const char* pipe_fname; 
}

unix域繼承域handle和stream。下面看一下他的具體實現邏輯。

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(父類)的字段。文章開頭說過,unix域的實現類似tcp的實現。遵循網絡socket編程那一套。服務端使用bind,listen等函數啓動服務。

// 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;
  handle->pipe_fname = pipe_fname; 
  // 保存socket fd,用於後面監聽
  handle->io_watcher.fd = sockfd;
  return 0;
}

uv_pipe_bind函數的邏輯也比較簡單,就是類似tcp的bind行爲。
1 申請一個socket套接字。
2 綁定unix域路徑到socket中。
綁定了路徑後,就可以調用listen函數開始監聽。

int uv_pipe_listen(uv_pipe_t* handle, int backlog, uv_connection_cb cb) {
  if (uv__stream_fd(handle) == -1)
    return UV_EINVAL;
  // 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觀察者的回調,有進程調用connect的時候時觸發(io觀察者的fd在init函數裏設置了)
  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回調。

這時候,使用unix域成功啓動了一個服務。接下來就是看客戶端的邏輯。

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) {
    err = uv__socket(AF_UNIX, SOCK_STREAM, 0);
    if (err < 0)
      goto out;
    // 保存socket對應的文件描述符到io觀察者
    handle->io_watcher.fd = err;
  }
  // 需要連接的服務器信息。主要是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節點會執行req對應的回調。錯誤碼是delayed_error
  if (err)
    uv__io_feed(handle->loop, &handle->io_watcher);
}

本文大致分析了unix域在libuv中是如何封裝的。大致的流程和網絡編程一樣。分爲服務端和客戶端兩面。libuv在操作系統提供的api的基礎上。和libuv的異步非阻塞結合。在libuv中爲進程間提供了一種通信方式。後續會繼續分析本文提到的內容。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章