protobuf学习笔记(二):结合grpc生成客户端和服务端

上一篇文章大概讲了如何将自定义的protobuf类型的message转换成相应的go文件,这次就结合grpc写一个比较认真的客户端和服务器端例子

一、项目结构

client存放rpc服务的客户端文件

server存放rpc服务的服务端文件

protobuf存放自定义的proto文件

grpc存放生成的grpc、potobuf转换后的文件

utils存放工具性的文件

补充一个整个项目完成后展开后的结构图:

二、依赖下载

上篇文章中我们主要是安装了protoc指令程序,这次我们要安装一下

go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2

 这是一个转换protobuf的同时生成grpc服务的插件

此外,项目之中还要现在这两个包

github.com/golang/protobuf
google.golang.org/grpc 

三、proto文件魔改

因为这个例子还算比较严肃,所以我魔改了一下原先的文件,现在的结构是这样的:

service.proto

其定义了我们rpc服务的类型,如下

syntax="proto3";
package service;
option go_package="../grpc;grpc";
import "request.proto";
import "user.proto";
// 我们将定义相关的rpc服务,主要是在请求信息去请求我们User message
// 在此之前我对之前的protobuf结构进行了修改
// 首先将内部的role/parent/user全部整合到user.proto一个文件中去
service Test{
    rpc GetUser1(request.Request)returns(user.User){};//这是一个单对单的服务,传入带名字和id的信息,返回一个user
    rpc GetUser2(stream request.Request)returns(user.UserGroup){};// 传入多个信息,返回一个用户组
    rpc GetUser3(request.IdGroup)returns(stream user.User){};// 传入单个信息,返回多个用户
    rpc GetUser4(stream request.Request)returns(stream user.User){};//传入多个用户信息,返回多个用户实例
}

 我们看到有四个服务,分别对应着不同的input/output类型(分别在user.proto和request.proto两个文件中定义),grpc使用的是http2协议,所以有个流式传输,也就是可以一直传输某些内容,特征是在参数或者return的值前面加上“stream”关键词。以服务GetUser3为例,用户传入一个id组,会根据这个id列表中的id一直返回对应的user。

注意这里的Test,是我们这次rpc服务的命名空间,会反映到生成的grpc服务结构体名上

request.proto

定义了两个参数类型

syntax="proto3";
package request;
import "google/protobuf/any.proto";//引入any类型
import "google/protobuf/timestamp.proto";//引入时间戳类型
option go_package="../grpc;grpc";

message Request{
    optional string name=1;//可选name参数
    uint64 id=2;//id参数
    optional google.protobuf.Any other_msg=3;//其他信息
    google.protobuf.Timestamp timestamp=4;//请求的时间
}

// 创建一个id组成的数组
message IdGroup{
    repeated uint64 ids=1;
    google.protobuf.Timestamp timestamp=2;//请求的时间
    optional google.protobuf.Any other_msg=3;//其他信息
}

分别是单独的request和id组成的数组

user.proto

syntax="proto3";//顶格声明protobuf版本,默认是protobuf2
// 注意一下语句后面要加";",否则不识别
package user;//定义一下protobuf的包名
import "google/protobuf/any.proto";//引入any类型
import "google/protobuf/timestamp.proto";//引入时间戳类型
/*
import public "some path"
这个public关键词用于控制这个包的依赖是否可以传递,例子如下

a.proto:
import "b.proto"
import public "c.proto"

index.proto:
import "a.proto"
那么这个index文件当中除了能使用a文件中定义的变量,还能使用c文件当中的遍历,但是b文件就不能使用

*/
option go_package="../grpc;grpc";//规定生成文件的输出文件,同时规定对应文件package的名称
//这里指定的 out_path 并不是绝对路径,只是相对路径或者说只是路径的一部分,和 protoc 的 --go_out 拼接后才是完整的路径。所以我的侧率就是不写go_out

// 这边我们写一个User结构体
//结构体的名称我们采取的是官网上的格式,注意和golang默认格式区分
// 具体的protobuf基础类型和对应语言之间对应表,参见https://protobuf.dev/programming-guides/proto3/#specifying-field-rules

