Add netutil.FormatRemoteAddr() to avoid repeated net.TCPAddr.String() allocations on the hot logging path. - IPv4 addresses use a zero-allocation fast path (custom uint8->ASCII). - IPv6 falls back to addr.String() with a 1024-entry LRU cache. - Update logging.LogAccess and variable $remote_addr/$remote_port getters to use the shared helper. Eliminates net.JoinHostPort and net.IP.String from top allocators when access logging is active.
198 lines
4.8 KiB
Go
198 lines
4.8 KiB
Go
// Package netutil 提供网络相关的通用工具函数。
|
||
//
|
||
// 该文件包含客户端 IP 提取相关的工具函数,
|
||
// 从 HTTP 请求中提取真实的客户端 IP 地址。
|
||
//
|
||
// 作者:xfy
|
||
package netutil
|
||
|
||
import (
|
||
"net"
|
||
"strings"
|
||
"sync"
|
||
|
||
"github.com/valyala/fasthttp"
|
||
)
|
||
|
||
// ExtractClientIP 从请求上下文中提取客户端 IP 地址(返回字符串)。
|
||
//
|
||
// 该函数按以下顺序提取 IP:
|
||
// 1. X-Forwarded-For 请求头的第一个 IP(最左侧)
|
||
// 2. X-Real-IP 请求头
|
||
// 3. RemoteAddr
|
||
//
|
||
// 注意:此函数不进行可信代理验证,适用于非安全场景(如日志记录)。
|
||
// 对于安全场景(如访问控制),应使用特定模块的安全实现。
|
||
//
|
||
// 参数:
|
||
// - ctx: FastHTTP 请求上下文
|
||
//
|
||
// 返回值:
|
||
// - string: 客户端 IP 地址字符串
|
||
func ExtractClientIP(ctx *fasthttp.RequestCtx) string {
|
||
// 首先检查 X-Forwarded-For 请求头
|
||
if xff := ctx.Request.Header.Peek("X-Forwarded-For"); len(xff) > 0 {
|
||
ips := strings.Split(string(xff), ",")
|
||
if len(ips) > 0 {
|
||
return strings.TrimSpace(ips[0])
|
||
}
|
||
}
|
||
|
||
// 检查 X-Real-IP 请求头
|
||
if xri := ctx.Request.Header.Peek("X-Real-IP"); len(xri) > 0 {
|
||
return string(xri)
|
||
}
|
||
|
||
// 回退到 RemoteAddr
|
||
if addr := ctx.RemoteAddr(); addr != nil {
|
||
if tcpAddr, ok := addr.(*net.TCPAddr); ok {
|
||
return tcpAddr.IP.String()
|
||
}
|
||
return addr.String()
|
||
}
|
||
|
||
return ""
|
||
}
|
||
|
||
// ExtractClientIPNet 从请求上下文中提取客户端 IP 地址(返回 net.IP)。
|
||
//
|
||
// 该函数与 ExtractClientIP 功能相同,但返回 net.IP 类型,
|
||
// 便于后续进行 IP 网络操作(如 CIDR 匹配)。
|
||
//
|
||
// 参数:
|
||
// - ctx: FastHTTP 请求上下文
|
||
//
|
||
// 返回值:
|
||
// - net.IP: 客户端 IP 地址,无法解析时返回 nil
|
||
func ExtractClientIPNet(ctx *fasthttp.RequestCtx) net.IP {
|
||
// 首先检查 X-Forwarded-For 请求头
|
||
if xff := ctx.Request.Header.Peek("X-Forwarded-For"); len(xff) > 0 {
|
||
ips := strings.Split(string(xff), ",")
|
||
if len(ips) > 0 {
|
||
ipStr := strings.TrimSpace(ips[0])
|
||
if ip := net.ParseIP(ipStr); ip != nil {
|
||
return ip
|
||
}
|
||
}
|
||
}
|
||
|
||
// 检查 X-Real-IP 请求头
|
||
if xri := ctx.Request.Header.Peek("X-Real-IP"); len(xri) > 0 {
|
||
if ip := net.ParseIP(string(xri)); ip != nil {
|
||
return ip
|
||
}
|
||
}
|
||
|
||
// 回退到 RemoteAddr
|
||
if addr := ctx.RemoteAddr(); addr != nil {
|
||
if tcpAddr, ok := addr.(*net.TCPAddr); ok {
|
||
return tcpAddr.IP
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// GetRemoteAddrIP 从 RemoteAddr 提取 IP 地址。
|
||
//
|
||
// 这是一个辅助函数,直接从连接的远程地址获取 IP,
|
||
// 不检查任何代理头。
|
||
//
|
||
// 参数:
|
||
// - ctx: FastHTTP 请求上下文
|
||
//
|
||
// 返回值:
|
||
// - net.IP: 客户端 IP 地址,无法获取时返回 nil
|
||
func GetRemoteAddrIP(ctx *fasthttp.RequestCtx) net.IP {
|
||
if addr := ctx.RemoteAddr(); addr != nil {
|
||
if tcpAddr, ok := addr.(*net.TCPAddr); ok {
|
||
return tcpAddr.IP
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// remoteAddrCache 缓存 RemoteAddr 字符串化结果,避免重复的 net.TCPAddr.String() 分配。
|
||
type remoteAddrCache struct {
|
||
mu sync.RWMutex
|
||
entries map[string]string
|
||
maxSize int
|
||
}
|
||
|
||
var globalRemoteAddrCache = &remoteAddrCache{
|
||
entries: make(map[string]string, 1024),
|
||
maxSize: 1024,
|
||
}
|
||
|
||
// FormatRemoteAddr 使用缓存格式化 RemoteAddr,避免重复的地址字符串分配。
|
||
// 优先使用 ctx.RemoteIP() 获取 IP,对 IPv4 直接零分配格式化,IPv6 回退到 addr.String()。
|
||
func FormatRemoteAddr(ctx *fasthttp.RequestCtx) string {
|
||
ip := ctx.RemoteIP()
|
||
if ip == nil {
|
||
addr := ctx.RemoteAddr()
|
||
if addr == nil {
|
||
return "-"
|
||
}
|
||
return addr.String()
|
||
}
|
||
|
||
// 优先尝试 IPv4 快速路径(零分配)
|
||
if ipv4 := ip.To4(); ipv4 != nil {
|
||
return formatIPv4(ipv4)
|
||
}
|
||
|
||
// IPv6:尝试缓存
|
||
ipStr := ip.String()
|
||
globalRemoteAddrCache.mu.RLock()
|
||
if cached, ok := globalRemoteAddrCache.entries[ipStr]; ok {
|
||
globalRemoteAddrCache.mu.RUnlock()
|
||
return cached
|
||
}
|
||
globalRemoteAddrCache.mu.RUnlock()
|
||
|
||
// 未命中缓存,回退到 addr.String()
|
||
addr := ctx.RemoteAddr()
|
||
if addr == nil {
|
||
return "-"
|
||
}
|
||
result := addr.String()
|
||
|
||
globalRemoteAddrCache.mu.Lock()
|
||
if len(globalRemoteAddrCache.entries) < globalRemoteAddrCache.maxSize {
|
||
globalRemoteAddrCache.entries[ipStr] = result
|
||
}
|
||
globalRemoteAddrCache.mu.Unlock()
|
||
return result
|
||
}
|
||
|
||
// formatIPv4 将 4 字节 IPv4 地址格式化为字符串(零分配)。
|
||
func formatIPv4(ip net.IP) string {
|
||
var buf [15]byte
|
||
n := 0
|
||
for i := 0; i < 4; i++ {
|
||
if i > 0 {
|
||
buf[n] = '.'
|
||
n++
|
||
}
|
||
n += writeUint8(buf[n:], ip[i])
|
||
}
|
||
return string(buf[:n])
|
||
}
|
||
|
||
// writeUint8 将 uint8 写入 buf,返回写入的字节数。
|
||
func writeUint8(buf []byte, v byte) int {
|
||
if v >= 100 {
|
||
buf[0] = byte('0' + v/100)
|
||
buf[1] = byte('0' + (v/10)%10)
|
||
buf[2] = byte('0' + v%10)
|
||
return 3
|
||
}
|
||
if v >= 10 {
|
||
buf[0] = byte('0' + v/10)
|
||
buf[1] = byte('0' + v%10)
|
||
return 2
|
||
}
|
||
buf[0] = byte('0' + v)
|
||
return 1
|
||
}
|