Files
OmniSocketGo/cmd/internal/transport/tcp_linux.go
2026-03-23 22:22:00 +08:00

468 lines
14 KiB
Go
Raw 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 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 的间隔。
linuxDataPollInterval = time.Millisecond // 轮询普通收发的间隔。
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
for written < len(frame) {
n, err := c.sendmsgDataOnce(frame[written:])
switch {
case err == nil:
if n <= 0 {
return io.ErrShortWrite
}
written += n
case isWouldBlock(err):
time.Sleep(linuxDataPollInterval)
default:
return err
}
}
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) {
for {
n, rxTimeNS, err := c.recvmsgDataOnce(buf)
switch {
case err == nil:
if n == 0 {
return 0, 0, io.EOF
}
return n, rxTimeNS, nil
case isWouldBlock(err):
time.Sleep(linuxDataPollInterval)
default:
return 0, 0, err
}
}
}
// 从控制消息里取 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)
}
func (c *TCPConn) sendmsgDataOnce(buf []byte) (int, error) {
var (
n int
opErr error
)
err := c.raw.Control(func(fd uintptr) {
n, opErr = syscall.SendmsgN(int(fd), buf, nil, nil, 0)
})
if err != nil {
return 0, err
}
return n, opErr
}
func (c *TCPConn) recvmsgDataOnce(buf []byte) (int, int64, error) {
var (
n int
rxTimeNS int64
opErr error
)
err := c.raw.Control(func(fd uintptr) {
oob := make([]byte, linuxTimestampControlBufferSize)
readN, oobN, _, _, recvErr := syscall.Recvmsg(int(fd), buf, oob, 0)
if recvErr != nil {
opErr = recvErr
return
}
n = readN
rxTimeNS = parseRXTimestampControlMessages(oob[:oobN])
})
if err != nil {
return 0, 0, err
}
return n, rxTimeNS, opErr
}