//go:build linux // Package handler 提供 HTTP 请求处理器,包括路由、静态文件服务和零拷贝传输。 // // 该文件包含 Linux 平台完整的 sendfile 实现(零拷贝)。 // // 作者:xfy package handler import ( "net" "os" "syscall" "time" "github.com/valyala/fasthttp" ) const ( // sendfileMaxRetries sendfile 系统调用最大重试次数。 // 用于处理 EAGAIN/EWOULDBLOCK 等临时性错误。 sendfileMaxRetries = 100 // sendfileRetryDelay sendfile 重试间隔等待时间。 // 短暂等待以允许 socket 缓冲区恢复。 sendfileRetryDelay = 1 * time.Millisecond ) // SendFile 零拷贝文件传输。 // // 大文件使用系统调用直接从文件传输到 socket,避免用户空间拷贝, // 从而减少 CPU 和内存开销,提升传输性能。 // // 参数: // - ctx: fasthttp 请求上下文,用于获取底层连接 // - file: 要传输的文件对象 // - offset: 文件起始偏移量(字节) // - length: 传输长度(字节),-1 表示传输到文件末尾 // // 返回值: // - error: 传输过程中的错误 func SendFile(ctx *fasthttp.RequestCtx, file *os.File, offset, length int64) error { // 小文件使用普通 io.Copy if length < MinSendfileSize { return copyFile(ctx, file, offset, length) } // 尝试获取 socket 文件描述符 conn := getNetConn(ctx) if conn == nil { return copyFile(ctx, file, offset, length) } // Linux 平台使用 sendfile 系统调用 err := linuxSendfile(conn, file.Fd(), offset, length) if err != nil { // EPIPE/ECONNRESET 表示客户端已断开,不应 fallback // 因为 HTTP 头可能已发送,fallback 会造成响应混乱 if err == syscall.EPIPE || err == syscall.ECONNRESET { return err // 直接返回错误,不 fallback } // 其他错误尝试 fallback 到 io.Copy return copyFile(ctx, file, offset, length) } return nil } // linuxSendfile Linux sendfile 系统调用。 // // 使用 Linux 特有的 sendfile 系统调用实现零拷贝传输。 // 正确处理临时错误(EAGAIN、EINTR)和连接断开(EPIPE、ECONNRESET)。 // // 参数: // - conn: 目标网络连接(必须是 TCPConn 或 UnixConn) // - fileFd: 源文件的文件描述符 // - offset: 文件起始偏移量(未使用,由内核自动处理) // - length: 传输长度(字节) // // 返回值: // - error: 传输过程中的错误,nil 表示成功 func linuxSendfile(conn net.Conn, fileFd uintptr, _, length int64) error { socketFile, err := getSocketFile(conn) if err != nil { return err } defer func() { _ = socketFile.Close() }() socketFd := socketFile.Fd() // Linux sendfile: sendfile(out_fd, in_fd, offset, count) var sent int64 remain := length retries := 0 for remain > 0 { n, err := syscall.Sendfile(int(socketFd), int(fileFd), nil, int(remain)) if err != nil { // 处理临时错误:socket 缓冲区满,等待后重试 if err == syscall.EAGAIN || err == syscall.EWOULDBLOCK { retries++ if retries > sendfileMaxRetries { // 超过最大重试次数,返回错误 return err } // socket 缓冲区满,短暂等待后重试 time.Sleep(sendfileRetryDelay) continue } // 被信号中断,重试 if err == syscall.EINTR { retries++ if retries > sendfileMaxRetries { return err } continue } // 客户端断开连接,返回错误让 fasthttp 知道请求未完成 // 注意:不要返回 nil,否则 fasthttp 会发送 200 + 空 body if err == syscall.EPIPE || err == syscall.ECONNRESET { return err // 返回错误,让 fasthttp 处理连接断开 } // 其他错误直接返回 return err } if n == 0 { break // EOF 或连接关闭 } // 成功发送数据,重置重试计数 retries = 0 sent += int64(n) remain -= int64(n) } return nil } // getSocketFd 获取 socket 文件描述符。 // // 从网络连接中提取底层的文件描述符,用于 sendfile 系统调用。 // 支持 TCPConn 和 UnixConn 两种连接类型。 // // 参数: // - conn: 网络连接对象 // // 返回值: // - uintptr: socket 文件描述符,失败时返回 0 // - error: 获取失败时的错误,不支持的连接类型返回 ENOTSUP func getSocketFile(conn net.Conn) (*os.File, error) { switch c := conn.(type) { case *net.TCPConn: return c.File() case *net.UnixConn: return c.File() default: return nil, syscall.ENOTSUP } }