Lost Temple

最近在学Go,对我愚笨的我来说,一个知识点,可能要看几百遍才能懂。比如之前看struct和interface,现在还不是特别熟练。然后项目上说要用protobuf,这个压缩效率比json高,对消息系统来说数据传输会更高效。

然后我就去网上找了个例子,写了一遍。这里就再写一遍,以来巩固一下自己所学的,而来给后来者可能有一些帮助。

首先的话要去装protobuf以及go的一些包,这里给出ubuntu的方法,mac的话大同小异,可以自行搜索解决。


apt-get update && \
apt-get -y install git unzip build-essential autoconf libtool
    
mkdir -p /tmp 
cd /tmp 
git clone https://github.com/google/protobuf 
cd protobuf 
./autogen.sh 
./configure 
make 
make check 
make install

go get github.com/golang/protobuf/proto   // golang的protobuf库文件
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway  //生成gateway代码的库文件
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger //生成swagger.json的库文件
go get -u github.com/golang/protobuf/protoc-gen-go  //// 用于根据protobuf生成golang代码,语法 protoc --go_out=. *.proto

当你能够在命令行里面敲下protoc弹出它的usage的时候,就证明你安装成功了。

以下是我的文件目录结构,放在 $GOPATH/src目录下。

demo
├── client
│   └── main.go
├── demo_proto
│   ├── demo_proto.pb.go
│   ├── demo_proto.pb.gw.go
│   ├── demo_proto.proto
│   └── demo_proto.swagger.json
├── main.go
├── make.sh
└── server
    └── main.go
  

client目录:是grpc的客户端。

demo_proto目录:放.proto文件的,剩下的后缀文件都是自动生成出来的。

main.go:是项目主要运行程序,在这里启动restful服务,放在根目录下

make.sh:一些shell脚本

server目录:grpc的服务端。

我们先看demo_proto目录下的proto文件

syntax = "proto3";

import "google/api/annotations.proto";

package demo_proto;

//定一个一个Hello的服务
service Hello{
    rpc SendHelpInfoWithGetMethod(DemoProtoHelloRequest) returns (DemoProtoHelloResponse){
        option (google.api.http).get ="/get";
    }

    rpc SendInfoWithPostMethod (DemoProtoHelloRequest) returns (DemoProtoHelloResponse){
        option (google.api.http) = {
        post: "/post"
        body: "*"
        };
    }

}


message DemoProtoHelloRequest{
    string name = 1;
}

message DemoProtoHelloResponse {
    string message = 1;
}

接下来看client目录下的main.go 文件 line1: syntax = “proto3”; 指定protobuf的版本 line2: 导入一个包,主要是要用到这个包的数据结构,这里包是带回用来生成proto代码需要导入的。 line3: 声明一个包名,一般与文件目录名相同 line4: 定一个Hello的服务,定义好接口名字,方法名,参数以及返回体

根据上述写好的service们,我们就可以用用protoc去把proto文件编译成go代码,生成的代码中包含了客户端能够进行RPC调用的方法和服务端需要实现的接口。具体可见我的make.sh里面的脚本,生成的pb.go文件,给grpc server用的,pb.gw.go文件,给grpc-gateway用的,用于grpc和restful的相互转化,swagger就是给swagger用的,哈哈。

编译demo_proto文件称demo_proto.pb.go文件:


protoc --go_out=plugins=grpc:. demo_proto.proto


```protoc

make.sh脚本如下:

```shell
proto="demo_proto/demo_proto.proto"

protoc -I/usr/local/include -I. \
        -I$GOPATH/src \
        -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
        --go_out=plugins=grpc:. ${proto}

protoc -I/usr/local/include -I. \
        -I$GOPATH/src \
        -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
        --grpc-gateway_out=logtostderr=true:. ${proto}
        
