Files
RemLink/internal/subnet/sender.go
T
qsc20001102 142e5dc7d6
ci / Go checks (ubuntu-latest) (push) Has been cancelled
ci / Go checks (windows-latest) (push) Has been cancelled
初版功能完成
2026-08-29 13:12:17 +08:00

139 lines
3.3 KiB
Go

package subnet
import (
"context"
"errors"
"fmt"
"net"
"net/netip"
"sync"
"sync/atomic"
)
const DefaultQueueCapacity = 1024
type SenderConfig struct {
SessionID uint64
LocalIP netip.Addr
PeerIP netip.Addr
PeerPort int
QueueCapacity int
}
// Sender performs ordinary UDP socket I/O on a dedicated goroutine.
type Sender struct {
conn *net.UDPConn
queue chan []byte
enqueueMu sync.RWMutex
closed bool
cancel context.CancelFunc
done chan struct{}
closeOnce sync.Once
sessionID uint64
sentBytes atomic.Uint64
sentPackets atomic.Uint64
dropped atomic.Uint64
lastError atomic.Value
}
type errorBox struct{ err error }
func NewSender(parent context.Context, config SenderConfig) (*Sender, error) {
if err := validateSenderConfig(config); err != nil {
return nil, err
}
if config.QueueCapacity <= 0 {
config.QueueCapacity = DefaultQueueCapacity
}
connection, err := net.DialUDP("udp4",
&net.UDPAddr{IP: net.IP(config.LocalIP.AsSlice())},
&net.UDPAddr{IP: net.IP(config.PeerIP.AsSlice()), Port: config.PeerPort},
)
if err != nil {
return nil, fmt.Errorf("bind Session Sender to %s and dial %s:%d: %w", config.LocalIP, config.PeerIP, config.PeerPort, err)
}
ctx, cancel := context.WithCancel(parent)
sender := &Sender{
conn: connection, queue: make(chan []byte, config.QueueCapacity), cancel: cancel,
done: make(chan struct{}), sessionID: config.SessionID,
}
go sender.run(ctx)
return sender, nil
}
func validateSenderConfig(config SenderConfig) error {
if config.SessionID == 0 || !config.LocalIP.Is4() || !config.PeerIP.Is4() || config.PeerPort < 1 || config.PeerPort > 65535 {
return errors.New("Sender requires SessionID, IPv4 local/peer addresses, and valid peer port")
}
if config.LocalIP.IsUnspecified() || config.PeerIP.IsUnspecified() || config.LocalIP.IsMulticast() || config.PeerIP.IsMulticast() || config.LocalIP == config.PeerIP {
return errors.New("Sender local/peer addresses must be distinct unicast addresses")
}
return nil
}
// Enqueue never blocks PacketMux. packet ownership transfers to Sender on true.
func (s *Sender) Enqueue(packet []byte) bool {
s.enqueueMu.RLock()
defer s.enqueueMu.RUnlock()
if s.closed {
return false
}
select {
case s.queue <- packet:
return true
default:
s.dropped.Add(1)
return false
}
}
func (s *Sender) Close() error {
s.closeOnce.Do(func() {
s.enqueueMu.Lock()
s.closed = true
s.enqueueMu.Unlock()
s.cancel()
_ = s.conn.Close()
<-s.done
})
return nil
}
func (s *Sender) Counters() (bytes, packets, dropped uint64) {
return s.sentBytes.Load(), s.sentPackets.Load(), s.dropped.Load()
}
func (s *Sender) Err() error {
value := s.lastError.Load()
if value == nil {
return nil
}
return value.(*errorBox).err
}
func (s *Sender) run(ctx context.Context) {
defer close(s.done)
for {
select {
case <-ctx.Done():
return
case packet := <-s.queue:
datagram, err := EncodeDatagram(s.sessionID, packet)
if err == nil {
var written int
written, err = s.conn.Write(datagram)
if err == nil && written != len(datagram) {
err = errors.New("short Session UDP write")
}
}
if err != nil {
s.lastError.Store(&errorBox{err: err})
s.dropped.Add(1)
continue
}
s.sentBytes.Add(uint64(len(packet)))
s.sentPackets.Add(1)
}
}
}