下载地址:
https://github.com/protocolbuffers/protobuf/releases
我选的是windows环境的
下载完之后,解压到文件夹内,并添加环境变量。
代码地址
https://github.com/grpc/grpc-go/tags
golang package
go get -u google.golang.org/grpc
go get google.golang.org/protobuf
// 会在go安装目录 go/bin下生成一个protoc-gen-go.exe
go get -u github.com/golang/protobuf/protoc-gen-go
// go/bin下protoc-gen-grpc-gateway.exe
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
docker 安装etcd并运行
docker pull bitnami/etcd:3.4.20
docker network create etcd-bridge --driver bridge
docker run -d --name etcd-server \
--network etcd-bridge \
--publish 2379:2379 \
--publish 2380:2380 \
--env ALLOW_NONE_AUTHENTICATION=yes \
--env ETCD_ADVERTISE_CLIENT_URLS=http://etcd-server:2379 \
bitnami/etcd:3.4.20
/rpc
/client
-main.go
/etcd_service
-etcd.go
/proto
-product.proto
/proto_service
-pruduct.pb.go
-pruduct.pb.gw.go
/server
-main.go
-pruduct.go
-main.go
// 使用的语法版本
syntax = "proto3";
// 生成的go文件包
option go_package = "./proto_service";
//service 服务 接口
service ProdService {
rpc GetProductStock(ProductRequest) returns (ProductResponse);
}
// 请求参数
message ProductRequest {
int32 prod_id = 1;
}
// 返回参数
message ProductResponse {
int32 prod_stock = 1;
}
生成go文件
pwd
/rpc/proto
protoc --go_out=plugins=grpc:../ product.proto
命令会直接生成proto_service目录和product.pb.go文件
product.go
package main
import (
"context"
"log"
"rpc/proto_service"
)
// 建一个实例
var ProductService = &productService{}
type productService struct {}
// 服务端实现接口业务内容
func (p productService) GetProductStock(ctx context.Context, request *proto_service.ProductRequest) (*proto_service.ProductResponse, error) {
log.Println("request: ", request)
return &proto_service.ProductResponse{ProdStock: request.ProdId*1000 + 996}, nil
}
main.go
package main
import (
"google.golang.org/grpc"
"log"
"net"
"rpc/proto_service"
)
const addr = "127.0.0.1:8001"
func main() {
rpcServer := grpc.NewServer()
proto_service.RegisterProdServiceServer(rpcServer, ProductService)
listener, err := net.Listen("tcp", addr)
if err != nil {
log.Fatal("启动监听出错", err)
}
err = rpcServer.Serve(listener)
if err != nil {
log.Fatal("启动服务出错", err)
}
}
开启服务
go run main.go
client/main.go
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"log"
"rpc/proto_service"
"time"
)
const addr = "127.0.0.1:8001"
func main() {
conn, err := grpc.Dial(addr,grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatal("服务端出错,连接不上:", err)
}
defer conn.Close()
client := proto_service.NewProdServiceClient(conn)
for i := 0; i < 100; i++ {
// 限制5秒自动返回请求失败
tctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
response, err := client.GetProductStock(tctx, &proto_service.ProductRequest{ProdId: int32(i) * 1000})
if err != nil {
log.Println("查询出错:", err)
}
fmt.Printf("%d response:%v \n", i, response)
time.Sleep(5 * time.Second)
}
}
etcd_service/etcd.go
package etcd_service
import (
"context"
"fmt"
clientv3 "go.etcd.io/etcd/client/v3"
"go.etcd.io/etcd/client/v3/naming/endpoints"
"log"
"time"
)
var (
config clientv3.Config
client *clientv3.Client
err error
)
func init() {
config = clientv3.Config{
Endpoints: []string{"172.17.219.154:2379"},
DialTimeout: 5 * time.Second,
}
client, err = clientv3.New(config)
if err != nil {
panic(err)
}
}
func Register(serviceName, addr string) error {
log.Printf("etcd etcd_service proto_service:%s, addr:%s \n", serviceName, addr)
leaseGrantResp, err := client.Grant(context.TODO(), 10)
if err != nil {
log.Println(err)
return err
}
leaseId := leaseGrantResp.ID
kv := clientv3.NewKV(client)
{
em, err := endpoints.NewManager(client, serviceName)
err = em.AddEndpoint(context.TODO(), fmt.Sprintf("%s/%s", serviceName, addr),
endpoints.Endpoint{Addr: addr}, clientv3.WithLease(leaseId))
if err != nil {
log.Println(err)
return err
}
}
// 查看一下
getResp, err := kv.Get(context.TODO(), fmt.Sprintf("%s/%s", serviceName, addr))
if err != nil {
log.Println(err)
return err
}
// 输出本次的Revision
for i, v := range getResp.Kvs {
log.Printf("%d Key is s %s , Value is %s \n", i, v.Key, v.Value)
}
// 自动续租
keepRespChan, err := client.KeepAlive(context.TODO(), leaseId)
if err != nil {
log.Println(err)
return err
}
go func() {
for {
select {
case keepResp, ok := <-keepRespChan:
if !ok || keepRespChan == nil {
log.Println("租约已经失效")
goto END
} else if ok {
//log.Println("收到自动续租", keepResp.ID)
_ = keepResp.ID
}
}
}
END:
}()
return nil
}
func UnRegister(serviceName, addr string) error {
log.Printf("etcd unregister proto_service:%s, addr:%s \n", serviceName, addr)
kv := clientv3.NewKV(client)
deleteResp, err := kv.Delete(context.TODO(), fmt.Sprintf("%s/%s", serviceName, addr), clientv3.WithPrevKV())
if err != nil {
log.Println(err)
return err
}
log.Println(deleteResp.Header.Revision)
if deleteResp.PrevKvs != nil {
for _, kvpair := range deleteResp.PrevKvs {
fmt.Printf("delete key is: %s , Value: %s \n", string(kvpair.Key), string(kvpair.Value))
}
}
return err
}
这里可以不用引用endpoints的方法,可以使用etcd.client的put键值对的方法把服务地址存进去,但是value要做成 {"Op":0,"Addr":"127.0.0.1:8001","Metadata":null}
这样的字符串,因为客户端自动解析的时候会做json解析
putResp, err := kv.Put(context.TODO(), fmt.Sprintf("%s/%s", serviceName, addr), fmt.Sprintf(`{"Op":0,"Addr":"%s","Metadata":null} `, addr), clientv3.WithPrevKV(), clientv3.WithLease(leaseId))
if err != nil {
log.Println(err)
return err
}
server/main.go
package main
import (
"google.golang.org/grpc"
"log"
"net"
"os"
"os/signal"
"syscall"
"rpc/etcd_service"
"rpc/proto_service"
)
const addr = "127.0.0.1:8001"
const serviceName = "aaa/"
func main() {
//关闭信号处理
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT, syscall.SIGKILL, syscall.SIGHUP, syscall.SIGQUIT)
go func() {
s := <-ch
// 监听关闭信号,关闭了就从etcd中删除
etcd_service.UnRegister(serviceName, addr)
if i, ok := s.(syscall.Signal); ok {
os.Exit(int(i))
} else {
os.Exit(0)
}
}()
// etcd 注册
etcd_service.Register(serviceName, addr)
rpcServer := grpc.NewServer()
proto_service.RegisterProdServiceServer(rpcServer, ProductService)
listener, err := net.Listen("tcp", addr)
if err != nil {
log.Fatal("启动监听出错", err)
}
err = rpcServer.Serve(listener)
if err != nil {
log.Fatal("启动服务出错", err)
}
}
修改addr开启三个服务
package main
import (
"context"
"fmt"
clientv3 "go.etcd.io/etcd/client/v3"
etcdResolver "go.etcd.io/etcd/client/v3/naming/resolver"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"log"
"test/rpc/proto_service"
"time"
)
const serviceName = "aaa/"
func main() {
etcdClient, _ := clientv3.New(clientv3.Config{
Endpoints: []string{"172.17.219.154:2379"},
DialTimeout: 5 * time.Second,
})
etcd_resolver, err := etcdResolver.NewBuilder(etcdClient)
if err != nil {
panic(err)
}
fmt.Println(etcd_resolver.Scheme())
conn, err := grpc.Dial(fmt.Sprintf("etcd:///%s", serviceName),
grpc.WithResolvers(etcd_resolver),
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
)
if err != nil {
log.Fatal("服务端出错,连接不上:", err)
}
defer conn.Close()
client := proto_service.NewProdServiceClient(conn)
for i := 0; i < 100; i++ {
tctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
response, err := client.GetProductStock(tctx, &proto_service.ProductRequest{ProdId: int32(i) * 1000})
if err != nil {
log.Println("查询出错:", err)
}
time.Sleep(5 * time.Second)
}
}
grpc.WithResolvers(etcd_resolver)
绑定etcd的解析
grpc.WithDefaultServiceConfig({"loadBalancingPolicy":"round_robin"}
), 采用轮询的方式请求服务地址
syntax = "proto3";
// option go_package = "path;name"; path gow文件存放地址,name go包名
option go_package = "./proto_service";
import "google/api/annotations.proto";
//service 服务
service ProdService {
rpc GetProductStock(ProductRequest) returns (ProductResponse);
rpc GetProduct(ProductRequest) returns (ProductResponse) {
option (google.api.http) = {
post: "/GetProduct"
body: "*"
};
};
}
message ProductRequest {
int32 prod_id = 1;
}
message ProductResponse {
int32 prod_stock = 1;
}
这里引用google/api/annotations.proto
文件来自于github.com/googleapis/googleapis
包,一般go get ~
的包会放在$GOPATH
下
然后运行命令生成文件
protoc -I . -I C:\Users\Administrator\go\pkg\mod\github.com\googleapis\[email protected] --go_out=plugins=grpc:../ product.proto
protoc -I . -I C:\Users\Administrator\go\pkg\mod\github.com\googleapis\[email protected] --grpc-gateway_out=../ product.proto
product.pb.go
product.pb.gw.go
C:\Users\Administrator\go是我电脑上的GOPATH
package main
import (
"context"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"google.golang.org/grpc"
"log"
"net"
"net/http"
"os"
"os/signal"
"syscall"
"test/rpc/etcd_service"
"test/rpc/proto_service"
)
const addr = "127.0.0.1:8001"
const httpAddr = "127.0.0.1:9001"
const serviceName = "aaa/"
func main() {
//关闭信号处理
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT, syscall.SIGKILL, syscall.SIGHUP, syscall.SIGQUIT)
go func() {
s := <-ch
etcd_service.UnRegister(serviceName, addr)
if i, ok := s.(syscall.Signal); ok {
os.Exit(int(i))
} else {
os.Exit(0)
}
}()
// etcd 注册
etcd_service.Register(serviceName, addr)
rpcServer := grpc.NewServer()
//开启http服务
go func() {
mux := runtime.NewServeMux()
err := proto_service.RegisterProdServiceHandlerServer(context.Background(), mux, ProductService)
//err := proto_service.RegisterProdServiceHandlerFromEndpoint(context.Background(), mux, "127.0.0.1:8001",
// []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())})
if err != nil {
log.Fatal("RegisterProdService err:", err)
}
err = http.ListenAndServe(httpAddr, mux)
if err != nil {
log.Fatal("ListenAndServe err:", err)
}
}()
// grpc server
proto_service.RegisterProdServiceServer(rpcServer, ProductService)
listener, err := net.Listen("tcp", addr)
if err != nil {
log.Fatal("启动监听出错", err)
}
err = rpcServer.Serve(listener)
if err != nil {
log.Fatal("启动服务出错", err)
}
}
使用postman测试一下http服务