diff --git a/main.go b/main.go new file mode 100644 index 0000000..1f80aba --- /dev/null +++ b/main.go @@ -0,0 +1,211 @@ +package main + +import ( + "context" + "fmt" + "log" + "time" + + clientv3 "go.etcd.io/etcd/client/v3" +) + +const ( + // etcd 服务器地址(gRPC 端口,默认 2379) + etcdEndpoint = "8.155.160.224:2379" + // 用户名 + username = "admin" + // 密码(根据 Docker 配置更新) + password = "my45638my" + // 服务注册的 key 前缀 + servicePrefix = "/services/" +) + +// ServiceInfo 服务信息 +type ServiceInfo struct { + Name string // 服务名称 + Address string // 服务地址 + Port int // 服务端口 + Metadata map[string]string // 元数据 +} + +// EtcdRegistry etcd 注册器 +type EtcdRegistry struct { + client *clientv3.Client + ctx context.Context +} + +// NewEtcdRegistry 创建 etcd 注册器 +func NewEtcdRegistry() (*EtcdRegistry, error) { + fmt.Printf("正在连接到 etcd 服务器: %s\n", etcdEndpoint) + + // 先尝试不使用认证连接(因为 Docker 配置可能没有启用 RBAC) + config := clientv3.Config{ + Endpoints: []string{etcdEndpoint}, + DialTimeout: 10 * time.Second, + // 暂时不设置用户名密码,先测试连接 + } + + // 创建客户端 + fmt.Println("正在创建 etcd 客户端...") + client, err := clientv3.New(config) + if err != nil { + return nil, fmt.Errorf("创建 etcd 客户端失败: %w", err) + } + + // 测试连接 + fmt.Println("正在测试连接...") + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // 尝试获取一个不存在的 key 来测试连接 + _, err = client.Get(ctx, "/test-connection", clientv3.WithLimit(1)) + if err != nil { + // 如果是 context deadline exceeded,可能是网络问题 + if ctx.Err() == context.DeadlineExceeded { + client.Close() + return nil, fmt.Errorf("连接 etcd 服务器超时\n可能的原因:\n1. etcd 服务器未启动或未正确运行\n2. 端口映射不正确 (Docker -p 2379:2379)\n3. 防火墙阻止了连接\n4. etcd 配置中的 --advertise-client-urls 应该使用实际 IP 而不是 0.0.0.0\n\n建议检查:\n- docker ps 查看 etcd 容器是否运行\n- docker logs etcd 查看 etcd 日志\n- 确认 Docker 端口映射正确") + } + // 如果是认证错误,尝试使用用户名密码 + errStr := err.Error() + if errStr == "etcdserver: invalid auth token" || + errStr == "etcdserver: user name is empty" || + errStr == "etcdserver: authentication failed" { + fmt.Printf("检测到需要认证,尝试使用用户名密码连接...\n") + client.Close() + return newEtcdRegistryWithAuth() + } + // 其他错误 + client.Close() + return nil, fmt.Errorf("连接测试失败: %w\n错误详情: %s", err, errStr) + } + + fmt.Printf("成功连接到 etcd 服务器: %s (无需认证)\n", etcdEndpoint) + + return &EtcdRegistry{ + client: client, + ctx: context.Background(), + }, nil +} + +// newEtcdRegistryWithAuth 使用认证创建 etcd 注册器 +func newEtcdRegistryWithAuth() (*EtcdRegistry, error) { + config := clientv3.Config{ + Endpoints: []string{etcdEndpoint}, + DialTimeout: 10 * time.Second, + Username: username, + Password: password, + } + + client, err := clientv3.New(config) + if err != nil { + return nil, fmt.Errorf("创建 etcd 客户端失败: %w", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + _, err = client.Get(ctx, "/test-connection", clientv3.WithLimit(1)) + if err != nil { + client.Close() + return nil, fmt.Errorf("使用认证连接失败: %w (请检查用户名和密码)", err) + } + + fmt.Printf("成功连接到 etcd 服务器: %s (用户: %s)\n", etcdEndpoint, username) + + return &EtcdRegistry{ + client: client, + ctx: context.Background(), + }, nil +} + +// Register 注册服务到 etcd +func (r *EtcdRegistry) Register(service *ServiceInfo) error { + // 构建服务 key + serviceKey := fmt.Sprintf("%s%s", servicePrefix, service.Name) + + // 构建服务 value(可以是 JSON 格式或其他格式) + serviceValue := fmt.Sprintf("%s:%d", service.Address, service.Port) + + // 设置租约,用于服务发现和健康检查 + // 租约时间为 30 秒,服务需要定期续约 + lease, err := r.client.Grant(r.ctx, 30) + if err != nil { + return fmt.Errorf("创建租约失败: %w", err) + } + + // 将服务信息写入 etcd,并关联租约 + _, err = r.client.Put(r.ctx, serviceKey, serviceValue, clientv3.WithLease(lease.ID)) + if err != nil { + return fmt.Errorf("注册服务失败: %w", err) + } + + fmt.Printf("服务注册成功: key=%s, value=%s\n", serviceKey, serviceValue) + + // 启动续约协程,保持服务在线 + go r.keepAlive(lease.ID) + + return nil +} + +// keepAlive 保持租约活跃 +func (r *EtcdRegistry) keepAlive(leaseID clientv3.LeaseID) { + ch, err := r.client.KeepAlive(r.ctx, leaseID) + if err != nil { + log.Printf("续约失败: %v\n", err) + return + } + + for ka := range ch { + if ka != nil { + log.Printf("租约续约成功, ID: %d, TTL: %d\n", ka.ID, ka.TTL) + } + } +} + +// Unregister 注销服务 +func (r *EtcdRegistry) Unregister(serviceName string) error { + serviceKey := fmt.Sprintf("%s%s", servicePrefix, serviceName) + _, err := r.client.Delete(r.ctx, serviceKey) + if err != nil { + return fmt.Errorf("注销服务失败: %w", err) + } + + fmt.Printf("服务注销成功: %s\n", serviceKey) + return nil +} + +// Close 关闭连接 +func (r *EtcdRegistry) Close() error { + return r.client.Close() +} + +func main() { + // 创建注册器 + registry, err := NewEtcdRegistry() + if err != nil { + log.Fatalf("初始化注册器失败: %v\n", err) + } + defer registry.Close() + + // 创建服务信息 + service := &ServiceInfo{ + Name: "my-service", + Address: "localhost", + Port: 8080, + Metadata: map[string]string{ + "version": "1.0.0", + "env": "production", + }, + } + + // 注册服务 + err = registry.Register(service) + if err != nil { + log.Fatalf("注册服务失败: %v\n", err) + } + + fmt.Println("服务已注册,按 Ctrl+C 退出...") + + // 保持程序运行 + select {} +}