message User{
    // 保留字段和保留数字,这个用法我不是很懂,我看的资料显示如下
    /*
    如果一个字段不再需要,如果删除或者注释掉,则其他人在修改时会再次使用这些字段编号,
    那么旧的引用程序就可能出现一些错误,所以使用保留字段,保留已弃用的字段编号或字段名 
    我个人觉得这应该涉及到可拓展性的问题(难道更改的时候不会去重新生成对应的文件吗)
    */
    reserved "hh";
    reserved 99 to 100;
    string name=1;
    uint64 id=2;
    bool merried=3;
    Role role=4;//这个就是role包引入的枚举类型,枚举定义在message内部就是独占枚举
    optional google.protobuf.Any other_msg=5;//any类型
    oneof child_name{
        string son_name=6;
        string daughter_name=7;//暂时看起来不同于枚举,oneof控制的事不同字段只能选一个
    }
    repeated string hobbies=8;//可重复字段,应该会生成一个切片

    //内嵌字段,注意tag只是在同一个message内不能重复,内嵌的字段不算
    // 内嵌的字段是能单独拿出来用的,比如在另一个字段中,可以使用TestUser.GameCharacter
    // 注意这里的行为只是定义,要使用可以如下这样写
    // 内嵌的role枚举
    enum Role{
        NORMAL_USER=0;
        VIP_USER=1;
        BOSS=3;
    };
    // 内嵌的parent结构
    message Parent{
        string name=1;
        uint32 age=2;
    }

    // 创建一个map
    map<string,Parent> parents=9;

    // 创建一个时间戳类型
    optional google.protobuf.Timestamp timestamp=10;
}
// 我尝试了一下extend关键词,试图拓展User,但是遭到了失败,不支持extension关键词,但是生成的时候又要,陷入两难

// 创建一个用户组
message UserGroup{
    repeated User users=1;
    google.protobuf.Timestamp timestamp=2;
    optional google.protobuf.Any other_msg=3;//any类型
}


这是我们上篇文章涉及到的文件,我对user进行了删改,并加上了用户组(user组成的数组)

other_msg.proto

syntax="proto3";
package otherMsg;
option go_package="../grpc;grpc";
// 这些信息是用来填充any类型的other_msg字段的
message MsgFromWeekday{
    string msg=1;
}
message MsgFromWeekend{
    string msg=1;
}

这个则是定义了protobuf中any的结构,需要注意,protobuf中的any不是说真的啥类型都可以的,因为要对any进行反序列化,所以这里要提前定义

四、生成服务所需要的文件

我们在protobuf文件下进入命令行,输入以下指令:

protoc --go_out=. --go-grpc_out=. *.proto 

--go_out是转换成的golang文件地址 

--go-grpc_out是grpc服务文件生成地址

*.proto 则是对protobuf文件下所有proto文件进行处理

则会在我们的grpc文件中生成如下文件:

我们可以看到,明显 多了一个service_grpc.pb.go.这个就是grpc服务器和客户端的构成文件。这个文件内容呢,他非常讨厌,有点多,我看是看得懂,但是说不清,主要是包含了客户端和服务器端的一些接口、方法。

五、写一下服务端

服务端是一个main.go文件

这里我可能写的有点复杂了,主要是把函数精细化了一下

照例先贴一下

package main

import (
	"context"
	"fmt"
	pb "grpc_test/grpc"
	"grpc_test/utils"
	"io"
	"log"
	"net"

	"google.golang.org/grpc"
)

//第一部分:虚假数据库

type User_Role int32

const (
	User_NORMAL_USER User_Role = 0
	User_VIP_USER    User_Role = 1
	User_BOSS        User_Role = 3
)

type Parent struct {
	Name string
	Age  uint32
}
type Child struct {
	Gender uint
	Name   string
}

type User struct {
	ID      uint
	Name    string
	Merried bool
	Role    User_Role
	hobbies []string
	Parents map[string]Parent
	Child   *Child
}

var users = map[uint]User{
	1: {ID: 1, Name: "黄烽", Merried: false, Role: User_VIP_USER, hobbies: []string{"sleep", "game"}},
	2: {ID: 2, Name: "张胜前", Merried: false, Role: User_NORMAL_USER},
	3: {ID: 3, Name: "王怡雯", Merried: true, Role: User_VIP_USER, Child: &Child{1, "汪汪"}},
	4: {ID: 4, Name: "王福林", Merried: false, Role: User_NORMAL_USER, hobbies: []string{"sleep", "game"}, Parents: map[string]Parent{
		"father":  {"me", 99},
		"father2": {"you", 99},
	}},
}

