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

89
cmd/peer/interactive.go Normal file
View File

@@ -0,0 +1,89 @@
package main
import (
"errors"
"fmt"
"io"
"strings"
)
const (
interactiveCommandHelp = "help"
interactiveCommandQuit = "quit"
interactiveCommandText = "text"
interactiveCommandFile = "file"
)
// 交互式命令行界面,允许用户在连接建立后反复发送文本或文件消息。
var errEmptyInteractiveCommand = errors.New("interactive command is empty")
type interactiveCommand struct {
name string
to string
value string
}
// 解析用户输入的交互式命令,支持发送文本或文件消息,以及查看帮助和退出。
func parseInteractiveCommand(line string) (interactiveCommand, error) {
commandName, rest, ok := cutInteractiveField(strings.TrimSpace(line))
if !ok {
return interactiveCommand{}, errEmptyInteractiveCommand
}
switch strings.ToLower(commandName) {
case "help", "h", "?":
return interactiveCommand{name: interactiveCommandHelp}, nil
case "quit", "exit":
return interactiveCommand{name: interactiveCommandQuit}, nil
case interactiveCommandText:
to, body, err := parseInteractiveTargetValue(rest, interactiveCommandText)
if err != nil {
return interactiveCommand{}, err
}
return interactiveCommand{name: interactiveCommandText, to: to, value: body}, nil
case interactiveCommandFile:
to, path, err := parseInteractiveTargetValue(rest, interactiveCommandFile)
if err != nil {
return interactiveCommand{}, err
}
return interactiveCommand{name: interactiveCommandFile, to: to, value: path}, nil
default:
return interactiveCommand{}, fmt.Errorf("unknown command %q; type help for usage", commandName)
}
}
func parseInteractiveTargetValue(rest, commandName string) (string, string, error) {
to, value, ok := cutInteractiveField(strings.TrimSpace(rest))
if !ok {
return "", "", fmt.Errorf("%s command requires a target peer and payload", commandName)
}
if strings.TrimSpace(value) == "" {
return "", "", fmt.Errorf("%s command requires a non-empty payload", commandName)
}
return to, strings.TrimSpace(value), nil
}
func cutInteractiveField(input string) (string, string, bool) {
trimmed := strings.TrimSpace(input)
if trimmed == "" {
return "", "", false
}
for i, r := range trimmed {
if r == ' ' || r == '\t' {
return trimmed[:i], strings.TrimSpace(trimmed[i+1:]), true
}
}
return trimmed, "", true
}
// 打印交互式命令帮助信息,列出可用的命令和用法说明。
func printInteractiveHelp(w io.Writer) {
_, _ = fmt.Fprintln(w, "interactive mode commands:")
_, _ = fmt.Fprintln(w, " help show this help")
_, _ = fmt.Fprintln(w, " text <peer> <message> send one text message over the existing connection")
_, _ = fmt.Fprintln(w, " file <peer> <path> send one file over the existing connection")
_, _ = fmt.Fprintln(w, " quit exit this peer process")
}

View File

@@ -0,0 +1,87 @@
package main
import "testing"
func TestParseInteractiveCommand(t *testing.T) {
tests := []struct {
name string
line string
want interactiveCommand
wantErr string
}{
{
name: "text command preserves spaces in body",
line: "text peer-b hello over the same connection",
want: interactiveCommand{
name: interactiveCommandText,
to: "peer-b",
value: "hello over the same connection",
},
},
{
name: "file command preserves spaces in path",
line: "file peer-b /tmp/demo payload.bin",
want: interactiveCommand{
name: interactiveCommandFile,
to: "peer-b",
value: "/tmp/demo payload.bin",
},
},
{
name: "help alias",
line: "?",
want: interactiveCommand{
name: interactiveCommandHelp,
},
},
{
name: "quit alias",
line: "exit",
want: interactiveCommand{
name: interactiveCommandQuit,
},
},
{
name: "empty command",
line: " ",
wantErr: errEmptyInteractiveCommand.Error(),
},
{
name: "text requires payload",
line: "text peer-b",
wantErr: "text command requires a non-empty payload",
},
{
name: "file requires target and payload",
line: "file",
wantErr: "file command requires a target peer and payload",
},
{
name: "unknown command",
line: "ping peer-b",
wantErr: `unknown command "ping"; type help for usage`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseInteractiveCommand(tt.line)
if tt.wantErr != "" {
if err == nil {
t.Fatalf("parseInteractiveCommand(%q) error = nil, want %q", tt.line, tt.wantErr)
}
if err.Error() != tt.wantErr {
t.Fatalf("parseInteractiveCommand(%q) error = %q, want %q", tt.line, err.Error(), tt.wantErr)
}
return
}
if err != nil {
t.Fatalf("parseInteractiveCommand(%q) error = %v", tt.line, err)
}
if got != tt.want {
t.Fatalf("parseInteractiveCommand(%q) = %+v, want %+v", tt.line, got, tt.want)
}
})
}
}

173
cmd/peer/main.go Normal file
View File

