libuv源碼分析之stream第二篇

上一篇分析了流的基礎知識和讀寫操作的實現。今天繼續分析。

1 關閉流的寫端

// 關閉流的寫端
int uv_shutdown(uv_shutdown_t* req, uv_stream_t* stream, uv_shutdown_cb cb) {
  
  // 流是可寫的,並且還沒關閉寫端,也不是處於正在關閉狀態
  if (!(stream->flags & UV_HANDLE_WRITABLE) ||
      stream->flags & UV_HANDLE_SHUT ||
      stream->flags & UV_HANDLE_SHUTTING ||
      uv__is_closing(stream)) {
    return UV_ENOTCONN;
  }

  // 初始化一個關閉請求,關聯的handle是stream
  uv__req_init(stream->loop, req, UV_SHUTDOWN);
  req->handle = stream;
  // 關閉後執行的回調
  req->cb = cb;
  stream->shutdown_req = req;
  // 設置正在關閉的標記
  stream->flags |= UV_HANDLE_SHUTTING;
  // 註冊等待可寫事件
  uv__io_start(stream->loop, &stream->io_watcher, POLLOUT);
 
  return 0;
}

關閉流的寫端就是相當於給流發送一個關閉請求,把請求掛載到流中,然後註冊等待可寫事件,在可寫事件觸發的時候就會執行關閉操作。這個我們後面分析。

2 關閉流

void uv__stream_close(uv_stream_t* handle) {
  unsigned int i;
  uv__stream_queued_fds_t* queued_fds;
  // 從事件循環中刪除io觀察者,移出pending隊列
  uv__io_close(handle->loop, &handle->io_watcher);
  // 停止讀
  uv_read_stop(handle);
  // 停掉handle
  uv__handle_stop(handle);
  // 不可讀、寫
  handle->flags &= ~(UV_HANDLE_READABLE | UV_HANDLE_WRITABLE);
  // 關閉非標準流的文件描述符
  if (handle->io_watcher.fd != -1) {
    /* Don't close stdio file descriptors.  Nothing good comes from it. */
    if (handle->io_watcher.fd > STDERR_FILENO)
      uv__close(handle->io_watcher.fd);
    handle->io_watcher.fd = -1;
  }
  // 關閉通信socket對應的文件描述符
  if (handle->accepted_fd != -1) {
    uv__close(handle->accepted_fd);
    handle->accepted_fd = -1;
  }

  /* Close all queued fds */
  // 同上,這是在排隊等待處理的通信socket
  if (handle->queued_fds != NULL) {
    queued_fds = handle->queued_fds;
    for (i = 0; i < queued_fds->offset; i++)
      uv__close(queued_fds->fds[i]);
    uv__free(handle->queued_fds);
    handle->queued_fds = NULL;
  }

  assert(!uv__io_active(&handle->io_watcher, POLLIN | POLLOUT));
}

3 連接流

連接流是針對tcp的,連接即建立三次握手。所以我們首先介紹一下一些網絡編程相關的內容。想要發起三次握手,首先我們先要有一個socket。我們看libuv中如何新建一個socket。

/*
1 獲取一個新的socket fd
2 把fd保存到handle裏,並根據flag進行相關設置
3 綁定到本機隨意的地址(如果設置了該標記的話)
*/
static int new_socket(uv_tcp_t* handle, int domain, unsigned long flags) {
  struct sockaddr_storage saddr;
  socklen_t slen;
  int sockfd;
  int err;
  // 獲取一個socket
  err = uv__socket(domain, SOCK_STREAM, 0);
  if (err < 0)
    return err;
  // 申請的fd
  sockfd = err;
  // 設置選項和保存socket的文件描述符到io觀察者中
  err = uv__stream_open((uv_stream_t*) handle, sockfd, flags);
  if (err) {
    uv__close(sockfd);
    return err;
  }
  // 設置了需要綁定標記UV_HANDLE_BOUND	
  if (flags & UV_HANDLE_BOUND) {
    slen = sizeof(saddr);
    memset(&saddr, 0, sizeof(saddr));
    // 獲取fd對應的socket信息,比如ip,端口,可能沒有
    if (getsockname(uv__stream_fd(handle), (struct sockaddr*) &saddr, &slen)) {
      uv__close(sockfd);
      return UV__ERR(errno);
    }
    // 綁定到socket中,如果沒有則綁定到系統隨機選擇的地址
    if (bind(uv__stream_fd(handle), (struct sockaddr*) &saddr, slen)) {
      uv__close(sockfd);
      return UV__ERR(errno);
    }
  }

  return 0;
}

上面的代碼就是在libuv申請一個socket的邏輯,他還支持新建的socket,可以綁定到一個用戶設置的,或者操作系統隨機選擇的地址。不過libuv並不直接使用這個函數。而是又封裝了一層。

