mirror of
https://github.com/ayflying/p2p.git
synced 2026-03-04 17:29:22 +00:00
首次提交
This commit is contained in:
80
internal/cmd/cmd.go
Normal file
80
internal/cmd/cmd.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/ayflying/p2p/internal/controller/p2p"
|
||||
"github.com/ayflying/p2p/internal/service"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gcmd"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/gogf/gf/v2/os/gtimer"
|
||||
)
|
||||
|
||||
func init() {
|
||||
err := Main.AddCommand(&Main, &Debug, &P2p)
|
||||
if err != nil {
|
||||
g.Log().Error(gctx.GetInitCtx(), err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
Main = gcmd.Command{
|
||||
Name: "main",
|
||||
Usage: "main",
|
||||
Brief: "start http server",
|
||||
Func: func(ctx context.Context, parser *gcmd.Parser) (err error) {
|
||||
|
||||
parser, err = gcmd.Parse(g.MapStrBool{
|
||||
"w,ws": true,
|
||||
"g,gateway": true,
|
||||
"p,port": true,
|
||||
})
|
||||
addr := g.Cfg().MustGet(ctx, "ws.address").String()
|
||||
ws := parser.GetOpt("ws", addr).String()
|
||||
//port := parser.GetOpt("port", 0).Int()
|
||||
|
||||
s := g.Server()
|
||||
s.Group("/", func(group *ghttp.RouterGroup) {
|
||||
group.Middleware(ghttp.MiddlewareHandlerResponse)
|
||||
group.Bind(
|
||||
p2p.NewV1(),
|
||||
)
|
||||
})
|
||||
|
||||
//启动p2p服务端网关
|
||||
s.Group("/ws", func(group *ghttp.RouterGroup) {
|
||||
group.Middleware(ghttp.MiddlewareHandlerResponse)
|
||||
err = service.P2P().GatewayStart(ctx, group)
|
||||
if err != nil {
|
||||
g.Log().Error(ctx, err)
|
||||
}
|
||||
})
|
||||
|
||||
//s.SetPort(port)
|
||||
|
||||
// 延迟启动
|
||||
gtimer.SetTimeout(ctx, time.Second*5, func(ctx context.Context) {
|
||||
g.Log().Debug(ctx, "开始执行客户端")
|
||||
// 启动p2p客户端
|
||||
err = service.P2P().Start(ctx, ws)
|
||||
|
||||
g.Log().Debugf(ctx, "当前监听端口:%v", s.GetListenedPort())
|
||||
addrs, _ := net.InterfaceAddrs()
|
||||
for _, addr := range addrs {
|
||||
if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() && ipnet.IP.To4() != nil {
|
||||
g.Log().Infof(ctx, "访问地址:http://%v:%d", ipnet.IP.String(), s.GetListenedPort())
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
s.Run()
|
||||
return nil
|
||||
},
|
||||
}
|
||||
)
|
||||
63
internal/cmd/debug.go
Normal file
63
internal/cmd/debug.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/dop251/goja"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gcmd"
|
||||
)
|
||||
|
||||
type DebugType struct {
|
||||
Action string `json:"action" dc:"操作渠道"`
|
||||
Number int64 `json:"number" dc:"数字"`
|
||||
String string `json:"string" dc:"字符串"`
|
||||
Json string `json:"json" dc:"符合参数"`
|
||||
}
|
||||
|
||||
var (
|
||||
Debug = gcmd.Command{
|
||||
Name: "debug",
|
||||
Usage: "debug",
|
||||
Brief: "调试接口",
|
||||
Func: func(ctx context.Context, parser *gcmd.Parser) (err error) {
|
||||
g.Log().SetConfigWithMap(g.Map{
|
||||
"level": "all",
|
||||
"stdout": true,
|
||||
})
|
||||
var req = &DebugType{
|
||||
Action: parser.GetOpt("a").String(),
|
||||
Number: parser.GetOpt("n").Int64(),
|
||||
String: parser.GetOpt("s").String(),
|
||||
Json: parser.GetOpt("j").String(),
|
||||
}
|
||||
|
||||
g.Log().Debug(ctx, "开始调试了")
|
||||
g.Log().Debugf(ctx, "开始执行:action:%v,number=%v,string=%v,json=%v", req.Action, req.Number, req.String, req.Json)
|
||||
var msg any
|
||||
switch req.Action {
|
||||
case "js":
|
||||
vm := goja.New()
|
||||
|
||||
if req.String == "" {
|
||||
req.String = "console.log('hello world');"
|
||||
}
|
||||
|
||||
res, err := vm.RunString(req.String)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
msg = res.Export()
|
||||
g.Dump(res.ToNumber())
|
||||
case "p2p":
|
||||
// host, err := service.P2P().Start(ctx)
|
||||
// if err != nil {
|
||||
// break
|
||||
// }
|
||||
// g.Dump(host.ID().String(), host.Addrs())
|
||||
}
|
||||
g.Log().Debug(ctx, msg)
|
||||
return
|
||||
},
|
||||
}
|
||||
)
|
||||
89
internal/cmd/p2p.go
Normal file
89
internal/cmd/p2p.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/ayflying/p2p/internal/service"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gcmd"
|
||||
)
|
||||
|
||||
// p2pHelpDescription 定义P2P命令的详细帮助信息
|
||||
const p2pHelpDescription = `
|
||||
P2P连接工具使用帮助:
|
||||
|
||||
模式1: 网关服务器
|
||||
功能: 拥有外网IP,接收客户端连接,协助P2P打洞
|
||||
命令: p2p -a gateway
|
||||
|
||||
模式2: 客户端
|
||||
功能: 连接到网关,通过打洞实现与其他客户端的长连接通讯
|
||||
命令: p2p -a client --gateway 网关ID
|
||||
|
||||
高级功能:
|
||||
客户端间连接: p2p --mode client --gateway 网关ID --action connect --target 目标客户端ID
|
||||
发送消息: p2p -mode client --gateway 网关ID --action send --target 目标客户端ID --message "消息内容"
|
||||
`
|
||||
|
||||
var (
|
||||
// P2p 命令定义了P2P连接工具的入口命令
|
||||
// 遵循GoFrame的Command对象定义规范,包含名称、用法、简短描述和执行函数
|
||||
P2p = gcmd.Command{
|
||||
// Name 为命令名称
|
||||
Name: "p2p",
|
||||
// Usage 描述命令的基本用法
|
||||
Usage: "p2p [options]",
|
||||
// Brief 提供命令的简短功能描述
|
||||
Brief: "P2P连接工具,支持网关和客户端模式,实现NAT穿透和点对点通信",
|
||||
// Description 提供命令的详细描述和使用帮助
|
||||
Description: p2pHelpDescription,
|
||||
// Func 为命令的执行函数,接收上下文和参数解析器
|
||||
Func: func(ctx context.Context, parser *gcmd.Parser) (err error) {
|
||||
s := g.Server()
|
||||
|
||||
// 配置日志输出
|
||||
g.Log().SetConfigWithMap(g.Map{
|
||||
"level": "all",
|
||||
"stdout": true,
|
||||
})
|
||||
|
||||
parser, err = gcmd.Parse(g.MapStrBool{
|
||||
"m,mode": true,
|
||||
"g,gateway": true,
|
||||
"a,action": true,
|
||||
"t,target": true,
|
||||
"m,message": true,
|
||||
})
|
||||
|
||||
// 获取运行模式参数
|
||||
mode := parser.GetOpt("mode").String()
|
||||
|
||||
// 根据不同模式调用服务层对应的方法
|
||||
switch mode {
|
||||
case "gateway":
|
||||
// 启动网关服务器模式
|
||||
g.Log().Debug(ctx, "开始执行gatway")
|
||||
s.Group("/ws", func(group *ghttp.RouterGroup) {
|
||||
group.Middleware(ghttp.MiddlewareHandlerResponse)
|
||||
service.P2P().GatewayStart(ctx, group)
|
||||
})
|
||||
s.Run()
|
||||
case "client":
|
||||
// 获取客户端模式所需的参数
|
||||
g.Log().Debug(ctx, "开始执行client")
|
||||
//addrs := []string{"/ip4/127.0.0.1/tcp/51888", "/ip4/192.168.50.173/tcp/51888"}
|
||||
//addr := "/ip4/192.168.50.173/tcp/51888/p2p/12D3KooWJKBB9bF9MjqgsFYUUsPBG249FDq7a3ZdaYc9iw8G78JQ"
|
||||
//addrs := "WyIvaXA0LzEyNy4wLjAuMS90Y3AvNTE4ODgiLCIvaXA0LzE5Mi4xNjguNTAuMTczL3RjcC81MTg4OCJd"
|
||||
wsStr := "ws://192.168.50.173:51888/ws"
|
||||
err = service.P2P().Start(ctx, wsStr)
|
||||
|
||||
default:
|
||||
// 显示帮助信息
|
||||
g.Log().Info(ctx, p2pHelpDescription)
|
||||
}
|
||||
|
||||
return
|
||||
},
|
||||
}
|
||||
)
|
||||
1
internal/consts/consts.go
Normal file
1
internal/consts/consts.go
Normal file
@@ -0,0 +1 @@
|
||||
package consts
|
||||
3
internal/consts/p2p.go
Normal file
3
internal/consts/p2p.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package consts
|
||||
|
||||
const ()
|
||||
5
internal/controller/hello/hello.go
Normal file
5
internal/controller/hello/hello.go
Normal file
@@ -0,0 +1,5 @@
|
||||
// =================================================================================
|
||||
// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish.
|
||||
// =================================================================================
|
||||
|
||||
package hello
|
||||
16
internal/controller/hello/hello_new.go
Normal file
16
internal/controller/hello/hello_new.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// =================================================================================
|
||||
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
|
||||
// =================================================================================
|
||||
|
||||
package hello
|
||||
|
||||
import (
|
||||
"github.com/ayflying/p2p/api/hello"
|
||||
)
|
||||
|
||||
type ControllerV1 struct{}
|
||||
|
||||
func NewV1() hello.IHelloV1 {
|
||||
return &ControllerV1{}
|
||||
}
|
||||
|
||||
13
internal/controller/hello/hello_v1_hello.go
Normal file
13
internal/controller/hello/hello_v1_hello.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package hello
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
"github.com/ayflying/p2p/api/hello/v1"
|
||||
)
|
||||
|
||||
func (c *ControllerV1) Hello(ctx context.Context, req *v1.HelloReq) (res *v1.HelloRes, err error) {
|
||||
g.RequestFromCtx(ctx).Response.Writeln("Hello World!")
|
||||
return
|
||||
}
|
||||
5
internal/controller/p2p/p2p.go
Normal file
5
internal/controller/p2p/p2p.go
Normal file
@@ -0,0 +1,5 @@
|
||||
// =================================================================================
|
||||
// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish.
|
||||
// =================================================================================
|
||||
|
||||
package p2p
|
||||
15
internal/controller/p2p/p2p_new.go
Normal file
15
internal/controller/p2p/p2p_new.go
Normal file
@@ -0,0 +1,15 @@
|
||||
// =================================================================================
|
||||
// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish.
|
||||
// =================================================================================
|
||||
|
||||
package p2p
|
||||
|
||||
import (
|
||||
"github.com/ayflying/p2p/api/p2p"
|
||||
)
|
||||
|
||||
type ControllerV1 struct{}
|
||||
|
||||
func NewV1() p2p.IP2PV1 {
|
||||
return &ControllerV1{}
|
||||
}
|
||||
13
internal/controller/p2p/p2p_v1_connect.go
Normal file
13
internal/controller/p2p/p2p_v1_connect.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package p2p
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/ayflying/p2p/api/p2p/v1"
|
||||
"github.com/ayflying/p2p/internal/service"
|
||||
)
|
||||
|
||||
func (c *ControllerV1) Connect(ctx context.Context, req *v1.ConnectReq) (res *v1.ConnectRes, err error) {
|
||||
err = service.P2P().DiscoverAndConnect(req.TargetID)
|
||||
return
|
||||
}
|
||||
13
internal/controller/p2p/p2p_v1_send.go
Normal file
13
internal/controller/p2p/p2p_v1_send.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package p2p
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/ayflying/p2p/api/p2p/v1"
|
||||
"github.com/ayflying/p2p/internal/service"
|
||||
)
|
||||
|
||||
func (c *ControllerV1) Send(ctx context.Context, req *v1.SendReq) (res *v1.SendRes, err error) {
|
||||
err = service.P2P().SendData(req.TargetID, []byte(req.Data))
|
||||
return
|
||||
}
|
||||
0
internal/dao/.gitkeep
Normal file
0
internal/dao/.gitkeep
Normal file
0
internal/logic/.gitkeep
Normal file
0
internal/logic/.gitkeep
Normal file
9
internal/logic/logic.go
Normal file
9
internal/logic/logic.go
Normal file
@@ -0,0 +1,9 @@
|
||||
// ==========================================================================
|
||||
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
|
||||
// ==========================================================================
|
||||
|
||||
package logic
|
||||
|
||||
import (
|
||||
_ "github.com/ayflying/p2p/internal/logic/p2p"
|
||||
)
|
||||
326
internal/logic/p2p/client.go
Normal file
326
internal/logic/p2p/client.go
Normal file
@@ -0,0 +1,326 @@
|
||||
package p2p
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/gogf/gf/v2/os/glog"
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/libp2p/go-libp2p"
|
||||
"github.com/libp2p/go-libp2p/core/host"
|
||||
"github.com/libp2p/go-libp2p/core/network"
|
||||
"github.com/libp2p/go-libp2p/core/peer"
|
||||
"github.com/libp2p/go-libp2p/core/peerstore"
|
||||
"github.com/libp2p/go-libp2p/core/protocol"
|
||||
"github.com/multiformats/go-multiaddr"
|
||||
)
|
||||
|
||||
// 客户端
|
||||
type Client struct {
|
||||
ctx context.Context
|
||||
Id string
|
||||
gatewayURL string
|
||||
host host.Host
|
||||
wsConn *websocket.Conn // WebSocket连接
|
||||
peers map[string]peer.ID // 存储已连接的节点
|
||||
}
|
||||
|
||||
func (s *sP2P) Start(ctx context.Context, wsStr string) (err error) {
|
||||
|
||||
hostObj, err := s.createLibp2pHost(ctx, 0)
|
||||
if err != nil {
|
||||
g.Log().Error(ctx, err)
|
||||
}
|
||||
//defer hostObj.Close()
|
||||
|
||||
// 创建客户端实例
|
||||
s.client = &Client{
|
||||
ctx: ctx,
|
||||
Id: uuid.New().String(),
|
||||
gatewayURL: wsStr,
|
||||
host: hostObj,
|
||||
peers: make(map[string]peer.ID),
|
||||
}
|
||||
|
||||
// 设置流处理函数(处理P2P消息)
|
||||
hostObj.SetStreamHandler(ProtocolID, s.handleStream)
|
||||
|
||||
// 连接网关(WebSocket)
|
||||
if err = s.connectGateway(); err != nil {
|
||||
glog.Fatalf(ctx, "连接网关失败: %v", err)
|
||||
}
|
||||
|
||||
// 注册到网关
|
||||
if err = s.register(); err != nil {
|
||||
glog.Fatalf(ctx, "注册到网关失败: %v", err)
|
||||
}
|
||||
|
||||
// 启动网关消息接收协程
|
||||
go s.receiveGatewayMessages()
|
||||
|
||||
g.Log().Infof(ctx, "已连接网关成功,客户端ID: %s", s.client.Id)
|
||||
//g.Log().Infof(ctx,"当前地址:http://127.0.0.1/")
|
||||
|
||||
//select {
|
||||
//case <-ctx.Done():
|
||||
//}
|
||||
return
|
||||
}
|
||||
|
||||
// 创建libp2p主机
|
||||
func (s *sP2P) createLibp2pHost(ctx context.Context, port int) (host.Host, error) {
|
||||
// 配置监听地址
|
||||
listenAddr := fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", port)
|
||||
|
||||
// 创建主机
|
||||
h, err := libp2p.New(
|
||||
libp2p.ListenAddrStrings(listenAddr),
|
||||
libp2p.DefaultTransports,
|
||||
libp2p.DefaultMuxers,
|
||||
libp2p.DefaultSecurity,
|
||||
)
|
||||
return h, err
|
||||
}
|
||||
|
||||
// 连接网关(WebSocket)
|
||||
func (s *sP2P) connectGateway() (err error) {
|
||||
conn, _, err := websocket.DefaultDialer.Dial(s.client.gatewayURL, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("WebSocket连接失败: %v", err)
|
||||
}
|
||||
//defer conn.Close()
|
||||
|
||||
s.client.wsConn = conn
|
||||
return
|
||||
}
|
||||
|
||||
// 注册到网关
|
||||
func (s *sP2P) register() error {
|
||||
// 收集地址信息
|
||||
addrs := make([]string, len(s.client.host.Addrs()))
|
||||
for i, addr := range s.client.host.Addrs() {
|
||||
addrs[i] = addr.String()
|
||||
}
|
||||
|
||||
// 构建注册消息
|
||||
msg := GatewayMessage{
|
||||
Type: MsgTypeRegister,
|
||||
From: s.client.Id,
|
||||
Data: gjson.MustEncode(g.Map{
|
||||
"peer_id": s.client.host.ID().String(),
|
||||
"addrs": addrs,
|
||||
}),
|
||||
}
|
||||
|
||||
return s.sendToGateway(msg)
|
||||
}
|
||||
|
||||
// 发现并连接目标节点
|
||||
func (s *sP2P) DiscoverAndConnect(targetID string) error {
|
||||
// 发送发现请求
|
||||
msg := GatewayMessage{
|
||||
Type: MsgTypeDiscover,
|
||||
From: s.client.Id, // 发送方是自己
|
||||
To: "gateway", // 接收方是网关
|
||||
Data: gjson.MustEncode(g.Map{
|
||||
"target_id": targetID,
|
||||
}),
|
||||
}
|
||||
if err := s.sendToGateway(msg); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 处理P2P流
|
||||
func (s *sP2P) handleStream(stream network.Stream) {
|
||||
defer stream.Close()
|
||||
|
||||
peerID := stream.Conn().RemotePeer().String()
|
||||
glog.Infof(ctx, "收到来自 %s 的连接", peerID)
|
||||
|
||||
// 读取数据
|
||||
buf := make([]byte, 1024)
|
||||
n, err := stream.Read(buf)
|
||||
if err != nil {
|
||||
glog.Errorf(ctx, "读取流数据失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
g.Log().Debugf(ctx, "收到来自 %s 的消息: %v> ", peerID, string(buf[:n]))
|
||||
}
|
||||
|
||||
// 发送数据到目标节点
|
||||
func (s *sP2P) SendData(targetID string, data []byte) error {
|
||||
peerID, exists := s.client.peers[targetID]
|
||||
if !exists {
|
||||
return fmt.Errorf("未找到目标节点 %s 的连接", targetID)
|
||||
}
|
||||
|
||||
// 创建流
|
||||
stream, err := s.client.host.NewStream(gctx.New(), peerID, protocol.ID("/p2p-chat/1.0.0"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stream.Close()
|
||||
|
||||
// 发送数据
|
||||
_, err = stream.Write(data)
|
||||
return err
|
||||
}
|
||||
|
||||
// 处理网关的打洞请求
|
||||
func (s *sP2P) handlePunchRequest(data json.RawMessage) error {
|
||||
var punchData struct {
|
||||
FromID string `json:"from_id"`
|
||||
PeerID string `json:"peer_id"`
|
||||
Addrs []string `json:"addrs"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &punchData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析PeerID
|
||||
peerID, err := peer.Decode(punchData.PeerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 解析地址
|
||||
addrs := make([]multiaddr.Multiaddr, len(punchData.Addrs))
|
||||
for i, addrStr := range punchData.Addrs {
|
||||
addr, err := multiaddr.NewMultiaddr(addrStr)
|
||||
if err != nil {
|
||||
glog.Errorf(ctx, "解析地址失败: %v", err)
|
||||
continue
|
||||
}
|
||||
addrs[i] = addr
|
||||
}
|
||||
|
||||
// 添加到peerstore
|
||||
s.client.host.Peerstore().AddAddrs(peerID, addrs, peerstore.PermanentAddrTTL)
|
||||
|
||||
// 立即尝试连接(关键:协调时机)
|
||||
glog.Infof(ctx, "收到打洞请求,尝试连接 %s", punchData.FromID)
|
||||
go func() {
|
||||
time.Sleep(500 * time.Millisecond) // 稍微延迟,确保双方都准备好
|
||||
if err := s.client.host.Connect(ctx, peer.AddrInfo{
|
||||
ID: peerID,
|
||||
Addrs: addrs,
|
||||
}); err != nil {
|
||||
glog.Errorf(ctx, "打洞连接失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
glog.Infof(ctx, "成功连接到 %s", punchData.FromID)
|
||||
s.client.peers[punchData.FromID] = peerID
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 发送消息到网关
|
||||
func (s *sP2P) sendToGateway(msg GatewayMessage) (err error) {
|
||||
data, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.client.wsConn.WriteMessage(websocket.TextMessage, data)
|
||||
return
|
||||
}
|
||||
|
||||
// 接收网关消息
|
||||
func (s *sP2P) receiveGatewayMessages() {
|
||||
for {
|
||||
_, data, err := s.client.wsConn.ReadMessage()
|
||||
if err != nil {
|
||||
glog.Errorf(ctx, "接收网关消息失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var msg GatewayMessage
|
||||
if err = json.Unmarshal(data, &msg); err != nil {
|
||||
glog.Errorf(ctx, "解析网关消息失败: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 验证消息是否发给自己(to必须是当前客户端ID或空)
|
||||
if msg.To != "" && msg.To != s.client.Id {
|
||||
g.Log().Debugf(ctx, "忽略非本客户端的消息,from=%s, to=%s", msg.From, msg.To)
|
||||
continue
|
||||
}
|
||||
|
||||
// 处理不同类型消息
|
||||
switch msg.Type {
|
||||
case MsgTypeRegisterAck:
|
||||
glog.Infof(ctx, "注册成功")
|
||||
|
||||
case MsgTypeDiscoverAck:
|
||||
var msgData struct {
|
||||
Found bool `json:"found"`
|
||||
PeerID string `json:"peer_id,omitempty"`
|
||||
Addrs []string `json:"addrs,omitempty"`
|
||||
}
|
||||
if err = gjson.DecodeTo(msg.Data, &msgData); err != nil {
|
||||
g.Log().Errorf(ctx, "解析发现响应失败: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if !msgData.Found {
|
||||
fmt.Println("未找到目标节点")
|
||||
continue
|
||||
}
|
||||
|
||||
// 解析并连接目标节点
|
||||
peerID, err := peer.Decode(msgData.PeerID)
|
||||
if err != nil {
|
||||
glog.Errorf(ctx, "解析PeerID失败: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
addrs := make([]multiaddr.Multiaddr, len(msgData.Addrs))
|
||||
for i, addrStr := range msgData.Addrs {
|
||||
addr, err := multiaddr.NewMultiaddr(addrStr)
|
||||
if err != nil {
|
||||
glog.Errorf(ctx, "解析地址失败: %v", err)
|
||||
continue
|
||||
}
|
||||
addrs[i] = addr
|
||||
}
|
||||
|
||||
s.client.host.Peerstore().AddAddrs(peerID, addrs, peerstore.PermanentAddrTTL)
|
||||
|
||||
// 立即尝试连接
|
||||
go func(targetPeerID peer.ID, targetID string) {
|
||||
time.Sleep(500 * time.Millisecond) // 协调时机
|
||||
if err = s.client.host.Connect(ctx, peer.AddrInfo{
|
||||
ID: targetPeerID,
|
||||
Addrs: addrs,
|
||||
}); err != nil {
|
||||
g.Log().Errorf(ctx, "连接失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "成功连接到目标节点:%v", targetID)
|
||||
s.client.peers[targetID] = targetPeerID
|
||||
}(peerID, msg.To)
|
||||
|
||||
case MsgTypePunchRequest:
|
||||
err = s.handlePunchRequest(msg.Data)
|
||||
|
||||
case MsgTypeError:
|
||||
var data struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
json.Unmarshal(msg.Data, &data)
|
||||
glog.Errorf(ctx, "网关错误: %s", data.Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
1
internal/logic/p2p/dht.go
Normal file
1
internal/logic/p2p/dht.go
Normal file
@@ -0,0 +1 @@
|
||||
package p2p
|
||||
268
internal/logic/p2p/gateway.go
Normal file
268
internal/logic/p2p/gateway.go
Normal file
@@ -0,0 +1,268 @@
|
||||
package p2p
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/gogf/gf/v2/os/glog"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var (
|
||||
Ws *websocket.Conn
|
||||
)
|
||||
|
||||
// 客户端连接信息
|
||||
type ClientConn struct {
|
||||
ID string
|
||||
PeerID string
|
||||
Addrs []string
|
||||
Conn *websocket.Conn
|
||||
LastActive time.Time
|
||||
}
|
||||
|
||||
// 消息结构
|
||||
type GatewayMessage struct {
|
||||
Type MsgType `json:"type"`
|
||||
From string `json:"from"`
|
||||
To string `json:"to,omitempty"`
|
||||
Data json.RawMessage `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
func (s *sP2P) GatewayStart(ctx context.Context, group *ghttp.RouterGroup) (err error) {
|
||||
var wsUpGrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
//// CheckOrigin allows any origin in development
|
||||
//// In production, implement proper origin checking for security
|
||||
//CheckOrigin: func(r *http.Request) bool {
|
||||
// return true
|
||||
//},
|
||||
//// Error handler for upgrade failures
|
||||
//Error: func(w http.ResponseWriter, r *http.Request, status int, reason error) {
|
||||
// // Implement error handling logic here
|
||||
//},
|
||||
}
|
||||
|
||||
group.Bind(func(r *ghttp.Request) {
|
||||
// Upgrade HTTP connection to WebSocket
|
||||
ws, err2 := wsUpGrader.Upgrade(r.Response.Writer, r.Request, nil)
|
||||
if err2 != nil {
|
||||
r.Response.Write(err2.Error())
|
||||
return
|
||||
}
|
||||
defer ws.Close()
|
||||
|
||||
// Message handling loop
|
||||
for {
|
||||
_, data, _err := ws.ReadMessage()
|
||||
if _err != nil {
|
||||
//g.Log().Errorf(ctx, "读取消息失败: %v", err)
|
||||
//s.sendError(ws, err.Error())
|
||||
break
|
||||
}
|
||||
|
||||
var msg GatewayMessage
|
||||
if err = json.Unmarshal(data, &msg); err != nil {
|
||||
//g.Log().Error(ctx, "消息格式错误")
|
||||
s.sendError(ws, "消息格式错误")
|
||||
continue
|
||||
}
|
||||
|
||||
// 处理不同类型的消息
|
||||
switch msg.Type {
|
||||
case MsgTypeRegister:
|
||||
s.handleRegister(ws, msg)
|
||||
case MsgTypeDiscover:
|
||||
s.handleDiscover(ws, msg)
|
||||
default:
|
||||
g.Log().Error(ctx, "未知消息类型: %s", msg.Type)
|
||||
}
|
||||
}
|
||||
// Log connection closure
|
||||
g.Log().Infof(ctx, "websocket %v connection closed", ws.RemoteAddr())
|
||||
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 处理注册请求
|
||||
func (s *sP2P) handleRegister(conn *websocket.Conn, msg GatewayMessage) {
|
||||
if msg.From == "" {
|
||||
g.Log().Error(ctx, "客户端ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
var data struct {
|
||||
PeerID string `json:"peer_id"`
|
||||
Addrs []string `json:"addrs"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(msg.Data, &data); err != nil {
|
||||
s.sendError(conn, "注册数据格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 保存客户端信息
|
||||
client := &ClientConn{
|
||||
ID: msg.From,
|
||||
PeerID: data.PeerID,
|
||||
Addrs: data.Addrs,
|
||||
Conn: conn,
|
||||
LastActive: time.Now(),
|
||||
}
|
||||
|
||||
s.lock.Lock()
|
||||
s.Clients[msg.From] = client
|
||||
s.lock.Unlock()
|
||||
|
||||
glog.Infof(ctx, "客户端 ip=%s,%s 注册成功,PeerID: %s", conn.RemoteAddr(), msg.From, data.PeerID)
|
||||
|
||||
// 发送注册成功响应
|
||||
err := s.sendMessage(conn, GatewayMessage{
|
||||
Type: MsgTypeRegisterAck,
|
||||
Data: json.RawMessage(`{"success": true, "message": "注册成功"}`),
|
||||
})
|
||||
if err != nil {
|
||||
s.sendError(conn, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// 清理超时客户端
|
||||
func (s *sP2P) cleanupClients(ctx context.Context) {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
now := time.Now()
|
||||
s.lock.Lock()
|
||||
for id, client := range s.Clients {
|
||||
if now.Sub(client.LastActive) > 60*time.Second {
|
||||
client.Conn.Close()
|
||||
delete(s.Clients, id)
|
||||
glog.Infof(ctx, "清理超时客户端: %s", id)
|
||||
}
|
||||
}
|
||||
s.lock.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 发送错误消息
|
||||
func (s *sP2P) sendError(conn *websocket.Conn, errMsg string) {
|
||||
s.sendMessage(conn, GatewayMessage{
|
||||
Type: "error",
|
||||
Data: json.RawMessage(fmt.Sprintf(`{"error": "%s"}`, errMsg)),
|
||||
})
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
func (s *sP2P) sendMessage(conn *websocket.Conn, msg GatewayMessage) error {
|
||||
data, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
glog.Errorf(gctx.New(), "序列化消息失败: %v", err)
|
||||
return err
|
||||
}
|
||||
return conn.WriteMessage(websocket.TextMessage, data)
|
||||
}
|
||||
|
||||
// 处理发现请求
|
||||
func (s *sP2P) handleDiscover(conn *websocket.Conn, msg GatewayMessage) {
|
||||
if msg.From == "" {
|
||||
s.sendError(conn, "消息缺少发送方ID(from)")
|
||||
return
|
||||
}
|
||||
|
||||
var data struct {
|
||||
TargetID string `json:"target_id"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(msg.Data, &data); err != nil {
|
||||
s.sendError(conn, "发现请求格式错误,需包含target_id")
|
||||
return
|
||||
}
|
||||
|
||||
if data.TargetID == "" {
|
||||
s.sendError(conn, "目标ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取请求方和目标方信息
|
||||
s.lock.RLock()
|
||||
//fromClient, fromExists := s.Clients[msg.From]
|
||||
//targetClient, targetExists := s.Clients[data.TargetID]
|
||||
fromClient := s.Clients[msg.From] // 发送方:msg.From
|
||||
targetClient := s.Clients[data.TargetID] // 目标方:data.TargetID
|
||||
s.lock.RUnlock()
|
||||
|
||||
//if !fromExists {
|
||||
// s.sendError(conn, "请先注册")
|
||||
// return
|
||||
//}
|
||||
|
||||
// 更新活动时间
|
||||
s.lock.Lock()
|
||||
fromClient.LastActive = time.Now()
|
||||
s.lock.Unlock()
|
||||
|
||||
if targetClient != nil {
|
||||
// 目标不存在
|
||||
s.sendMessage(conn, GatewayMessage{
|
||||
Type: MsgTypeDiscoverAck,
|
||||
From: "gateway",
|
||||
To: msg.From,
|
||||
//Data: json.RawMessage(`{"found": false}`),
|
||||
Data: gjson.MustEncode(g.Map{
|
||||
"found": false,
|
||||
}),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 向请求方发送目标信息
|
||||
s.sendMessage(conn, GatewayMessage{
|
||||
Type: MsgTypeDiscoverAck,
|
||||
From: "gateway", // 发送方是网关
|
||||
To: msg.From, // 接收方是原请求方
|
||||
Data: gjson.MustEncode(g.Map{
|
||||
"found": true,
|
||||
"peer_id": targetClient.PeerID,
|
||||
//"addrs": s.getAddrsJSON(targetClient.Addrs),
|
||||
"addrs": targetClient.Addrs,
|
||||
}),
|
||||
})
|
||||
|
||||
// 向目标方发送打洞请求(协调时机)
|
||||
s.sendMessage(targetClient.Conn, GatewayMessage{
|
||||
Type: MsgTypePunchRequest,
|
||||
From: msg.From, // 发送方是原请求方
|
||||
To: data.TargetID, // 接收方是目标方
|
||||
Data: gjson.MustEncode(g.Map{
|
||||
"from_id": msg.From,
|
||||
"peer_id": fromClient.PeerID,
|
||||
//"addrs": s.getAddrsJSON(fromClient.Addrs),
|
||||
"addrs": targetClient.Addrs,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
// 获取地址JSON字符串
|
||||
func (s *sP2P) getAddrsJSON(addrs []string) string {
|
||||
strs := make([]string, len(addrs))
|
||||
for i, addr := range addrs {
|
||||
strs[i] = fmt.Sprintf("%q", addr)
|
||||
}
|
||||
return fmt.Sprintf("[%s]", strings.Join(strs, ","))
|
||||
}
|
||||
56
internal/logic/p2p/p2p.go
Normal file
56
internal/logic/p2p/p2p.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package p2p
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/ayflying/p2p/internal/service"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/libp2p/go-libp2p/core/protocol"
|
||||
)
|
||||
|
||||
var (
|
||||
ctx = gctx.New()
|
||||
)
|
||||
|
||||
// 常量定义
|
||||
const (
|
||||
ProtocolID protocol.ID = "/ay/1.0.0"
|
||||
DefaultPort = 51888
|
||||
)
|
||||
|
||||
type MsgType string
|
||||
|
||||
const (
|
||||
MsgTypeRegister = "register" // 客户端注册
|
||||
MsgTypeRegisterAck = "register_ack" // 注册响应
|
||||
MsgTypeDiscover = "discover" // 发现目标客户端
|
||||
MsgTypeDiscoverAck = "discover_ack" // 发现响应
|
||||
MsgTypeConnectionReq = "connection_req" // 连接请求通知
|
||||
MsgTypePunchRequest = "punch_request"
|
||||
MsgTypeError = "error" // 错误消息
|
||||
)
|
||||
|
||||
// 注册请求数据
|
||||
type RegisterData struct {
|
||||
ClientID string `json:"client_id"`
|
||||
}
|
||||
|
||||
// sP2P 结构体实现 IP2P 接口
|
||||
type sP2P struct {
|
||||
Clients map[string]*ClientConn // 客户端ID -> 连接
|
||||
lock sync.RWMutex
|
||||
|
||||
client *Client
|
||||
}
|
||||
|
||||
// New 创建一个新的 P2P 服务实例
|
||||
func New() *sP2P {
|
||||
return &sP2P{
|
||||
Clients: make(map[string]*ClientConn),
|
||||
client: &Client{},
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
service.RegisterP2P(New())
|
||||
}
|
||||
0
internal/model/.gitkeep
Normal file
0
internal/model/.gitkeep
Normal file
0
internal/model/do/.gitkeep
Normal file
0
internal/model/do/.gitkeep
Normal file
0
internal/model/entity/.gitkeep
Normal file
0
internal/model/entity/.gitkeep
Normal file
1
internal/packed/packed.go
Normal file
1
internal/packed/packed.go
Normal file
@@ -0,0 +1 @@
|
||||
package packed
|
||||
0
internal/service/.gitkeep
Normal file
0
internal/service/.gitkeep
Normal file
38
internal/service/p_2_p.go
Normal file
38
internal/service/p_2_p.go
Normal file
@@ -0,0 +1,38 @@
|
||||
// ================================================================================
|
||||
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
|
||||
// You can delete these comments if you wish manually maintain this interface file.
|
||||
// ================================================================================
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
)
|
||||
|
||||
type (
|
||||
IP2P interface {
|
||||
Start(ctx context.Context, wsStr string) (err error)
|
||||
// 发现并连接目标节点
|
||||
DiscoverAndConnect(targetID string) error
|
||||
// 发送数据到目标节点
|
||||
SendData(targetID string, data []byte) error
|
||||
GatewayStart(ctx context.Context, group *ghttp.RouterGroup) (err error)
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
localP2P IP2P
|
||||
)
|
||||
|
||||
func P2P() IP2P {
|
||||
if localP2P == nil {
|
||||
panic("implement not found for interface IP2P, forgot register?")
|
||||
}
|
||||
return localP2P
|
||||
}
|
||||
|
||||
func RegisterP2P(i IP2P) {
|
||||
localP2P = i
|
||||
}
|
||||
Reference in New Issue
Block a user