// 第二部分:定义一下对应的四个服务
type TestServer struct {
	pb.UnimplementedTestServer
}

// 第一个服务
func (s *TestServer) GetUser1(ctx context.Context, in *pb.Request) (resp *pb.User, err error) {
	//GetUser1是一个单对单的服务
	fmt.Println("进入GetUser1服务")

	// 打印一下输入的基本信息
	err = Broadcast(in)
	if err != nil {
		log.Println(err)
	}

	// 根据id查找一下用户,这里就使用一个map代替数据库
	u, ok := users[uint(in.Id)]
	if ok {
		resp = GenaratePbUser(&u)
		// other_msg部分进行处理
		any, err := utils.GenerateOtherMsg("this msg is from weekday", true)
		if err != nil {
			log.Println(err)
		}
		resp.OtherMsg = any

		// 时间戳部分进行处理
		resp.Timestamp = utils.GenerateTimestamp()
	}
	fmt.Println("GetUser1服务结束")
	fmt.Println("---------------")
	return
}

// 第二个服务
func (s *TestServer) GetUser2(server pb.Test_GetUser2Server) (err error) {
	// 该服务会不断接受requst,然后返回一个用户组
	var pbUsers []*pb.User
	fmt.Println("进入GetUser2服务")
	// 接受一下流信息
	for {
		req, err := server.Recv()
		// 判断接受是否完成
		if err == io.EOF {
			fmt.Println("接受所有请求完成")
			break
		}
		if err != nil {
			log.Println("接受流信息失败:", err)
			break
		}

		//进行req的处理
		// 首先广播一下信息
		err = Broadcast(req)
		if err != nil {
			log.Println(err)
		}

		// 查找对象并返回结果切片中
		u, ok := users[uint(req.Id)]
		if ok {
			pbUsers = append(pbUsers, GenaratePbUser(&u))
		}
	}

	// 进行返回操作
	res := &pb.UserGroup{
		Users: pbUsers,
	}
	// 添加other_msg和时间戳
	any, err := utils.GenerateOtherMsg("this msg is from weekday", true)
	if err != nil {
		log.Println(err)
	}
	res.OtherMsg = any
	res.Timestamp = utils.GenerateTimestamp()
	//发送信息并关闭通道
	server.SendAndClose(res)
	fmt.Println("GetUser2服务结束")
	fmt.Println("---------------")
	return
}

// 第三个服务
func (s *TestServer) GetUser3(iG *pb.IdGroup, server pb.Test_GetUser3Server) (err error) {
	// 第三个服务,发过来一个id组,流式返回用户信息
	fmt.Println("进入GetUser3服务")
	// 打印一下传递进来的id
	fmt.Println("ids:", iG.Ids)
	// 打印一下other_msg和时间戳
	if iG.OtherMsg != nil {
		if err = utils.BroadcastMsg(iG); err != nil {
			log.Println(err)
		}
	}
	utils.BroadcastTimestamp(iG)

	// 查找用户并进行返回
	for _, v := range iG.Ids {
		u, ok := users[uint(v)]
		if ok {
			resp := GenaratePbUser(&u)
			// 创建信息
			any, err := utils.GenerateOtherMsg("this msg is from weekend", false)
			if err != nil {
				log.Println(err)
			}
			resp.OtherMsg = any
			// 时间戳部分进行处理
			resp.Timestamp = utils.GenerateTimestamp()
			err = server.Send(resp)
			if err != nil {
				log.Println("发送失败:", err)
				break
			}
		}
	}
	fmt.Println("GetUser3服务结束")
	fmt.Println("---------------")
	return
}

