Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion event/hevent.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 4 additions & 0 deletions event/hevent.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions event/hloop.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hio_sendfile doc comment is misleading/incomplete for consumers of hwrite_cb: sendfile bytes are not part of the write queue, so completion is not just “write_queue is empty”; it also requires sendfile_remain == 0. Also, the buf pointer passed to hwrite_cb can be NULL for sendfile writes (__write_cb(io, NULL, nsent)), so callbacks must not assume buf != NULL. Please clarify these semantics in the API comment.

Suggested change
// hwrite_cb is called as data is sent. When complete, write_queue is empty.
// hwrite_cb is called as data is sent. For sendfile transfers, the bytes being
// sent are not queued in write_queue; completion is when write_queue is empty
// AND the internal sendfile_remain counter has reached 0.
// NOTE: For sendfile writes, hwrite_cb may be invoked with buf == NULL
// (e.g. __write_cb(io, NULL, nsent)); callbacks must not assume buf != NULL
// and should rely on the length argument (e.g. nwrite) rather than buf contents.

Copilot uses AI. Check for mistakes.
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);
Expand Down
178 changes: 176 additions & 2 deletions event/nio.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@
#include "herr.h"
#include "hthread.h"

#ifdef OS_LINUX
#include <sys/sendfile.h>
#endif
#if defined(OS_DARWIN) || defined(OS_FREEBSD)
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/uio.h>
#endif

static void __connect_timeout_cb(htimer_t* timer) {
hio_t* io = (hio_t*)timer->privdata;
if (io) {
Expand Down Expand Up @@ -304,6 +313,60 @@ 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: 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 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;
}
*offset += total_written;
return total_written;
#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;
Expand Down Expand Up @@ -361,6 +424,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;
Expand Down Expand Up @@ -407,6 +474,33 @@ 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;
Comment on lines +490 to +491
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the sendfile path, treating nsent == 0 as a stream disconnect is not necessarily correct: sendfile(2) (and the pread+write fallback) can return 0 on EOF, even when the socket is still healthy. This currently jumps to disconnect and closes the connection, and it also prevents any completion notification when EOF occurs. Consider handling nsent == 0 as completion (set sendfile_remain = 0 / clear sendfile_fd) or as an explicit sendfile error (set io->error) rather than a socket disconnect.

Suggested change
if (nsent == 0 && (io->io_type & HIO_TYPE_SOCK_STREAM)) {
goto disconnect;
/* Treat nsent == 0 as sendfile completion (EOF on file), not as a socket disconnect. */
if (nsent == 0) {
io->sendfile_remain = 0;

Copilot uses AI. Check for mistakes.
}
int complete = (io->sendfile_remain == 0);
hrecursive_mutex_unlock(&io->write_mutex);
if (nsent > 0) {
__write_cb(io, NULL, nsent);
}
if (complete) {
io->sendfile_fd = -1;
}
return;
}

write_error:
disconnect:
hrecursive_mutex_unlock(&io->write_mutex);
Expand All @@ -426,9 +520,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);
Expand Down Expand Up @@ -590,6 +685,83 @@ 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 (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;
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: %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;
remaining -= nread;
}
Comment on lines +699 to +721
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SSL fallback reads the entire [offset, offset+length) synchronously in a tight loop and calls hio_write repeatedly. This can block the event-loop thread on disk I/O and can enqueue up to length bytes into the write queue (triggering max_write_bufsize overflow / unexpected connection close) which defeats the large-file streaming goal. Consider implementing the SSL path as incremental chunking driven by HV_WRITE (similar to the non-SSL sendfile state machine), or at least stop reading once the write queue reaches a threshold and resume on write-complete events.

Suggested change
// 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;
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: %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;
remaining -= nread;
}
// SSL: fall back to read + hio_write (sendfile cannot bypass SSL encryption).
// To avoid blocking the event-loop thread and overfilling the write queue,
// limit how much data we read and enqueue in a single call.
if (io->io_type == HIO_TYPE_SSL) {
char buf[65536];
off_t cur_offset = offset;
size_t remaining = length;
/* Cap the amount of data we enqueue in one call to hio_sendfile over SSL.
* This bounds both disk I/O time and queued bytes, even if 'length' is large. */
const size_t MAX_SSL_SENDFILE_ENQUEUE = 1024 * 1024; /* 1 MiB per call */
size_t total_enqueued = 0;
while (remaining > 0 && total_enqueued < MAX_SSL_SENDFILE_ENQUEUE) {
size_t to_read = remaining < sizeof(buf) ? remaining : sizeof(buf);
if (to_read > (MAX_SSL_SENDFILE_ENQUEUE - total_enqueued)) {
to_read = MAX_SSL_SENDFILE_ENQUEUE - total_enqueued;
}
ssize_t nread;
do {
nread = pread(in_fd, buf, to_read, cur_offset);
} while (nread < 0 && errno == EINTR);
if (nread < 0) {
if (errno == EAGAIN) {
/* Caller can retry later; don't treat as fatal here. */
break;
}
hloge("hio_sendfile pread error: %s", strerror(errno));
return -1;
}
if (nread == 0) {
/* Reached EOF before sending the requested length. */
hlogw("hio_sendfile: unexpected EOF at offset %lld", (long long)cur_offset);
break;
}
int nwrite = hio_write(io, buf, (size_t)nread);
if (nwrite < 0) {
return nwrite;
}
cur_offset += nread;
remaining -= (size_t)nread;
total_enqueued += (size_t)nread;
}
/* If 'remaining' is still > 0, the caller may invoke hio_sendfile again
* with an updated offset/length to continue sending. */

Copilot uses AI. Check for mistakes.
return 0;
}

