上一篇分析了流的基礎知識和讀寫操作的實現。今天繼續分析。
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
- 如果流設置了綁定地址的標記,但是已經通過libuv綁定了一個地址(Libuv會設置UV_HANDLE_BOUND標記,用戶也可能是直接調bind函數綁定了)。則不需要再次綁定,更新flags就行。
- 如果流設置了綁定地址的標記,但是還沒有通過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。
總結:目前分析的是一些基礎的概念和實現。後續會串起來再分析一下具體的過程。