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,暂时先分析这么多了。。。

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值