@@ -0,0 +1,173 @@
package main
import (
"bufio"
"flag"
"fmt"
"io"
"log"
"os"
"omnisocketgo/cmd/internal/latencylog"
peerpkg "omnisocketgo/cmd/internal/peer"
"omnisocketgo/cmd/internal/protocol"
)
func main() {
peerID := flag.String("id", "peer-a", "peer identity") // peer 的标识
serverAddr := flag.String("server", "127.0.0.1:9000", "server address") // server 的地址
targetPeer := flag.String("to", "", "optional target peer for one outgoing message") // 可选的目标 peer 标识
text := flag.String("text", "", "optional text to send after connecting") // 可选的文本消息内容
filePath := flag.String("file", "", "optional file path to send after connecting")
inboxDir := flag.String("inbox-dir", "inbox", "directory used to persist received text and file messages")
logPath := flag.String("latency-log", "", "optional JSONL file path for latency timestamp logs")
interactive := flag.Bool("interactive", true, "enable interactive REPL for repeated text/file sends on the same connection")
flag.Parse()
clientOptions := make([]peerpkg.Option, 0, 1)
if *logPath != "" {
logger, err := latencylog.NewJSONLLogger(*logPath)
if err != nil {
log.Fatalf("create latency logger %s: %v", *logPath, err)
}
defer logger.Close()
clientOptions = append(clientOptions, peerpkg.WithLogger(logger))
}
client, err := peerpkg.Dial(*serverAddr, *peerID, clientOptions...)
if err != nil {
log.Fatalf("dial server %s: %v", *serverAddr, err)
}
defer client.Close()
log.Printf("connected to %s as %s", *serverAddr, client.ID())
receiveErr := make(chan error, 1)
go func() {
receiveErr <- client.ReceiveLoop(func(msg protocol.Message) error {
switch msg.Type {
case protocol.MessageTypeText:
path, err := client.PersistMessage(msg, *inboxDir)
if err != nil {
return err
}
log.Printf("received text from %s to %s and persisted to %s", msg.From, msg.To, path)
case protocol.MessageTypeFile:
path, err := client.PersistMessage(msg, *inboxDir)
if err != nil {
return err
}
log.Printf("received file from %s to %s: %s (%d bytes) -> %s", msg.From, msg.To, msg.FileName, len(msg.Body), path)
case protocol.MessageTypeError:
log.Printf("received %s from %s to %s: %s", msg.Type, msg.From, msg.To, string(msg.Body))
default:
log.Printf("received unexpected message type %s from %s", msg.Type, msg.From)
}
return nil
})
}()
if *text != "" && *filePath != "" {
log.Fatal("only one of -text or -file may be specified")
}
if (*text != "" || *filePath != "") && *targetPeer == "" {
log.Fatal("flag -to is required when sending text or file")
}
//如果指定了目标 peer 和文本消息内容,则向目标 peer 发送一条文本消息,如果发送失败,打印错误日志并退出。
if *targetPeer != "" && *text != "" {
if err := client.SendText(*targetPeer, *text); err != nil {
log.Fatalf("send text to %s: %v", *targetPeer, err)
}
log.Printf("sent text to %s", *targetPeer)
}
if *targetPeer != "" && *filePath != "" {
if err := client.SendFilePath(*targetPeer, *filePath); err != nil {
log.Fatalf("send file %s to %s: %v", *filePath, *targetPeer, err)
}
log.Printf("sent file %s to %s", *filePath, *targetPeer)
}
if *interactive {
if err := runInteractiveShell(client, os.Stdin, os.Stdout, receiveErr); err != nil {
log.Printf("interactive shell ended: %v", err)
}
return
}
if err := <-receiveErr; err != nil {
log.Printf("receive loop ended: %v", err)
}
}
func runInteractiveShell(client *peerpkg.Client, in io.Reader, out io.Writer, receiveErr <-chan error) error {
printInteractiveHelp(out)
lines, inputErr := readInteractiveLines(in, out, fmt.Sprintf("%s> ", client.ID()))
for {
select {
case err := <-receiveErr:
return err
case line, ok := <-lines:
if !ok {
return <-inputErr
}
command, err := parseInteractiveCommand(line)
if err != nil {
if err == errEmptyInteractiveCommand {
continue
}
log.Printf("interactive command error: %v", err)
continue
}
switch command.name {
case interactiveCommandHelp:
printInteractiveHelp(out)
case interactiveCommandQuit:
return nil
case interactiveCommandText:
if err := client.SendText(command.to, command.value); err != nil {
log.Printf("send text to %s: %v", command.to, err)
continue
}
log.Printf("sent text to %s", command.to)
case interactiveCommandFile:
if err := client.SendFilePath(command.to, command.value); err != nil {
log.Printf("send file %s to %s: %v", command.value, command.to, err)
continue
}
log.Printf("sent file %s to %s", command.value, command.to)
}
}
}
}
func readInteractiveLines(in io.Reader, out io.Writer, prompt string) (<-chan string, <-chan error) {
lines := make(chan string)
errs := make(chan error, 1)
go func() {
defer close(lines)
scanner := bufio.NewScanner(in)
scanner.Buffer(make([]byte, 0, 1024), 1024*1024)
for {
if _, err := fmt.Fprint(out, prompt); err != nil {
errs <- err
return
}
if !scanner.Scan() {
errs <- scanner.Err()
return
}
lines <- scanner.Text()
}
}()
return lines, errs
}