命令记不住

登陆

gRPC 简单模式 / 长连接实战

gRPC简单模式/长连接实战

1.GRPC环境配置

1.安装gRPC package
go get -u google.golang.org/grpc
2.安装protobuf

linux安装protobuf

# 安装依赖
yum install autoconf automake libtool curl make g++ unzip libffi-dev  glibc-headers gcc-c++ -y

# 下载
git clone https://github.com/protocolbuffers/protobuf.git

# 安装
unzip protobuf.zip
cd protobuf
./autogen.sh
./configure
make && make install
ldconfig        # 刷新共享库

# 测试
protoc --version

mac安装protobuf,推荐使用homebrew安装:

# 安装
brew install protobuf
brew install automake
brew install libtool

# 测试
protoc --version

win安装protobuf
下载地址: https://github.com/google/protobuf/releases
下载win版本后,配置文件中的bin目录到Path环境变量下即可。

protoc --version    #查看protoc的版本
libprotoc 3.12.0

安装代码golang契约生成工具

//正确方式
go get -u  github.com/golang/protobuf/protoc-gen-go //将protoc-gen-go安装到您的$GOPATH/bin 
go get google.golang.org/genproto/
新版不能使用,继续沿用旧版
go get google.golang.org/protobuf/cmd/protoc-gen-go         //不使用
go install google.golang.org/protobuf/cmd/protoc-gen-go     //不使用
//新版本添加声明
option go_package = ".;io_grpc_examples_hello";

注意和gRPC版本保持同步

2.获得demo源代码

proto锲约示例:
syntax = "proto3"; //语法使用 protocol buffer proto3
option go_package = ".;hello";
package protoes;
service Chat {
  rpc BidStream(stream Request) returns (stream Response){}
}
message Request{
  string input = 1;
}
message Response {
  string output = 1;
}

3.gRPC简单模式实战

说明:一定要使用mod管理,protoes.server,client都要生成mod管理

契约代码:
syntax = "proto3";

option go_package = ".;io_grpc_examples_hello";

package protoes;

service HelloServer{
    rpc SayHi(HelloRequest)returns(HelloReplay){}
    rpc GetMsg(HelloRequest)returns(HelloMessage){}
}

message HelloRequest{
    string name = 1 ;
}

message HelloReplay{
    string message = 1;
}

message HelloMessage{
    string msg = 1;
}
生成Golang代码:
protoc --go_out=plugins=grpc:. *.proto
考察生成的代码
type HelloServerClient interface {
    SayHi(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReplay, error)
    GetMsg(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloMessage, error)
}

type HelloServerServer interface {
    SayHi(context.Context, *HelloRequest) (*HelloReplay, error)
    GetMsg(context.Context, *HelloRequest) (*HelloMessage, error)
}

继续观察结构体HelloRequest


server代码示例:
package main

