diff --git a/build/anjian/main.go b/build/anjian/main.go new file mode 100644 index 0000000..8d3f01d --- /dev/null +++ b/build/anjian/main.go @@ -0,0 +1,79 @@ +package main + +import ( + "syscall" + "time" + "unsafe" +) + +// 定义Windows API所需的结构体和常量(对应SendInput函数参数) +const ( + INPUT_MOUSE = 0x0000 // 输入类型:鼠标 + MOUSEEVENTF_LEFTDOWN = 0x0002 // 左键按下 + MOUSEEVENTF_LEFTUP = 0x0004 // 左键释放 +) + +// INPUT结构体:SendInput的输入参数 +type INPUT struct { + Type uint32 + Mi MOUSEINPUT +} + +// MOUSEINPUT结构体:鼠标输入详情 +type MOUSEINPUT struct { + Dx int32 + Dy int32 + MouseData uint32 + DwFlags uint32 + Time uint32 + DwExtraInfo uintptr +} + +func main() { + // 加载user32.dll并获取SendInput函数地址 + user32, err := syscall.LoadLibrary("user32.dll") + if err != nil { + panic(err) + } + defer syscall.FreeLibrary(user32) + + sendInputProc, err := syscall.GetProcAddress(user32, "SendInput") + if err != nil { + panic(err) + } + + // 定义一次完整点击的输入(按下+释放) + inputs := []INPUT{ + { + Type: INPUT_MOUSE, + Mi: MOUSEINPUT{ + DwFlags: MOUSEEVENTF_LEFTDOWN, // 左键按下 + }, + }, + { + Type: INPUT_MOUSE, + Mi: MOUSEINPUT{ + DwFlags: MOUSEEVENTF_LEFTUP, // 左键释放 + }, + }, + } + + // 循环执行1000次点击,间隔10毫秒 + for i := 0; i < 1000; i++ { + // 调用SendInput发送鼠标事件(参数:输入数量、输入数组指针、单个输入大小) + var args []uintptr + args = append(args, uintptr(len(inputs))) + args = append(args, uintptr(unsafe.Pointer(&inputs[0]))) + args = append(args, uintptr(unsafe.Sizeof(INPUT{}))) + + ret, _, _ := syscall.SyscallN(sendInputProc, args...) + if ret == 0 { + panic("发送鼠标事件失败") + } + + // 间隔10毫秒 + time.Sleep(10 * time.Millisecond) + } + + println("已完成1000次鼠标点击") +} diff --git a/build/ico/main.go b/build/ico/main.go new file mode 100644 index 0000000..30e8238 --- /dev/null +++ b/build/ico/main.go @@ -0,0 +1,50 @@ +package main + +import ( + "image" + "os" + + "github.com/icza/icox" // 替换为icox库 + "github.com/nfnt/resize" +) + +func main() { + // 1. 读取PNG文件 + inputPath := "manifest/images/logo.png" // 输入PNG路径 + outputPath := "manifest/images/favicon.ico" // 输出ICO路径 + + pngFile, err := os.Open(inputPath) + if err != nil { + panic("无法打开PNG文件: " + err.Error()) + } + defer pngFile.Close() + + // 2. 解码PNG为image.Image对象 + img, _, err := image.Decode(pngFile) + if err != nil { + panic("PNG解码失败: " + err.Error()) + } + + // 3. 定义ICO需要包含的尺寸(常见尺寸) + sizes := []uint{16, 32, 64, 128} // 可根据需求添加更多尺寸 + var icoImages []image.Image + + // 4. 缩放图像到每个目标尺寸并收集 + for _, size := range sizes { + // 使用Lanczos3算法缩放(高质量) + resized := resize.Resize(size, size, img, resize.Lanczos3) + icoImages = append(icoImages, resized) + } + + // 5. 编码为ICO并写入文件(关键修改:使用icox.Encode) + icoFile, err := os.Create(outputPath) + if err != nil { + panic("无法创建ICO文件: " + err.Error()) + } + defer icoFile.Close() + + // icox.Encode直接支持多尺寸切片[]image.Image + if err := icox.Encode(icoFile, icoImages); err != nil { + panic("ICO编码失败: " + err.Error()) + } +} diff --git a/go.mod b/go.mod index 392f68a..18b595f 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/ayflying/p2p go 1.24.0 require ( + github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9 github.com/dop251/goja v0.0.0-20250630131328-58d95d85e994 github.com/getlantern/systray v1.2.2 github.com/gogf/gf/contrib/nosql/redis/v2 v2.9.3 @@ -12,6 +13,7 @@ require ( github.com/libp2p/go-libp2p v0.43.0 github.com/libp2p/go-libp2p-kad-dht v0.35.1 github.com/multiformats/go-multiaddr v0.16.1 + github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 ) require ( diff --git a/go.sum b/go.sum index 5d1f67a..bc5e977 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGy github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9 h1:1ltqoej5GtaWF8jaiA49HwsZD459jqm9YFz9ZtMFpQA= +github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9/go.mod h1:7uhhqiBaR4CpN0k9rMjOtjpcfGd6DG2m04zQxKnWQ0I= github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= @@ -258,6 +260,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index 210f470..1b89ce5 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -14,7 +14,7 @@ import ( ) func init() { - err := Main.AddCommand(&Main, &Debug, &P2p) + err := Main.AddCommand(&Main, &Debug, &P2p, &DHT) if err != nil { g.Log().Error(gctx.GetInitCtx(), err) return @@ -29,6 +29,7 @@ var ( Usage: "main", Brief: "start http server", Func: func(ctx context.Context, parser *gcmd.Parser) (err error) { + g.Log().Debug(ctx, "开始执行main") parser, err = gcmd.Parse(g.MapStrBool{ "w,ws": true, diff --git a/internal/cmd/debug.go b/internal/cmd/debug.go index 990630b..0526ada 100644 --- a/internal/cmd/debug.go +++ b/internal/cmd/debug.go @@ -22,6 +22,8 @@ var ( Usage: "debug", Brief: "调试接口", Func: func(ctx context.Context, parser *gcmd.Parser) (err error) { + g.Log().Debug(ctx, "开始执行debug") + g.Log().SetConfigWithMap(g.Map{ "level": "all", "stdout": true, diff --git a/internal/cmd/dht.go b/internal/cmd/dht.go new file mode 100644 index 0000000..41c6eea --- /dev/null +++ b/internal/cmd/dht.go @@ -0,0 +1,54 @@ +package cmd + +import ( + "context" + "fmt" + + "github.com/ayflying/p2p/internal/service" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gcmd" +) + +var ( + // DHT 命令定义了P2P连接工具的入口命令 + // 遵循GoFrame的Command对象定义规范,包含名称、用法、简短描述和执行函数 + DHT = gcmd.Command{ + // Name 为命令名称 + Name: "dht", + // Usage 描述命令的基本用法 + Usage: "dht [options]", + // Brief 提供命令的简短功能描述 + Brief: "P2P连接工具,支持网关和客户端模式,实现NAT穿透和点对点通信", + // Description 提供命令的详细描述和使用帮助 + Description: p2pHelpDescription, + // Func 为命令的执行函数,接收上下文和参数解析器 + Func: func(ctx context.Context, parser *gcmd.Parser) (err error) { + g.Log().Debug(ctx, "开始执行dht") + + parser, err = gcmd.Parse(g.MapStrBool{ + "p,port": true, + }) + port := parser.GetOpt("port", "0").Int() + + h, _ := service.P2P().CreateLibp2pHost(ctx, port) + err = service.P2P().DHTStart(ctx, h, nil) + if err != nil { + g.Log().Error(ctx, err) + } + + go func() { + publicIp, _ := service.P2P().GetIPv4PublicIP() + validKey := fmt.Sprintf("%v/ip", h.ID()) + dataValue := fmt.Sprintf("来自节点 %s 的数据:%v", h.ID().ShortString(), publicIp) + if err = service.P2P().StoreToDHT(ctx, validKey, dataValue); err != nil { + fmt.Printf("❌ 存储失败: %v\n", err) + } else { + fmt.Printf("✅ 存储成功\nKey: %s\nValue: %s\n", validKey, dataValue) + } + }() + + s.Run() + return + }, + } +) diff --git a/internal/cmd/p2p.go b/internal/cmd/p2p.go index 7baf030..2bdb53b 100644 --- a/internal/cmd/p2p.go +++ b/internal/cmd/p2p.go @@ -2,6 +2,7 @@ package cmd import ( "context" + "fmt" "github.com/ayflying/p2p/internal/service" "github.com/gogf/gf/v2/frame/g" @@ -40,6 +41,8 @@ var ( Description: p2pHelpDescription, // Func 为命令的执行函数,接收上下文和参数解析器 Func: func(ctx context.Context, parser *gcmd.Parser) (err error) { + g.Log().Debug(ctx, "开始执行p2p") + s := g.Server() // 配置日志输出 @@ -76,23 +79,50 @@ var ( wsStr := "ws://192.168.50.173:51888/ws" err = service.P2P().Start(ctx, wsStr) case "dht": + g.Log().Debug(ctx, "开始执行dht") h, _ := service.P2P().CreateLibp2pHost(ctx, 0) - err := service.P2P().DHTStart(ctx, h) + + err := service.P2P().DHTStart(ctx, h, nil) if err != nil { g.Log().Error(ctx, err) } publicIp, err := service.P2P().GetIPv4PublicIP() - err = service.P2P().StoreAddrToDHT(ctx, "ip", publicIp) + validKey := "ip" + dataValue := fmt.Sprintf("来自节点 %s 的数据:%v", h.ID().ShortString(), publicIp) + if err := service.P2P().StoreToDHT(ctx, validKey, dataValue); err != nil { + fmt.Printf("❌ 存储失败: %v\n", err) + } else { + fmt.Printf("✅ 存储成功\nKey: %s\nValue: %s\n", validKey, dataValue) + } case "dht2": + g.Log().Debug(ctx, "开始执行dht2") h, _ := service.P2P().CreateLibp2pHost(ctx, 0) - err := service.P2P().DHTStart(ctx, h) + + addr := []string{ + //"/ip4/192.168.50.173/tcp/23333/p2p/12D3KooWQsb1137nCzqbMMCzwHsyU8aaCZeFnBUBTkWVsfp8gs26", + //"/ip4/192.168.50.173/udp/23333/quic-v1/p2p/12D3KooWQsb1137nCzqbMMCzwHsyU8aaCZeFnBUBTkWVsfp8gs26", + //"/ip4/114.132.176.115/tcp/23333/p2p/12D3KooWJQMiYyptqSrx4PPsGLY9hjLbaDdxmBXmGtKmSWuiP79D", + //"/ip4/114.132.176.115/udp/23333/quic-v1/p2p/12D3KooWJQMiYyptqSrx4PPsGLY9hjLbaDdxmBXmGtKmSWuiP79D", + } + + id := gcmd.GetOpt("id").String() + err := service.P2P().DHTStart(ctx, h, addr) if err != nil { g.Log().Error(ctx, err) } - get, _ := service.P2P().FindAddrFromDHT(ctx, "ip") - g.Dump(get) + validKey := id + go func() { + // 5. 查找数据(从网络中的节点获取,不依赖初始 Bootstrap 节点) + foundValue, err := service.P2P().FindFromDHT(ctx, validKey) + if err != nil { + fmt.Printf("❌ 查找失败: %v\n", err) + } else { + fmt.Printf("✅ 查找成功\nValue: %s\n", foundValue) + } + }() + s.SetPort(0) default: // 显示帮助信息 diff --git a/internal/logic/p2p/client.go b/internal/logic/p2p/client.go index b8526c1..c24bbad 100644 --- a/internal/logic/p2p/client.go +++ b/internal/logic/p2p/client.go @@ -18,7 +18,6 @@ import ( "github.com/google/uuid" "github.com/gorilla/websocket" "github.com/libp2p/go-libp2p" - "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/peer" @@ -123,7 +122,8 @@ func (s *sP2P) CreateLibp2pHost(ctx context.Context, port int) (host.Host, error } // 1. 生成密钥对并初始化节点(确保身份有效) - s.privKey, _, _ = crypto.GenerateKeyPair(crypto.Ed25519, 0) // 推荐使用Ed25519 + //s.privKey, _, _ = crypto.GenerateKeyPair(crypto.Ed25519, 0) // 推荐使用Ed25519 + s.privKey, _ = s.generateFixedKey() // 创建主机 h, err := libp2p.New( diff --git a/internal/logic/p2p/dht.go b/internal/logic/p2p/dht.go index 92238b8..0bde3b1 100644 --- a/internal/logic/p2p/dht.go +++ b/internal/logic/p2p/dht.go @@ -3,102 +3,58 @@ package p2p import ( "context" "fmt" + "log" + "net" + "time" + "github.com/gogf/gf/v2/crypto/gsha1" "github.com/gogf/gf/v2/frame/g" + //"github.com/ipfs/boxo/ipns" dht "github.com/libp2p/go-libp2p-kad-dht" "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/peerstore" + "github.com/multiformats/go-multiaddr" ) type DHTType struct { - KadDHT *dht.IpfsDHT + KadDHT *dht.IpfsDHT + bootstrapPeers []string } +var ( +//bootstrapPeers = []string{ +// "/ip4/192.168.50.173/tcp/53486/p2p/12D3KooWE3v9623SLukT9dKUQLjqAJrPvzoyRjoUh5MAVGDg69Rw", +// "/ip4/192.168.50.173/udp/53486/quic-v1/p2p/12D3KooWE3v9623SLukT9dKUQLjqAJrPvzoyRjoUh5MAVGDg69Rw", +//} +) + // 初始化无服务器DHT(作为节点加入DHT网络) -func (s *sP2P) DHTStart(ctx context.Context, h host.Host) (err error) { - // 创建自定义 DHT 选项,配置验证器 - dhtOpts := []dht.Option{ - //设置为“客户端+服务端模式”(既可以查找数据,也可以存储数据) - dht.Mode(dht.ModeServer), - } +func (s *sP2P) DHTStart(ctx context.Context, h host.Host, bootstrapPeers []string) (err error) { + //打印节点地址(供其他节点手动加入时使用) + s.printNodeAddrs(h) + s.dht.bootstrapPeers = bootstrapPeers - // 创建DHT实例, - s.dht.KadDHT, err = dht.New( - ctx, - h, - dhtOpts..., - //dht.Mode(dht.ModeServer), - ) + // 2. 通过官方 Bootstrap 节点加入公共 DHT 网络(完全去中心化入口) + s.dht.KadDHT, err = s.joinGlobalDHT(ctx, h) if err != nil { - err = fmt.Errorf("初始化DHT失败: %v", err) - return + log.Fatalf("加入 DHT 网络失败: %v", err) } + fmt.Println("✅ 成功加入完全去中心化 DHT 网络") - // 关键:直接替换 DHT 实例的验证器 - // v0.35.1 版本中,IpfsDHT 结构体的 Validator 字段是公开可修改的 - s.dht.KadDHT.Validator = &NoOpValidator{} - - // 连接到DHT bootstrap节点(种子节点,帮助加入网络) - // 这里使用libp2p官方的公共bootstrap节点,生产环境可替换为自己的节点 - bootstrapPeers := dht.DefaultBootstrapPeers - for _, addr := range bootstrapPeers { - peerInfo, _ := peer.AddrInfoFromP2pAddr(addr) - h.Peerstore().AddAddrs(peerInfo.ID, peerInfo.Addrs, peerstore.PermanentAddrTTL) - if err = h.Connect(ctx, *peerInfo); err != nil { - fmt.Printf("连接bootstrap节点 %s 失败: %v\n", peerInfo.ID, err) - } else { - fmt.Printf("已连接bootstrap节点: %s\n", peerInfo.ID) - } - - } - - // 启动DHT - if err = s.dht.KadDHT.Bootstrap(ctx); err != nil { - return - } + // 3. 定期打印路由表(观察节点自动发现效果) + go s.printRoutingTable(s.dht.KadDHT, 60*time.Second) return } -// 存储数据到DHT(比如存储“目标节点ID-公网地址”的映射) -func (s *sP2P) StoreAddrToDHT(ctx context.Context, key string, addr string) (err error) { - // Key:目标节点ID(作为哈希键),Value:公网地址(需转成二进制) - //key = "/ipns/" + key - //key = s.generateStringDHTKey(key) - key = fmt.Sprintf("%s/%s", ProtocolID, key) - value := []byte(addr) - - // 存储数据(DHT会自动找到负责存储该Key的节点,并同步数据) - if err = s.dht.KadDHT.PutValue(ctx, key, value); err != nil { - return fmt.Errorf("key=%s,存储地址到DHT失败: %v", key, err) - } - - g.Log().Info(ctx, "成功存储地址到DHT,Key=%s, Value=%s", key, addr) - return -} - -// 从DHT查找数据(比如根据节点ID查找其公网地址) -func (s *sP2P) FindAddrFromDHT(ctx context.Context, key string) (string, error) { - // 查找数据(DHT会通过路由表层层跳转,找到负责存储该Key的节点并获取数据) - //key = s.generateStringDHTKey(key) - //key = "/ipns/" + key - key = fmt.Sprintf("%s/%s", ProtocolID, key) - g.Log().Debugf(ctx, "从DHT查找地址中...,Key=%s", key) - value, err := s.dht.KadDHT.GetValue(ctx, key) - if err != nil { - return "", fmt.Errorf("从DHT查找地址失败: %v", err) - } - - addr := string(value) - fmt.Printf("从DHT找到地址,Key=%s, Value=%s\n", key, addr) - return addr, nil -} - // 生成符合DHT规范的字符串Key func (s *sP2P) generateStringDHTKey(str string) string { - return "" + return gsha1.Encrypt(str) + //fullKey := fmt.Sprintf("%s/%s", ProtocolID, str) + //hash, _ := multihash.Sum([]byte(fullKey), multihash.SHA2_256, -1) + //return ipns.key } // 自定义验证器:不做任何校验,接受所有数据 @@ -113,3 +69,192 @@ func (v *NoOpValidator) Validate(key string, value []byte) error { func (v *NoOpValidator) Select(key string, values [][]byte) (int, error) { return 0, nil } + +// 加入全球公共 DHT 网络(通过官方 Bootstrap 节点,实现完全去中心化) +func (s *sP2P) joinGlobalDHT(ctx context.Context, localHost host.Host) (*dht.IpfsDHT, error) { + // 创建 DHT 实例(ModeServer:作为完整节点参与存储和路由) + kadDHT, err := dht.New(ctx, localHost, dht.Mode(dht.ModeServer)) + if err != nil { + return nil, err + } + kadDHT.Validator = &NoOpValidator{} + success := false + + if len(s.dht.bootstrapPeers) > 0 { + fmt.Println("正在连接本地种子节点...") + seedPeers, _ := s.parseSeedNodes(s.dht.bootstrapPeers) + for _, p := range seedPeers { + localHost.Peerstore().AddAddrs(p.ID, p.Addrs, peerstore.PermanentAddrTTL) + if err = localHost.Connect(ctx, p); err != nil { + fmt.Printf("⚠️ 连接本地种子节点 %s 失败: %v\n", p.ID.ShortString(), err) + } else { + fmt.Printf("✅ 连接本地种子节点成功: %s\n", p.ID.ShortString()) + } + if err != nil { + fmt.Printf("⚠️ 连接私有节点 %s 失败: %v\n", p.ID.ShortString(), err) + continue + } + fmt.Printf("✅ 连接本地种子节点成功: %s\n", p.ID.ShortString()) + success = true + } + if !success { + return nil, fmt.Errorf("所有本地种子节点连接失败") + } + } else { + + // 连接 libp2p 官方 Bootstrap 节点(仅作为初始入口) + officialBootstrapPeers := dht.DefaultBootstrapPeers // 官方节点列表 + fmt.Println("正在连接官方 Bootstrap 节点(初始入口)...") + + for _, addr := range officialBootstrapPeers { + peerInfo, err := peer.AddrInfoFromP2pAddr(addr) + if err != nil { + fmt.Printf("⚠️ 解析官方节点失败: %v\n", err) + continue + } + + // 添加节点地址到本地地址簿 + localHost.Peerstore().AddAddrs(peerInfo.ID, peerInfo.Addrs, peerstore.PermanentAddrTTL) + + // 尝试连接(超时 10 秒) + connCtx, connCancel := context.WithTimeout(ctx, 10*time.Second) + err = localHost.Connect(connCtx, *peerInfo) + connCancel() + + if err != nil { + fmt.Printf("⚠️ 连接官方节点 %s 失败: %v\n", peerInfo.ID.ShortString(), err) + continue + } + fmt.Printf("✅ 连接官方节点成功: %s\n", peerInfo.ID.ShortString()) + success = true + } + + // 只要连接上至少一个官方节点,即可加入网络(后续会自动发现更多节点) + if !success { + return nil, fmt.Errorf("无法连接任何官方 Bootstrap 节点,无法加入网络") + } + } + + // 启动 DHT(自动发现其他节点,构建路由表,脱离对官方节点的依赖) + if err = kadDHT.Bootstrap(ctx); err != nil { + return nil, fmt.Errorf("DHT 初始化失败: %v", err) + } + + // 等待路由表初步填充(新增:给路由表留出初始化时间) + time.Sleep(5 * time.Second) + return kadDHT, nil +} + +// StoreToDHT 存储数据到 DHT(自动分布式存储) +func (s *sP2P) StoreToDHT(ctx context.Context, key string, value string) (err error) { + key = s.generateStringDHTKey(key) + key = fmt.Sprintf("%s/%s", ProtocolID, key) + g.Log().Debugf(ctx, "StoreToDHT key: %s, value: %s", key, value) + + // 存储到本地 + ctx, cancel := context.WithTimeout(ctx, 60*time.Second) + defer cancel() + if err = s.dht.KadDHT.PutValue(ctx, key, []byte(value)); err != nil { + return fmt.Errorf("本地存储失败: %v", err) + } + + return +} + +// FindFromDHT 从 DHT 查找数据(从网络节点获取) +func (s *sP2P) FindFromDHT(ctx context.Context, key string) (string, error) { + maxRetries := 10 // 最多重试5次 + retryInterval := 60 * time.Second // 每次重试间隔2秒(本地网络快) + + key = s.generateStringDHTKey(key) + key = fmt.Sprintf("%s/%s", ProtocolID, key) + g.Log().Debugf(ctx, "FindFromDHT key: %s", key) + + // 1. 先检查本地是否存储了数据(本地节点可能已保存) + localValue, err := s.dht.KadDHT.GetValue(ctx, key) + if err == nil { + g.Log().Debugf(ctx, "✅ 本地查找成功(数据在当前节点)") + return string(localValue), nil + } + g.Log().Debugf(ctx, "⚠️ 本地查找失败: %v,开始重试网络查找...", err) + + // 2. 多次重试网络查找 + for i := 0; i < maxRetries; i++ { + ctx2, cancel := context.WithTimeout(ctx, 120*time.Second) // 本地测试超时短一些 + defer cancel() + + g.Log().Debugf(ctx2, "🔍 第%d次查找(共%d次)...", i+1, maxRetries) + value, err := s.dht.KadDHT.GetValue(ctx2, key) + if err == nil { + g.Log().Debugf(ctx2, "✅ 第%d次查找成功", i+1) + return string(value), nil + } + g.Log().Debugf(ctx2, "⚠️ 第%d次查找失败: %v,等待重试...", i+1, err) + time.Sleep(retryInterval) + } + + return "", fmt.Errorf("超过最大重试次数(%d次),未找到数据", maxRetries) +} + +// 定期打印路由表(观察节点自动发现情况) +func (s *sP2P) printRoutingTable(kadDHT *dht.IpfsDHT, interval time.Duration) { + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + <-ticker.C + peers := kadDHT.RoutingTable().ListPeers() + fmt.Printf("\n📊 当前路由表节点数: %d(完全去中心化网络节点)\n", len(peers)) + if len(peers) > 0 { + fmt.Println("前 5 个节点 ID:") + for i, p := range peers[:min(5, len(peers))] { + fmt.Printf(" %d. %s\n", i+1, p.ShortString()) + } + } + } +} + +//// 定期打印节点状态(公网地址+路由表) +//func (s *sP2P) printStatus(interval time.Duration) { +// ticker := time.NewTicker(interval) +// for { +// <-ticker.C +// //publicIp, err := service.P2P().GetIPv4PublicIP() +// //publicAddrs := s.getPublicAddrs() +// peers := s.dht.KadDHT.RoutingTable().ListPeers() +// fmt.Printf("\n===== 节点状态 =====") +// fmt.Printf("\n公网地址数: %d(0表示穿透失败)\n", len(publicAddrs)) +// fmt.Printf("路由表节点数: %d(自动扩散结果)\n", len(peers)) +// fmt.Println("====================") +// } +//} + +// 打印节点地址(供其他节点手动加入时使用) +func (s *sP2P) printNodeAddrs(host host.Host) { + fmt.Println("节点地址(公网地址将自动同步到DHT):") + for _, addr := range host.Addrs() { + fullAddr := fmt.Sprintf("%s/p2p/%s", addr, host.ID()) + ipStr, _ := addr.ValueForProtocol(multiaddr.P_IP4) + ip := net.ParseIP(ipStr) + if ip.IsPrivate() || ip.IsLoopback() { + fmt.Printf(" [内网] %s\n", fullAddr) + } else { + fmt.Printf(" [公网] %s\n", fullAddr) + } + } +} + +func (s *sP2P) parseSeedNodes(seedAddrs []string) ([]peer.AddrInfo, error) { + peers := make([]peer.AddrInfo, 0, len(seedAddrs)) + for _, addrStr := range seedAddrs { + addr, err := multiaddr.NewMultiaddr(addrStr) + if err != nil { + return nil, err + } + peerInfo, err := peer.AddrInfoFromP2pAddr(addr) + if err != nil { + return nil, err + } + peers = append(peers, *peerInfo) + } + return peers, nil +} diff --git a/internal/logic/p2p/p2p.go b/internal/logic/p2p/p2p.go index 40b911a..3fc0ed9 100644 --- a/internal/logic/p2p/p2p.go +++ b/internal/logic/p2p/p2p.go @@ -1,7 +1,10 @@ package p2p import ( + "encoding/binary" + "encoding/pem" "fmt" + "math/rand" "net" "net/http" "strings" @@ -11,16 +14,19 @@ import ( "github.com/ayflying/p2p/internal/service" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gctx" + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/util/grand" "github.com/libp2p/go-libp2p/core/crypto" ) var ( ctx = gctx.New() + ip string ) // 常量定义 const ( - ProtocolID string = "/ay/1.0.0" + ProtocolID string = "/ay" DefaultPort = 51888 ) @@ -60,6 +66,7 @@ type sP2P struct { // New 创建一个新的 P2P 服务实例 func New() *sP2P { + return &sP2P{ Clients: make(map[string]*ClientConn), client: &Client{}, @@ -69,6 +76,7 @@ func New() *sP2P { func init() { service.RegisterP2P(New()) + ip, _ = service.P2P().GetIPv4PublicIP() } // 获取公网IP并判断类型(ipv4/ipv6) @@ -177,3 +185,49 @@ func (s *sP2P) removeDuplicates(strs []string) []string { } return result } + +// 生成固定密钥(核心:通过固定种子生成相同密钥) +func (s *sP2P) generateFixedKey() (crypto.PrivKey, error) { + privKeyPath := "runtime/p2p.key" + if ok := gfile.Exists(privKeyPath); ok { + // 从文件读取密钥 + keyBytes := gfile.GetBytes(privKeyPath) + // 2. 解析PEM块(关键:提取真正的私钥数据) + pemBlock, _ := pem.Decode(keyBytes) + if pemBlock == nil { + return nil, fmt.Errorf("私钥文件格式错误(非PEM格式)") + } + privKey, err := crypto.UnmarshalPrivateKey(pemBlock.Bytes) + if err != nil { + return nil, err + } + return privKey, nil + } + + // 固定种子(修改此种子可生成不同的固定密钥) + var fixedSeed = []byte(grand.S(10)) // 自定义固定种子 + + // 用固定种子初始化随机数生成器 + seed := binary.BigEndian.Uint64(fixedSeed[:8]) // 取种子前8字节作为随机数种子 + r := rand.New(rand.NewSource(int64(seed))) + + // 生成ED25519密钥对(基于固定种子,每次生成结果相同) + privKey, _, err := crypto.GenerateEd25519Key(r) + keyBytes, err := crypto.MarshalPrivateKey(privKey) + // 用PEM格式包装(标准格式,便于存储和解析) + pemBlock := &pem.Block{ + Type: "LIBP2P PRIVATE KEY", // 标识为libp2p私钥 + Bytes: keyBytes, + } + err = gfile.PutBytes(privKeyPath, pem.EncodeToMemory(pemBlock)) + if err != nil { + panic(fmt.Sprintf("保存私钥失败: %v", err)) + } + //if err := os.WriteFile(privKeyPath, pem.EncodeToMemory(pemBlock), 0600); err != nil { + // panic(fmt.Sprintf("保存私钥失败: %v", err)) + //} + + fmt.Println("私钥生成成功,文件路径:", privKeyPath) + + return privKey, err +} diff --git a/internal/service/p_2_p.go b/internal/service/p_2_p.go index df94d71..c1a5afb 100644 --- a/internal/service/p_2_p.go +++ b/internal/service/p_2_p.go @@ -22,11 +22,11 @@ type ( // 发现并连接目标节点 DiscoverAndConnect(targetID string) error // 初始化无服务器DHT(作为节点加入DHT网络) - DHTStart(ctx context.Context, h host.Host) (err error) - // 存储数据到DHT(比如存储“目标节点ID-公网地址”的映射) - StoreAddrToDHT(ctx context.Context, key string, addr string) (err error) - // 从DHT查找数据(比如根据节点ID查找其公网地址) - FindAddrFromDHT(ctx context.Context, key string) (string, error) + DHTStart(ctx context.Context, h host.Host, bootstrapPeers []string) (err error) + // StoreToDHT 存储数据到 DHT(自动分布式存储) + StoreToDHT(ctx context.Context, key string, value string) (err error) + // FindFromDHT 从 DHT 查找数据(从网络节点获取) + FindFromDHT(ctx context.Context, key string) (string, error) GatewayStart(ctx context.Context, group *ghttp.RouterGroup) (err error) // 只获取IPv4公网IP(过滤IPv6结果) GetIPv4PublicIP() (string, error) diff --git a/main.go b/main.go index d353b1d..1093202 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( _ "github.com/ayflying/p2p/internal/logic" _ "github.com/ayflying/p2p/internal/packed" "github.com/ayflying/p2p/internal/service" + "github.com/gogf/gf/v2/os/gfile" //步骤1:加载驱动 _ "github.com/gogf/gf/contrib/nosql/redis/v2" @@ -21,5 +22,9 @@ func main() { // 启动系统托盘 service.OS().Load(consts.Name, consts.Name+"服务端", "manifest/images/favicon.ico") + if ok := gfile.Exists("runtime"); !ok { + gfile.Mkdir("runtime") + } + cmd.Main.Run(ctx) } diff --git a/runtime/p2p.key b/runtime/p2p.key new file mode 100644 index 0000000..42c1745 --- /dev/null +++ b/runtime/p2p.key @@ -0,0 +1,4 @@ +-----BEGIN LIBP2P PRIVATE KEY----- +CAESQMGVYKvJb7HdTGRqhTfTeM74bkNZKu8AjJSAFD8o/BwwkpE5mddDYxGo3MID +fCS7r+XqcqegalHv4xZKKk1kdIs= +-----END LIBP2P PRIVATE KEY-----