lolly/internal/handler/sendfile_linux.go
xfy 27e00b84a8 fix(proxy,handler,server,stream,ratelimit): fix resource leaks and functional bugs
- proxy/proxy.go: decrement connection count on dangerous path rejection
  (line 724) to prevent connection count leak
- handler/sendfile_linux.go: return *os.File from getSocketFile and let
  linuxSendfile close it, fixing EBADF from deferred close in getSocketFd
- proxy/websocket.go: return bufio.Reader from readWebSocketUpgradeResponse
  and wrap targetConn with bufferedConn to consume pre-buffered frame data,
  preventing first-frame loss
- server/pool.go: use non-blocking send after starting new worker to avoid
  deadlock when queue is full
- stream/stream.go: check stopCh on non-timeout UDP read errors to prevent
  infinite loop and shutdown deadlock
- middleware/ratelimit: replace select-based close guard with sync.Once in
  StopCleanup to prevent double-close panic
2026-06-11 16:35:10 +08:00

163 lines
4.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//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
}
}