init
This commit is contained in:
179
cmd/internal/peer/client.go
Normal file
179
cmd/internal/peer/client.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package peer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
|
||||
"omnisocketgo/cmd/internal/latencylog"
|
||||
"omnisocketgo/cmd/internal/protocol"
|
||||
"omnisocketgo/cmd/internal/transport"
|
||||
)
|
||||
|
||||
var dialServer = net.Dial
|
||||
|
||||
type clientOptions struct {
|
||||
logger latencylog.Logger
|
||||
}
|
||||
|
||||
// Option 用于配置 Client 的可选行为,例如时延日志。
|
||||
type Option func(*clientOptions)
|
||||
|
||||
// WithLogger 为 client 注入时延日志记录器。
|
||||
func WithLogger(logger latencylog.Logger) Option {
|
||||
return func(options *clientOptions) {
|
||||
options.logger = logger
|
||||
}
|
||||
}
|
||||
|
||||
// Client 表示一个已经连接到 server 的 peer。
|
||||
type Client struct {
|
||||
id string
|
||||
conn *transport.TCPConn
|
||||
logger latencylog.Logger
|
||||
|
||||
nextID uint64
|
||||
}
|
||||
|
||||
// Dial 连接到 server,并立即发送 register 消息完成身份注册。
|
||||
func Dial(serverAddr, peerID string, opts ...Option) (*Client, error) {
|
||||
options := clientOptions{
|
||||
logger: latencylog.NoopLogger{},
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
}
|
||||
if options.logger == nil {
|
||||
options.logger = latencylog.NoopLogger{}
|
||||
}
|
||||
|
||||
rawConn, err := dialServer("tcp", serverAddr) //使用 net.Dial 连接到 serverAddr 指定的 TCP 地址,返回一个 net.Conn。
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("peer: dial server: %w", err)
|
||||
}
|
||||
|
||||
conn, err := transport.NewTCPConn(
|
||||
rawConn,
|
||||
transport.WithLogger(options.logger, latencylog.NodeRolePeer, peerID),
|
||||
)
|
||||
if err != nil {
|
||||
_ = rawConn.Close()
|
||||
return nil, fmt.Errorf("peer: create transport conn: %w", err)
|
||||
}
|
||||
client := &Client{
|
||||
id: peerID,
|
||||
conn: conn,
|
||||
logger: options.logger,
|
||||
}
|
||||
|
||||
if err := conn.Send(protocol.Message{ //向 server 发送一条 register 消息,完成身份注册。
|
||||
Type: protocol.MessageTypeRegister,
|
||||
From: peerID,
|
||||
To: protocol.ServerPeerID,
|
||||
}); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, fmt.Errorf("peer: register with server: %w", err)
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// ID 返回当前 client 的 peer 标识。
|
||||
func (c *Client) ID() string {
|
||||
return c.id
|
||||
}
|
||||
|
||||
// SendText 向目标 peer 发送一条文本消息。
|
||||
func (c *Client) SendText(to, body string) error {
|
||||
msg := protocol.Message{
|
||||
Type: protocol.MessageTypeText,
|
||||
ID: c.nextMessageID(),
|
||||
From: c.id,
|
||||
To: to,
|
||||
}
|
||||
// 记录 A 端应用开始准备消息的时间点。
|
||||
latencylog.LogMessageEvent(c.logger, latencylog.NodeRolePeer, c.id, latencylog.EventAAppPrepBegin, msg)
|
||||
|
||||
msg.Body = []byte(body)
|
||||
|
||||
return c.conn.Send(msg)
|
||||
}
|
||||
|
||||
// SendFile 向目标 peer 发送一条文件消息。
|
||||
func (c *Client) SendFile(to, fileName string, body []byte) error {
|
||||
msg := protocol.Message{
|
||||
Type: protocol.MessageTypeFile,
|
||||
ID: c.nextMessageID(),
|
||||
From: c.id,
|
||||
To: to,
|
||||
FileName: fileName,
|
||||
}
|
||||
// 记录 A 端应用开始准备消息的时间点。
|
||||
latencylog.LogMessageEvent(c.logger, latencylog.NodeRolePeer, c.id, latencylog.EventAAppPrepBegin, msg)
|
||||
|
||||
bodyCopy := make([]byte, len(body))
|
||||
copy(bodyCopy, body)
|
||||
|
||||
msg.Body = bodyCopy
|
||||
|
||||
return c.conn.Send(msg)
|
||||
}
|
||||
|
||||
// SendFilePath 从本地文件读取内容并发送给目标 peer。
|
||||
func (c *Client) SendFilePath(to, path string) error {
|
||||
msg := protocol.Message{
|
||||
Type: protocol.MessageTypeFile,
|
||||
ID: c.nextMessageID(),
|
||||
From: c.id,
|
||||
To: to,
|
||||
FileName: filepath.Base(path),
|
||||
}
|
||||
// 记录 A 端应用开始准备消息的时间点。
|
||||
latencylog.LogMessageEvent(c.logger, latencylog.NodeRolePeer, c.id, latencylog.EventAAppPrepBegin, msg)
|
||||
|
||||
body, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("peer: read file %s: %w", path, err)
|
||||
}
|
||||
|
||||
msg.Body = body
|
||||
|
||||
return c.conn.Send(msg)
|
||||
}
|
||||
|
||||
// Receive 读取一条来自 server 的消息。
|
||||
func (c *Client) Receive() (protocol.Message, error) {
|
||||
msg, err := c.conn.Receive() //从底层 TCP 连接读取一条消息,返回一个 protocol.Message 结构体。
|
||||
if err != nil {
|
||||
return protocol.Message{}, fmt.Errorf("peer: receive from server: %w", err)
|
||||
}
|
||||
|
||||
latencylog.LogMessageEvent(c.logger, latencylog.NodeRolePeer, c.id, latencylog.EventBAppRecv, msg)
|
||||
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
// ReceiveLoop 持续接收 server 消息并交给 handler 处理。
|
||||
func (c *Client) ReceiveLoop(handler func(protocol.Message) error) error {
|
||||
return c.conn.ReceiveLoop(func(msg protocol.Message) error {
|
||||
switch msg.Type {
|
||||
case protocol.MessageTypeText, protocol.MessageTypeFile, protocol.MessageTypeError:
|
||||
// 记录 B 端应用真正读到完整消息的时间点。
|
||||
latencylog.LogMessageEvent(c.logger, latencylog.NodeRolePeer, c.id, latencylog.EventBAppRecv, msg)
|
||||
return handler(msg)
|
||||
default:
|
||||
return fmt.Errorf("peer: unexpected message type from server: %s", msg.Type)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Close 关闭与 server 的连接。
|
||||
func (c *Client) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
func (c *Client) nextMessageID() uint64 {
|
||||
return atomic.AddUint64(&c.nextID, 1)
|
||||
}
|
||||
188
cmd/internal/peer/client_linux_test.go
Normal file
188
cmd/internal/peer/client_linux_test.go
Normal file
@@ -0,0 +1,188 @@
|
||||
//go:build linux
|
||||
|
||||
package peer
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"omnisocketgo/cmd/internal/latencylog"
|
||||
"omnisocketgo/cmd/internal/protocol"
|
||||
"omnisocketgo/cmd/internal/server"
|
||||
)
|
||||
|
||||
func TestClientsExchangeMessagesWithLinuxTimestamps(t *testing.T) {
|
||||
hub := server.NewHub()
|
||||
serverAddr, cleanup := startRealHubServer(t, hub)
|
||||
defer cleanup()
|
||||
|
||||
peerALogger := &recordingLogger{}
|
||||
peerA, err := Dial(serverAddr, "peer-a", WithLogger(peerALogger))
|
||||
if err != nil {
|
||||
t.Fatalf("Dial(peer-a) error = %v", err)
|
||||
}
|
||||
defer func() { _ = peerA.Close() }()
|
||||
|
||||
peerBLogger := &recordingLogger{}
|
||||
peerB, err := Dial(serverAddr, "peer-b", WithLogger(peerBLogger))
|
||||
if err != nil {
|
||||
t.Fatalf("Dial(peer-b) error = %v", err)
|
||||
}
|
||||
defer func() { _ = peerB.Close() }()
|
||||
|
||||
inboxDir := t.TempDir()
|
||||
|
||||
waitFor(t, func() bool { return hub.HasPeer("peer-a") && hub.HasPeer("peer-b") }, "both peers to be registered")
|
||||
|
||||
if err := peerA.SendText("peer-b", "hello"); err != nil {
|
||||
t.Fatalf("SendText() error = %v", err)
|
||||
}
|
||||
textMsg, err := peerB.Receive()
|
||||
if err != nil {
|
||||
t.Fatalf("peerB.Receive(text) error = %v", err)
|
||||
}
|
||||
if _, err := peerB.PersistMessage(textMsg, inboxDir); err != nil {
|
||||
t.Fatalf("peerB.PersistMessage(text) error = %v", err)
|
||||
}
|
||||
|
||||
if err := peerA.SendFile("peer-b", "payload.bin", []byte{0x01, 0x02, 0x03}); err != nil {
|
||||
t.Fatalf("SendFile() error = %v", err)
|
||||
}
|
||||
fileMsg, err := peerB.Receive()
|
||||
if err != nil {
|
||||
t.Fatalf("peerB.Receive(file) error = %v", err)
|
||||
}
|
||||
if _, err := peerB.PersistMessage(fileMsg, inboxDir); err != nil {
|
||||
t.Fatalf("peerB.PersistMessage(file) error = %v", err)
|
||||
}
|
||||
|
||||
waitFor(t, func() bool { return hasMessageEvents(peerALogger.Events(), 1, latencylog.EventAAppPrepBegin, latencylog.EventATXSched, latencylog.EventATXSoftware) }, "peer-a text kernel timestamps")
|
||||
waitFor(t, func() bool { return hasMessageEvents(peerALogger.Events(), 2, latencylog.EventAAppPrepBegin, latencylog.EventATXSched, latencylog.EventATXSoftware) }, "peer-a file kernel timestamps")
|
||||
waitFor(t, func() bool { return hasMessageEvents(peerBLogger.Events(), 1, latencylog.EventBRXSoftware, latencylog.EventBAppRecv, latencylog.EventBPersistBegin, latencylog.EventBPersistEnd) }, "peer-b text receive timestamps")
|
||||
waitFor(t, func() bool { return hasMessageEvents(peerBLogger.Events(), 2, latencylog.EventBRXSoftware, latencylog.EventBAppRecv, latencylog.EventBPersistBegin, latencylog.EventBPersistEnd) }, "peer-b file receive timestamps")
|
||||
}
|
||||
|
||||
func startRealHubServer(t *testing.T, hub *server.Hub) (string, func()) {
|
||||
t.Helper()
|
||||
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("net.Listen() error = %v", err)
|
||||
}
|
||||
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
stop = make(chan struct{})
|
||||
errOnce sync.Once
|
||||
)
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for {
|
||||
conn, acceptErr := listener.Accept()
|
||||
if acceptErr != nil {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
default:
|
||||
}
|
||||
if strings.Contains(acceptErr.Error(), "closed") {
|
||||
return
|
||||
}
|
||||
t.Errorf("listener.Accept() error = %v", acceptErr)
|
||||
return
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func(rawConn net.Conn) {
|
||||
defer wg.Done()
|
||||
if serveErr := hub.ServeConn(rawConn); serveErr != nil && !isExpectedHubServeExit(serveErr) {
|
||||
errOnce.Do(func() {
|
||||
t.Logf("hub.ServeConn() ended with %v", serveErr)
|
||||
})
|
||||
}
|
||||
}(conn)
|
||||
}
|
||||
}()
|
||||
|
||||
cleanup := func() {
|
||||
close(stop)
|
||||
_ = listener.Close()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
return listener.Addr().String(), cleanup
|
||||
}
|
||||
|
||||
func hasMessageEvents(events []latencylog.Event, messageID uint64, wantEvents ...string) bool {
|
||||
seen := make(map[string]bool, len(wantEvents))
|
||||
for _, event := range events {
|
||||
if event.MessageID != messageID {
|
||||
continue
|
||||
}
|
||||
if event.TsUnixNano <= 0 {
|
||||
return false
|
||||
}
|
||||
seen[event.Event] = true
|
||||
}
|
||||
|
||||
for _, wantEvent := range wantEvents {
|
||||
if !seen[wantEvent] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func isExpectedHubServeExit(err error) bool {
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
message := err.Error()
|
||||
return strings.Contains(message, "closed") || strings.Contains(message, "protocol: read frame: EOF")
|
||||
}
|
||||
|
||||
func TestLinuxTimestampedReceivePreservesBusinessMessageShape(t *testing.T) {
|
||||
hub := server.NewHub()
|
||||
serverAddr, cleanup := startRealHubServer(t, hub)
|
||||
defer cleanup()
|
||||
|
||||
peerA, err := Dial(serverAddr, "peer-a")
|
||||
if err != nil {
|
||||
t.Fatalf("Dial(peer-a) error = %v", err)
|
||||
}
|
||||
defer func() { _ = peerA.Close() }()
|
||||
|
||||
peerB, err := Dial(serverAddr, "peer-b")
|
||||
if err != nil {
|
||||
t.Fatalf("Dial(peer-b) error = %v", err)
|
||||
}
|
||||
defer func() { _ = peerB.Close() }()
|
||||
|
||||
waitFor(t, func() bool { return hub.HasPeer("peer-a") && hub.HasPeer("peer-b") }, "both peers to be registered")
|
||||
|
||||
want := protocol.Message{
|
||||
Type: protocol.MessageTypeFile,
|
||||
ID: 1,
|
||||
From: "peer-a",
|
||||
To: "peer-b",
|
||||
FileName: "payload.bin",
|
||||
Body: []byte{0xde, 0xad, 0xbe, 0xef},
|
||||
}
|
||||
if err := peerA.SendFile(want.To, want.FileName, want.Body); err != nil {
|
||||
t.Fatalf("SendFile() error = %v", err)
|
||||
}
|
||||
|
||||
got, err := peerB.Receive()
|
||||
if err != nil {
|
||||
t.Fatalf("peerB.Receive() error = %v", err)
|
||||
}
|
||||
if got.Type != want.Type || got.ID != want.ID || got.From != want.From || got.To != want.To || got.FileName != want.FileName || string(got.Body) != string(want.Body) {
|
||||
t.Fatalf("received message mismatch: got %+v want %+v", got, want)
|
||||
}
|
||||
}
|
||||
770
cmd/internal/peer/client_test.go
Normal file
770
cmd/internal/peer/client_test.go
Normal file
@@ -0,0 +1,770 @@
|
||||
package peer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"omnisocketgo/cmd/internal/latencylog"
|
||||
"omnisocketgo/cmd/internal/protocol"
|
||||
"omnisocketgo/cmd/internal/server"
|
||||
"omnisocketgo/cmd/internal/transport"
|
||||
)
|
||||
|
||||
type recordingLogger struct {
|
||||
mu sync.Mutex
|
||||
events []latencylog.Event
|
||||
}
|
||||
|
||||
func (l *recordingLogger) LogEvent(event latencylog.Event) error {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
l.events = append(l.events, event)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *recordingLogger) Events() []latencylog.Event {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
return append([]latencylog.Event(nil), l.events...)
|
||||
}
|
||||
|
||||
type failingLogger struct{}
|
||||
|
||||
func (failingLogger) LogEvent(latencylog.Event) error {
|
||||
return net.ErrClosed
|
||||
}
|
||||
|
||||
func TestDialRegistersPeer(t *testing.T) {
|
||||
hub := server.NewHub()
|
||||
cleanup := stubDialToHub(t, hub)
|
||||
defer cleanup()
|
||||
|
||||
client, err := Dial("ignored", "peer-a")
|
||||
if err != nil {
|
||||
t.Fatalf("Dial() error = %v", err)
|
||||
}
|
||||
defer func() { _ = client.Close() }()
|
||||
|
||||
waitFor(t, func() bool { return hub.HasPeer("peer-a") }, "peer-a to be registered")
|
||||
}
|
||||
|
||||
func TestClientsExchangeTextAndFileMessages(t *testing.T) {
|
||||
hub := server.NewHub()
|
||||
cleanup := stubDialToHub(t, hub)
|
||||
defer cleanup()
|
||||
|
||||
peerA, err := Dial("ignored", "peer-a")
|
||||
if err != nil {
|
||||
t.Fatalf("Dial(peer-a) error = %v", err)
|
||||
}
|
||||
defer func() { _ = peerA.Close() }()
|
||||
|
||||
peerB, err := Dial("ignored", "peer-b")
|
||||
if err != nil {
|
||||
t.Fatalf("Dial(peer-b) error = %v", err)
|
||||
}
|
||||
defer func() { _ = peerB.Close() }()
|
||||
|
||||
waitFor(t, func() bool { return hub.HasPeer("peer-a") && hub.HasPeer("peer-b") }, "both peers to be registered")
|
||||
|
||||
received := make(chan protocol.Message, 2)
|
||||
receiveErr := make(chan error, 1)
|
||||
go func() {
|
||||
for i := 0; i < 2; i++ {
|
||||
msg, err := peerB.Receive()
|
||||
if err != nil {
|
||||
receiveErr <- err
|
||||
return
|
||||
}
|
||||
received <- msg
|
||||
}
|
||||
receiveErr <- nil
|
||||
}()
|
||||
|
||||
if err := peerA.SendText("peer-b", "hello"); err != nil {
|
||||
t.Fatalf("SendText() error = %v", err)
|
||||
}
|
||||
fileBody := []byte{0x01, 0x02, 0x03}
|
||||
if err := peerA.SendFile("peer-b", "payload.bin", fileBody); err != nil {
|
||||
t.Fatalf("SendFile() error = %v", err)
|
||||
}
|
||||
|
||||
if err := <-receiveErr; err != nil {
|
||||
t.Fatalf("peerB.Receive() error = %v", err)
|
||||
}
|
||||
|
||||
gotFirst := <-received
|
||||
wantFirst := protocol.Message{
|
||||
Type: protocol.MessageTypeText,
|
||||
ID: 1,
|
||||
From: "peer-a",
|
||||
To: "peer-b",
|
||||
Body: []byte("hello"),
|
||||
}
|
||||
if !reflect.DeepEqual(gotFirst, wantFirst) {
|
||||
t.Fatalf("first message mismatch: got %+v want %+v", gotFirst, wantFirst)
|
||||
}
|
||||
|
||||
gotSecond := <-received
|
||||
wantSecond := protocol.Message{
|
||||
Type: protocol.MessageTypeFile,
|
||||
ID: 2,
|
||||
From: "peer-a",
|
||||
To: "peer-b",
|
||||
FileName: "payload.bin",
|
||||
Body: fileBody,
|
||||
}
|
||||
if !reflect.DeepEqual(gotSecond, wantSecond) {
|
||||
t.Fatalf("second message mismatch: got %+v want %+v", gotSecond, wantSecond)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientReceivesServerErrorForUnknownTarget(t *testing.T) {
|
||||
hub := server.NewHub()
|
||||
cleanup := stubDialToHub(t, hub)
|
||||
defer cleanup()
|
||||
|
||||
client, err := Dial("ignored", "peer-a")
|
||||
if err != nil {
|
||||
t.Fatalf("Dial() error = %v", err)
|
||||
}
|
||||
defer func() { _ = client.Close() }()
|
||||
|
||||
waitFor(t, func() bool { return hub.HasPeer("peer-a") }, "peer-a to be registered")
|
||||
|
||||
if err := client.SendText("missing-peer", "hello"); err != nil {
|
||||
t.Fatalf("SendText() error = %v", err)
|
||||
}
|
||||
|
||||
got, err := client.Receive()
|
||||
if err != nil {
|
||||
t.Fatalf("Receive() error = %v", err)
|
||||
}
|
||||
if got.Type != protocol.MessageTypeError {
|
||||
t.Fatalf("got type %s, want %s", got.Type, protocol.MessageTypeError)
|
||||
}
|
||||
if string(got.Body) != "unknown target: missing-peer" {
|
||||
t.Fatalf("error body = %q, want unknown target message", got.Body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientReceiveLoopHandlesForwardedMessages(t *testing.T) {
|
||||
hub := server.NewHub()
|
||||
cleanup := stubDialToHub(t, hub)
|
||||
defer cleanup()
|
||||
|
||||
peerA, err := Dial("ignored", "peer-a")
|
||||
if err != nil {
|
||||
t.Fatalf("Dial(peer-a) error = %v", err)
|
||||
}
|
||||
defer func() { _ = peerA.Close() }()
|
||||
|
||||
peerB, err := Dial("ignored", "peer-b")
|
||||
if err != nil {
|
||||
t.Fatalf("Dial(peer-b) error = %v", err)
|
||||
}
|
||||
defer func() { _ = peerB.Close() }()
|
||||
|
||||
waitFor(t, func() bool { return hub.HasPeer("peer-a") && hub.HasPeer("peer-b") }, "both peers to be registered")
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
got []protocol.Message
|
||||
)
|
||||
loopErr := make(chan error, 1)
|
||||
go func() {
|
||||
loopErr <- peerB.ReceiveLoop(func(msg protocol.Message) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
got = append(got, msg)
|
||||
if len(got) == 1 {
|
||||
return peerB.Close()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}()
|
||||
|
||||
if err := peerA.SendText("peer-b", "hello"); err != nil {
|
||||
t.Fatalf("SendText() error = %v", err)
|
||||
}
|
||||
|
||||
err = <-loopErr
|
||||
if err == nil || (!strings.Contains(err.Error(), "closed") && !strings.Contains(err.Error(), "use of closed network connection")) {
|
||||
t.Fatalf("ReceiveLoop() error = %v, want close-related error", err)
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
want := []protocol.Message{
|
||||
{
|
||||
Type: protocol.MessageTypeText,
|
||||
ID: 1,
|
||||
From: "peer-a",
|
||||
To: "peer-b",
|
||||
Body: []byte("hello"),
|
||||
},
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("received messages mismatch: got %+v want %+v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientSendLogsLatencyEvents(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(*testing.T) string
|
||||
send func(*Client, string) error
|
||||
wantMsg protocol.Message
|
||||
wantEvents []string
|
||||
}{
|
||||
{
|
||||
name: "text",
|
||||
send: func(client *Client, _ string) error {
|
||||
return client.SendText("peer-b", "hello")
|
||||
},
|
||||
wantMsg: protocol.Message{
|
||||
Type: protocol.MessageTypeText,
|
||||
ID: 1,
|
||||
From: "peer-a",
|
||||
To: "peer-b",
|
||||
Body: []byte("hello"),
|
||||
},
|
||||
wantEvents: []string{
|
||||
latencylog.EventAAppPrepBegin,
|
||||
latencylog.EventSendHandoffBegin,
|
||||
latencylog.EventATXSched,
|
||||
latencylog.EventATXSoftware,
|
||||
latencylog.EventSendHandoffEnd,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "file-bytes",
|
||||
send: func(client *Client, _ string) error {
|
||||
return client.SendFile("peer-b", "payload.bin", []byte{0x01, 0x02, 0x03})
|
||||
},
|
||||
wantMsg: protocol.Message{
|
||||
Type: protocol.MessageTypeFile,
|
||||
ID: 1,
|
||||
From: "peer-a",
|
||||
To: "peer-b",
|
||||
FileName: "payload.bin",
|
||||
Body: []byte{0x01, 0x02, 0x03},
|
||||
},
|
||||
wantEvents: []string{
|
||||
latencylog.EventAAppPrepBegin,
|
||||
latencylog.EventSendHandoffBegin,
|
||||
latencylog.EventATXSched,
|
||||
latencylog.EventATXSoftware,
|
||||
latencylog.EventSendHandoffEnd,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "file-path",
|
||||
setup: func(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
path := filepath.Join(t.TempDir(), "payload.bin")
|
||||
if err := os.WriteFile(path, []byte{0x01, 0x02, 0x03}, 0o644); err != nil {
|
||||
t.Fatalf("os.WriteFile() error = %v", err)
|
||||
}
|
||||
|
||||
return path
|
||||
},
|
||||
send: func(client *Client, path string) error {
|
||||
return client.SendFilePath("peer-b", path)
|
||||
},
|
||||
wantMsg: protocol.Message{
|
||||
Type: protocol.MessageTypeFile,
|
||||
ID: 1,
|
||||
From: "peer-a",
|
||||
To: "peer-b",
|
||||
FileName: "payload.bin",
|
||||
Body: []byte{0x01, 0x02, 0x03},
|
||||
},
|
||||
wantEvents: []string{
|
||||
latencylog.EventAAppPrepBegin,
|
||||
latencylog.EventSendHandoffBegin,
|
||||
latencylog.EventATXSched,
|
||||
latencylog.EventATXSoftware,
|
||||
latencylog.EventSendHandoffEnd,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
inputPath := ""
|
||||
if tt.setup != nil {
|
||||
inputPath = tt.setup(t)
|
||||
}
|
||||
|
||||
logger := &recordingLogger{}
|
||||
clientConn, receiver := newClientTransportPair(
|
||||
t,
|
||||
[]transport.Option{transport.WithLogger(logger, latencylog.NodeRolePeer, "peer-a")},
|
||||
nil,
|
||||
)
|
||||
client := &Client{
|
||||
id: "peer-a",
|
||||
conn: clientConn,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
sendErr := make(chan error, 1)
|
||||
go func() {
|
||||
sendErr <- tt.send(client, inputPath)
|
||||
}()
|
||||
|
||||
got, err := receiver.Receive()
|
||||
if err != nil {
|
||||
t.Fatalf("receiver.Receive() error = %v", err)
|
||||
}
|
||||
if err := <-sendErr; err != nil {
|
||||
t.Fatalf("send() error = %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.wantMsg) {
|
||||
t.Fatalf("message mismatch: got %+v want %+v", got, tt.wantMsg)
|
||||
}
|
||||
|
||||
events := logger.Events()
|
||||
if len(events) != len(tt.wantEvents) {
|
||||
t.Fatalf("event count = %d, want %d", len(events), len(tt.wantEvents))
|
||||
}
|
||||
for i, wantEvent := range tt.wantEvents {
|
||||
if events[i].Event != wantEvent {
|
||||
t.Fatalf("event[%d] = %q, want %q", i, events[i].Event, wantEvent)
|
||||
}
|
||||
if events[i].MessageID != tt.wantMsg.ID || events[i].From != tt.wantMsg.From || events[i].To != tt.wantMsg.To {
|
||||
t.Fatalf("event[%d] metadata mismatch: %+v", i, events[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientReceiveLogsOnlyBusinessMessages(t *testing.T) {
|
||||
logger := &recordingLogger{}
|
||||
clientConn, sender := newClientTransportPair(
|
||||
t,
|
||||
[]transport.Option{transport.WithLogger(logger, latencylog.NodeRolePeer, "peer-b")},
|
||||
nil,
|
||||
)
|
||||
client := &Client{
|
||||
id: "peer-b",
|
||||
conn: clientConn,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
textMsg := protocol.Message{
|
||||
Type: protocol.MessageTypeText,
|
||||
ID: 21,
|
||||
From: "peer-a",
|
||||
To: "peer-b",
|
||||
Body: []byte("hello"),
|
||||
}
|
||||
sendErr := make(chan error, 1)
|
||||
go func() {
|
||||
sendErr <- sender.Send(textMsg)
|
||||
}()
|
||||
if _, err := client.Receive(); err != nil {
|
||||
t.Fatalf("client.Receive(text) error = %v", err)
|
||||
}
|
||||
if err := <-sendErr; err != nil {
|
||||
t.Fatalf("sender.Send(text) error = %v", err)
|
||||
}
|
||||
|
||||
errorMsg := protocol.Message{
|
||||
Type: protocol.MessageTypeError,
|
||||
ID: 22,
|
||||
From: protocol.ServerPeerID,
|
||||
To: "peer-b",
|
||||
Body: []byte("failure"),
|
||||
}
|
||||
sendErr = make(chan error, 1)
|
||||
go func() {
|
||||
sendErr <- sender.Send(errorMsg)
|
||||
}()
|
||||
if _, err := client.Receive(); err != nil {
|
||||
t.Fatalf("client.Receive(error) error = %v", err)
|
||||
}
|
||||
if err := <-sendErr; err != nil {
|
||||
t.Fatalf("sender.Send(error) error = %v", err)
|
||||
}
|
||||
|
||||
events := logger.Events()
|
||||
if len(events) != 2 {
|
||||
t.Fatalf("event count = %d, want 2", len(events))
|
||||
}
|
||||
if events[0].Event != latencylog.EventBRXSoftware {
|
||||
t.Fatalf("first event = %q, want %q", events[0].Event, latencylog.EventBRXSoftware)
|
||||
}
|
||||
if events[1].Event != latencylog.EventBAppRecv {
|
||||
t.Fatalf("second event = %q, want %q", events[1].Event, latencylog.EventBAppRecv)
|
||||
}
|
||||
if events[0].MessageID != textMsg.ID || events[1].MessageID != textMsg.ID {
|
||||
t.Fatalf("message IDs = %d,%d, want %d", events[0].MessageID, events[1].MessageID, textMsg.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientPersistTextMessageWritesInboxFileAndLogs(t *testing.T) {
|
||||
inboxDir := t.TempDir()
|
||||
logger := &recordingLogger{}
|
||||
client := &Client{
|
||||
id: "peer-b",
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
msg := protocol.Message{
|
||||
Type: protocol.MessageTypeText,
|
||||
ID: 31,
|
||||
From: "peer-a",
|
||||
To: "peer-b",
|
||||
Body: []byte("hello"),
|
||||
}
|
||||
|
||||
path, err := client.PersistMessage(msg, inboxDir)
|
||||
if err != nil {
|
||||
t.Fatalf("PersistMessage() error = %v", err)
|
||||
}
|
||||
|
||||
if path != filepath.Join(inboxDir, textInboxFileName) {
|
||||
t.Fatalf("path = %q, want %q", path, filepath.Join(inboxDir, textInboxFileName))
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("os.ReadFile() error = %v", err)
|
||||
}
|
||||
|
||||
var record textInboxRecord
|
||||
if err := json.Unmarshal(bytes.TrimSpace(data), &record); err != nil {
|
||||
t.Fatalf("json.Unmarshal() error = %v", err)
|
||||
}
|
||||
if record.MessageID != msg.ID || record.From != msg.From || record.To != msg.To || record.Body != "hello" {
|
||||
t.Fatalf("record mismatch: got %+v want message %+v", record, msg)
|
||||
}
|
||||
|
||||
events := logger.Events()
|
||||
if len(events) != 2 {
|
||||
t.Fatalf("event count = %d, want 2", len(events))
|
||||
}
|
||||
if events[0].Event != latencylog.EventBPersistBegin {
|
||||
t.Fatalf("first event = %q, want %q", events[0].Event, latencylog.EventBPersistBegin)
|
||||
}
|
||||
if events[1].Event != latencylog.EventBPersistEnd {
|
||||
t.Fatalf("second event = %q, want %q", events[1].Event, latencylog.EventBPersistEnd)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientPersistFileMessageWritesInboxFileAndLogs(t *testing.T) {
|
||||
inboxDir := t.TempDir()
|
||||
logger := &recordingLogger{}
|
||||
client := &Client{
|
||||
id: "peer-b",
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
msg := protocol.Message{
|
||||
Type: protocol.MessageTypeFile,
|
||||
ID: 32,
|
||||
From: "peer-a",
|
||||
To: "peer-b",
|
||||
FileName: "payload.bin",
|
||||
Body: []byte{0x01, 0x02, 0x03},
|
||||
}
|
||||
|
||||
path, err := client.PersistMessage(msg, inboxDir)
|
||||
if err != nil {
|
||||
t.Fatalf("PersistMessage() error = %v", err)
|
||||
}
|
||||
|
||||
wantPath := filepath.Join(inboxDir, "peer-a-32-payload.bin")
|
||||
if path != wantPath {
|
||||
t.Fatalf("path = %q, want %q", path, wantPath)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("os.ReadFile() error = %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(data, msg.Body) {
|
||||
t.Fatalf("file body mismatch: got %v want %v", data, msg.Body)
|
||||
}
|
||||
|
||||
events := logger.Events()
|
||||
if len(events) != 2 {
|
||||
t.Fatalf("event count = %d, want 2", len(events))
|
||||
}
|
||||
if events[0].Event != latencylog.EventBPersistBegin {
|
||||
t.Fatalf("first event = %q, want %q", events[0].Event, latencylog.EventBPersistBegin)
|
||||
}
|
||||
if events[1].Event != latencylog.EventBPersistEnd {
|
||||
t.Fatalf("second event = %q, want %q", events[1].Event, latencylog.EventBPersistEnd)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientPersistMessageReturnsErrorOnWriteFailure(t *testing.T) {
|
||||
blocker := filepath.Join(t.TempDir(), "blocker")
|
||||
if err := os.WriteFile(blocker, []byte("not a directory"), 0o644); err != nil {
|
||||
t.Fatalf("os.WriteFile() error = %v", err)
|
||||
}
|
||||
|
||||
logger := &recordingLogger{}
|
||||
client := &Client{
|
||||
id: "peer-b",
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
msg := protocol.Message{
|
||||
Type: protocol.MessageTypeText,
|
||||
ID: 33,
|
||||
From: "peer-a",
|
||||
To: "peer-b",
|
||||
Body: []byte("hello"),
|
||||
}
|
||||
|
||||
if _, err := client.PersistMessage(msg, blocker); err == nil {
|
||||
t.Fatal("PersistMessage() error = nil, want non-nil")
|
||||
}
|
||||
|
||||
events := logger.Events()
|
||||
if len(events) != 1 {
|
||||
t.Fatalf("event count = %d, want 1", len(events))
|
||||
}
|
||||
if events[0].Event != latencylog.EventBPersistBegin {
|
||||
t.Fatalf("event = %q, want %q", events[0].Event, latencylog.EventBPersistBegin)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientIgnoresLoggerFailure(t *testing.T) {
|
||||
clientConn, receiver := newClientTransportPair(
|
||||
t,
|
||||
[]transport.Option{transport.WithLogger(failingLogger{}, latencylog.NodeRolePeer, "peer-a")},
|
||||
nil,
|
||||
)
|
||||
client := &Client{
|
||||
id: "peer-a",
|
||||
conn: clientConn,
|
||||
logger: failingLogger{},
|
||||
}
|
||||
|
||||
sendErr := make(chan error, 1)
|
||||
go func() {
|
||||
sendErr <- client.SendText("peer-b", "hello")
|
||||
}()
|
||||
|
||||
got, err := receiver.Receive()
|
||||
if err != nil {
|
||||
t.Fatalf("receiver.Receive() error = %v", err)
|
||||
}
|
||||
if err := <-sendErr; err != nil {
|
||||
t.Fatalf("SendText() error = %v, want nil even if logger fails", err)
|
||||
}
|
||||
if string(got.Body) != "hello" {
|
||||
t.Fatalf("body = %q, want hello", got.Body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientPersistIgnoresLoggerFailure(t *testing.T) {
|
||||
client := &Client{
|
||||
id: "peer-b",
|
||||
logger: failingLogger{},
|
||||
}
|
||||
|
||||
msg := protocol.Message{
|
||||
Type: protocol.MessageTypeText,
|
||||
ID: 34,
|
||||
From: "peer-a",
|
||||
To: "peer-b",
|
||||
Body: []byte("hello"),
|
||||
}
|
||||
|
||||
path, err := client.PersistMessage(msg, t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatalf("PersistMessage() error = %v, want nil even if logger fails", err)
|
||||
}
|
||||
if path == "" {
|
||||
t.Fatal("PersistMessage() path = empty, want non-empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientsExchangeMessagesWithLatencyLogs(t *testing.T) {
|
||||
hub := server.NewHub()
|
||||
cleanup := stubDialToHub(t, hub)
|
||||
defer cleanup()
|
||||
|
||||
peerALogger := &recordingLogger{}
|
||||
peerA, err := Dial("ignored", "peer-a", WithLogger(peerALogger))
|
||||
if err != nil {
|
||||
t.Fatalf("Dial(peer-a) error = %v", err)
|
||||
}
|
||||
defer func() { _ = peerA.Close() }()
|
||||
|
||||
peerBLogger := &recordingLogger{}
|
||||
peerB, err := Dial("ignored", "peer-b", WithLogger(peerBLogger))
|
||||
if err != nil {
|
||||
t.Fatalf("Dial(peer-b) error = %v", err)
|
||||
}
|
||||
defer func() { _ = peerB.Close() }()
|
||||
|
||||
inboxDir := t.TempDir()
|
||||
|
||||
waitFor(t, func() bool { return hub.HasPeer("peer-a") && hub.HasPeer("peer-b") }, "both peers to be registered")
|
||||
|
||||
if err := peerA.SendText("peer-b", "hello"); err != nil {
|
||||
t.Fatalf("SendText() error = %v", err)
|
||||
}
|
||||
textMsg, err := peerB.Receive()
|
||||
if err != nil {
|
||||
t.Fatalf("peerB.Receive(text) error = %v", err)
|
||||
}
|
||||
if _, err := peerB.PersistMessage(textMsg, inboxDir); err != nil {
|
||||
t.Fatalf("peerB.PersistMessage(text) error = %v", err)
|
||||
}
|
||||
|
||||
if err := peerA.SendFile("peer-b", "payload.bin", []byte{0x01, 0x02, 0x03}); err != nil {
|
||||
t.Fatalf("SendFile() error = %v", err)
|
||||
}
|
||||
fileMsg, err := peerB.Receive()
|
||||
if err != nil {
|
||||
t.Fatalf("peerB.Receive(file) error = %v", err)
|
||||
}
|
||||
if _, err := peerB.PersistMessage(fileMsg, inboxDir); err != nil {
|
||||
t.Fatalf("peerB.PersistMessage(file) error = %v", err)
|
||||
}
|
||||
|
||||
waitFor(t, func() bool { return len(peerALogger.Events()) == 10 }, "peer-a latency events")
|
||||
waitFor(t, func() bool { return len(peerBLogger.Events()) == 8 }, "peer-b latency events")
|
||||
|
||||
assertEventSequencesByMessage(t, peerALogger.Events(), map[uint64][]string{
|
||||
1: {latencylog.EventAAppPrepBegin, latencylog.EventSendHandoffBegin, latencylog.EventATXSched, latencylog.EventATXSoftware, latencylog.EventSendHandoffEnd},
|
||||
2: {latencylog.EventAAppPrepBegin, latencylog.EventSendHandoffBegin, latencylog.EventATXSched, latencylog.EventATXSoftware, latencylog.EventSendHandoffEnd},
|
||||
})
|
||||
assertEventSequencesByMessage(t, peerBLogger.Events(), map[uint64][]string{
|
||||
1: {latencylog.EventBRXSoftware, latencylog.EventBAppRecv, latencylog.EventBPersistBegin, latencylog.EventBPersistEnd},
|
||||
2: {latencylog.EventBRXSoftware, latencylog.EventBAppRecv, latencylog.EventBPersistBegin, latencylog.EventBPersistEnd},
|
||||
})
|
||||
}
|
||||
|
||||
func stubDialToHub(t *testing.T, hub *server.Hub) func() {
|
||||
t.Helper()
|
||||
|
||||
originalDial := dialServer
|
||||
serverAddr, cleanup := startRealHubServer(t, hub)
|
||||
|
||||
dialServer = func(network, addr string) (net.Conn, error) {
|
||||
return net.Dial(network, serverAddr)
|
||||
}
|
||||
|
||||
return func() {
|
||||
dialServer = originalDial
|
||||
cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
func waitFor(t *testing.T, condition func() bool, description string) {
|
||||
t.Helper()
|
||||
|
||||
deadline := time.Now().Add(500 * time.Millisecond)
|
||||
for time.Now().Before(deadline) {
|
||||
if condition() {
|
||||
return
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
t.Fatalf("timed out waiting for %s", description)
|
||||
}
|
||||
|
||||
func assertEventSequencesByMessage(t *testing.T, events []latencylog.Event, want map[uint64][]string) {
|
||||
t.Helper()
|
||||
|
||||
grouped := make(map[uint64][]latencylog.Event)
|
||||
for _, event := range events {
|
||||
grouped[event.MessageID] = append(grouped[event.MessageID], event)
|
||||
if event.TsUnixNano <= 0 {
|
||||
t.Fatalf("event timestamp must be positive: %+v", event)
|
||||
}
|
||||
}
|
||||
|
||||
if len(grouped) != len(want) {
|
||||
t.Fatalf("message group count = %d, want %d", len(grouped), len(want))
|
||||
}
|
||||
|
||||
for messageID, wantEvents := range want {
|
||||
gotEvents := grouped[messageID]
|
||||
if len(gotEvents) != len(wantEvents) {
|
||||
t.Fatalf("message %d event count = %d, want %d", messageID, len(gotEvents), len(wantEvents))
|
||||
}
|
||||
for i, wantEvent := range wantEvents {
|
||||
if gotEvents[i].Event != wantEvent {
|
||||
t.Fatalf("message %d event[%d] = %q, want %q", messageID, i, gotEvents[i].Event, wantEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newClientTransportPair(t *testing.T, clientOpts []transport.Option, peerOpts []transport.Option) (*transport.TCPConn, *transport.TCPConn) {
|
||||
t.Helper()
|
||||
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("net.Listen() error = %v", err)
|
||||
}
|
||||
|
||||
type acceptResult struct {
|
||||
conn net.Conn
|
||||
err error
|
||||
}
|
||||
|
||||
accepted := make(chan acceptResult, 1)
|
||||
go func() {
|
||||
conn, acceptErr := listener.Accept()
|
||||
accepted <- acceptResult{conn: conn, err: acceptErr}
|
||||
}()
|
||||
|
||||
clientSide, err := net.Dial("tcp", listener.Addr().String())
|
||||
if err != nil {
|
||||
_ = listener.Close()
|
||||
t.Fatalf("net.Dial() error = %v", err)
|
||||
}
|
||||
|
||||
result := <-accepted
|
||||
if err := listener.Close(); err != nil {
|
||||
t.Fatalf("listener.Close() error = %v", err)
|
||||
}
|
||||
if result.err != nil {
|
||||
_ = clientSide.Close()
|
||||
t.Fatalf("listener.Accept() error = %v", result.err)
|
||||
}
|
||||
|
||||
clientConn, err := transport.NewTCPConn(clientSide, clientOpts...)
|
||||
if err != nil {
|
||||
_ = clientSide.Close()
|
||||
_ = result.conn.Close()
|
||||
t.Fatalf("transport.NewTCPConn(client) error = %v", err)
|
||||
}
|
||||
peerConn, err := transport.NewTCPConn(result.conn, peerOpts...)
|
||||
if err != nil {
|
||||
_ = clientConn.Close()
|
||||
_ = result.conn.Close()
|
||||
t.Fatalf("transport.NewTCPConn(peer) error = %v", err)
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
_ = clientConn.Close()
|
||||
_ = peerConn.Close()
|
||||
})
|
||||
|
||||
return clientConn, peerConn
|
||||
}
|
||||
99
cmd/internal/peer/persist.go
Normal file
99
cmd/internal/peer/persist.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package peer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"omnisocketgo/cmd/internal/latencylog"
|
||||
"omnisocketgo/cmd/internal/protocol"
|
||||
)
|
||||
|
||||
const textInboxFileName = "messages.log"
|
||||
|
||||
type textInboxRecord struct {
|
||||
MessageType protocol.MessageType `json:"message_type"`
|
||||
MessageID uint64 `json:"message_id"`
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
// PersistMessage 将收到的业务消息写入本地磁盘,并记录处理完成节点。
|
||||
func (c *Client) PersistMessage(msg protocol.Message, inboxDir string) (string, error) {
|
||||
if !latencylog.IsBusinessMessage(msg) {
|
||||
return "", fmt.Errorf("peer: cannot persist message type %s", msg.Type)
|
||||
}
|
||||
if inboxDir == "" {
|
||||
return "", fmt.Errorf("peer: inbox directory is required")
|
||||
}
|
||||
|
||||
latencylog.LogMessageEvent(c.logger, latencylog.NodeRolePeer, c.id, latencylog.EventBPersistBegin, msg)
|
||||
|
||||
if err := os.MkdirAll(inboxDir, 0o755); err != nil {
|
||||
return "", fmt.Errorf("peer: create inbox dir %s: %w", inboxDir, err)
|
||||
}
|
||||
|
||||
path, err := persistMessageToDisk(msg, inboxDir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
latencylog.LogMessageEvent(c.logger, latencylog.NodeRolePeer, c.id, latencylog.EventBPersistEnd, msg)
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// persistMessageToDisk 根据消息类型将消息内容写入磁盘,文本消息追加到文本日志文件,文件消息写成独立文件。
|
||||
func persistMessageToDisk(msg protocol.Message, inboxDir string) (string, error) {
|
||||
switch msg.Type {
|
||||
case protocol.MessageTypeText:
|
||||
return persistTextMessage(msg, inboxDir)
|
||||
case protocol.MessageTypeFile:
|
||||
return persistFileMessage(msg, inboxDir)
|
||||
default:
|
||||
return "", fmt.Errorf("peer: cannot persist unsupported message type %s", msg.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// registerPeer 验证 peer ID 的合法性和唯一性,并将其与连接关联起来。
|
||||
func persistTextMessage(msg protocol.Message, inboxDir string) (string, error) {
|
||||
record := textInboxRecord{
|
||||
MessageType: msg.Type,
|
||||
MessageID: msg.ID,
|
||||
From: msg.From,
|
||||
To: msg.To,
|
||||
Body: string(msg.Body),
|
||||
}
|
||||
|
||||
line, err := json.Marshal(record)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("peer: encode text inbox record: %w", err)
|
||||
}
|
||||
|
||||
path := filepath.Join(inboxDir, textInboxFileName)
|
||||
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("peer: open text inbox %s: %w", path, err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if _, err := file.Write(append(line, '\n')); err != nil {
|
||||
return "", fmt.Errorf("peer: append text inbox %s: %w", path, err)
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// persistFileMessage 将文件消息的内容写成独立文件,文件名包含发送方、消息 ID 和原始文件名,保证唯一性和可读性。
|
||||
func persistFileMessage(msg protocol.Message, inboxDir string) (string, error) {
|
||||
fileName := filepath.Base(msg.FileName)
|
||||
path := filepath.Join(inboxDir, fmt.Sprintf("%s-%d-%s", msg.From, msg.ID, fileName))
|
||||
|
||||
if err := os.WriteFile(path, msg.Body, 0o644); err != nil {
|
||||
return "", fmt.Errorf("peer: write received file %s: %w", path, err)
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
Reference in New Issue
Block a user