// 如果流還沒有對應的fd,則申請一個新的,如果有則修改流的配置
static int maybe_new_socket(uv_tcp_t* handle, int domain, unsigned long flags) {
  struct sockaddr_storage saddr;
  socklen_t slen;

  if (domain == AF_UNSPEC) {
    handle->flags |= flags;
    return 0;
  }
  // 已經有socket fd了
  if (uv__stream_fd(handle) != -1) {
    // 該流需要綁定到一個地址
    if (flags & UV_HANDLE_BOUND) {
      /*
	      流是否已經綁定到一個地址了。handle的flag是在new_socket裏設置的,
	      如果有這個標記說明已經執行過綁定了,直接更新flags就行。
      */
      if (handle->flags & UV_HANDLE_BOUND) {
        handle->flags |= flags;
        return 0;
      }
      // 有socket fd,但是可能還沒綁定到一個地址
      slen = sizeof(saddr);
      memset(&saddr, 0, sizeof(saddr));
      // 獲取socket綁定到的地址
      if (getsockname(uv__stream_fd(handle), (struct sockaddr*) &saddr, &slen))
        return UV__ERR(errno);
      // 綁定過了socket地址,則更新flags就行
      if ((saddr.ss_family == AF_INET6 &&
          ((struct sockaddr_in6*) &saddr)->sin6_port != 0) ||
          (saddr.ss_family == AF_INET &&
          ((struct sockaddr_in*) &saddr)->sin_port != 0)) {
        /* Handle is already bound to a port. */
        handle->flags |= flags;
        return 0;
      }
      // 沒綁定則綁定到隨機地址,bind中實現
      if (bind(uv__stream_fd(handle), (struct sockaddr*) &saddr, slen))
        return UV__ERR(errno);
    }

    handle->flags |= flags;
    return 0;
  }
  // 申請一個新的fd關聯到流
  return new_socket(handle, domain, flags);
}

maybe_new_socket函數的邏輯分支很多
1 如果流還沒有關聯到fd,則申請一個新的fd關聯到流上。如果設置了綁定標記,fd還會和一個地址進行綁定。
2 如果流已經關聯了一個fd

  1. 如果流設置了綁定地址的標記,但是已經通過libuv綁定了一個地址(Libuv會設置UV_HANDLE_BOUND標記,用戶也可能是直接調bind函數綁定了)。則不需要再次綁定,更新flags就行。
  2. 如果流設置了綁定地址的標記,但是還沒有通過libuv綁定一個地址,這時候通過getsocketname判斷用戶是否自己通過bind函數綁定了一個地址,是的話則不需要再次執行綁定操作。否則隨機綁定到一個地址。
    以上兩個函數的邏輯主要是申請一個socket和給socket綁定一個地址。下面我們開看一下連接流的實現。
int uv__tcp_connect(uv_connect_t* req,
                    uv_tcp_t* handle,
                    const struct sockaddr* addr,
                    unsigned int addrlen,
                    uv_connect_cb cb) {
  int err;
  int r;

  // 已經發起了connect了
  if (handle->connect_req != NULL)
    return UV_EALREADY;  
  // 申請一個socket和綁定一個地址,如果還沒有的話
  err = maybe_new_socket(handle,
                         addr->sa_family,
                         UV_HANDLE_READABLE | UV_HANDLE_WRITABLE);
  if (err)
    return err;

  handle->delayed_error = 0;

  do {
    // 清除全局錯誤變量的值
    errno = 0;
    // 發起三次握手
    r = connect(uv__stream_fd(handle), addr, addrlen);
  } while (r == -1 && errno == EINTR);

  if (r == -1 && errno != 0) {
    // 三次握手還沒有完成
    if (errno == EINPROGRESS)
      ; /* not an error */
    else if (errno == ECONNREFUSED)
      // 對方拒絕建立連接,延遲報錯
      handle->delayed_error = UV__ERR(errno);
    else
      // 直接報錯
      return UV__ERR(errno);
  }
  // 初始化一個連接型request,並設置某些字段
  uv__req_init(handle->loop, req, UV_CONNECT);
  req->cb = cb;
  req->handle = (uv_stream_t*) handle;
  QUEUE_INIT(&req->queue);
  handle->connect_req = req;
  // 註冊到libuv觀察者隊列
  uv__io_start(handle->loop, &handle->io_watcher, POLLOUT);
  // 連接出錯,插入pending隊尾
  if (handle->delayed_error)
    uv__io_feed(handle->loop, &handle->io_watcher);

  return 0;
}

