hio_write
之前的部落格已經提到傳送資料要比接收資料難,因為傳送資料是主動的,接收資料是被動的。而且因為libhv採用的是level trigger,因此只有在需要時才關注寫事件,否則就會造成busy loop。原因可以參考上一篇部落格。
int hio_write (hio_t* io, const void* buf, size_t len) {
//判斷io是否處於關閉狀態
if (io->closed) {
hloge("hio_write called but fd[%d] already closed!", io->fd);
return -1;
}
int nwrite = 0;
//判斷寫佇列是否為空,如果不為空,不能直接寫,要先處理寫佇列中的資料,否則會照成資料亂序
//所以當佇列中有資料時,直接將本次的資料加入到佇列尾
if (write_queue_empty(&io->write_queue)) {
try_write:
//傳送資料
nwrite = __nio_write(io, buf, len);
//printd("write retval=%d\n", nwrite);
if (nwrite < 0) {
//如果是EAGAIN,那麼需要之後再嘗試傳送,所以這裡先入佇列
if (socket_errno() == EAGAIN) {
nwrite = 0;
hlogw("try_write failed, enqueue!");
goto enqueue;
}
else {
// 出現錯誤,關閉連線
io->error = socket_errno();
goto write_error;
}
}
if (nwrite == 0) {
goto disconnect;
}
__write_cb(io, buf, nwrite);
//如果一次性傳送完成,直接就返回,不需要使用寫佇列
if (nwrite == len) {
//goto write_done;
return nwrite;
}
enqueue:
//註冊寫事件
hio_add(io, hio_handle_events, HV_WRITE);
}
//如果沒有一次性傳送完成,需要將資料加入寫佇列
if (nwrite < len) {
offset_buf_t rest;
rest.len = len; //資料總長度
rest.offset = nwrite; //偏移量
// NOTE: free in nio_write
HV_ALLOC(rest.base, rest.len);
memcpy(rest.base, buf, rest.len);
if (io->write_queue.maxsize == 0) {
write_queue_init(&io->write_queue, 4);
}
//加入佇列尾
write_queue_push_back(&io->write_queue, &rest);
}
return nwrite;
write_error:
disconnect:
hio_close(io);
return nwrite;
}
hio_write中有幾個比較重要的地方,第一個就是寫佇列,當寫資料比較多時,可能無法一次寫完,又因為是非阻塞套接字,所以在傳送緩衝區沒有空間時會直接返回(如果是阻塞套接字會一直阻塞等待,直到傳送完成),這時候即使再嘗試寫剩下的資料,很大概率會返回EAGAIN,之前就提到過,像libhv這樣的網路庫,應該只能阻塞在loop等待事件發生的介面(例如epoll_wait),而不能阻塞在read和write這些io處理介面,所以這裡不能等待寫完所有的資料,只能先將資料儲存到寫佇列中,並將寫事件新增到io事件監視器。在分析IO事件監視器對寫事件的處理時,先看下寫佇列,在前面的部落格巨集定義黑魔法中提到過一點佇列的實現。使用佇列,先進先出,保證傳送資料不會出現亂序。
相關的定義,佇列的細節可以參考程式碼,比較簡單
struct write_queue write_queue; // for hwrite
QUEUE_DECL(offset_buf_t, write_queue);
#define QUEUE_DECL(type, qtype) \
struct qtype { \
type* ptr; \
size_t size; \
size_t maxsize;\
size_t _offset;\
};
typedef struct offset_buf_s {
char* base;
size_t len;
size_t offset;
#ifdef __cplusplus
offset_buf_s() {
base = NULL;
len = offset = 0;
}
offset_buf_s(void* data, size_t len) {
this->base = (char*)data;
this->len = len;
}
#endif
} offset_buf_t;
write_queue就是一個內容為offset_buf_s的佇列,offset_buf_s是用來存放未寫完的資料的,也比較簡單。
hio_write中還有一個值得注意的地方是__write_cb,這個在前面的keepalive那篇部落格中提到過
static void __write_cb(hio_t* io, const void* buf, int writebytes) {
// printd("< %.*s\n", writebytes, buf);
if (io->keepalive_timer) {
htimer_reset(io->keepalive_timer);
}
if (io->write_cb) {
// printd("write_cb------\n");
io->write_cb(io, buf, writebytes);
// printd("write_cb======\n");
}
}
在hio_write中會更新keepalive定時器,而且如果有寫回撥函式,會在這裡呼叫回撥函式,關於該回撥的使用後面分析nio_write時會繼續說明。
如上分析,當呼叫hio_write傳送資料量比較大,無法一次寫完成時,需要關注寫事件。hio_add(io, hio_handle_events, HV_WRITE)。根據上一篇部落格的分析,可以得知,等到有足夠的空間可以寫時,寫事件會觸發,當寫事件觸發時,hio_handle_events被呼叫,這個介面前面的部落格已經多次提到了,但每次關注的地方都不一樣。
static void hio_handle_events(hio_t* io) {
if ((io->events & HV_READ) && (io->revents & HV_READ)) {
if (io->accept) {
nio_accept(io);
}
else {
nio_read(io);
}
}
if ((io->events & HV_WRITE) && (io->revents & HV_WRITE)) {
// NOTE: del HV_WRITE, if write_queue empty
if (write_queue_empty(&io->write_queue)) {
iowatcher_del_event(io->loop, io->fd, HV_WRITE);
io->events &= ~HV_WRITE;
}
if (io->connect) {
// NOTE: connect just do once
// ONESHOT
io->connect = 0;
nio_connect(io);
}
else {
nio_write(io);
}
}
io->revents = 0;
}
本次關注的是io的寫,nio_write(io);
static void nio_write(hio_t* io) {
//printd("nio_write fd=%d\n", io->fd);
int nwrite = 0;
write:
//如果佇列為空,判斷是否需要關閉io
if (write_queue_empty(&io->write_queue)) {
if (io->close) {
io->close = 0;
hio_close(io);
}
return;
}
//從寫佇列中拿資料
offset_buf_t* pbuf = write_queue_front(&io->write_queue);
//找到還未寫的資料
char* buf = pbuf->base + pbuf->offset;
int len = pbuf->len - pbuf->offset;
nwrite = __nio_write(io, buf, len);
//printd("write retval=%d\n", nwrite);
if (nwrite < 0) {
if (socket_errno() == EAGAIN) {
//goto write_done;
return;
}
else {
io->error = socket_errno();
// perror("write");
goto write_error;
}
}
if (nwrite == 0) {
goto disconnect;
}
__write_cb(io, buf, nwrite);
pbuf->offset += nwrite;
//傳送完成,從佇列中刪除
if (nwrite == len) {
HV_FREE(pbuf->base);
write_queue_pop_front(&io->write_queue);
// write next
goto write;
}
return;
write_error:
disconnect:
hio_close(io);
}
最開始判斷佇列是否為空,如果為空,會處理close標誌。這裡的原因我們可以看下hio_close的實現,下面的程式碼只包含相關的部分。
int hio_close (hio_t* io) {
if (io->closed) return 0;
if (!write_queue_empty(&io->write_queue) && io->error == 0 && io->close == 0) {
io->close = 1;
hlogw("write_queue not empty, close later.");
int timeout_ms = io->close_timeout ? io->close_timeout : HIO_DEFAULT_CLOSE_TIMEOUT;
io->close_timer = htimer_add(io->loop, __close_timeout_cb, timeout_ms, 1);
io->close_timer->privdata = io;
return 0;
}
在hio_close中,會判斷寫佇列是否為空,如果不為空,只設定了close標誌,但實際上並沒有真正關閉io,這裡其實是在等待未寫完的資料繼續寫完。所以在nio_write中,佇列為空時,判斷close標誌,如果已經關閉了,在hio_write中再次呼叫hio_close關閉該io。當然,在hio_close中,設定了一個定時器,如果長時間未關閉,定時器觸發,呼叫__close_timeout_cb
static void __close_timeout_cb(htimer_t* timer) {
hio_t* io = (hio_t*)timer->privdata;
if (io) {
char localaddrstr[SOCKADDR_STRLEN] = {0};
char peeraddrstr[SOCKADDR_STRLEN] = {0};
hlogw("close timeout [%s] <=> [%s]",
SOCKADDR_STR(io->localaddr, localaddrstr),
SOCKADDR_STR(io->peeraddr, peeraddrstr));
io->error = ETIMEDOUT;
hio_close(io);
}
}
在該回撥中,設定了io的error,所以再次呼叫hio_close,強制關閉io。
再回到nio_write,這個介面就是從寫佇列中拿到未寫完的資料,然後繼續寫。在這個介面裡也呼叫了__write_cb(io, buf, nwrite)。因為在libhv中暫時沒發現使用寫回撥的例子,所以在這裡我使用在muduo中看到的一個用法,使用寫回撥控制傳送流量,在muduo中,該功能叫做WriteCompleteCallback,是在每次寫完成後呼叫的回撥函式。使用該回撥函式,可以有效的控制傳送方傳送資料的速度。例如,一個伺服器要向一個客戶端持續傳送一些資料,可以在傳送完一條資料後,也就是在WriteCompleteCallback中繼續傳送下一條資料,避免出現傳送方傳送資料的速度高於對方接收資料的速度,造成本地記憶體堆積。
當資料傳送完成後,需要從io事件監視器中刪除對寫事件關注,該功能也是在上面的hio_handle_events中實現的
if (write_queue_empty(&io->write_queue)) {
iowatcher_del_event(io->loop, io->fd, HV_WRITE);
io->events &= ~HV_WRITE;
}
等寫完成後,由於沒有刪除對寫事件的關注,所以寫事件會繼續觸發(原因參考上一篇部落格),在hio_handle_events回撥中判斷寫佇列為空,刪除對寫事件的關注。
ok,暫時先分析這麼多了。。。