// 第四个服务
func (s *TestServer) GetUser4(server pb.Test_GetUser4Server) (err error) {
	// 第四个服务是双向流
	fmt.Println("进入GetUser4服务")
	for {
		req, err := server.Recv()
		// 判断一下是否全部接受完成
		if err == io.EOF {
			fmt.Println("全部接受完成")
			break
		}
		if err != nil {
			log.Println("接受信息失败:", err)
			break
		}
		// 打印一下信息
		if err = Broadcast(req); err != nil {
			log.Println(err)
		}
		// 查找一下用户
		u, ok := users[uint(req.Id)]
		if ok {
			resp := GenaratePbUser(&u)
			any, err := utils.GenerateOtherMsg("this msg is from weekend", false)
			if err != nil {
				log.Println(err)
			}
			resp.OtherMsg = any
			// 时间戳部分进行处理
			resp.Timestamp = utils.GenerateTimestamp()
			err = server.Send(resp)
			if err != nil {
				log.Println("发送失败:", err)
				break
			}
		}
	}
	fmt.Println("GetUser4服务结束")
	fmt.Println("---------------")
	return
}

// 第四个部分:开启服务器
func main() {
	// main函数之中开启一下服务器
	listener, err := net.Listen("tcp", "localhost:996")
	if err != nil {
		log.Fatalln("listen fail: ", err)
	}
	var opt []grpc.ServerOption
	grpcServer := grpc.NewServer(opt...)
	pb.RegisterTestServer(grpcServer, &TestServer{})
	fmt.Println("grpc客户端启动,正在localhost:996进行监听")
	err = grpcServer.Serve(listener)
	if err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

// 第三个部分:一些工具函数

// 根据user结构体创建pb.User类型的函数
func GenaratePbUser(u *User) (pbUser *pb.User) {
	// 对结果进行赋值
	// 能直接赋值的直接赋值
	pbUser = &pb.User{
		Name:    u.Name,
		Id:      uint64(u.ID),
		Merried: u.Merried,
		Role:    pb.User_Role(u.Role),
		Hobbies: u.hobbies,
		Parents: make(map[string]*pb.User_Parent),
	}
	// 需要经过处理的部分
	// 孩子部分
	if u.Child != nil {
		// 判断一下孩子性别
		if u.Child.Gender == 0 {
			pbUser.ChildName = &pb.User_SonName{SonName: u.Child.Name}
		} else {
			pbUser.ChildName = &pb.User_DaughterName{DaughterName: u.Child.Name}
		}
	}
	// 家长map部分进行处理
	if u.Parents != nil {
		for k, v := range u.Parents {
			pbUser.Parents[k] = &pb.User_Parent{Name: v.Name, Age: v.Age}
		}
	}
	return
}

// 打印Requst之中传入的信息
func Broadcast(in *pb.Request) (err error) {
	// 用户id必定存在可以不判断
	fmt.Println("用户id:", in.GetId())
	// 用户name是可选属性,所以要进行一下判断
	if in.Name != nil {
		fmt.Println("用户name:", in.GetName())
	}

	// 打印一下other_msg,这就涉及到对protobuf自定义any类型的反序列化
	// 因为是可选所以做一下处理
	if in.OtherMsg != nil {
		if err = utils.BroadcastMsg(in); err != nil {
			log.Println(err)
		}
	}

	// 打印一下时间的信息,这个涉及到对protobuf时间戳类型进行反序列化
	utils.BroadcastTimestamp(in)
	return
}

这个文件有点长啊,涉及到主要是四个部分

第一部分:假数据库

我创建一个比较虚假的数据库,并对一些用户进行了初始化,那样就可以按照id去查找用户

第二部分:创建服务器结构体并定义四个服务

重新定义server并重新定义四个方法。在写rpc服务我们可以看到,我们写的是一个类似golang接口的东西,具体服务的实现是没有定义的,那我们这里就要将其定义起来。核心流程就是打印request之中的信息、查找用户、返回用户。

那这部分的方法的框架应该如何定义呢?很简单,在grpc下的service_grpc.pb.go这个我们生成的rpc服务中,找到TestServer这个接口就行了,我生成的这个接口给大家看一下:

type TestServer interface {
	GetUser1(context.Context, *Request) (*User, error)
	GetUser2(Test_GetUser2Server) error
	GetUser3(*IdGroup, Test_GetUser3Server) error
	GetUser4(Test_GetUser4Server) error
	mustEmbedUnimplementedTestServer()
}

如果你看过这个文件就知道了,我们是基于pb.UnimplementedTestServer这个备用结构体的基础上重新创建了自己的TestServer,然后重新定义了四个服务

type TestServer struct {
	pb.UnimplementedTestServer
}

第三部分:工具函数

就是GenaratePbUserBroadcast工具函数,这部分我写的不满意,主要是写的有点复杂(各个函数的功能已经写在注释里了),此外这部分本来应该和utils包合并的,但我真心不想改,同时放一下utils包中的内容(看不懂结构的可以借助最上面的项目结构)

package utils

import (
	"fmt"
	pb "grpc_test/grpc"

	"google.golang.org/protobuf/types/known/anypb"
	"google.golang.org/protobuf/types/known/timestamppb"
)

// 生成any类型的other_msg信息
func GenerateOtherMsg(msg string, isWeekday bool) (any *anypb.Any, err error) {
	if isWeekday {
		any, err = anypb.New(&pb.MsgFromWeekday{Msg: msg})
	} else {
		any, err = anypb.New(&pb.MsgFromWeekday{Msg: msg})
	}
	return
}

// 生成timestamp信息
func GenerateTimestamp() *timestamppb.Timestamp {
	return timestamppb.Now()
}

// 打印other_msg的函数
// 因为需要重用,所以创建一个接口
type Messager interface {
	GetOtherMsg() *anypb.Any
}

func BroadcastMsg(in Messager) (err error) {
	// protobuf中的any类型还是需要转化成protobuf之中的自定义类型(需要提前生成)
	//这里我选择预先不知道any所代表类型的方式进行处理
	// 我在这里写一个other_msg.proto去定义一下Request的other_msg的具体类型
	var anyMsg string
	m, err := in.GetOtherMsg().UnmarshalNew()
	if err != nil {
		return err
	}
	// 因为这个other_msg可能对应两种类型,所以我们要对其进行判断
	switch m := m.(type) {
	case *pb.MsgFromWeekday:
		anyMsg = m.GetMsg()
	case *pb.MsgFromWeekend:
		anyMsg = m.GetMsg()
	default:
		anyMsg = "不是默认类型"
		// err = errors.New("传入非默认类型信息")
	}
	fmt.Println("其他信息:", anyMsg)
	return
}

// 打印时间戳的函数
// 因为需要重用,所以还是要创建接口
type TimeCarrier interface {
	GetTimestamp() *timestamppb.Timestamp
}

func BroadcastTimestamp(in TimeCarrier) {
	fmt.Println("请求/回复时间:", in.GetTimestamp().AsTime().Format("2006-01-02T15:04:05 -07:00:00"))
}

多说一下utils包的中的内容,如果你看过上篇内容就知道我们使用了protobuf的any和时间戳类型,这里GenerateOtherMsg、GenerateTimestamp分别对应了对any类型、时间戳类型的序列化,BroadcastMsg、BroadcastTimestamp则对应了反序列化(重点看一下!)

第四部分:服务端创建

grpc服务端监听,官网上抄的,后续可能会在这里加一些内容

六、客户端的创建

客户端也是一个main.go的文件,照例贴一下

package main

import (
	"context"
	"fmt"
	pb "grpc_test/grpc"
	"grpc_test/utils"
	"io"
	"log"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/protobuf/types/known/anypb"
)

func FirstRequest(client pb.TestClient, msg *anypb.Any, id uint64, ctx context.Context) (err error) {
	// 该请求是发送一个请求,返回一个结果
	fmt.Println("开始第一个请求")
	req := &pb.Request{Id: id}
	req.OtherMsg = msg
	req.Timestamp = utils.GenerateTimestamp()
	resp, err := client.GetUser1(ctx, req)
	if err != nil {
		log.Println("GetUser1 请求失败")
		return
	}
	// 简单打印一下用户名和id,otherMsg以及时间戳
	fmt.Println("收到反馈!")
	fmt.Println("用户id:", resp.Id)
	fmt.Println("用户名称:", resp.Name)
	utils.BroadcastMsg(resp)
	utils.BroadcastTimestamp(resp)
	fmt.Println("第一次请求结束!")
	fmt.Println("---------------")
	return
}

func SecondRequest(client pb.TestClient, msg *anypb.Any, ids []uint64, ctx context.Context) (err error) {
	fmt.Println("开始第二个请求")
	var req = &pb.Request{}
	getUser2Client, err := client.GetUser2(ctx)
	if err != nil {
		log.Println("GetUser2 请求失败")
		return
	}
	for k, v := range ids {
		req.Id = v
		req.OtherMsg = msg
		req.Timestamp = utils.GenerateTimestamp()
		getUser2Client.Send(req)
		if k == len(ids)-1 {
			getUser2Client.CloseSend()
			break
		}
	}
	userGroup, err := getUser2Client.CloseAndRecv()
	if err != nil {
		log.Println("GetUser2 接受返回值失败")
		return
	}
	// 打印一下所有的userGroup的id和name、时间戳
	for _, u := range userGroup.GetUsers() {
		fmt.Println("收到反馈!")
		fmt.Printf("%v:%v\n", u.Id, u.Name)
	}
	utils.BroadcastTimestamp(userGroup)
	fmt.Println("第二次请求结束!")
	fmt.Println("---------------")
	return
}

func ThirdRequest(client pb.TestClient, msg *anypb.Any, ids []uint64, ctx context.Context) (err error) {
	fmt.Println("开始第三个请求")
	iG := &pb.IdGroup{
		Ids:       ids,
		Timestamp: utils.GenerateTimestamp(),
		OtherMsg:  msg,
	}
	Test_GetUser3Client, err := client.GetUser3(ctx, iG)
	if err != nil {
		log.Println("GetUser3 请求失败")
		return
	}
	for {
		user, err := Test_GetUser3Client.Recv()
		// 首先判断一下是否完全接收
		if err == io.EOF {
			fmt.Println("所有回复接收完成")
			break
		}
		if err != nil {
			log.Println("GetUser3 接受返回值失败")
			return err
		}
		// 打印一下user的信息
		fmt.Println("收到反馈!")
		fmt.Println("用户id:", user.Id)
		fmt.Println("用户名称:", user.Name)
		utils.BroadcastTimestamp(user)
	}
	fmt.Println("第三次请求结束!")
	fmt.Println("---------------")
	return
}
func ForthRequest(client pb.TestClient, msg *anypb.Any, ids []uint64, ctx context.Context) (err error) {
	fmt.Println("开始第四个请求")
	var req = &pb.Request{}
	test_GetUser4Client, err := client.GetUser4(ctx)
	if err != nil {
		log.Println("GetUser4 请求失败")
		return
	}
	for i, v := range ids {
		// 复用上面的req
		req.Id = v
		req.Timestamp = utils.GenerateTimestamp()
		req.OtherMsg = msg
		err = test_GetUser4Client.Send(req)
		if err != nil {
			log.Println("GetUser4 发送请求失败")
			return
		}
		u, err := test_GetUser4Client.Recv()
		if err != nil {
			log.Println("GetUser4 接受信息失败")
			return err
		}
		// 打印一下返回值的信息
		fmt.Println("收到反馈!")
		fmt.Println("用户id:", u.Id)
		fmt.Println("用户名称:", u.Name)
		utils.BroadcastTimestamp(u)
		// 加一个关闭send的判断
		if i == len(ids)-1 {
			err = test_GetUser4Client.CloseSend()
			if err != nil {
				log.Println("关闭send失败:", err)
			}
			break
		}
	}
	fmt.Println("第四次请求结束!")
	fmt.Println("---------------")
	return
}

func main() {
	// 连接服务器
	conn, err := grpc.Dial("localhost:996", grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatal("dial fail: ", err)
	}
	defer conn.Close()
	testClient := pb.NewTestClient(conn)

	// 创建共用的ctx
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	// 因为就两个any类型的message,所以这边事先创建一下
	weekdayMsg, err := utils.GenerateOtherMsg("this msg is from weekday", true)
	if err != nil {
		log.Println("创建message失败", err)
	}
	weekendMsg, err := utils.GenerateOtherMsg("this msg is from weekend", false)
	if err != nil {
		log.Println("创建message失败", err)
	}

	// 开始进行任务

	// 请求第一个服务
	// 该请求是一来一回
	err = FirstRequest(testClient, weekdayMsg, 1, ctx)
	if err != nil {
		log.Println("FirstRequest", err)
		return
	}

	// 进行第二个请求
	// 该请求是发送多个请求,返回一个用户组
	err = SecondRequest(testClient, weekendMsg, []uint64{1, 2}, ctx)
	if err != nil {
		log.Println("SecondRequest", err)
		return
	}

	// 进行第三个请求
	// 发过来一个id组,流式返回用户信息
	err = ThirdRequest(testClient, weekdayMsg, []uint64{3, 4}, ctx)
	if err != nil {
		log.Println("ThirdRequest", err)
		return
	}

	// 进行第四个请求
	// 双方都是流式
	err = ForthRequest(testClient, weekendMsg, []uint64{2, 3}, ctx)
	if err != nil {
		log.Println("ForthRequest", err)
		return
	}

}

客户端比较简单,但我为了清楚,还是对四个服务的请求各自写了一个函数,主要还是请求接受回复那一套,还是比较清楚的。

需要注意的是,在第四次请求中,也就是对应GetUser4服务的这个请求(如果你记性好的话,应该记得他是一个“流对流”的服务),context值一定不能是初始值(即ctx:=context.Background()),一定要有时间的限制,否则会报错

七、服务端和客户端的运行测试

期待已久的时刻开启了,现在在项目中的server和client文件下打开命令行(server和client下都是两个main.go文件),先server,后client分别输入

go run .

完美传输

本文结束,有机会上个gitee地址。 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/501441.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【LeetCode: 面试题 16.05. 阶乘尾数 + 阶乘】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

ThreadPool-线程池使用及原理

1. 线程池使用方式 示例代码&#xff1a; // 一池N线程 Executors.newFixedThreadPool(int) // 一个任务一个任务执行&#xff0c;一池一线程 Executors.newSingleThreadExecutorO // 线程池根据需求创建线程&#xff0c;可扩容&#xff0c;遇强则强 Executors.newCachedThre…

谷歌seo站内优化需要做什么?

说一个比较重要的点&#xff0c;那就是页面的标签优化&#xff0c;网站的title标签跟Descriptions标签的重要性不言而喻&#xff0c;但其他页面的标签也是同样重要&#xff0c;毕竟客户看见在谷歌搜索引擎里看见的就是你的页面标题以及描述 所以页面的标题以及描述是很重要的&a…

电机试验平台的结构

电机试验平台的结构通常包括以下部分&#xff1a; 1.电机&#xff1a;试验平台主要是为了对电机进行各种试验&#xff0c;因此电机是平台的核心组成部分。电机通常由定子和转子组成&#xff0c;根据试验需要可以是直流电机、交流电机或者是特殊类型的电机。 2.控制系统&#…

一次性了解C语言中文件和文件操作

P. S.&#xff1a;以下代码均在VS2019环境下测试&#xff0c;不代表所有编译器均可通过。 P. S.&#xff1a;测试代码均未展示头文件stdio.h的声明&#xff0c;使用时请自行添加。 文件及文件操作 前言1. 文件分类1.1 文本文件1.2 二进制文件1.3 文本文件和二进制文件的区别 2…

Linux:程序地址空间详解

目录 一、堆、栈、环境参数所在位置 二、进程地址空间底层实现原理 ​编辑 三、什么是地址空间 四、为什么要有进程地址空间 五、细谈写实拷贝的实现及意义 在C/C学习中&#xff0c;都学习过如上图所示的一套存储结构&#xff0c;我们大致知道一般存储空间分为堆区&#…

数据结构:归并排序

归并排序 时间复杂度O(N*logN) 如果两个序列有序,通过归并,可以让两个序列合并后也有序,变成一个有序的新数组 对于一个数组,如果他的左右区间都有序,就可以进行归并了 归并的方法 将数组的左右两个有序区间比较,每次都取出一个最小的,然后放入临时数组(不能在原数组上修改…

纯小白蓝桥杯备赛笔记--DAY8(必备排序算法)

冒泡排序 算法思想 每次将最大的一下一下地运到最右边&#xff0c;然后确定这个最大的&#xff0c;接着可以发现该问题变成一个更小的子问题。具体操作&#xff1a;从左向右扫描&#xff0c;如a[i]>a[i1]&#xff0c;执行swap操作。代码格式 #include<bits/stdc.h> …

Mamba: Linear-Time Sequence Modeling with Selective State Spaces(论文笔记)

What can I say? 2024年我还能说什么&#xff1f; Mamba out! 曼巴出来了&#xff01; 原文链接&#xff1a; [2312.00752] Mamba: Linear-Time Sequence Modeling with Selective State Spaces (arxiv.org) 原文笔记&#xff1a; What&#xff1a; Mamba: Linear-Time …

双非本,拿到美团测开实习了——经验分享

前言 最近是春招、暑期实习的高峰期&#xff0c;自己也凭借着持续的准备和一部分运气&#xff0c;较早拿到了美团的测开暑期实习。 以前接到美团的短信&#xff0c;都是外卖送达的通知&#xff0c;没想到自己有一天&#xff0c;也能收到offer录用的通知。虽然是测试开发的岗位…

考研数学|《1800》+《660》精华搭配混合用(经验分享)

肯定不行&#xff0c;考研数学哪有这么容易的&#xff01; 先说说这两本习题册&#xff0c;李永乐老师推出的新版660题&#xff0c;相较于18年前的版本&#xff0c;难度略有降低&#xff0c;更加适合初学者。因此&#xff0c;对于处于基础阶段的学习者来说&#xff0c;新版660…

2、Cocos Creator 下载安装

Cocos Creator 从 v2.3.2 开始接入了全新的 Dashboard 系统&#xff0c;能够同时对多版本引擎和项目进行统一升级和管理&#xff01;Cocos Dashboard 将做为 Creator 各引擎统一的下载器和启动入口&#xff0c;方便升级和管理多个版本的 Creator。还集成了统一的项目管理及创建…

算法之并查集

并查集&#xff08;Union-find Data Structure&#xff09;是一种树型的数据结构。它的特点是由子结点找到父亲结点&#xff0c;用于处理一些不交集&#xff08;Disjoint Sets&#xff09;的合并及查询问题。 Find&#xff1a;确定元素属于哪一个子集。它可以被用来确定两个元…

2024年AI大模型基础设施栈市场地图

2024年大模型(LLM)基础架构的组件和工具,最近看到国外的一篇深度分析,技术负责人可以重点关注:附带图谱: 一、现代LLM基础设施栈定义 根据Menlo Ventures的数据,2023年企业在现代AI基础设施栈上的支出超过11亿美元,成为生成式AI中最大的新市场,为初创企业提供了巨大的…

[OpenCV学习笔记]Qt+OpenCV实现图像灰度反转、对数变换和伽马变换

目录 1、介绍1.1 灰度反转1.2 图像对数变换1.3 图像伽马变换 2、效果图3、代码实现4、源码展示 1、介绍 1.1 灰度反转 灰度反转是一种线性变换&#xff0c;是将某个范围的灰度值映射到另一个范围内&#xff0c;一般是通过灰度的对调&#xff0c;突出想要查看的灰度区间。 S …

基于Springboot旅游网站管理系统设计和实现

基于Springboot旅游网站管理系统设计和实现 博主介绍&#xff1a;多年java开发经验&#xff0c;专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 央顺技术团队 Java毕设项目精品实战案例《1000套》 欢迎点赞 收藏 ⭐留言 文末获取源码联系…

下拉选中搜索angularjs-dropdown-multiselect.js

需要引入angularjs-dropdown-multiselect.js 页面 <div ng-dropdown-multiselect"" options"supplierList_data" selected-model"supplierList_select" events"changSelValue_supplierList" extra-settings"mucommonsetti…

python中pow()函数的使用

在Python中&#xff0c;pow() 函数用于计算指定数字的幂。它的语法如下&#xff1a; pow(x, y) 这个函数返回 x 的 y 次方。相当于 x**y。 pow() 函数也可以接受一个可选的第三个参数&#xff0c;用于指定一个取模值&#xff0c;即计算结果与该模值的余数。其语法如下&#…

ping的基础指令

-t Ping 指定的主机&#xff0c;直到停止。 若要查看统计信息并继续操作&#xff0c;请键入 CtrlBreak&#xff1b; 若要停止&#xff0c;请键入 CtrlC。 -a 将地址解析为主机名。 -n count 要发送的回显请求数。 -l size 发送缓冲…

[已解决]Vue3+Element-plus使用el-dialog对话框无法显示

文章目录 问题发现原因分析解决方法 问题发现 点击按钮&#xff0c;没有想要的弹框 代码如下 修改 el-dialog到body中&#xff0c;还是不能显示 原因分析 使用devtool中vue工具进行查看组件结构 原因在于&#xff0c;在一个局部组件(Detail->ElTabPane->…)中使用…