連接流的邏輯,大致如下
1 申請一個socket,綁定一個地址。
2 根據給定的服務器地址,發起三次握手,非阻塞的,會直接返回繼續執行,不會等到三次握手完成。
3 往流上掛載一個connect型的請求。
4 設置io觀察者感興趣的事件爲可寫。然後把io觀察者插入事件循環的io觀察者隊列。等待可寫的時候時候(完成三次握手),就會執行cb回調。

4 監聽流

int uv_tcp_listen(uv_tcp_t* tcp, int backlog, uv_connection_cb cb) {
  static int single_accept = -1;
  unsigned long flags;
  int err;

  if (tcp->delayed_error)
    return tcp->delayed_error;
  // 是否設置了不連續accept。默認是連續accept。
  if (single_accept == -1) {
    const char* val = getenv("UV_TCP_SINGLE_ACCEPT");
    single_accept = (val != NULL && atoi(val) != 0);  /* Off by default. */
  }
  // 設置不連續accept
  if (single_accept)
    tcp->flags |= UV_HANDLE_TCP_SINGLE_ACCEPT;

  flags = 0;
  /*
    可能還沒有用於listen的fd,socket地址等。
    這裏申請一個socket和綁定到一個地址(如果調listen之前沒有調bind則綁定到隨機地址)
  */
  err = maybe_new_socket(tcp, AF_INET, flags);
  if (err)
    return err;
  // 設置fd爲listen狀態
  if (listen(tcp->io_watcher.fd, backlog))
    return UV__ERR(errno);
  // 建立連接後的業務回調
  tcp->connection_cb = cb;
  tcp->flags |= UV_HANDLE_BOUND;
  // 有連接到來時的libuv層回調
  tcp->io_watcher.cb = uv__server_io;
  // 註冊讀事件,等待連接到來
  uv__io_start(tcp->loop, &tcp->io_watcher, POLLIN);

  return 0;
}

監聽流的邏輯看起來邏輯很多,但是主要的邏輯是把流對的fd改成listen狀態,這樣流就可以接收請求了。然後設置連接到來時執行的回調。最後註冊io觀察者到事件循環。等待連接到來。就會執行uv__server_io。uv__server_io再執行connection_cb。監聽流和其他流的一個區別是,當io觀察者的事件觸發時,監聽流執行的回調是uv__server_io函數。而其他流是在uv__stream_io裏統一處理。

流的類型分析得差不多了,最後分析一下監聽流的處理函數uv__server_io,統一處理其他流的函數是uv__stream_io,這個下次分析。

剛纔已經說到有連接到來的時候,libuv會執行uv__server_io,下面看一下他做了什麼事情。

// 有tcp連接到來時執行該函數
void uv__server_io(uv_loop_t* loop, uv__io_t* w, unsigned int events) {
  uv_stream_t* stream;
  int err;
  // 拿到io觀察者所在的流
  stream = container_of(w, uv_stream_t, io_watcher);
  // 繼續註冊事件,等待連接
  uv__io_start(stream->loop, &stream->io_watcher, POLLIN);

  /* connection_cb can close the server socket while we're
   * in the loop so check it on each iteration.
   */
  while (uv__stream_fd(stream) != -1) {
    // 有連接到來,進行accept
    err = uv__accept(uv__stream_fd(stream));
    if (err < 0) {
      // 忽略出錯處理
      // accept出錯,觸發回調
      stream->connection_cb(stream, err);
      continue;
    }
    // 保存通信socket對應的文件描述符
    stream->accepted_fd = err;
    /*
	    有連接,執行上層回調,connection_cb一般會調用uv_accept消費accepted_fd。
	    然後重新註冊等待可讀事件
    */
    stream->connection_cb(stream, 0);
    /*
	    用戶還沒有消費accept_fd。先解除io的事件,
	    等到用戶調用uv_accept消費了accepted_fd再重新註冊事件
    */
    if (stream->accepted_fd != -1) {
      uv__io_stop(loop, &stream->io_watcher, POLLIN);
      return;
    }
    // 定時睡眠一會(可被信號喚醒),分點給別的進程accept
    if (stream->type == UV_TCP &&
        (stream->flags & UV_HANDLE_TCP_SINGLE_ACCEPT)) {
      struct timespec timeout = { 0, 1 };
      nanosleep(&timeout, NULL);
    }
  }
}

整個函數的邏輯如下
1 調用accept摘下一個完成了三次握手的節點。
2 然後執行上層回調。上層回調會調用uv_accept消費accept返回的fd。然後再次註冊等待可讀事件(當然也可以不消費)。
3 如果2沒有消費調fd。則撤銷等待可讀事件,即處理完一個fd後,再accept下一個。如果2中消費了fd。再判斷有沒有設置UV_HANDLE_TCP_SINGLE_ACCEPT標記,如果有則休眠一會,分點給別的進程accept。否則繼續accept。

總結:目前分析的是一些基礎的概念和實現。後續會串起來再分析一下具體的過程。

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