This commit is contained in:
nnbcccscdscdsc
2026-03-23 20:18:53 +08:00
commit 4824675244
28 changed files with 5569 additions and 0 deletions

View File

@@ -0,0 +1,462 @@
//go:build linux
package transport
import (
"encoding/binary"
"errors"
"fmt"
"io"
"syscall"
"time"
"omnisocketgo/cmd/internal/latencylog"
"omnisocketgo/cmd/internal/protocol"
)
const (
linuxTimestampControlBufferSize = 256 // 控制消息缓冲区。
linuxTXTimestampWaitTimeout = 250 * time.Millisecond // 等待 TX 时间戳的上限。
linuxTXTimestampPollInterval = time.Millisecond // 轮询 errqueue 的间隔。
linuxSOTimestampingNew = 0x41
linuxSCMTimestampingNew = linuxSOTimestampingNew
linuxSOEEOriginTimestamping = 4 // timestamping errqueue 事件。
linuxSCMTstampSnd = 0 // 对应 A_TX_SOFTWARE。
linuxSCMTstampSched = 1 // 对应 A_TX_SCHED。
linuxSOFTimestampingTXSoftware = 1 << 1 // 打开 TX software timestamp。
linuxSOFTimestampingRXSoftware = 1 << 3 // 打开 RX software timestamp。
linuxSOFTimestampingSoftware = 1 << 4 // software timestamp 总开关。
linuxSOFTimestampingOptID = 1 << 7 // 给时间戳关联 ID。
linuxSOFTimestampingTXSched = 1 << 8 // 打开 TX sched timestamp。
linuxSOFTimestampingOptTSONLY = 1 << 11 // 只回时间戳。
linuxSOFTimestampingOptIDTCP = 1 << 16 // 让 TCP 也带 timestamp ID。
)
// 拿到底层 fd并打开 Linux timestamping。
func (c *TCPConn) initLinuxTimestamping() error {
sysConn, ok := c.conn.(interface {
SyscallConn() (syscall.RawConn, error)
})
if !ok {
return fmt.Errorf("transport: connection does not support SyscallConn")
}
rawConn, err := sysConn.SyscallConn()
if err != nil || rawConn == nil {
if err != nil {
return fmt.Errorf("transport: get syscall conn: %w", err)
}
return fmt.Errorf("transport: missing syscall conn")
}
//socket是否可以成功打开 timestamping 取决于内核版本和配置,尝试多个 flag 组合直到成功或遇到非 EINVAL 错误。
if err := enableLinuxTimestamping(rawConn); err != nil {
return fmt.Errorf("transport: enable linux timestamping: %w", err)
}
//成功打开 timestamping 后rawConn 就可以用来收 TX/RX 时间戳了。
c.raw = rawConn
return nil
}
// 给 socket开权限打开TX software timestamping。
func enableLinuxTimestamping(rawConn syscall.RawConn) error {
flagCandidates := []int{ //不同linux版本可能支持不同的 flag 组合,尝试多个组合直到成功。
linuxSOFTimestampingTXSched |
linuxSOFTimestampingTXSoftware |
linuxSOFTimestampingRXSoftware |
linuxSOFTimestampingSoftware |
linuxSOFTimestampingOptID | //TCP 协议栈给每个时间戳生成一个序列号
linuxSOFTimestampingOptIDTCP |
linuxSOFTimestampingOptTSONLY,
linuxSOFTimestampingTXSched |
linuxSOFTimestampingTXSoftware |
linuxSOFTimestampingRXSoftware |
linuxSOFTimestampingSoftware |
linuxSOFTimestampingOptID |
linuxSOFTimestampingOptTSONLY,
linuxSOFTimestampingTXSched |
linuxSOFTimestampingTXSoftware |
linuxSOFTimestampingRXSoftware |
linuxSOFTimestampingSoftware |
linuxSOFTimestampingOptTSONLY,
}
var lastErr error
for _, flags := range flagCandidates { //尝试不同的 flag 组合,直到成功或遇到非 EINVAL 错误。
// 内核根据 fd 找到对应的内存结构体Socket 缓冲区)
err := rawConn.Control(func(fd uintptr) { //Control 方法保证在回调里 fd 是有效的,可以安全地调用 syscall.SetsockoptInt。
lastErr = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, linuxSOTimestampingNew, flags)
})
if err != nil {
return err
}
if lastErr == nil {
return nil
}
if !errors.Is(lastErr, syscall.EINVAL) {
return lastErr
}
}
return lastErr
}
// sendMessageLinux 编码消息、写完整帧,再记录 TX 时间戳。
func (c *TCPConn) sendMessageLinux(msg protocol.Message) error {
payload, err := protocol.EncodeMessage(msg)
if err != nil {
return fmt.Errorf("protocol: encode message: %w", err)
}
//编码后的消息 payload 前面加 4 字节长度,构成完整帧。
frame := make([]byte, 4+len(payload))
binary.BigEndian.PutUint32(frame[:4], uint32(len(payload)))
copy(frame[4:], payload)
if err := c.writeFrameLinux(frame); err != nil {
return fmt.Errorf("protocol: write frame: %w", err)
}
//记录发送延时日志
c.logTXTimestampEvents(msg)
return nil
}
// writeFrameLinux 用 sendmsg 写完整帧。
func (c *TCPConn) writeFrameLinux(frame []byte) error {
written := 0
var opErr error
err := c.raw.Write(func(fd uintptr) bool {
if written >= len(frame) {
return true
}
n, sendErr := syscall.SendmsgN(int(fd), frame[written:], nil, nil, 0)
switch {
case sendErr == nil:
if n <= 0 {
opErr = io.ErrShortWrite
return true
}
written += n
return written >= len(frame)
case errors.Is(sendErr, syscall.EAGAIN), errors.Is(sendErr, syscall.EWOULDBLOCK):
return false
default:
opErr = sendErr
return true
}
})
if err != nil {
return err
}
if opErr != nil {
return opErr
}
if written != len(frame) {
return io.ErrShortWrite
}
return nil
}
// 把 A_TX_SCHED / A_TX_SOFTWARE 写入日志。(发送过程中)
func (c *TCPConn) logTXTimestampEvents(msg protocol.Message) {
timestamps := c.collectTXTimestampEvents()
if ts, ok := timestamps[latencylog.EventATXSched]; ok {
latencylog.LogMessageEventAt(c.logger, c.nodeRole, c.nodeID, latencylog.EventATXSched, ts, msg)
}
if ts, ok := timestamps[latencylog.EventATXSoftware]; ok {
latencylog.LogMessageEventAt(c.logger, c.nodeRole, c.nodeID, latencylog.EventATXSoftware, ts, msg)
}
}
// 在 errqueue 里等两类 TX 时间戳。
func (c *TCPConn) collectTXTimestampEvents() map[string]int64 {
timestamps := make(map[string]int64, 2)
//设置合理等待上限
deadline := time.Now().Add(linuxTXTimestampWaitTimeout)
//轮询 errqueue 直到拿到两类时间戳,或超时,或遇到非 EAGAIN 错误。
for len(timestamps) < 2 && time.Now().Before(deadline) {
eventName, ts, err := c.recvTXTimestampOnce()
if err != nil {
if isWouldBlock(err) {
time.Sleep(linuxTXTimestampPollInterval)
continue
}
break
}
if eventName == "" || ts <= 0 {
continue
}
if _, exists := timestamps[eventName]; !exists {
timestamps[eventName] = ts
}
}
return timestamps
}
// recvTXTimestampOnce 从 errqueue 读一次时间戳事件。
func (c *TCPConn) recvTXTimestampOnce() (string, int64, error) {
var (
eventName string // 事件名,例如 A_TX_SCHED 或 A_TX_SOFTWARE。
tsUnixNS int64 // 时间戳的 UnixNano 表示。
opErr error
)
err := c.raw.Control(func(fd uintptr) {
//设置足够大的 oob buffer 来接收控制消息,调用 recvmsg 从 errqueue 读一条消息。
oob := make([]byte, linuxTimestampControlBufferSize)
//recvmsg 的 flags 里必须带 MSG_ERRQUEUE才能从 errqueue 里读消息,非阻塞模式下如果没有消息可读会返回 EAGAIN。
_, oobn, _, _, recvErr := syscall.Recvmsg(int(fd), nil, oob, syscall.MSG_ERRQUEUE|syscall.MSG_DONTWAIT)
if recvErr != nil {
opErr = recvErr
return
}
//解析控制消息,看看是不是我们关心的 TX 时间戳事件,如果是就拿到事件名和时间戳。
eventName, tsUnixNS = parseTXTimestampControlMessages(oob[:oobn])
})
if err != nil {
return "", 0, err
}
if opErr != nil {
return "", 0, opErr
}
return eventName, tsUnixNS, nil //如果成功拿到时间戳事件eventName 会是 A_TX_SCHED 或 A_TX_SOFTWARE 之一tsUnixNS 是对应的时间戳如果没有拿到事件或时间戳无效eventName 会是空字符串tsUnixNS 会是 0。
}
// 把底层时间戳映射成日志事件名。
func parseTXTimestampControlMessages(oob []byte) (string, int64) {
if len(oob) == 0 {
return "", 0
}
//解析控制消息,看看是不是我们关心的 TX 时间戳事件,如果是就拿到事件名和时间戳。
controlMessages, err := syscall.ParseSocketControlMessage(oob)
if err != nil {
return "", 0
}
var (
tsUnixNS int64 //时间戳的 UnixNano 表示。
tsKind uint32 //extended err里告诉我们这个时间戳是 sched 还是 software。
hasTS bool // 是否拿到时间戳了。
hasKind bool // 是否拿到时间戳类型了。
)
//一个 recvmsg 可能会收到多个控制消息,循环找我们关心的时间戳事件,拿到时间戳和事件类型。
for _, controlMessage := range controlMessages {
switch {
case controlMessage.Header.Level == syscall.SOL_SOCKET && controlMessage.Header.Type == linuxSCMTimestampingNew:
if ts := parseSCMTimestampingData(controlMessage.Data); ts > 0 {
tsUnixNS = ts
hasTS = true
}
case isSocketExtendedErr(controlMessage): //判断时间戳是否进入了errqueue
if info, ok := parseSocketExtendedErrInfo(controlMessage.Data); ok {
tsKind = info //时间戳类型被内核放在 extended err 的附加信息里,解析出来。
hasKind = true
}
}
}
if !hasTS || !hasKind {
return "", 0
}
switch tsKind { //把内核的时间戳类型映射成日志事件名。(记录时只关心 sched 和 software 两类时间戳)
case linuxSCMTstampSched:
return latencylog.EventATXSched, tsUnixNS
case linuxSCMTstampSnd:
return latencylog.EventATXSoftware, tsUnixNS
default:
return "", 0
}
}
// 判断控制消息是否来自 socket extended err。
// 内核产生的时间戳并不会混合在普通的数据流里,而是被包装成一种特殊的“错误消息”丢进 Error Queue。
func isSocketExtendedErr(controlMessage syscall.SocketControlMessage) bool {
switch {
case controlMessage.Header.Level == syscall.SOL_IP && controlMessage.Header.Type == syscall.IP_RECVERR:
return true
case controlMessage.Header.Level == syscall.SOL_IPV6 && controlMessage.Header.Type == syscall.IPV6_RECVERR:
return true
default:
return false
}
}
// 从 socket extended err 的数据里取 origin timestamping 信息。
func parseSocketExtendedErrInfo(data []byte) (uint32, bool) {
if len(data) < 16 {
return 0, false
}
if data[4] != linuxSOEEOriginTimestamping {
return 0, false
}
return binary.NativeEndian.Uint32(data[8:12]), true
}
// 读一条完整消息,并记录 B_RX_SOFTWARE。
func (c *TCPConn) receiveMessageLinux() (protocol.Message, error) {
payload, rxTimestamp, err := c.readFrameLinux()
if err != nil {
return protocol.Message{}, fmt.Errorf("protocol: read frame: %w", err)
}
msg, err := protocol.DecodeMessage(payload)
if err != nil {
return protocol.Message{}, fmt.Errorf("protocol: decode message: %w", err)
}
if rxTimestamp > 0 {
latencylog.LogMessageEventAt(c.logger, c.nodeRole, c.nodeID, latencylog.EventBRXSoftware, rxTimestamp, msg)
}
return msg, nil
}
// readFrameLinux 先读 4 字节长度,再读整条 payload。
func (c *TCPConn) readFrameLinux() ([]byte, int64, error) {
var frameHeader [4]byte
rxTimestamp, err := c.readFullLinux(frameHeader[:])
if err != nil {
return nil, rxTimestamp, err
}
size := binary.BigEndian.Uint32(frameHeader[:])
switch {
case size == 0:
return nil, rxTimestamp, protocol.ErrInvalidFrameLength
case size > protocol.MaxFrameSize:
return nil, rxTimestamp, protocol.ErrFrameTooLarge
}
payload := make([]byte, int(size))
bodyTimestamp, err := c.readFullLinux(payload)
if rxTimestamp == 0 {
rxTimestamp = bodyTimestamp
}
if err != nil {
return nil, rxTimestamp, err
}
return payload, rxTimestamp, nil
}
// 读满 buf并保留首个 RX_SOFTWARE返回进入tcp协议栈的时间戳
func (c *TCPConn) readFullLinux(buf []byte) (int64, error) {
if len(buf) == 0 {
return 0, nil
}
var (
offset int
firstRXTime int64
)
for offset < len(buf) {
n, rxTimestamp, err := c.recvmsgLinux(buf[offset:])
if firstRXTime == 0 && rxTimestamp > 0 {
firstRXTime = rxTimestamp
}
if err != nil {
if errors.Is(err, io.EOF) && offset > 0 {
return firstRXTime, io.ErrUnexpectedEOF
}
return firstRXTime, err
}
offset += n
}
return firstRXTime, nil
}
// recvmsgLinux 用 recvmsg 同时读取数据和控制消息。
func (c *TCPConn) recvmsgLinux(buf []byte) (int, int64, error) {
var (
n int
rxTimeNS int64
opErr error
)
err := c.raw.Read(func(fd uintptr) bool {
oob := make([]byte, linuxTimestampControlBufferSize)
readN, oobN, _, _, recvErr := syscall.Recvmsg(int(fd), buf, oob, 0)
switch {
case recvErr == nil:
if readN == 0 {
opErr = io.EOF
return true
}
n = readN
rxTimeNS = parseRXTimestampControlMessages(oob[:oobN])
return true
case errors.Is(recvErr, syscall.EAGAIN), errors.Is(recvErr, syscall.EWOULDBLOCK):
return false
default:
opErr = recvErr
return true
}
})
if err != nil {
return 0, 0, err
}
if opErr != nil {
return 0, 0, opErr
}
return n, rxTimeNS, nil
}
// 从控制消息里取 RX_SOFTWARE。
func parseRXTimestampControlMessages(oob []byte) int64 {
if len(oob) == 0 {
return 0
}
controlMessages, err := syscall.ParseSocketControlMessage(oob)
if err != nil {
return 0
}
for _, controlMessage := range controlMessages {
if controlMessage.Header.Level != syscall.SOL_SOCKET || controlMessage.Header.Type != linuxSCMTimestampingNew {
continue
}
if ts := parseSCMTimestampingData(controlMessage.Data); ts > 0 {
return ts
}
}
return 0
}
// 取第一个非零 timespec。
func parseSCMTimestampingData(data []byte) int64 {
const timespec64Size = 16
for offset := 0; offset+timespec64Size <= len(data); offset += timespec64Size {
sec := int64(binary.NativeEndian.Uint64(data[offset : offset+8]))
nsec := int64(binary.NativeEndian.Uint64(data[offset+8 : offset+16]))
if sec == 0 && nsec == 0 {
continue
}
return sec*int64(time.Second) + nsec
}
return 0
}
// 判断错误是否是 EAGAIN 或 EWOULDBLOCK。
func isWouldBlock(err error) bool {
return errors.Is(err, syscall.EAGAIN) || errors.Is(err, syscall.EWOULDBLOCK)
}