import (
    "fmt"
    pb "protoes"
    "net"

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

// 对象要和proto内定义的服务一致
type server struct {
}

// 实现rpc的 SayHi接口
func (s *server) SayHi(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReplay, error) {
    return &pb.HelloReplay{
        Message: "Hi " + in.Name,
    }, nil
}

// 实现rpc的 GetMsg接口
func (s *server) GetMsg(ctx context.Context, in *pb.HelloRequest) (*pb.HelloMessage, error) {
    return &pb.HelloMessage{
        Msg: "Server msg is coming...",
    }, nil
}

func main() {

    // 监听网络
    ln, err := net.Listen("tcp", "127.0.0.1:3000")
    if err != nil {
        fmt.Println("网络异常:", err)
        return
    }

    // 创建grpc句柄
    srv := grpc.NewServer()

    // 将server结构体注册到grpc服务中
    pb.RegisterHelloServerServer(srv, &server{})

    // 监听服务
    err = srv.Serve(ln)  //阻塞
    if err != nil {
        fmt.Println("监听异常:", err)
        return
    }
}
run:
.\server.exe

client代码示例:
package main

import (
    "fmt"
    pb "protoes"

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

func main() {

    // 客户端连接服务器
    conn, err := grpc.Dial("127.0.0.1:3000", grpc.WithInsecure())
    if err != nil {
        fmt.Println("连接服务器失败", err)
    }
    // defer的好处
    defer conn.Close()

    // 获得grpc句柄。连接是共享的
    c := pb.NewHelloServerClient(conn)

    // 远程单调用 SayHi 接口。比较和gorpc的差别,编译访问和字符串访问
    r1, err := c.SayHi(
        context.Background(),
        &pb.HelloRequest{
            Name: "Kitty",
        },
    )
    if err != nil {
        fmt.Println("Can not get SayHi:", err)
        return
    }
    fmt.Println("SayHi 响应:", r1)

    // 远程单调用 GetMsg 接口
    r2, err := c.GetMsg(
        context.Background(),
        &pb.HelloRequest{
            Name: "Kitty",
        },
    )
    if err != nil {
        fmt.Println("Can not get GetMsg:", err)
        return
    }
    fmt.Println("GetMsg 响应:", r2)

}
run:
.\client.exe
SayHi 响应: message:"Hi Kitty"
GetMsg 响应: msg:"Server msg is coming..."

5.gRPC长连接实战

契约代码:
syntax = "proto3"; // 语法使用 protocol buffer proto3

option go_package = ".;io_grpc_examples_hello";

package protoes;
/*
 服务名: Chat,
 其中只有 名为“BidStream”的一个RPC服务,
 输入是 Request格式的数据流, 输出是 Response 格式的数据流
*/
service Chat {
 rpc BidStream(stream Request) returns (stream Response) {}
}
// 请求数据 Request格式定义
message Request {
 string input = 1;
}
// 响应数据Response格式定义
message Response {
 string output = 1;
}
生成Golang代码
protoc --go_out=plugins=grpc:. *.proto
生成的代码分析
// ChatServer is the server API for Chat service.
type ChatServer interface {
	BidStream(Chat_BidStreamServer) error
}

type Chat_BidStreamServer interface {
	Send(*Response) error
	Recv() (*Request, error)
	grpc.ServerStream
}

type ChatClient interface {
	BidStream(ctx context.Context, opts ...grpc.CallOption) (Chat_BidStreamClient, error)
}

type Chat_BidStreamClient interface {
	Send(*Request) error
	Recv() (*Response, error)
	grpc.ClientStream
}


server代码示例:
package main

import (
	"io"
	"log"
	"net"
	proto "protoes"
	"strconv"

	"google.golang.org/grpc"
)

// Streamer 服务端
type Streamer struct{}

// BidStream 实现了 ChatServer 接口中定义的 BidStream 方法
func (s *Streamer) BidStream(stream proto.Chat_BidStreamServer) error {
	ctx := stream.Context()
	for {
		select {
		case <-ctx.Done():
			log.Println("收到客户端通过context发出的终止信号")
			return ctx.Err()
		default:
			// 接收从客户端发来的消息
			输入, err := stream.Recv()
			if err == io.EOF {
				log.Println("客户端发送的数据流结束")
				return nil
			}
			if err != nil {
				log.Println("接收数据出错:", err)
				return err
			}
			// 如果接收正常,则根据接收到的 字符串 执行相应的指令
			switch 输入.Input {
			case "结束对话":
				log.Println("收到'结束对话'指令")
				if err := stream.Send(&proto.Response{Output: "收到结束指令"}); err != nil {
					return err
				}
				// 收到结束指令时,通过 return nil 终止双向数据流
				return nil
			case "返回数据流":
				log.Println("收到'返回数据流'指令")
				// 收到 收到'返回数据流'指令, 连续返回 10 条数据
				for i := 0; i < 10; i++ {
					if err := stream.Send(&proto.Response{Output: "数据流 #" + strconv.Itoa(i)}); err != nil {
						return err
					}
				}
			default:
				// 缺省情况下, 返回 '服务端返回: ' + 输入信息
				log.Printf("[收到消息]: %s", 输入.Input)
				if err := stream.Send(&proto.Response{Output: "服务端返回: " + 输入.Input}); err != nil {
					return err
				}
			}
		}
	}
}
func main() {
	log.Println("启动服务端...")
	server := grpc.NewServer()
	// 注册 ChatServer
	proto.RegisterChatServer(server, &Streamer{})
	address, err := net.Listen("tcp", ":3000")
	if err != nil {
		panic(err)
	}
	if err := server.Serve(address); err != nil {
		panic(err)
	}
}
run
.\server.exe
client代码示例:
package main

import (
	"bufio"
	"context"
	"io"
	"log"
	"os"
	proto "protoes"

	"google.golang.org/grpc"
)

func main() {
	// 创建连接
	conn, err := grpc.Dial("localhost:3000", grpc.WithInsecure())
	if err != nil {
		log.Printf("连接失败: [%v] ", err)
		return
	}
	defer conn.Close()
	// 声明客户端
	client := proto.NewChatClient(conn)
	// 声明 context
	ctx := context.Background()
	// 创建双向数据流
	stream, err := client.BidStream(ctx)
	if err != nil {
		log.Printf("创建数据流失败: [%v] ", err)
	}
	// 启动一个 goroutine 接收命令行输入的指令
	go func() {
		log.Println("请输入消息...")
		scanner := bufio.NewScanner(os.Stdin)
		for scanner.Scan() {
			// 向服务端发送 指令
			if err := stream.Send(&proto.Request{Input: scanner.Text()}); err != nil {
				return
			}
		}
	}()
	for {
		// 接收从 服务端返回的数据流
		响应, err := stream.Recv()
		if err == io.EOF {
			log.Println(" 收到服务端的结束信号")
			break //如果收到结束信号,则退出“接收循环”,结束客户端程序
		}
		if err != nil {
			// TODO: 处理接收错误
			log.Println("接收数据出错:", err)
		}
		// 没有错误的情况下,打印来自服务端的消息
		log.Printf("[客户端收到]: %s", 响应.Output)
	}
}

run

.\client.exe
2020/04/18 21:50:46 请输入消息...
hello
2020/04/18 21:50:49 [客户端收到]: 服务端返回: hello
PS D:\mygo\src\grpcdemo-bistream\client> .\client.exe
2020/04/18 21:51:18 请输入消息...
hello
2020/04/18 21:51:21 [客户端收到]: 服务端返回: hello
返回数据流
2020/04/18 21:51:27 [客户端收到]: 数据流 #0
2020/04/18 21:51:27 [客户端收到]: 数据流 #1
2020/04/18 21:51:27 [客户端收到]: 数据流 #2
2020/04/18 21:51:27 [客户端收到]: 数据流 #3
2020/04/18 21:51:27 [客户端收到]: 数据流 #4
2020/04/18 21:51:27 [客户端收到]: 数据流 #5
2020/04/18 21:51:27 [客户端收到]: 数据流 #6
2020/04/18 21:51:27 [客户端收到]: 数据流 #7
2020/04/18 21:51:27 [客户端收到]: 数据流 #8
2020/04/18 21:51:27 [客户端收到]: 数据流 #9
结束对话
2020/04/18 21:51:32 [客户端收到]: 收到结束指令
2020/04/18 21:51:32  收到服务端的结束信号

连接多个客户端试试?

思考,有多少goroutine在等待用户输入?

推荐阅读