From 17b80d23ac9e9030a48531b5bd2f5a13ee592947 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 03:11:51 +0000 Subject: [PATCH 1/3] Initial plan From bbbd87a9d55b7cfdb6a5f89f5ee13147966aadc3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 03:26:28 +0000 Subject: [PATCH 2/3] Add hio_sendfile() API for zero-copy file transfer using sendfile(2) Implements: - Platform-abstracted sendfile in nio.c (Linux sendfile, macOS/FreeBSD sendfile, generic pread+write fallback) - hio_sendfile() public API in hloop.h with write queue integration - Sendfile state tracking in hio_t (sendfile_fd, sendfile_offset, sendfile_remain) - SSL fallback using pread + hio_write - Windows fallback in overlapio.c - Integration into HTTP large file handler for unlimited rate mode - hio_write_bufsize accounts for sendfile remaining bytes Co-authored-by: ithewei <26049660+ithewei@users.noreply.github.com> Agent-Logs-Url: https://github.com/ithewei/libhv/sessions/e59504ed-e745-410d-a8ab-e99946d80a3c --- event/hevent.c | 5 +- event/hevent.h | 4 + event/hloop.h | 9 +++ event/nio.c | 156 +++++++++++++++++++++++++++++++++++- event/overlapio.c | 19 +++++ http/server/HttpHandler.cpp | 16 +++- 6 files changed, 205 insertions(+), 4 deletions(-) diff --git a/event/hevent.c b/event/hevent.c index 25bfb8b4f..b1a95f8b1 100644 --- a/event/hevent.c +++ b/event/hevent.c @@ -77,6 +77,9 @@ void hio_init(hio_t* io) { // write_queue_init(&io->write_queue, 4); hrecursive_mutex_init(&io->write_mutex); + io->sendfile_fd = -1; + io->sendfile_offset = 0; + io->sendfile_remain = 0; } void hio_ready(hio_t* io) { @@ -752,7 +755,7 @@ void hio_set_max_write_bufsize(hio_t* io, uint32_t size) { } size_t hio_write_bufsize(hio_t* io) { - return io->write_bufsize; + return io->write_bufsize + io->sendfile_remain; } int hio_read_once (hio_t* io) { diff --git a/event/hevent.h b/event/hevent.h index 1650b22a5..ac3dc3910 100644 --- a/event/hevent.h +++ b/event/hevent.h @@ -175,6 +175,10 @@ struct hio_s { char* hostname; // for hssl_set_sni_hostname // context void* ctx; // for hio_context / hio_set_context + // sendfile + int sendfile_fd; // file descriptor for sendfile, -1 when inactive + off_t sendfile_offset; // current offset in file + size_t sendfile_remain; // remaining bytes to send // private: #if defined(EVENT_POLL) || defined(EVENT_KQUEUE) int event_index[2]; // for poll,kqueue diff --git a/event/hloop.h b/event/hloop.h index 3e268fc3b..b7c950af5 100644 --- a/event/hloop.h +++ b/event/hloop.h @@ -380,6 +380,15 @@ HV_EXPORT int hio_read_remain(hio_t* io); HV_EXPORT int hio_write (hio_t* io, const void* buf, size_t len); HV_EXPORT int hio_sendto (hio_t* io, const void* buf, size_t len, struct sockaddr* addr); +// NOTE: hio_sendfile uses zero-copy sendfile(2) on Linux, sendfile(2) on macOS/FreeBSD. +// Falls back to read+write for SSL connections and unsupported platforms. +// @param in_fd: file descriptor of the file to send from +// @param offset: starting offset in the file +// @param length: number of bytes to send +// @return 0 on success (async operation started), -1 on error +// hwrite_cb is called as data is sent. When complete, write_queue is empty. +HV_EXPORT int hio_sendfile(hio_t* io, int in_fd, off_t offset, size_t length); + // NOTE: hio_close is thread-safe, hio_close_async will be called actually in other thread. // hio_del(io, HV_RDWR) => close => hclose_cb HV_EXPORT int hio_close (hio_t* io); diff --git a/event/nio.c b/event/nio.c index 337299ac0..cbcb54481 100644 --- a/event/nio.c +++ b/event/nio.c @@ -7,6 +7,15 @@ #include "herr.h" #include "hthread.h" +#ifdef OS_LINUX +#include +#endif +#if defined(OS_DARWIN) || defined(OS_FREEBSD) +#include +#include +#include +#endif + static void __connect_timeout_cb(htimer_t* timer) { hio_t* io = (hio_t*)timer->privdata; if (io) { @@ -304,6 +313,50 @@ static int __nio_write(hio_t* io, const void* buf, int len, struct sockaddr* add return nwrite; } +// Platform-abstracted sendfile: returns bytes sent, -1 on error +static ssize_t __nio_sendfile_sys(int out_fd, int in_fd, off_t* offset, size_t count) { +#ifdef OS_LINUX + return sendfile(out_fd, in_fd, offset, count); +#elif defined(OS_DARWIN) + off_t len = count; + int ret = sendfile(in_fd, out_fd, *offset, &len, NULL, 0); + if (ret == 0 || (ret == -1 && errno == EAGAIN && len > 0)) { + *offset += len; + return (ssize_t)len; + } + return -1; +#elif defined(OS_FREEBSD) + off_t sbytes = 0; + int ret = sendfile(in_fd, out_fd, *offset, count, NULL, &sbytes, 0); + if (ret == 0 || (ret == -1 && errno == EAGAIN && sbytes > 0)) { + *offset += sbytes; + return (ssize_t)sbytes; + } + return -1; +#else + // Fallback: read + write + char buf[65536]; + size_t to_read = count < sizeof(buf) ? count : sizeof(buf); + ssize_t nread = pread(in_fd, buf, to_read, *offset); + if (nread <= 0) return nread; + ssize_t nwrite = write(out_fd, buf, nread); + if (nwrite > 0) { + *offset += nwrite; + } + return nwrite; +#endif +} + +// Try sendfile for the current io, called with write_mutex held +// Returns: > 0 bytes sent, 0 if nothing sent, < 0 on error +static ssize_t __nio_sendfile(hio_t* io) { + ssize_t nsent = __nio_sendfile_sys(io->fd, io->sendfile_fd, &io->sendfile_offset, io->sendfile_remain); + if (nsent > 0) { + io->sendfile_remain -= nsent; + } + return nsent; +} + static void nio_read(hio_t* io) { // printd("nio_read fd=%d\n", io->fd); void* buf; @@ -361,6 +414,10 @@ static void nio_write(hio_t* io) { hrecursive_mutex_lock(&io->write_mutex); write: if (write_queue_empty(&io->write_queue)) { + // Check for pending sendfile after write queue is drained + if (io->sendfile_fd >= 0 && io->sendfile_remain > 0) { + goto do_sendfile; + } hrecursive_mutex_unlock(&io->write_mutex); if (io->close) { io->close = 0; @@ -407,6 +464,32 @@ static void nio_write(hio_t* io) { } hrecursive_mutex_unlock(&io->write_mutex); return; + +do_sendfile: + { + ssize_t nsent = __nio_sendfile(io); + if (nsent < 0) { + err = socket_errno(); + if (err == EAGAIN || err == EINTR) { + hrecursive_mutex_unlock(&io->write_mutex); + return; + } + io->error = err; + goto write_error; + } + if (nsent == 0 && (io->io_type & HIO_TYPE_SOCK_STREAM)) { + goto disconnect; + } + hrecursive_mutex_unlock(&io->write_mutex); + if (nsent > 0) { + __write_cb(io, NULL, nsent); + } + if (io->sendfile_remain == 0) { + io->sendfile_fd = -1; + } + return; + } + write_error: disconnect: hrecursive_mutex_unlock(&io->write_mutex); @@ -426,9 +509,10 @@ static void hio_handle_events(hio_t* io) { } if ((io->events & HV_WRITE) && (io->revents & HV_WRITE)) { - // NOTE: del HV_WRITE, if write_queue empty + // NOTE: del HV_WRITE, if write_queue empty and no pending sendfile hrecursive_mutex_lock(&io->write_mutex); - if (write_queue_empty(&io->write_queue)) { + if (write_queue_empty(&io->write_queue) && + !(io->sendfile_fd >= 0 && io->sendfile_remain > 0)) { hio_del(io, HV_WRITE); } hrecursive_mutex_unlock(&io->write_mutex); @@ -590,6 +674,72 @@ int hio_sendto (hio_t* io, const void* buf, size_t len, struct sockaddr* addr) { return hio_write4(io, buf, len, addr ? addr : io->peeraddr); } +int hio_sendfile (hio_t* io, int in_fd, off_t offset, size_t length) { + if (io->closed) { + hloge("hio_sendfile called but fd[%d] already closed!", io->fd); + return -1; + } + if (length == 0) return 0; + + // SSL: fall back to read + hio_write (sendfile cannot bypass SSL encryption) + if (io->io_type == HIO_TYPE_SSL) { + char buf[65536]; + off_t cur_offset = offset; + size_t remaining = length; + while (remaining > 0) { + size_t to_read = remaining < sizeof(buf) ? remaining : sizeof(buf); + ssize_t nread = pread(in_fd, buf, to_read, cur_offset); + if (nread <= 0) { + hloge("hio_sendfile pread error!"); + return -1; + } + int nwrite = hio_write(io, buf, nread); + if (nwrite < 0) return nwrite; + cur_offset += nread; + remaining -= nread; + } + return 0; + } + + hrecursive_mutex_lock(&io->write_mutex); + + io->sendfile_fd = in_fd; + io->sendfile_offset = offset; + io->sendfile_remain = length; + + // If write queue is empty, try sendfile immediately + if (write_queue_empty(&io->write_queue)) { + ssize_t nsent = __nio_sendfile(io); + if (nsent < 0) { + int err = socket_errno(); + if (err != EAGAIN && err != EINTR) { + io->error = err; + io->sendfile_fd = -1; + hrecursive_mutex_unlock(&io->write_mutex); + hio_close_async(io); + return -1; + } + } + if (nsent > 0) { + hrecursive_mutex_unlock(&io->write_mutex); + __write_cb(io, NULL, nsent); + if (io->sendfile_remain == 0) { + io->sendfile_fd = -1; + return 0; + } + hrecursive_mutex_lock(&io->write_mutex); + } + } + + // If still remaining, register for writable event + if (io->sendfile_remain > 0) { + hio_add(io, hio_handle_events, HV_WRITE); + } + + hrecursive_mutex_unlock(&io->write_mutex); + return 0; +} + int hio_close (hio_t* io) { if (io->closed) return 0; if (io->destroy == 0 && hv_gettid() != io->loop->tid) { @@ -611,6 +761,8 @@ int hio_close (hio_t* io) { return 0; } io->closed = 1; + io->sendfile_fd = -1; + io->sendfile_remain = 0; hrecursive_mutex_unlock(&io->write_mutex); hio_done(io); diff --git a/event/overlapio.c b/event/overlapio.c index 61f24937f..d2fd1099a 100644 --- a/event/overlapio.c +++ b/event/overlapio.c @@ -387,6 +387,25 @@ int hio_sendto (hio_t* io, const void* buf, size_t len, struct sockaddr* addr) { return hio_write4(io, buf, len, addr ? addr : io->peeraddr); } +int hio_sendfile (hio_t* io, int in_fd, off_t offset, size_t length) { + if (io->closed) return -1; + if (length == 0) return 0; + // Fallback: read + hio_write + char buf[65536]; + off_t cur_offset = offset; + size_t remaining = length; + while (remaining > 0) { + size_t to_read = remaining < sizeof(buf) ? remaining : sizeof(buf); + ssize_t nread = _lseeki64(in_fd, cur_offset, SEEK_SET) >= 0 ? _read(in_fd, buf, (unsigned int)to_read) : -1; + if (nread <= 0) return -1; + int nwrite = hio_write(io, buf, nread); + if (nwrite < 0) return nwrite; + cur_offset += nread; + remaining -= nread; + } + return 0; +} + int hio_close (hio_t* io) { if (io->closed) return 0; io->closed = 1; diff --git a/http/server/HttpHandler.cpp b/http/server/HttpHandler.cpp index 005967328..a8dd9680a 100644 --- a/http/server/HttpHandler.cpp +++ b/http/server/HttpHandler.cpp @@ -659,11 +659,25 @@ int HttpHandler::defaultLargeFileHandler(const std::string &filepath) { // forbidden to send large file resp->content_length = 0; resp->status_code = HTTP_STATUS_FORBIDDEN; + } else if (service->limit_rate < 0 && file->fp) { + // unlimited: use zero-copy sendfile if possible + int filefd = fileno(file->fp); + size_t length = resp->content_length; + writer->EndHeaders(); + writer->onwrite = [this](HBuf* buf) { + if (writer->isWriteComplete()) { + resp->content_length = 0; + writer->End(); + closeFile(); + } + }; + hio_sendfile(io, filefd, 0, length); + return HTTP_STATUS_UNFINISHED; } else { size_t bufsize = 40960; // 40K file->buf.resize(bufsize); if (service->limit_rate < 0) { - // unlimited: sendFile when writable + // unlimited: sendFile when writable (fallback when fileno unavailable) writer->onwrite = [this](HBuf* buf) { if (writer->isWriteComplete()) { sendFile(); From 81bda76e66b8b76f217782b565f17ce460c7e586 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Mar 2026 03:33:20 +0000 Subject: [PATCH 3/3] Address code review: add fd validation, fix partial write handling, improve error messages - Add in_fd validation in hio_sendfile() for both nio.c and overlapio.c - Fix generic sendfile fallback to handle partial writes correctly - Check sendfile_remain under mutex before unlocking in nio_write - Improve SSL fallback with better EOF/error distinction - Add fileno() validation in HTTP handler - Improve Windows fallback with separate seek/read error handling Co-authored-by: ithewei <26049660+ithewei@users.noreply.github.com> Agent-Logs-Url: https://github.com/ithewei/libhv/sessions/e59504ed-e745-410d-a8ab-e99946d80a3c --- event/nio.c | 40 ++++++++++++++++++++++++++++--------- event/overlapio.c | 10 +++++++--- http/server/HttpHandler.cpp | 6 +++--- 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/event/nio.c b/event/nio.c index cbcb54481..92259343c 100644 --- a/event/nio.c +++ b/event/nio.c @@ -334,16 +334,26 @@ static ssize_t __nio_sendfile_sys(int out_fd, int in_fd, off_t* offset, size_t c } return -1; #else - // Fallback: read + write + // Fallback: pread + write (sends one chunk per call to integrate with event loop) char buf[65536]; size_t to_read = count < sizeof(buf) ? count : sizeof(buf); ssize_t nread = pread(in_fd, buf, to_read, *offset); if (nread <= 0) return nread; - ssize_t nwrite = write(out_fd, buf, nread); - if (nwrite > 0) { - *offset += nwrite; + ssize_t total_written = 0; + while (total_written < nread) { + ssize_t nwrite = write(out_fd, buf + total_written, nread - total_written); + if (nwrite < 0) { + if (total_written > 0) { + *offset += total_written; + return total_written; + } + return -1; + } + if (nwrite == 0) break; + total_written += nwrite; } - return nwrite; + *offset += total_written; + return total_written; #endif } @@ -480,11 +490,12 @@ static void nio_write(hio_t* io) { if (nsent == 0 && (io->io_type & HIO_TYPE_SOCK_STREAM)) { goto disconnect; } + int complete = (io->sendfile_remain == 0); hrecursive_mutex_unlock(&io->write_mutex); if (nsent > 0) { __write_cb(io, NULL, nsent); } - if (io->sendfile_remain == 0) { + if (complete) { io->sendfile_fd = -1; } return; @@ -679,9 +690,15 @@ int hio_sendfile (hio_t* io, int in_fd, off_t offset, size_t length) { hloge("hio_sendfile called but fd[%d] already closed!", io->fd); return -1; } + if (in_fd < 0) { + hloge("hio_sendfile invalid file descriptor: %d", in_fd); + return -1; + } if (length == 0) return 0; // SSL: fall back to read + hio_write (sendfile cannot bypass SSL encryption) + // NOTE: hio_write is non-blocking and queues data internally, + // so this won't block the event loop even for large files. if (io->io_type == HIO_TYPE_SSL) { char buf[65536]; off_t cur_offset = offset; @@ -689,10 +706,14 @@ int hio_sendfile (hio_t* io, int in_fd, off_t offset, size_t length) { while (remaining > 0) { size_t to_read = remaining < sizeof(buf) ? remaining : sizeof(buf); ssize_t nread = pread(in_fd, buf, to_read, cur_offset); - if (nread <= 0) { - hloge("hio_sendfile pread error!"); + if (nread < 0) { + hloge("hio_sendfile pread error: %s", strerror(errno)); return -1; } + if (nread == 0) { + hlogw("hio_sendfile: unexpected EOF at offset %lld", (long long)cur_offset); + break; + } int nwrite = hio_write(io, buf, nread); if (nwrite < 0) return nwrite; cur_offset += nread; @@ -721,9 +742,10 @@ int hio_sendfile (hio_t* io, int in_fd, off_t offset, size_t length) { } } if (nsent > 0) { + int complete = (io->sendfile_remain == 0); hrecursive_mutex_unlock(&io->write_mutex); __write_cb(io, NULL, nsent); - if (io->sendfile_remain == 0) { + if (complete) { io->sendfile_fd = -1; return 0; } diff --git a/event/overlapio.c b/event/overlapio.c index d2fd1099a..da563fe71 100644 --- a/event/overlapio.c +++ b/event/overlapio.c @@ -389,15 +389,19 @@ int hio_sendto (hio_t* io, const void* buf, size_t len, struct sockaddr* addr) { int hio_sendfile (hio_t* io, int in_fd, off_t offset, size_t length) { if (io->closed) return -1; + if (in_fd < 0) return -1; if (length == 0) return 0; - // Fallback: read + hio_write + // NOTE: Windows fallback uses read + hio_write. + // hio_write is non-blocking (queues via IOCP), so this won't block. char buf[65536]; off_t cur_offset = offset; size_t remaining = length; while (remaining > 0) { size_t to_read = remaining < sizeof(buf) ? remaining : sizeof(buf); - ssize_t nread = _lseeki64(in_fd, cur_offset, SEEK_SET) >= 0 ? _read(in_fd, buf, (unsigned int)to_read) : -1; - if (nread <= 0) return -1; + if (_lseeki64(in_fd, cur_offset, SEEK_SET) < 0) return -1; + ssize_t nread = _read(in_fd, buf, (unsigned int)to_read); + if (nread < 0) return -1; + if (nread == 0) break; // EOF int nwrite = hio_write(io, buf, nread); if (nwrite < 0) return nwrite; cur_offset += nread; diff --git a/http/server/HttpHandler.cpp b/http/server/HttpHandler.cpp index a8dd9680a..f17f69f7b 100644 --- a/http/server/HttpHandler.cpp +++ b/http/server/HttpHandler.cpp @@ -659,8 +659,8 @@ int HttpHandler::defaultLargeFileHandler(const std::string &filepath) { // forbidden to send large file resp->content_length = 0; resp->status_code = HTTP_STATUS_FORBIDDEN; - } else if (service->limit_rate < 0 && file->fp) { - // unlimited: use zero-copy sendfile if possible + } else if (service->limit_rate < 0 && file->fp && fileno(file->fp) >= 0) { + // unlimited: use zero-copy sendfile int filefd = fileno(file->fp); size_t length = resp->content_length; writer->EndHeaders(); @@ -677,7 +677,7 @@ int HttpHandler::defaultLargeFileHandler(const std::string &filepath) { size_t bufsize = 40960; // 40K file->buf.resize(bufsize); if (service->limit_rate < 0) { - // unlimited: sendFile when writable (fallback when fileno unavailable) + // unlimited: sendFile when writable writer->onwrite = [this](HBuf* buf) { if (writer->isWriteComplete()) { sendFile();