Cousul
1.安装consul,直接去官网下载。
Consul官网:https://www.consul.io/
Consul下载地址:https://developer.hashicorp.com/consul/install?product_intent=consul
windows下载
2.运行consul
consul agent -dev
具体配置可以看这篇文章。 https://blog.csdn.net/qq_45774406/article/details/138251814
运行成功之后就可以在localhost:8500看到consul界面了。
具体服务
现在使用golang在本地通过协程启动多个服务,并注册到consul。
1.定义proto
先新建一个目录proto,然后新建service.proto,复制下面的内容进去
bash
syntax = "proto3";
package proto;
option go_package = "./proto";
// 定义示例服务
service ExampleService {
// 简单的Echo方法
rpc Echo (EchoRequest) returns (EchoResponse);
}
// Echo请求消息
message EchoRequest {
string message = 1;
}
// Echo响应消息
message EchoResponse {
string message = 1;
string server_id = 2;
}
2.把proto转为go文件
需要安装以下插件:
go
protoc
protoc-gen-go
安装方法和proto文件说明参考
https://blog.csdn.net/weixin_42413966/article/details/128497429
安装好之后运行下面的语句,会自动生成service_grpc.pb.go和service.pb.go
bash
protoc --go_out=. --go-grpc_out=. proto/service.proto
service.pb.go 包含了Protocol Buffers消息类型的定义和序列化相关的代码。
service_grpc.pb.go 包含了gRPC服务相关的代码,包括客户端和服务端的接口定义、方法实现等。
3.服务注册
下面是服务注册的代码
main.go
bash
package main
import (
"consul/pkg/service"
"context"
"fmt"
"log"
"os"
"os/signal"
"syscall"
)
func main() {
// 创建一个带取消的上下文
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// 创建服务实例
srv, err := service.NewService(
"service-1", // 服务ID
"example-service", // 服务名称
8080, // HTTP端口
50051, // gRPC端口
"localhost:8500", // Consul地址
func() {
fmt.Println("服务已启动:")
fmt.Println("- HTTP服务地址:http://localhost:8080")
fmt.Println("- gRPC服务地址:localhost:50051")
},
)
if err != nil {
log.Fatalf("创建服务失败: %v", err)
}
// 设置信号处理
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
// 启动服务
go func() {
if err := srv.Start(ctx); err != nil {
log.Printf("服务运行错误: %v", err)
cancel()
}
}()
// 等待信号
<-sigCh
cancel()
log.Println("正在关闭服务...")
}
service.go
bash
package service
import (
"consul/pkg/registry"
"consul/proto"
"context"
"fmt"
"log"
"net"
"net/http"
"sync"
"github.com/gin-gonic/gin"
"google.golang.org/grpc"
)
type Service struct {
ID string
Name string
HTTPAddr string
HTTPPort int
GRPCAddr string
GRPCPort int
Registry *registry.ServiceRegistry
httpSrv *http.Server
grpcSrv *grpc.Server
wg sync.WaitGroup
onStarted func()
}
func NewService(id, name string, httpPort, grpcPort int, consulAddr string, onStarted func()) (*Service, error) {
reg, err := registry.NewServiceRegistry(consulAddr)
if err != nil {
return nil, err
}
return &Service{
ID: id,
Name: name,
HTTPAddr: "localhost",
HTTPPort: httpPort,
GRPCAddr: "localhost",
GRPCPort: grpcPort,
Registry: reg,
onStarted: onStarted,
}, nil
}
func (s *Service) Start(ctx context.Context) error {
// 创建一个等待组来等待服务启动
var startWg sync.WaitGroup
startWg.Add(2) // 等待HTTP和gRPC服务
// 启动HTTP服务
s.wg.Add(1)
go func() {
defer s.wg.Done()
s.startHTTP(&startWg)
}()
// 启动gRPC服务
s.wg.Add(1)
go func() {
defer s.wg.Done()
s.startGRPC(&startWg)
}()
// 注册服务到Consul
err := s.Registry.RegisterService(
s.ID,
s.Name,
s.HTTPAddr,
s.HTTPPort,
s.GRPCPort,
[]string{"http", "grpc"},
)
if err != nil {
return err
}
// 等待服务启动完成
startWg.Wait()
if s.onStarted != nil {
s.onStarted()
}
// 等待上下文取消
<-ctx.Done()
return s.Stop()
}
func (s *Service) Stop() error {
// 从Consul注销服务
if err := s.Registry.DeregisterService(s.ID); err != nil {
log.Printf("注销服务失败: %v", err)
}
// 停止HTTP服务
if s.httpSrv != nil {
if err := s.httpSrv.Shutdown(context.Background()); err != nil {
log.Printf("停止HTTP服务失败: %v", err)
}
}
// 停止gRPC服务
if s.grpcSrv != nil {
s.grpcSrv.GracefulStop()
}
// 等待所有服务停止
s.wg.Wait()
return nil
}
func (s *Service) startHTTP(wg *sync.WaitGroup) {
router := gin.Default()
// 健康检查端点
router.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
// 示例API端点
router.GET("/api/echo", func(c *gin.Context) {
msg := c.Query("message")
c.JSON(200, gin.H{
"message": msg,
"server_id": s.ID,
})
})
// Greeter服务HTTP端点
router.GET("/helloworld/:name", func(c *gin.Context) {
name := c.Param("name")
reply, err := (&GreeterImpl{ID: s.ID}).SayHello(c, &proto.HelloRequest{Name: name})
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
c.JSON(200, reply)
})
s.httpSrv = &http.Server{
Addr: fmt.Sprintf("%s:%d", s.HTTPAddr, s.HTTPPort),
Handler: router,
}
// 创建监听器
lis, err := net.Listen("tcp", s.httpSrv.Addr)
if err != nil {
log.Printf("HTTP服务监听失败: %v", err)
return
}
// 通知服务已启动
wg.Done()
if err := s.httpSrv.Serve(lis); err != http.ErrServerClosed {
log.Printf("HTTP服务错误: %v", err)
}
}
func (s *Service) startGRPC(wg *sync.WaitGroup) {
lis, err := net.Listen("tcp", fmt.Sprintf("%s:%d", s.GRPCAddr, s.GRPCPort))
if err != nil {
log.Printf("gRPC服务监听失败: %v", err)
return
}
s.grpcSrv = grpc.NewServer()
// 注册gRPC服务实现
proto.RegisterExampleServiceServer(s.grpcSrv, &grpcImpl{ID: s.ID})
// 注册Greeter服务
proto.RegisterGreeterServer(s.grpcSrv, &GreeterImpl{ID: s.ID})
// 通知服务已启动
wg.Done()
if err := s.grpcSrv.Serve(lis); err != nil {
log.Printf("gRPC服务错误: %v", err)
}
}
以及注册consul用到的consul.go
bash
package registry
import (
"fmt"
"log"
"github.com/hashicorp/consul/api"
)
type ServiceRegistry struct {
client *api.Client
}
// NewServiceRegistry 创建一个新的服务注册器
func NewServiceRegistry(consulAddr string) (*ServiceRegistry, error) {
config := api.DefaultConfig()
config.Address = consulAddr
client, err := api.NewClient(config)
if err != nil {
return nil, fmt.Errorf("创建Consul客户端失败: %v", err)
}
return &ServiceRegistry{client: client}, nil
}
// RegisterService 注册服务到Consul
func (r *ServiceRegistry) RegisterService(serviceID, serviceName string, address string, httpPort int, grpcPort int, tags []string) error {
// 在Meta中添加gRPC端口信息
meta := map[string]string{
"grpc_port": fmt.Sprintf("%d", grpcPort),
}
reg := &api.AgentServiceRegistration{
ID: serviceID,
Name: serviceName,
Address: address,
Port: httpPort,
Tags: tags,
Meta: meta,
Check: &api.AgentServiceCheck{
HTTP: fmt.Sprintf("http://%s:%d/health", address, httpPort),
Interval: "10s",
Timeout: "5s",
},
}
err := r.client.Agent().ServiceRegister(reg)
if err != nil {
return fmt.Errorf("注册服务失败: %v", err)
}
log.Printf("服务 %s 已成功注册到Consul", serviceID)
return nil
}
// DeregisterService 从Consul注销服务
func (r *ServiceRegistry) DeregisterService(serviceID string) error {
err := r.client.Agent().ServiceDeregister(serviceID)
if err != nil {
return fmt.Errorf("注销服务失败: %v", err)
}
log.Printf("服务 %s 已从Consul注销", serviceID)
return nil
}
启动成功,分别监听了 http 8080和grpc 50051端口
然后consul里面也能够看到
现在在另外一个目录下新建一个项目,用来连接consul然后进行grpc请求。
新建consul_client目录,然后在此目录下新建proto目录。
将之前的proto文件放到proto目录下。
然后执行
bash
protoc --go_out=. --go-grpc_out=. --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative ./proto/example.proto
然后新建main.go
bash
package main
import (
"context"
"fmt"
"log"
"time"
pb "consul_client/proto"
consulapi "github.com/hashicorp/consul/api"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
// 创建 Consul 客户端配置
config := consulapi.DefaultConfig()
config.Address = "http://localhost:8500"
// 创建 Consul 客户端
client, err := consulapi.NewClient(config)
if err != nil {
log.Fatal("创建 Consul 客户端失败:", err)
}
// 查询服务
services, _, err := client.Health().Service("example-service", "", true, nil)
if err != nil {
log.Fatal("获取服务失败:", err)
}
if len(services) == 0 {
log.Fatal("没有找到可用的服务实例")
}
// 获取服务实例信息
service := services[0].Service
// 打印服务元数据
fmt.Println("服务元数据:")
for key, value := range service.Meta {
fmt.Printf(" %s: %s\n", key, value)
}
// 尝试从元数据中获取 gRPC 端口
grpcPort := service.Port
if port, ok := service.Meta["grpc_port"]; ok {
fmt.Sscanf(port, "%d", &grpcPort)
} else {
log.Fatal("无法获取 gRPC 端口")
}
serviceAddr := fmt.Sprintf("%s:%d", service.Address, grpcPort)
fmt.Printf("找到 gRPC 服务地址: %s\n", serviceAddr)
// 创建 gRPC 连接
conn, err := grpc.NewClient(serviceAddr,
grpc.WithTransportCredentials(insecure.NewCredentials()),
// 使用 WithTimeout 的替代方案
grpc.WithConnectParams(grpc.ConnectParams{
MinConnectTimeout: 5 * time.Second,
}),
)
if err != nil {
log.Fatal("无法连接到 gRPC 服务:", err)
}
defer conn.Close()
// 创建 gRPC 客户端
grpcClient := pb.NewExampleServiceClient(conn)
// 设置上下文超时
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
// 发送 Echo 请求
response, err := grpcClient.Echo(ctx, &pb.EchoRequest{
Message: "Hello from gRPC client!",
})
if err != nil {
log.Fatal("调用 Echo 服务失败:", err)
}
fmt.Printf("服务响应: Message=%s, ServerID=%s\n", response.Message, response.ServerId)
}
运行一下,可以看到已经通过consul找到了对应的地址和端口,并且按proto的格发送了请求并得到了回复。