hrecursive_mutex_lock(&io->write_mutex);

io->sendfile_fd = in_fd;
io->sendfile_offset = offset;
io->sendfile_remain = length;

Comment on lines +725 to +730
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hio_sendfile overwrites io->sendfile_fd/offset/remain unconditionally. If a caller invokes hio_sendfile again while a previous sendfile is still active, the in-flight transfer state will be corrupted. Please add a guard that returns an error (e.g., busy) when sendfile_fd >= 0 && sendfile_remain > 0, or explicitly cancel/finish the previous sendfile before starting a new one.

Copilot uses AI. Check for mistakes.
// 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) {
int complete = (io->sendfile_remain == 0);
hrecursive_mutex_unlock(&io->write_mutex);
__write_cb(io, NULL, nsent);
if (complete) {
io->sendfile_fd = -1;
Comment on lines +746 to +749
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

io->sendfile_fd = -1; is written after releasing write_mutex (both in nio_write completion handling and in the immediate-send path here). Since hio_sendfile/write code is otherwise synchronized via write_mutex, updating sendfile state outside the lock introduces a data race with other threads calling hio_sendfile/hio_write_bufsize/event logic. Consider setting/clearing sendfile_fd (and any related fields) while holding write_mutex, or making these fields atomic if they must be touched lock-free.

Suggested change
hrecursive_mutex_unlock(&io->write_mutex);
__write_cb(io, NULL, nsent);
if (complete) {
io->sendfile_fd = -1;
if (complete) {
io->sendfile_fd = -1;
}
hrecursive_mutex_unlock(&io->write_mutex);
__write_cb(io, NULL, nsent);
if (complete) {

Copilot uses AI. Check for mistakes.
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) {
Expand All @@ -611,6 +783,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);
Expand Down
23 changes: 23 additions & 0 deletions event/overlapio.c
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,29 @@ 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 (in_fd < 0) return -1;
if (length == 0) return 0;
// 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);
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;
remaining -= nread;
}
return 0;
Comment on lines +390 to +410
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Windows implementation loops until length is fully read and calls hio_write for every 64KB chunk. On IOCP this allocates a new buffer per queued send (see hio_write4), so this can rapidly consume large amounts of memory for big files and can also block the loop thread on synchronous _read calls. This should be reworked to be incremental (one chunk per writable/completion event) with backpressure similar to the Unix implementation, instead of enqueueing the entire file in one call.

Copilot uses AI. Check for mistakes.
}

int hio_close (hio_t* io) {
if (io->closed) return 0;
io->closed = 1;
Expand Down
14 changes: 14 additions & 0 deletions http/server/HttpHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,20 @@ 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 && fileno(file->fp) >= 0) {
// unlimited: use zero-copy sendfile
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);
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hio_sendfile(io, ...) return value is ignored. If it fails (e.g., io already closed / invalid fd), this handler still returns HTTP_STATUS_UNFINISHED, leaves the file open, and relies on onwrite that may never fire to call closeFile()/writer->End(). Handle hio_sendfile errors (set an appropriate HTTP error, closeFile(), and end/close the writer) before returning.

Suggested change
hio_sendfile(io, filefd, 0, length);
int rv = hio_sendfile(io, filefd, 0, length);
if (rv != 0) {
// sendfile failed synchronously: clean up and report error
resp->status_code = HTTP_STATUS_INTERNAL_SERVER_ERROR;
resp->content_length = 0;
closeFile();
writer->End();
return resp->status_code;
}

Copilot uses AI. Check for mistakes.
return HTTP_STATUS_UNFINISHED;
} else {
size_t bufsize = 40960; // 40K
file->buf.resize(bufsize);
Expand Down
Loading