流的實現在libuv裏佔了很大篇幅,今天分析一下流的實現。首先看數據結構。流在libuv裏用uv_stream_s表示,他屬於handle族。繼承於uv_handle_s。
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;
}
流的實現中,最核心的字段是io觀察者,其餘的字段是和流的性質相關的。io觀察者封裝了流對應的文件描述符和文件描述符事件觸發時的回調。比如讀一個流,寫一個流,關閉一個流,連接一個流,監聽一個流,在uv_stream_s中都有對應的字段去支持。但是本質上是靠io觀察者去驅動的。
1 讀一個流,就是io觀察者中的文件描述符。可讀事件觸發時,執行用戶的讀回調。
2 寫一個流,先把數據寫到流中,然後io觀察者中的文件描述符。可寫事件觸發時,執行最後的寫入,並執行用戶的寫完成回調。
3 關閉一個流,就是io觀察者中的文件描述符。可寫事件觸發時,如果待寫的數據已經寫完(比如發送完),然後執行關閉流的寫端。接着執行用戶的回調。
4 連接一個流,比如作爲客戶端去連接服務器。就是io觀察者中的文件描述符。可讀事件觸發時(建立三次握手成功),執行用戶的回調。
5 監聽一個流,就是io觀察者中的文件描述符。可讀事件觸發時(有完成三次握手的連接),執行用戶的回調。
今天我們具體分析一下流讀寫操作的實現。首先我們看一下如何初始化一個流。
// 初始化流
void uv__stream_init(uv_loop_t* loop,
uv_stream_t* stream,
uv_handle_type type) {
int err;
// 記錄handle的類型
uv__handle_init(loop, (uv_handle_t*)stream, type);
stream->read_cb = NULL;
stream->alloc_cb = NULL;
stream->close_cb = NULL;
stream->connection_cb = NULL;
stream->connect_req = NULL;
stream->shutdown_req = NULL;
stream->accepted_fd = -1;
stream->queued_fds = NULL;
stream->delayed_error = 0;
QUEUE_INIT(&stream->write_queue);
QUEUE_INIT(&stream->write_completed_queue);
stream->write_queue_size = 0;
// 這個邏輯看起來是爲了拿到一個備用的文件描述符,如果以後觸發UV_EMFILE錯誤(打開的文件太多)時,使用這個備用的fd
if (loop->emfile_fd == -1) {
err = uv__open_cloexec("/dev/null", O_RDONLY);
if (err < 0)
err = uv__open_cloexec("/", O_RDONLY);
if (err >= 0)
loop->emfile_fd = err;
}
// 初始化io觀察者,把文件描述符(這裏還沒有,所以是-1)和回調uv__stream_io記錄在io_watcher上
uv__io_init(&stream->io_watcher, uv__stream_io, -1);
}
我們看到流的初始化沒有太多邏輯。主要是初始化一些字段。接着我們看一下如何打開(激活)一個流。
// 關閉nagle,開啓長連接,保存fd
int uv__stream_open(uv_stream_t* stream, int fd, int flags) {
// 還沒有設置fd或者設置的同一個fd則繼續,否則返回busy
if (!(stream->io_watcher.fd == -1 || stream->io_watcher.fd == fd))
return UV_EBUSY;
// 設置流的標記
stream->flags |= flags;
if (stream->type == UV_TCP) {
// 關閉nagle算法
if ((stream->flags & UV_HANDLE_TCP_NODELAY) && uv__tcp_nodelay(fd, 1))
return UV__ERR(errno);
// 開啓SO_KEEPALIVE,使用tcp長連接,一定時間後沒有收到數據包會發送心跳包
if ((stream->flags & UV_HANDLE_TCP_KEEPALIVE) &&
uv__tcp_keepalive(fd, 1, 60)) {
return UV__ERR(errno);
}
}
// 保存socket對應的文件描述符到io觀察者中,libuv會在io poll階段監聽該文件描述符
stream->io_watcher.fd = fd;
return 0;
}
打開一個流,本質上就是給這個流關聯一個文件描述符。還有一些屬性的設置。有了文件描述符,後續就可以操作這個流了。下面我們逐個操作分析。
1 讀
我們在一個流上執行uv_read_start。流的數據(如果有的話)就會源源不斷地流向調用方。
int uv_read_start(uv_stream_t* stream,
uv_alloc_cb alloc_cb,
uv_read_cb read_cb) {
assert(stream->type == UV_TCP || stream->type == UV_NAMED_PIPE ||
stream->type == UV_TTY);
// 流已經關閉,不能讀
if (stream->flags & UV_HANDLE_CLOSING)
return UV_EINVAL;
// 流不可讀,說明可能是隻寫流
if (!(stream->flags & UV_HANDLE_READABLE))
return -ENOTCONN;
// 標記正在讀
stream->flags |= UV_HANDLE_READING;
// 記錄讀回調,有數據的時候會執行這個回調
stream->read_cb = read_cb;
// 分配內存函數,用於存儲讀取的數據
stream->alloc_cb = alloc_cb;
// 註冊讀事件
uv__io_start(stream->loop, &stream->io_watcher, POLLIN);
// 激活handle,有激活的handle,事件循環不會退出
uv__handle_start(stream);
return 0;
}
執行uv_read_start本質上是給流對應的文件描述符在epoll中註冊了一個可讀事件。並且給一些字段賦值,比如讀回調函數,分配內存的函數。打上正在做讀取操作的標記。然後在可讀事件觸發的時候,讀回調就會被執行,這個邏輯我們後面分析。除了開始讀取數據,還有一個讀操作就是停止讀取。對應的函數是uv_read_stop。
int uv_read_stop(uv_stream_t* stream) {
// 是否正在執行讀取操作,如果不是,則沒有必要停止
if (!(stream->flags & UV_HANDLE_READING))
return 0;
// 清除 正在讀取 的標記
stream->flags &= ~UV_HANDLE_READING;
// 撤銷 等待讀事件
uv__io_stop(stream->loop, &stream->io_watcher, POLLIN);
// 對寫事件也不感興趣,停掉handle。允許事件循環退出
if (!uv__io_active(&stream->io_watcher, POLLOUT))
uv__handle_stop(stream);
stream->read_cb = NULL;
stream->alloc_cb = NULL;
return 0;
}
和start相反,start是註冊等待可讀事件和打上正在讀取這個標記,stop就是撤銷等待可讀事件和清除這個標記。另外還有一個輔助函數,判斷流是否設置了可讀屬性。
int uv_is_readable(const uv_stream_t* stream) {
return !!(stream->flags & UV_HANDLE_READABLE);
}
2 寫
我們在流上執行uv_write就可以往流中寫入數據。
int uv_write(
// 一個寫請求,記錄了需要寫入的數據和信息。數據來自下面的const uv_buf_t bufs[]
uv_write_t* req,
// 往哪個流寫
uv_stream_t* handle,
// 需要寫入的數據
const uv_buf_t bufs[],
// 個數
unsigned int nbufs,
// 寫完後執行的回調
uv_write_cb cb
) {
return uv_write2(req, handle, bufs, nbufs, NULL, cb);
}
uv_write是直接調用uv_write2。第四個參數是NULL。代表是一般的寫數據,不傳遞文件描述符。
int uv_write2(
uv_write_t* req,
uv_stream_t* stream,
const uv_buf_t bufs[],
unsigned int nbufs,
// 需要傳遞的文件描述符所在的流,這裏不分析,在分析unix的時候再分析
uv_stream_t* send_handle,
uv_write_cb cb
)
{
int empty_queue;
// 是不可寫流
if (!(stream->flags & UV_HANDLE_WRITABLE))
return -EPIPE;
// 流中緩存的數據大小是否爲0
empty_queue = (stream->write_queue_size == 0);
// 初始化一個寫請求
uv__req_init(stream->loop, req, UV_WRITE);
// 寫完後執行的回調
req->cb = cb;
// 往哪個流寫
req->handle = stream;
// 寫出錯的錯誤碼,初始化爲0
req->error = 0;
QUEUE_INIT(&req->queue);
// 默認buf
req->bufs = req->bufsml;
// 不夠則擴容
if (nbufs > ARRAY_SIZE(req->bufsml))
req->bufs = uv__malloc(nbufs * sizeof(bufs[0]));
// 把需要寫入的數據填充到req中
memcpy(req->bufs, bufs, nbufs * sizeof(bufs[0]));
// 需要寫入的buf個數
req->nbufs = nbufs;
// 目前寫入的buf個數,初始化是0
req->write_index = 0;
// 更新流中待寫數據的總長度,就是每個buf的數據大小加起來
stream->write_queue_size += uv__count_bufs(bufs, nbufs);
// 插入待寫隊列
QUEUE_INSERT_TAIL(&stream->write_queue, &req->queue);
/*
stream->connect_req非空說明是作爲客戶端,並且正在建立三次握手,建立成功會置connect_req爲NULL。
這裏非空說明還沒有建立成功或者不是作爲客戶端(不是連接流)。即沒有用到connect_req這個字段。
*/
if (stream->connect_req) {
/* Still connecting, do nothing. */
}
else if (empty_queue) {
// 待寫隊列爲空,則直接觸發寫動作,即操作文件描述符
uv__write(stream);
}
else {
/*
隊列非空,說明往底層寫,uv__write中不一樣會註冊等待可寫事件,所以這裏註冊一下
給流注冊等待可寫事件,觸發的時候,把數據消費掉
*/
uv__io_start(stream->loop, &stream->io_watcher, POLLOUT);
}
return 0;
}
uv_write2的主要邏輯就是封裝一個寫請求,插入到流的待寫隊列。然後根據當前流的情況。看是直接寫入還是等待會再寫入。架構大致如下。
我們繼續看真正的寫操作。
static void uv__write(uv_stream_t* stream) {
struct iovec* iov;
QUEUE* q;
uv_write_t* req;
int iovmax;
int iovcnt;
ssize_t n;
int err;
start:
// 待寫隊列爲空,沒得寫
if (QUEUE_EMPTY(&stream->write_queue))
return;
// 遍歷待寫隊列,把每個節點的數據寫入底層
q = QUEUE_HEAD(&stream->write_queue);
req = QUEUE_DATA(q, uv_write_t, queue);
/*
struct iovec {
ptr_t iov_base; // 數據首地址
size_t iov_len; // 數據長度
};
iovec和bufs結構體的定義一樣
*/
// 轉成iovec格式發送
iov = (struct iovec*) &(req->bufs[req->write_index]);
// 待寫的buf個數,nbufs是總數,write_index是當前已寫的個數
iovcnt = req->nbufs - req->write_index;
// 最多能寫幾個
iovmax = uv__getiovmax();
// 取最小值
if (iovcnt > iovmax)
iovcnt = iovmax;
// 有需要傳遞的描述符
if (req->send_handle) {
// 需要傳遞文件描述符的邏輯,分析unix域的時候再分析
} else { // 單純發送數據,則直接寫
do {
if (iovcnt == 1) {
n = write(uv__stream_fd(stream), iov[0].iov_base, iov[0].iov_len);
} else {
n = writev(uv__stream_fd(stream), iov, iovcnt);
}
} while (n == -1 && errno == EINTR);
}
// 發送出錯
if (n < 0) {
// 發送失敗的邏輯,我們不具體分析
} else {
// 寫成功,n是寫成功的字節數
while (n >= 0) {
// 本次待寫數據的首地址
uv_buf_t* buf = &(req->bufs[req->write_index]);
// 某個buf的數據長度
size_t len = buf->len;
// len如果大於n說明本buf的數據部分被寫入
if ((size_t)n < len) {
// 更新指針,指向下次待發送的數據首地址
buf->base += n;
// 更新待發送數據的長度
buf->len -= n;
// 更新待寫數據的總長度
stream->write_queue_size -= n;
n = 0;
// 設置了一直寫標記,則繼續寫
if (stream->flags & UV_HANDLE_BLOCKING_WRITES) {
goto start;
} else {
// 否則等待可寫事件觸發的時候再寫
break;
}
} else {
// 本buf的數據完成被寫入,更新下一個待寫入的buf位置
req->write_index++;
n -= len;
// 更新待寫數據總長度
stream->write_queue_size -= len;
// 如果寫完了全部buf,觸發回調
if (req->write_index == req->nbufs) {
// 寫完了本請求的數據,做後續處理
uv__write_req_finish(req);
return;
}
}
}
}
// 到這說明數據還沒有完全被寫入,註冊等待可寫事件,等待繼續寫
uv__io_start(stream->loop, &stream->io_watcher, POLLOUT);
return;
error:
// 寫出錯
req->error = err;
uv__write_req_finish(req);
// 撤銷等待可寫事件
uv__io_stop(stream->loop, &stream->io_watcher, POLLOUT);
// 沒有註冊了等待可讀事件,則停掉流
if (!uv__io_active(&stream->io_watcher, POLLIN))
uv__handle_stop(stream);
}
我們看一下寫完一個請求後,libuv如何處理他。邏輯在uv__write_req_finish函數。
// 把buf的數據寫入完成或寫出錯後觸發的回調
static void uv__write_req_finish(uv_write_t* req) {
uv_stream_t* stream = req->handle;
// 移出隊列
QUEUE_REMOVE(&req->queue);
// 寫入成功了
if (req->error == 0) {
/*
bufsml是默認的buf數,如果不夠,則bufs指向新的內存,
然後再儲存數據。兩者不等說明申請了額外的內存,需要free掉
*/
if (req->bufs != req->bufsml)
uv__free(req->bufs);
req->bufs = NULL;
}
// 插入寫完成隊列
QUEUE_INSERT_TAIL(&stream->write_completed_queue, &req->queue);
// 插入pending隊列,在pending階段執行回調
uv__io_feed(stream->loop, &stream->io_watcher);
}
uv__write_req_finish的邏輯比較簡單,就是把節點從待寫隊列中移除。然後插入寫完成隊列。最後把io
觀察者插入pending隊列。在pending節點會知道io觀察者的回調(uv__stream_io)。流模塊的邏輯比較多,今天先分析到這裏。後續繼續分析其他操作。