protoc -I/usr/local/include -I. \
		-I${GOPATH}/src \
		-I${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
		--swagger_out=logtostderr=true:. ${proto}

这羊就在demo_proto目录下生成了demo_proto.pb.go文件

然后咱们在看server下的main.go代码

package main

import (
	"log"
	"net"

	pb "demo/demo_proto"

	"golang.org/x/net/context"
	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"
)

const (
	port = ":10000"
)

type server struct{}

 
//当接收到请求的时候回调用该方法  参数由grpc自己根据请求进行构造 

func (s *server) SendHelpInfoWithGetMethod(ctx context.Context, in *pb.DemoProtoHelloRequest) (*pb.DemoProtoHelloResponse, error) {
	return &pb.DemoProtoHelloResponse{Message: "接收GET方法:" + in.Name}, nil
}

func (s *server) SendInfoWithPostMethod(ctx context.Context, in *pb.DemoProtoHelloRequest) (*pb.DemoProtoHelloResponse, error) {
	return &pb.DemoProtoHelloResponse{Message: "接收POST方法" + in.Name}, nil
}

func main() {
	lis, err := net.Listen("tcp", port)
	if err != nil {
		log.Fatal("监听端口失败: %v", err)
	}

	s := grpc.NewServer()
	pb.RegisterHelloServer(s, &server{})
	reflection.Register(s)
	if err := s.Serve(lis); err != nil {
		log.Fatal("启动服务失败:%v", err)
	}
}

可以看到server端主要就是实现了刚刚我们定义好的Hello服务。一个是SendHelpInfoWithGetMethod,另一个是SendInfoWithPostMethod。

咱们再来看看client中的main.go代码

package main

import (
	pb "demo/demo_proto"
	"log"
	"os"

	"golang.org/x/net/context"
	"google.golang.org/grpc"
)

const (
	address     = "localhost:10000"
	defaultName = "Zhiwei Yang"
)

func main() {
	// 建立一个grpc连接
	conn, err := grpc.Dial(address, grpc.WithInsecure())

	if err != nil {
		log.Fatal("did not connect:%v", err)
	}

	defer conn.Close()
	// 新建一个客户端,方法为:NewXXXClinent(conn),XXX为你在proto定义的服务的名字
	c := pb.NewHelloClient(conn)

	name := defaultName

	if len(os.Args) > 1 {
		name = os.Args[1]
	}

	// 调用远程,并得到返回
	r, err := c.SendHelpInfoWithGetMethod(context.Background(), &pb.DemoProtoHelloRequest{Name: name})
	if err != nil {
		log.Fatal("打不了招呼,错误:%v", err)
	}
	log.Printf("客户端 get方法:%s", r.Message)

	r, err = c.SendInfoWithPostMethod(context.Background(), &pb.DemoProtoHelloRequest{Name: name})

	if err != nil {
		log.Fatal("不能打招呼啦,;%v", err)
	}

	log.Printf("客户端发请求:%s", r.Message)
}

client端干的事情是,首先新建了一个grpc连接,然后新建了一个对应服务的客户端,建好这个客户端后,通过这个客户端去调用远程server端已经实现好的服务,如c.SendHelpInfoWithGetMethod和c.SendInfoWithPostMethod 这两个服务。

到这里,我们的grpc的客户端和服务端都写完了。但是我现在特别想通过curl这样的工具来调用服务端的两个服务,咋搞呢?

于是我们就又在根目录下写了一个main.go服务。

package main

import (
	"flag"
	"net/http"
	"path"
	"strings"

	"github.com/golang/glog"

	"google.golang.org/grpc"
	
	gw "demo/demo_proto"
	
	"github.com/grpc-ecosystem/grpc-gateway/runtime"
	"golang.org/x/net/context"
)

const (
	grpcPort = "10000"
)

var (
	getEndPoint = flag.String("get", "localhost:"+grpcPort, "endPoint of your service")
	postPoint   = flag.String("post", "localhost:"+grpcPort, "endpoint of you service")

	swaggerDir = flag.String("swagger_dir", "demo_proto", "含有swagger json文件的路径")
)

func newGateway(ctx context.Context, opts ...runtime.ServeMuxOption) (http.Handler, error) {
	mux := runtime.NewServeMux(opts...)
	dialOpts := []grpc.DialOption{grpc.WithInsecure()}
	err := gw.RegisterHelloHandlerFromEndpoint(ctx, mux, *getEndPoint, dialOpts)

	if err != nil {
		return nil, err
	}

	err = gw.RegisterHelloHandlerFromEndpoint(ctx, mux, *postPoint, dialOpts)
	if err != nil {
		return nil, err
	}
	return mux, err
}

func Run(address string, opts ...runtime.ServeMuxOption) error {
	ctx := context.Background()
	ctx, cancel := context.WithCancel(ctx)

	defer cancel()

	mux := http.NewServeMux()

	mux.HandleFunc("/swagger/", serveSwagger)
	// serverSwaggerUI(mux)
	gw, err := newGateway(ctx, opts...)
	if err != nil {
		return err
	}

	mux.Handle("/", gw)
	return http.ListenAndServe(address, mux)
}

func serveSwagger(w http.ResponseWriter, r *http.Request) {
	if !strings.HasSuffix(r.URL.Path, ".swagger.json") {
		glog.Errorf("Swagger JSON not found: %s", r.URL.Path)
		http.NotFound(w, r)
		return
	}

	glog.Infof("Serving %s", r.URL.Path)
	p := strings.TrimPrefix(r.URL.Path, "/swagger/")
	p = path.Join(*swaggerDir, p)
	http.ServeFile(w, r, p)
}

func serverSwaggerUI(mux *http.ServeMux) {
	fileServer := http.FileServer(&assetfs.AssetFS{
		Asset: swagger.Asset,
		AssetDir: swagger.AssetDir,
		Prefix: "third_party/swagger-ui",
	})

	prefix :="/swagger-ui/"
	mux.Handle(prefix,http.StripPrefix(prefix,fileServer))
}

func main() {
	flag.Parse()
	defer glog.Flush()

	if err := Run(":8080"); err != nil {
		glog.Fatal(err)
	}
}

可以看到上面的我们定义好的get和post的endpoint,后面我们会根据这个来调用。

newGateway这个方法就是把我们grpc里面的两个服务注册一下,然后返回一个http的handler。然后我们在run里面用newGateway这个函数,就完成了restful和grpc的相互转化。

serveSwagger这个函数就是用来把swagger的json文件暴露给用户。其实我想在本地实现显示swagger-ui的,不知道为什么,一跑起main.go函数,内存就猛烈增长到10个G,电脑一下就卡死了。先放弃。如果有需要展示,可以把json文件复制到这个网站(editor.swagger.io)上去,就可以可视化API了。

main函数里面就是把http server启起来了,端口是8080。

最后,我们只要将server/main.go和demo/main.go 这两个同时启起来,就可以调用了。


curl -X POST http://127.0.0.1:8080/post -d '{"name":"今天是个好天气,我是jerry"}'

curl -X POST http://127.0.0.1:8080/post -d ‘{“name”:“今天是个好天气,我是jerry”}’


curl -X GET 'http://127.0.0.1:8080/get?name=jerry'

curl -X GET ‘http://127.0.0.1:8080/get?name=jerry’

流程如下:curl用post向gateway发送请求,gateway作为proxy将请求转化一下通过grpc转发给Hello的server端,Hello的server端通过grpc返回结果,gateway收到结果后,转化成json返回给前端。

#Grpc