go的微服务之RPC与GRPC

rpc 介绍

RPC(Remote Procedure Call,远程过程调用)是一种通信方式,它让你像调用本地函数一样调用另一台机器上的函数。

1.rpc 远程过程调用,简单理解是一个节点请求另一个节点提供服务

2.对应RPC的是本地过程调用,函数调用是最常见的本地过程调用

3.将本地调用换成远程调用会面临很多问题

(1)原本的本地函数放到另一台服务器上运行但是引入很多问题

(2)Call 的ID映射

(3)序列化和反序列化

(4)网络传输

 

例如:

本地函数调用

func Add(a, b int) int {
	return a + b
}

func main() {
	result := Add(1, 2)
	fmt.Println(result)
}

这里 Add() 在本机执行。


RPC 调用

客户端:

client.Add(1, 2)

实际上:

  1. 客户端把参数 1、2 序列化

  2. 通过网络发送给服务器

  3. 服务器执行 Add()

  4. 返回结果 3

  5. 客户端收到结果

对程序员来说:

result := client.Add(1, 2)

感觉和本地调用差不多。


RPC 架构

+----------+            网络             +----------+
| Client   | --------------------------> | Server   |
|          | <-------------------------- |          |
+----------+                             +----------+

例如电商系统:

订单服务
    ↓ RPC
库存服务
    ↓ RPC
支付服务

Go 常见 RPC 框架

1. net/rpc(Go 标准库)

Go 自带:

import "net/rpc"

优点:

  • 简单

  • 学习 RPC 原理方便

缺点:

  • 功能较少

  • 企业中很少直接使用


2. gRPC(最主流)

由 Google 开发。

特点:

  • 基于 HTTP/2

  • 使用 Protocol Buffers

  • 高性能

  • 支持流式传输

  • 多语言

互联网公司大量使用。


gRPC 工作流程

第一步:定义 proto 文件

syntax = "proto3";

service UserService {
  rpc GetUser(UserRequest) returns (UserResponse);
}

message UserRequest {
  int32 id = 1;
}

message UserResponse {
  string name = 1;
}

第二步:生成 Go 代码

protoc \
  --go_out=. \
  --go-grpc_out=. \
  user.proto

需要安装:

go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

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

第三步:编写服务端

type UserServer struct {
	pb.UnimplementedUserServiceServer
}

func (s *UserServer) GetUser(
	ctx context.Context,
	req *pb.UserRequest,
) (*pb.UserResponse, error) {

	return &pb.UserResponse{
		Name: "Tom",
	}, nil
}

第四步:客户端调用

conn, _ := grpc.Dial(
	"localhost:8080",
	grpc.WithInsecure(),
)

client := pb.NewUserServiceClient(conn)

resp, _ := client.GetUser(
	context.Background(),
	&pb.UserRequest{Id: 1},
)

fmt.Println(resp.Name)

 

  server 示例

package main

import (
	"encoding/json"
	"fmt"
	//"html/template"
	"net/http"
	"strconv"
)

func main() {
	http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
		_=r.ParseForm()//解析参数
		fmt.Println("path:", r.URL.Path)
		// fmt.Println("scheme:", r.URL.Scheme)
		// fmt.Println(r.Form)
		a,_:=strconv.Atoi(r.Form["a"][0])
		b,_:=strconv.Atoi(r.Form["b"][0])
		w.Header().Set("Content-Type","application/json")
		jData,_:=json.Marshal(map[string]int{
			"data":a+b,
		})
		_,_=w.Write(jData)
	})
	_=http.ListenAndServe(":8000",nil)
}

  客户端测试 浏览器访问http://127.0.0.1:8000/hello?a=1&b=2

package main

import (
	"encoding/json"
	"fmt"
	//"image/jpeg"

	"github.com/kirinlabs/HttpRequest"
)
type ResponseData struct{
	Data int `json:"data"`
	
}
func Add(a,b int) int {
	req:=HttpRequest.NewRequest()
	res,_:=req.Get(fmt.Sprintf("http://127.0.0.1:8000/%s?a=%d&b=%d","hello",a,b))
	body,_:=res.Body()
	//fmt.Println(string(body))
	Rsp:=ResponseData{}
	json.Unmarshal(body,&Rsp)
	return Rsp.Data
}
func main(){
	fmt.Println(Add(4,9))

}
执行结果
13

  

RPC(Remote Procedure Call,远程过程调用)框架用于让不同服务之间像调用本地函数一样进行网络通信,是微服务架构的核心组件之一。

RPC 框架核心组成

一个完整 RPC 框架通常包含:

客户端
   ↓
序列化(Protobuf、JSON)
   ↓
网络传输(TCP、HTTP/2)
   ↓
服务发现
   ↓
服务端

Go 常见 RPC 框架

1. Go 标准库 net/rpc(学习首选)

Go 自带:

import "net/rpc"

特点:

  • 简单

  • 理解 RPC 原理很好

  • 支持 TCP、HTTP

缺点:

  • 功能少

  • 生产环境基本不用

示例:

type Args struct {
	A, B int
}

type Math int

func (m *Math) Add(args Args, reply *int) error {
	*reply = args.A + args.B
	return nil
}

2. gRPC(目前最主流)

由 Google 开发。

特点:

  • 基于 HTTP/2

  • 使用 Protocol Buffers

  • 高性能

  • 支持流式通信

  • 多语言支持

适合:

  • 微服务

  • 云原生

  • Kubernetes

定义接口:

syntax = "proto3";

service UserService {
    rpc GetUser(UserRequest)
        returns (UserResponse);
}

生成代码:

protoc \
  --go_out=. \
  --go-grpc_out=. \
  user.proto

3. go-zero

Go 生态很火的微服务框架。

go-zero 官方网站

特点:

  • 内置 RPC

  • 服务发现

  • 熔断

  • 限流

  • 链路追踪

定义:

service user {
    rpc getUser(GetUserReq)
        returns(GetUserResp);
}

生成:

goctl rpc protoc user.proto --go_out=. --go-grpc_out=.

4. Kratos

由 Bilibili 开源。

Kratos 官方网站

特点:

  • gRPC + HTTP 双协议

  • 云原生友好

  • 企业级


5. Dubbo-go

来自 Apache Software Foundation 生态。

Apache Dubbo

特点:

  • Java Dubbo 对应的 Go 实现

  • 适合 Java + Go 混合架构


RPC 框架对比

框架学习价值企业使用难度
net/rpc ⭐⭐⭐⭐⭐ 简单
gRPC ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ 中等
go-zero ⭐⭐⭐⭐ ⭐⭐⭐⭐ 中等
Kratos ⭐⭐⭐⭐ ⭐⭐⭐⭐ 中等
Dubbo-go ⭐⭐⭐ ⭐⭐⭐ 较高

 

客户端服务器端简单示例

服务器端
package main

import (
	//"net"
	"net"
	"net/rpc"
)
type HelloService struct{

}
func (s *HelloService)Hello(requst string,reply *string) error{
	*reply = "hello,"+requst
	return nil
}

func main() {
	//1.实例化一个server
	listener,_:=net.Listen("tcp",":1234")
	//2.注册处理逻辑
	
	_=rpc.RegisterName("HelloService",&HelloService{})  //注册rpc
	//启动服务
	conn,_:=listener.Accept()//当一个新链接进来的时候
	rpc.ServeConn(conn)
}

  客户端

package main

import (
	"fmt"
	"net/rpc"
)

func main() {
	//建立链接
	client, err := rpc.Dial("tcp","localhost:1234")
	if err !=nil{
		panic("链接建立失败")
	}
	//var reply *string=new(string)
	var reply string
	err=client.Call("HelloService.Hello","bobby",&reply)
	if err !=nil{
		panic("调用失败")
	}
	fmt.Println(reply)

}

  

//一连串的代码大部分问题都是net的包好像跟和rpc没啥关系
//不行。rpc调用中有几个问题需要解决1.call ID 2.序列化和反序列化编码和解码
//python 下的开发语言这个显得不好用
//可以跨语言调用,go语言rpc的序列化和反序列协议是什么(Gob);2.能否替换常见的序列化协议

替换rpc 换协议序列号协议服务端

package main

import (
	//"net"
	//"encoding/json"
	"net"
	"net/rpc"
	"net/rpc/jsonrpc"
)
type HelloService struct{

}
func (s *HelloService)Hello(requst string,reply *string) error{
	*reply = "hello,"+requst
	return nil
}

func main() {
	//1.实例化一个server
	listener,_:=net.Listen("tcp",":1234")
	//2.注册处理逻辑
	
	_=rpc.RegisterName("HelloService",&HelloService{})  //注册rpc
	//启动服务
	for{
		conn,_:=listener.Accept()//当一个新链接进来的时候
		go rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
	}
}

  使用rpc 序列号协议客户端

package main

import (
	"fmt"
	"net"
	"net/rpc"
	"net/rpc/jsonrpc"
)

func main() {
	//建立链接
	conn, err := net.Dial("tcp","localhost:1234")
	if err !=nil{
		panic("链接建立失败")
	}
	//var reply *string=new(string)
	var reply string
	client:=rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
	err=client.Call("HelloService.Hello","bobby",&reply)
	if err !=nil{
		panic("调用失败")
	}
	fmt.Println(reply)
	//一连串的代码大部分问题都是net的包好像跟和rpc没啥关系
	//不行。rpc调用中有几个问题需要解决1.call ID 2.序列化和反序列化编码和解码
	//python 下的开发语言这个显得不好用
	//可以跨语言调用,go语言rpc的序列化和反序列协议是什么(Gob);2.能否替换常见的序列化协议
}
执行结果
PS D:\golang\goproject\src\lnhgo> go run day05\json_rpc_test\client\client.go
hello,bobby

使用python 编写客户端请求序列化

import json
import socket
request = {
    "method":"HelloService.Hello",
    "params":["booy"],
    "id":0,
}
client=socket.create_connection(("localhost",1234))
client.sendall(json.dumps(request).encode())
#获取服务器返回的数据
rsc= client.recv(1024)
rsc= json.loads(rsc.decode())
print(rsc["result"])

执行结果
D:\python_demo\.venv\Scripts\python.exe D:\python_demo\基础\go_json_rpc_client.py 
hello,booy

  替换rpc 传输协议为http

package main

import (
	//"net"
	//"encoding/json"
	//"net"
	"io"
	"net/http"
	"net/rpc"
	"net/rpc/jsonrpc"
	//"structs"
)
type HelloService struct{

}
func (s *HelloService)Hello(requst string,reply *string) error{
	*reply = "hello,"+requst
	return nil
}

func main() {
	_=rpc.RegisterName("HelloService",&HelloService{})  //注册rpc
	//1.实例化一个server
	http.HandleFunc("/jsonrpc",func(w http.ResponseWriter, r *http.Request){
		var conn io.ReadWriteCloser=struct {
			io.Writer
			io.ReadCloser
		}{
			ReadCloser:  r.Body,
			Writer: w,
		}
		rpc.ServeRequest(jsonrpc.NewServerCodec(conn))
	})
	http.ListenAndServe(":1234",nil)

	
}

  python 客户端

import requests
request = {
    "method":"HelloService.Hello",
    "params":["booy"],
    "id":0,
}
rsq=requests.post("http://localhost:1234/jsonrpc",json=request)
print(rsq.text)

  初步升级rpc调用的代码

image

 公共代码

package hanlder
const HelloServiceName= "hanlder/HelloService"

  server代码

package main

import (
	//"container/list"
	"lnhgo/day05/new_hellowoder_test/hanlder"
	"net"
	"net/rpc"
)

type HelloService struct{}

func (s *HelloService) Hello(request string, reply *string) error {
	//返回值是通过修改reply 的值
	*reply = "Hello," + request
	return nil
}
func main() {
	//实例化server
	listener,_:= net.Listen("tcp",":1234")
	//注册逻辑
	_=rpc.RegisterName(hanlder.HelloServiceName,&HelloService{})
	//启动服务
	conn,_:=listener.Accept()//当新连接进来后
	rpc.ServeConn(conn)
//一串的代码net的包好像和rpc没有关系
//不行,rpc调用中有几个问题需要解决,1.call id 2.序列化和反序列化
/*
python 下的开发而言这个显得不好用
可以跨语言调用
1.go语言的rpc的序列化和反序列化是什么(Gob)
2.能否替换成常见的序列号

*/
}

  client代码

package main

import (
	"fmt"
	"lnhgo/day05/new_hellowoder_test/hanlder"
	//"net"
	"net/rpc"
	//"net/rpc/jsonrpc"
)

func main() {
	//建立链接

	client, err := rpc.Dial("tcp","localhost:1234")
	if err !=nil{
		panic("链接建立失败")
	}
	//var reply *string=new(string)
	var reply string
	//client:=rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
	err=client.Call(hanlder.HelloServiceName+".Hello","bobby",&reply)
	if err !=nil{
		panic("调用失败")
	}
	fmt.Println(reply)
	//一连串的代码大部分问题都是net的包好像跟和rpc没啥关系
	//不行。rpc调用中有几个问题需要解决1.call ID 2.序列化和反序列化编码和解码
	//python 下的开发语言这个显得不好用
	//可以跨语言调用,go语言rpc的序列化和反序列协议是什么(Gob);2.能否替换常见的序列化协议
}

  升级2

image

server代码

package main

import (
	//"container/list"
	"lnhgo/day05/new_hellowoder_test/hanlder"
	"net"
	"net/rpc"
)

type HelloService struct{}

func (s *HelloService) Hello(request string, reply *string) error {
	//返回值是通过修改reply 的值
	*reply = "Hello," + request
	return nil
}
func main() {
	//实例化server
	listener,_:= net.Listen("tcp",":1234")
	//注册逻辑
	_=rpc.RegisterName(hanlder.HelloServiceName,&HelloService{})
	//启动服务
	for{
	conn,_:=listener.Accept()//当新连接进来后
	rpc.ServeConn(conn)
	}
//一串的代码net的包好像和rpc没有关系
//不行,rpc调用中有几个问题需要解决,1.call id 2.序列化和反序列化
/*
python 下的开发而言这个显得不好用
可以跨语言调用
1.go语言的rpc的序列化和反序列化是什么(Gob)
2.能否替换成常见的序列号

*/
}

client_proxy代码

package client_proxy

import (
	"lnhgo/day05/new_hellowoder_test/hanlder"
	"net/rpc"
)

type HelloServiceStub struct {
	*rpc.Client
}
func NewHelloServiveClient(protol,address string) HelloServiceStub{
	conn,err:= rpc.Dial(protol,address)
	if err!=nil{
		panic("链接建立失败")
	}
	return HelloServiceStub{conn}


}

func (h *HelloServiceStub) Hello(request string, reply *string) error {
	err:=h.Call(hanlder.HelloServiceName+".Hello",request,reply)
	if err !=nil{
		return  err
	}
	return nil
}

  client 代码

package main

import (
	"fmt"
	"lnhgo/day05/new_hellowoder_test/client_proxy"
	//"lnhgo/day05/new_hellowoder_test/hanlder"

	//"net"
	//"net/rpc"
	//"net/rpc/jsonrpc"
)

func main() {
	//建立链接
	client:=client_proxy.NewHelloServiveClient("tcp","localhost:1234")
	// client, err := rpc.Dial("tcp","localhost:1234")
	// if err !=nil{
	// 	panic("链接建立失败")
	// }
	//var reply *string=new(string)
	var reply string
	//client:=rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
	err:=client.Hello("bobby",&reply)
	if err !=nil{
		panic("调用失败")
	}
	fmt.Println(reply)
	//一连串的代码大部分问题都是net的包好像跟和rpc没啥关系
	//不行。rpc调用中有几个问题需要解决1.call ID 2.序列化和反序列化编码和解码
	//python 下的开发语言这个显得不好用
	//可以跨语言调用,go语言rpc的序列化和反序列协议是什么(Gob);2.能否替换常见的序列化协议
}

  升级3(解耦)

image

 hanlder里的代码

package hanlder
const HelloServiceName= "hanlder/HelloService"
type NewHelloService struct{}

func (s *NewHelloService) Hello(request string, reply *string) error {
	//返回值是通过修改reply 的值
	*reply = "Hello," + request
	return nil
}

client_proxy代码

package client_proxy

import (
	"lnhgo/day05/new_hellowoder_test/hanlder"
	"net/rpc"
)

type HelloServiceStub struct {
	*rpc.Client
}
func NewHelloServiveClient(protol,address string) HelloServiceStub{
	conn,err:= rpc.Dial(protol,address)
	if err!=nil{
		panic("链接建立失败")
	}
	return HelloServiceStub{conn}


}

func (h *HelloServiceStub) Hello(request string, reply *string) error {
	err:=h.Call(hanlder.HelloServiceName+".Hello",request,reply)
	if err !=nil{
		return  err
	}
	return nil
}

 client代码

package main

import (
	"fmt"
	"lnhgo/day05/new_hellowoder_test/client_proxy"
	//"lnhgo/day05/new_hellowoder_test/hanlder"

	//"net"
	//"net/rpc"
	//"net/rpc/jsonrpc"
)

func main() {
	//建立链接
	client:=client_proxy.NewHelloServiveClient("tcp","localhost:1234")
	// client, err := rpc.Dial("tcp","localhost:1234")
	// if err !=nil{
	// 	panic("链接建立失败")
	// }
	//var reply *string=new(string)
	var reply string
	//client:=rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
	err:=client.Hello("bobby",&reply)
	if err !=nil{
		panic("调用失败")
	}
	fmt.Println(reply)
	//一连串的代码大部分问题都是net的包好像跟和rpc没啥关系
	//不行。rpc调用中有几个问题需要解决1.call ID 2.序列化和反序列化编码和解码
	//python 下的开发语言这个显得不好用
	//可以跨语言调用,go语言rpc的序列化和反序列协议是什么(Gob);2.能否替换常见的序列化协议
}

  server_proxy的代码

package server_proxy

import (
	"lnhgo/day05/new_hellowoder_test/hanlder"
	"net/rpc"
)
type HelloService interface{
	Hello(request string,reply *string) error
}
//如果做到解耦-我们关心的是方法,也就是接口

func RegisterHelloService(srv HelloService) error {
	return  rpc.RegisterName(hanlder.HelloServiceName,srv)

}

  server的代码

package main

import (
	//"container/list"
	"lnhgo/day05/new_hellowoder_test/hanlder"
	"lnhgo/day05/new_hellowoder_test/server_proxy"
	"net"
	"net/rpc"
)


func main() {
	//实例化server
	listener,_:= net.Listen("tcp",":1234")
	//注册逻辑
	_=server_proxy.RegisterHelloService(&hanlder.NewHelloService{})
	//_=rpc.RegisterName(hanlder.HelloServiceName,&hanlder.HelloService{})
	//启动服务
	for{
	conn,_:=listener.Accept()//当新连接进来后
	rpc.ServeConn(conn)
	}
//一串的代码net的包好像和rpc没有关系
//不行,rpc调用中有几个问题需要解决,1.call id 2.序列化和反序列化
/*
python 下的开发而言这个显得不好用
可以跨语言调用
1.go语言的rpc的序列化和反序列化是什么(Gob)
2.能否替换成常见的序列号

*/
}

  1.这些概念在grpc 中有体现

2. server_proxy与client_proxy能否自动生成,同时满足多语言生成;

3.这两个要求都可以满足,这就是protobuf +grpc

GRPC介绍

如果说 RPC 是远程调用思想,那么 gRPC 是 Google 开发的一套高性能 RPC 框架。它目前已经成为 Go 后端、微服务和云原生领域最主流的 RPC 框架之一,尤其与 Kubernetes、Istio 等生态配合得非常好。


一、什么是 gRPC?

假设有两个程序:

客户端(Client)                    服务端(Server)

GetUser(1)
    │
    │ 网络
    ▼
-----------------------------
| UserService.GetUser(1)     |
-----------------------------
            │
            ▼
      查询数据库

            │
            ▼
返回 User{Name:"Tom"}

客户端看起来像调用本地函数一样:

user := client.GetUser(1)

实际上:

GetUser()

↓

序列化

↓

发送网络请求

↓

服务器执行

↓

返回结果

↓

反序列化

↓

得到 user

整个网络通信都由 gRPC 帮你完成。


二、gRPC 的组成

                 gRPC

         ┌─────────────┐
         │ Protocol Buffers │
         └─────────────┘
                 │
                 ▼
             HTTP/2
                 │
                 ▼
             TCP连接

它主要包括:

  • Protocol Buffers(protobuf)

  • HTTP/2

  • RPC


三、为什么不用 HTTP + JSON?

例如查询用户:

HTTP:

GET /user/1

返回

{
    "id":1,
    "name":"Tom"
}

gRPC:

client.GetUser(ctx, req)

底层直接传输二进制。

因此:

HTTP JSONgRPC
文本(JSON) 二进制(Protobuf)
较慢 更快
数据较大 更小
易读 不易读

四、gRPC 为什么快?

主要原因有三个:

① Protobuf

JSON:

{
    "id":1,
    "name":"Tom"
}

Protobuf:

011010101010001...

特点:

  • 更小

  • 更快

  • CPU 消耗低


② HTTP/2

传统 HTTP/1.1:

请求1

等待

响应1

请求2

等待

响应2

HTTP/2:

请求1 ─┐
请求2 ─┼──── 同时发送
请求3 ─┘

支持:

  • 多路复用

  • Header 压缩

  • 长连接


③ 自动生成代码

例如:

user.proto

生成:

Go代码
Java代码
Python代码
C++代码

客户端不用自己写解析代码。


五、Protocol Buffers

gRPC 最大特点就是 先写接口,再生成代码

例如:

syntax = "proto3";

service HelloService {
    rpc Hello(HelloRequest) returns (HelloReply);
}

message HelloRequest {
    string name = 1;
}

message HelloReply {
    string message = 1;
}

保存为:

hello.proto

然后运行:

protoc

自动生成:

hello.pb.go
hello_grpc.pb.go

里面已经有:

HelloClient

HelloServer

Hello()

RegisterHelloServer()

六、Server

服务器只需要实现接口:

type HelloServer struct {
    pb.UnimplementedHelloServiceServer
}

func (s *HelloServer) Hello(
    ctx context.Context,
    req *pb.HelloRequest,
) (*pb.HelloReply, error) {

    return &pb.HelloReply{
        Message: "hello " + req.Name,
    }, nil
}

然后注册:

grpcServer := grpc.NewServer()

pb.RegisterHelloServiceServer(
    grpcServer,
    &HelloServer{},
)

七、Client

客户端:

conn, _ := grpc.Dial(
    "localhost:50051",
    grpc.WithInsecure(),
)

client := pb.NewHelloServiceClient(conn)

resp, _ := client.Hello(
    context.Background(),
    &pb.HelloRequest{
        Name: "Tom",
    },
)

fmt.Println(resp.Message)

是不是和调用本地函数几乎一样?


八、gRPC 四种通信方式

① 普通 RPC(一问一答)

Client ----Request---->

Server ----Reply------>

Go:

resp, _ := client.Hello(ctx, req)

最常用。


② 服务端流(Server Streaming)

Client ------Request------>

Server --->数据1

Server --->数据2

Server --->数据3

例如:

  • 下载文件

  • 实时日志

  • 股票行情


③ 客户端流(Client Streaming)

Client --->数据1

Client --->数据2

Client --->数据3

Server ----Reply---->

例如:

  • 上传大文件

  • 上传图片

  • 上传视频


④ 双向流(Bidirectional Streaming)

Client --->

Client --->

        双向

Server --->

Server --->

双方可以同时发送消息。

例如:

  • 聊天

  • 游戏

  • 音视频


九、与 net/rpc 的区别

比较net/rpcgRPC
官方支持 已基本停止演进 官方重点维护
协议 Gob、JSON-RPC Protobuf
性能 一般 很高
HTTP/2
流式通信
多语言 较差 很好
微服务 一般 非常适合
自动生成代码

十、什么时候使用 gRPC?

适合:

  • 微服务之间通信

  • 高并发、高性能接口

  • 多语言系统(Go、Java、Python、C++ 等)

  • 云原生应用

  • 需要流式通信(如聊天、日志、实时数据)

如果只是开发一个简单的网站或开放给浏览器调用的 REST API,HTTP + JSON 往往更简单直接;而在服务与服务之间的内部通信中,gRPC 通常是更好的选择。

grpc 依赖工具下载地址:https://github.com/protocolbuffers/protobuf/releases

下载解压配置环境变量

下载go 语言依赖包

go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
安装成功后,一般会生成两个可执行文件:

protoc-gen-go.exe
protoc-gen-go-grpc.exe

GOPATH = D:\goproject
GOBIN  = (空)
dir D:\goproject\bin
如果能看到,说明插件已经安装好了,只是 PATH 没配置。

  

  proto vscode 插件安装

image

 

编辑proto

syntax = "proto3";
package helloworld;
option go_package = "./;helloworld";
message HelloRequest {
    string name = 1;//编号不是值
    //int32 age = 不支持int类型必须是int32或int64

}

   进入所在文件目录

image

 

执行生成命令

PS D:\golang\goproject\src\lnhgo\day05\helloworld\proto> protoc --go_out=. --go-grpc_out=. helloworld.proto

  生成的文件

// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// 	protoc-gen-go v1.36.11
// 	protoc        v7.35.1
// source: helloworld.proto

package helloworld

import (
	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
	reflect "reflect"
	sync "sync"
	unsafe "unsafe"
)

const (
	// Verify that this generated code is sufficiently up-to-date.
	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
	// Verify that runtime/protoimpl is sufficiently up-to-date.
	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)

type HelloRequest struct {
	state         protoimpl.MessageState `protogen:"open.v1"`
	Name          string                 `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` //编号不是值
	unknownFields protoimpl.UnknownFields
	sizeCache     protoimpl.SizeCache
}

func (x *HelloRequest) Reset() {
	*x = HelloRequest{}
	mi := &file_helloworld_proto_msgTypes[0]
	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
	ms.StoreMessageInfo(mi)
}

func (x *HelloRequest) String() string {
	return protoimpl.X.MessageStringOf(x)
}

func (*HelloRequest) ProtoMessage() {}

func (x *HelloRequest) ProtoReflect() protoreflect.Message {
	mi := &file_helloworld_proto_msgTypes[0]
	if x != nil {
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
		if ms.LoadMessageInfo() == nil {
			ms.StoreMessageInfo(mi)
		}
		return ms
	}
	return mi.MessageOf(x)
}

// Deprecated: Use HelloRequest.ProtoReflect.Descriptor instead.
func (*HelloRequest) Descriptor() ([]byte, []int) {
	return file_helloworld_proto_rawDescGZIP(), []int{0}
}

func (x *HelloRequest) GetName() string {
	if x != nil {
		return x.Name
	}
	return ""
}

var File_helloworld_proto protoreflect.FileDescriptor

const file_helloworld_proto_rawDesc = "" +
	"\n" +
	"\x10helloworld.proto\x12\n" +
	"helloworld\"\"\n" +
	"\fHelloRequest\x12\x12\n" +
	"\x04name\x18\x01 \x01(\tR\x04nameB\x0fZ\r./;helloworldb\x06proto3"

var (
	file_helloworld_proto_rawDescOnce sync.Once
	file_helloworld_proto_rawDescData []byte
)

func file_helloworld_proto_rawDescGZIP() []byte {
	file_helloworld_proto_rawDescOnce.Do(func() {
		file_helloworld_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_helloworld_proto_rawDesc), len(file_helloworld_proto_rawDesc)))
	})
	return file_helloworld_proto_rawDescData
}

var file_helloworld_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_helloworld_proto_goTypes = []any{
	(*HelloRequest)(nil), // 0: helloworld.HelloRequest
}
var file_helloworld_proto_depIdxs = []int32{
	0, // [0:0] is the sub-list for method output_type
	0, // [0:0] is the sub-list for method input_type
	0, // [0:0] is the sub-list for extension type_name
	0, // [0:0] is the sub-list for extension extendee
	0, // [0:0] is the sub-list for field type_name
}

func init() { file_helloworld_proto_init() }
func file_helloworld_proto_init() {
	if File_helloworld_proto != nil {
		return
	}
	type x struct{}
	out := protoimpl.TypeBuilder{
		File: protoimpl.DescBuilder{
			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
			RawDescriptor: unsafe.Slice(unsafe.StringData(file_helloworld_proto_rawDesc), len(file_helloworld_proto_rawDesc)),
			NumEnums:      0,
			NumMessages:   1,
			NumExtensions: 0,
			NumServices:   0,
		},
		GoTypes:           file_helloworld_proto_goTypes,
		DependencyIndexes: file_helloworld_proto_depIdxs,
		MessageInfos:      file_helloworld_proto_msgTypes,
	}.Build()
	File_helloworld_proto = out.File
	file_helloworld_proto_goTypes = nil
	file_helloworld_proto_depIdxs = nil
}

*注意最新版本需要添加

protoc-gen-go: unable to determine Go import path for "helloworld.proto" Please specify either: • a "go_package" option in the .proto source file, or • a "M" argument on the command line. See https://protobuf.dev/reference/go/go-generated#package for more information. --go_out: protoc-gen-go: Plugin failed with status code 1.



在你的 helloworld.proto 文件开头加上:
syntax = "proto3";

package helloworld;

option go_package = "./;helloworld";

  编写测试代码

package main

import (
	"fmt"
	"lnhgo/day05/helloworld/proto"

	"google.golang.org/protobuf/proto"
)
func main(){
	req:=helloworld.HelloRequest{
		Name: "bobby",
	}
	rsp,_:=proto.Marshal(&req)//
	fmt.Println(string(rsp))
}
执行结果
bobby

  brotobuf 的原理

既然你已经开始学习 gRPC,那么理解 Protocol Buffers(Protobuf) 的原理非常重要。很多人会用 .proto,但不知道为什么它比 JSON 快。理解原理之后,gRPC 就容易理解了。


一、什么是 Protobuf?

Protobuf(Protocol Buffers)是 Google 开发的一种数据序列化协议

简单来说,它的作用就是:

把内存中的对象转换成二进制数据进行传输或存储,需要时再恢复成对象。

例如:

Go 结构体:

type User struct {
    Id   int32
    Name string
}

内存中:

User
 │
 ├── Id = 1
 └── Name = "Tom"

网络不能直接传 Go 对象,因此需要序列化。


JSON 的做法

例如:

{
    "id":1,
    "name":"Tom"
}

发送的是文本。


Protobuf 的做法

发送的是:

08 01 12 03 54 6f 6d

完全是二进制。

因此:

Go对象
    │
Marshal()
    │
    ▼
二进制
    │
网络
    │
    ▼
Unmarshal()
    │
Go对象

二、为什么 JSON 慢?

假设:

{
    "id":100,
    "name":"Tom"
}

网络上传输的是:

{
"
i
d
"
:
1
0
0
,
...
}

里面有很多字符:

{
}
"
:
,

还有:

"id"
"name"

这些字段名每次都会发送。


例如:

发送 100 万个对象:

"id"

要发送 100 万次。

"name"

也要发送 100 万次。

浪费了大量带宽。


三、Protobuf 为什么快?

因为它不会发送字段名。

例如:

message User{
    int32 id = 1;
    string name = 2;
}

这里:

1

代表:

id
2

代表:

name

所以发送:

id = 100

实际上发送:

字段编号1
值100

而不是:

"id":100

因此体积非常小。


四、为什么要写 .proto

例如:

message User{
    int32 id = 1;
    string name = 2;
}

它实际上就是一份"协议"。

告诉所有语言:

编号1

↓

int32

↓

id
编号2

↓

string

↓

name

因此:

Go

Java

Python

C++

都能理解。

这就是为什么 gRPC 支持几十种语言。


五、为什么需要 protoc

例如:

message User{
    int32 id = 1;
    string name = 2;
}

Go 编译器看不懂。

所以:

.proto

↓

protoc

↓

Go代码

生成:

type User struct {
    Id int32
    Name string
}

还有:

func (x *User) Marshal()

func (x *User) Unmarshal()

这些代码都是自动生成的。


六、编号为什么不能乱改?

例如:

message User{
    int32 id = 1;
    string name = 2;
}

网络真正发送:

1
↓

100

如果以后改成:

message User{
    string name = 1;
    int32 id = 2;
}

以前的数据:

1

↓

100

现在会被解释成:

name="100"

整个协议就乱了。

所以:

字段编号一旦发布,就不要修改。

可以删除字段,但不要重复使用旧编号。


七、为什么新增字段不会影响旧程序?

旧版本:

message User{
    int32 id = 1;
}

发送:

1

↓

100

后来:

message User{
    int32 id = 1;
    string name = 2;
}

发送:

1

↓

100

2

↓

Tom

旧程序:

认识:

1

↓

id

不认识:

2

↓

直接跳过

因此:

Protobuf 天然支持向前、向后兼容

这也是大型分布式系统广泛采用它的重要原因。


八、为什么 Protobuf 比 JSON 快?

主要有四个原因:

  1. 二进制编码:不需要解析大量文本字符。

  2. 字段编号代替字段名:减少传输的数据量。

  3. 生成的代码直接读写:不依赖运行时反射去解析字段。

  4. 编码规则紧凑:整数等类型采用可变长度编码(Varint),小整数占用的字节更少。

例如整数:

1

JSON:

"1"

Protobuf:

00000001

110100 这类较小的数字,Protobuf 往往只需要很少的字节就能表示。


九、整个工作流程

编写协议
        │
        ▼
helloworld.proto
        │
        ▼
protoc 编译
        │
        ▼
生成 Go 代码
        │
        ▼
客户端调用
        │
        ▼
Marshal
        │
        ▼
二进制数据
        │
网络传输
        │
        ▼
服务端 Unmarshal
        │
        ▼
Go结构体

 测试2

package main

import (
	//"encoding/json"
	"fmt"
	"lnhgo/day05/helloworld/proto"

	"google.golang.org/protobuf/proto"
)
type Hello struct{
	Name string `json:"name"`
	Age int `json:"age"`
	Courses []string `json:"courses"`
	
}
func main(){
	req:=helloworld.HelloRequest{
		Name: "bobby",
		Age: 18,
		Courses: []string{"go","gin","微服务"},
	}
	// jsonStruct:=Hello{Name: "bobby",
	// 	Age: 18,
	// 	Courses: []string{"go","gin","微服务"},
	// }
	//jsonrsp,_:=json.Marshal(jsonStruct)
	//fmt.Println(len(string(jsonrsp)))
	rsp,_:=proto.Marshal(&req)//
	newReq:=helloworld.HelloRequest{}
	proto.Unmarshal(rsp,&newReq)
	fmt.Println(len(string(rsp)))
	fmt.Println(newReq.Age,newReq.Name,newReq.Courses)
}
执行结果
PS D:\golang\goproject\src\lnhgo> go run day05\helloworld\proto\main\main.go
29
18 bobby [go gin 微服务]

  grpc 的入门

image

 

proto文件

syntax = "proto3";
//package helloworld;
option go_package = ".;proto";
service Greeter {
    rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest {
    string name =1;
}
message HelloReply {
    string message =1;
}
PS D:\golang\goproject\src\lnhgo\day05\grpc\proto> protoc --go_out=. --go-grpc_out=. helloworld.proto

  server 文件

package main

import (
	"context"
	"lnhgo/day05/grpc/proto"
	"net"

	"google.golang.org/grpc"
)

type Server struct{
	proto.UnimplementedGreeterServer
}

func (s *Server) SayHello(c context.Context,request *proto.HelloRequest)(*proto.HelloReply,error){
	return &proto.HelloReply{
		Message: "hello "+request.Name,
	},nil
}
//func (s *Server)mustEmbedUnimplementedGreeterServer(){}
func main(){
	g:=grpc.NewServer()
	proto.RegisterGreeterServer(g,&Server{})
	lis,err:=net.Listen("tcp","0.0.0.0:8080")
	if err !=nil {
		panic("监听失败")
	}
	err=g.Serve(lis)
	if err !=nil {
		panic("启动失败")
	}
}

  client 文件

package main

import (
	"context"
	"fmt"
	"lnhgo/day05/grpc/proto"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

func main() {
	conn, err := grpc.NewClient(
    "127.0.0.1:8080",
    grpc.WithTransportCredentials(insecure.NewCredentials()),
)//客户端连接
	if err !=nil{
		panic("连接失败")
	}
	defer conn.Close()
	c:=proto.NewGreeterClient(conn)
	r,err:=c.SayHello(context.Background(),&proto.HelloRequest{Name: "chenxi"})
	if err != nil{
		panic("调用失败")
	}
	fmt.Println(r.Message)
}

 测试结果

PS D:\golang\goproject\src\lnhgo> go run day05\grpc\client\client.go
hello chenxi

  grpc 的四种数据流

gRPC 的一个重要特性就是数据流(Streaming)。如果说普通 RPC 是“一问一答”,那么数据流允许客户端和服务端连续发送多条消息,而不用每发送一条就重新建立一次请求。

一、普通 RPC(Unary RPC)

这是你目前最熟悉的模式。

客户端                  服务端
   |  HelloRequest  -----> |
   | <----- HelloReply     |

对应 .proto

service Greeter {
    rpc SayHello(HelloRequest) returns (HelloReply);
}

特点:

  • 一次请求

  • 一次响应

  • 最常用

例如:

resp, err := client.SayHello(ctx, &proto.HelloRequest{
    Name: "Tom",
})

二、服务端流(Server Streaming)

客户端发送一次请求。

服务器返回很多数据。

例如:

客户端                      服务端
   | Request -----------> |
   | <----- msg1          |
   | <----- msg2          |
   | <----- msg3          |
   | <----- msg4          |

例如:

直播:

客户端:

我要看直播

服务器:

第一帧

第二帧

第三帧

第四帧...

.proto:

service Greeter {
    rpc GetStream(StreamReq) returns (stream StreamRes);
}

注意:

stream StreamRes

表示:

服务器不停发送。

客户端:

stream, err := client.GetStream(ctx, req)

for {
    res, err := stream.Recv()
    if err == io.EOF {
        break
    }

    fmt.Println(res.Data)
}

一直:

Recv()

直到结束。


三、客户端流(Client Streaming)

客户端一直发送。

服务器最后回复一次。

客户端                      服务端
   | msg1 -----------> |
   | msg2 -----------> |
   | msg3 -----------> |
   | msg4 -----------> |
   | <------ Result    |

例如:

上传文件。

客户端:

第一块

第二块

第三块

第四块

全部上传完。

服务器:

上传成功

.proto:

service Greeter {
    rpc PutStream(stream UploadReq)
        returns (UploadReply);
}

客户端:

stream, _ := client.PutStream(ctx)

stream.Send(...)

stream.Send(...)

stream.Send(...)

reply, err := stream.CloseAndRecv()

最后:

CloseAndRecv()

服务器才返回。


四、双向流(Bidirectional Streaming)

客户端可以一直发送。

服务器也可以一直发送。

互不影响。

客户端                     服务端

msg1 ------------------>

          <---------------- reply1

msg2 ------------------>

          <---------------- reply2

msg3 ------------------>

          <---------------- reply3

例如:

聊天室。

A:

你好

服务器:

转发

B:

你好

服务器:

转发

.proto:

service Greeter{
    rpc AllStream(stream ChatMessage)
        returns (stream ChatMessage);
}

客户端:

发送:

go func() {
    for {
        stream.Send(...)
    }
}()

接收:

go func() {
    for {
        msg, _ := stream.Recv()
        fmt.Println(msg)
    }
}()

两个 Goroutine:

一个负责 Send()

一个负责 Recv()

互不影响。


四种 RPC 总结

类型客户端发送服务端返回使用场景
Unary 1 次 1 次 登录、查询、下单
Server Streaming 1 次 多次 日志推送、实时行情、视频流
Client Streaming 多次 1 次 文件上传、批量数据上传
Bidirectional Streaming 多次 多次 聊天室、在线游戏、协同编辑

底层原理

很多人以为:

Send()

↓

网络

↓

Recv()

实际上不是。

gRPC 底层使用 HTTP/2

HTTP/2 的一个重要能力就是多路复用,它把数据拆成很多小的 Frame(帧) 在同一条 TCP 连接上传输。

例如:

TCP连接
───────────────────────────

Frame1

Frame2

Frame3

Frame4

Frame5

───────────────────────────

每个 Frame 都属于某一个 Stream。

因此:

一个 TCP

↓

多个 Stream

↓

每个 Stream

↓

很多 protobuf Message

所以:

客户端:

Send()

Send()

Send()

实际上:

protobuf

↓

HTTP/2 Frame

↓

TCP

服务器收到后再重新组装成消息。

流模式

image

proto 文件

syntax = "proto3";
option go_package = ".;proto";
service Greeter {
    rpc GetStream (StreamReqDate) returns (stream StreamResData);//服务端流模式
    rpc PostStream (stream  StreamReqDate) returns (StreamResData);//客户端流模式
    rpc AllStream (stream  StreamReqDate) returns (stream StreamResData);//双向流模式
}
message StreamReqDate {
    string data =1;
}
message StreamResData {
    string data =1;
}

PS D:\golang\goproject\src\lnhgo> cd day05\stream_grpc_test\proto
PS D:\golang\goproject\src\lnhgo\day05\stream_grpc_test\proto> protoc --go_out=. --go-grpc_out=. stream.proto      
PS D:\golang\goproject\src\lnhgo\day05\stream_grpc_test\proto> 

  服务端流模式

server 文件

package main

import (
	"fmt"	
	"net"
	"time"

	"google.golang.org/grpc"

	"lnhgo/day05/stream_grpc_test/proto"
)

const PORT = ":50052"

type server struct {
	proto.UnimplementedGreeterServer
}

func (s *server)GetStream(req *proto.StreamReqDate, res grpc.ServerStreamingServer[proto.StreamResData]) error{
	i :=0
	for{
		i++
		_=res.Send(&proto.StreamResData{
			Data: fmt.Sprintf("%v",time.Now().Unix()),
		})
		time.Sleep(time.Second)
		if i>10{
			break
		}

	}
	return nil
}
func (s *server)PostStream(cliStr grpc.ClientStreamingServer[proto.StreamReqDate, proto.StreamResData]) error{
	// for {
	// 	if a,err:=cliStr.Recv();err!=nil{
	// 		fmt.Println(err)
	// 		break
	// 	}else{
	// 		fmt.Println(a.Data)
	// 	}
	// }
	return nil
}
func (s *server)AllStream(grpc.BidiStreamingServer[proto.StreamReqDate, proto.StreamResData]) error{
	return nil
}
//func (s *server)mustEmbedUnimplementedGreeterServer(){}
func main(){
	list,err:=net.Listen("tcp",PORT)
	if err !=nil{
		panic("端口启动失败")
	}
	s:=grpc.NewServer()
	proto.RegisterGreeterServer(s,&server{})
	err=s.Serve(list)
	if err !=nil{
		panic("启动失败")
	}

}

  client 文件

package main

import (
	"context"
	"fmt"

	"lnhgo/day05/stream_grpc_test/proto"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)
func main() {
	conn, err := grpc.NewClient(
		"127.0.0.1:50052",
    	grpc.WithTransportCredentials(insecure.NewCredentials()),
	)	
	if err !=nil{
		panic("连接失败")
	}
	defer conn.Close()

	c := proto.NewGreeterClient(conn)
	res,_:=c.GetStream(context.Background(),&proto.StreamReqDate{Data: "慕课网",},)
	for{
		a,err:=res.Recv()
		if err !=nil{
			fmt.Println(err)
			break
		}
		fmt.Println(a)
	}
}

  客户端流模式

server 文件

package main

import (
	"fmt"	
	"net"
	"time"

	"google.golang.org/grpc"

	"lnhgo/day05/stream_grpc_test/proto"
)

const PORT = ":50052"

type server struct {
	proto.UnimplementedGreeterServer
}

func (s *server)GetStream(req *proto.StreamReqDate, res grpc.ServerStreamingServer[proto.StreamResData]) error{
	i :=0
	for{
		i++
		_=res.Send(&proto.StreamResData{
			Data: fmt.Sprintf("%v",time.Now().Unix()),
		})
		time.Sleep(time.Second)
		if i>10{
			break
		}

	}
	return nil
}
func (s *server)PostStream(cliStr grpc.ClientStreamingServer[proto.StreamReqDate, proto.StreamResData]) error{
	for {
		if a,err:=cliStr.Recv();err!=nil{
			fmt.Println(err)
			break
		}else{
			fmt.Println(a.Data)
		}
	}
	return nil
}
func (s *server)AllStream(grpc.BidiStreamingServer[proto.StreamReqDate, proto.StreamResData]) error{
	return nil
}
//func (s *server)mustEmbedUnimplementedGreeterServer(){}
func main(){
	list,err:=net.Listen("tcp",PORT)
	if err !=nil{
		panic("端口启动失败")
	}
	s:=grpc.NewServer()
	proto.RegisterGreeterServer(s,&server{})
	err=s.Serve(list)
	if err !=nil{
		panic("启动失败")
	}

}

  client文件

package main

import (
	"context"
	"fmt"
	"time"

	"lnhgo/day05/stream_grpc_test/proto"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)
func main() {
	conn, err := grpc.NewClient(
		"127.0.0.1:50052",
    	grpc.WithTransportCredentials(insecure.NewCredentials()),
	)	
	if err !=nil{
		panic("连接失败")
	}
	defer conn.Close()

	c := proto.NewGreeterClient(conn)
	res,_:=c.GetStream(context.Background(),&proto.StreamReqDate{Data: "慕课网",},)
	for{
		a,err:=res.Recv()
		if err !=nil{
			fmt.Println(err)
			break
		}
		fmt.Println(a)
	}
	//客户端流模式
	putS,_:=c.PostStream(context.Background())
	i:=0
	for{
		i++
		_=putS.Send(&proto.StreamReqDate{
			Data: fmt.Sprintf("go 中文网%d",i),

		})
		time.Sleep(time.Second)
		if i>10{
			break
		}

	}
}

  测试

server 启动
PS D:\golang\goproject\src\lnhgo> go run day05\stream_grpc_test\server\server.go
go 中文网1
go 中文网2
go 中文网3
go 中文网4
go 中文网5
go 中文网6
go 中文网7
go 中文网8
go 中文网9
go 中文网10
go 中文网11
rpc error: code = Canceled desc = context canceled
client启动
PS D:\golang\goproject\src\lnhgo> go run day05\stream_grpc_test\client\client.go
data:"1782619647"
data:"1782619648"
data:"1782619649"
data:"1782619650"
data:"1782619651"
data:"1782619652"
data:"1782619653"
data:"1782619654"
data:"1782619655"
data:"1782619656"
data:"1782619657"
EOF
PS D:\golang\goproject\src\lnhgo> 

  双向推送

server端

package main

import (
	"fmt"
	"net"
	"sync"
	"time"

	"google.golang.org/grpc"

	"lnhgo/day05/stream_grpc_test/proto"
)

const PORT = ":50052"

type server struct {
	proto.UnimplementedGreeterServer
}

func (s *server)GetStream(req *proto.StreamReqDate, res grpc.ServerStreamingServer[proto.StreamResData]) error{
	i :=0
	for{
		i++
		_=res.Send(&proto.StreamResData{
			Data: fmt.Sprintf("%v",time.Now().Unix()),
		})
		time.Sleep(time.Second)
		if i>10{
			break
		}

	}
	return nil
}
func (s *server)PostStream(cliStr grpc.ClientStreamingServer[proto.StreamReqDate, proto.StreamResData]) error{
	for {
		if a,err:=cliStr.Recv();err!=nil{
			fmt.Println(err)
			break
		}else{
			fmt.Println(a.Data)
		}
	}
	return nil
}
func (s *server)AllStream(allStr grpc.BidiStreamingServer[proto.StreamReqDate, proto.StreamResData]) error{
	// go allStr.Recv()
	// allStr.Send()\
	wg := sync.WaitGroup{}
	wg.Add(2)
	go func() {
		defer wg.Done()
		for {
			data,_:=allStr.Recv()
			fmt.Println("收到客户端消息:"+data.Data)
		}
	}()
	go func ()  {
		defer wg.Done()
		for {
			_=allStr.Send(&proto.StreamResData{Data: "你好我是服务器"})
			time.Sleep(time.Second)
		}
	}()
   wg.Wait()
	return  nil
}
//func (s *server)mustEmbedUnimplementedGreeterServer(){}
func main(){
	list,err:=net.Listen("tcp",PORT)
	if err !=nil{
		panic("端口启动失败")
	}
	s:=grpc.NewServer()
	proto.RegisterGreeterServer(s,&server{})
	err=s.Serve(list)
	if err !=nil{
		panic("启动失败")
	}

}

  client 端

package main

import (
	"context"
	"fmt"
	"time"
	"sync"

	"lnhgo/day05/stream_grpc_test/proto"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)
func main() {
	conn, err := grpc.NewClient(
		"127.0.0.1:50052",
    	grpc.WithTransportCredentials(insecure.NewCredentials()),
	)	
	if err !=nil{
		panic("连接失败")
	}
	defer conn.Close()

	c := proto.NewGreeterClient(conn)
	res,_:=c.GetStream(context.Background(),&proto.StreamReqDate{Data: "慕课网",},)
	for{
		a,err:=res.Recv()
		if err !=nil{
			fmt.Println(err)
			break
		}
		fmt.Println(a)
	}
	//客户端流模式
	putS,_:=c.PostStream(context.Background())
	i:=0
	for{
		i++
		_=putS.Send(&proto.StreamReqDate{
			Data: fmt.Sprintf("go 中文网%d",i),

		})
		time.Sleep(time.Second)
		if i>10{
			break
		}

	}
	//双向流模式
	allStr,_:=c.AllStream(context.Background())
	wg := sync.WaitGroup{}
	wg.Add(2)
	go func() {
		defer wg.Done()
		for {
			data,_:=allStr.Recv()
			fmt.Println("收到客户端消息:"+data.Data)
		}
	}()
	go func ()  {
		defer wg.Done()
		for {
			_=allStr.Send(&proto.StreamReqDate{Data: "你好我是go"})
			time.Sleep(time.Second)
		}
	}()
	defer wg.Wait()

}

  测试

PS D:\golang\goproject\src\lnhgo> go run day05\stream_grpc_test\server\server.go
go 中文网1
go 中文网2
go 中文网3
go 中文网4
go 中文网5
go 中文网6
go 中文网7
go 中文网8
go 中文网9
go 中文网10
go 中文网11
收到客户端消息:你好我是go
收到客户端消息:你好我是go
收到客户端消息:你好我是go
client
PS D:\golang\goproject\src\lnhgo> go run day05\stream_grpc_test\client\client.go
data:"1782621008"
data:"1782621009"
data:"1782621010"
data:"1782621011"
data:"1782621012"
data:"1782621013"
data:"1782621014"
data:"1782621015"
data:"1782621016"
data:"1782621017"
data:"1782621018"
EOF
收到客户端消息:你好我是服务器
收到客户端消息:你好我是服务器
收到客户端消息:你好我是服务器
收到客户端消息:你好我是服务器
收到客户端消息:你好我是服务器
收到客户端消息:你好我是服务器
收到客户端消息:你好我是服务器
收到客户端消息:你好我是服务器
收到客户端消息:你好我是服务器
收到客户端消息:你好我是服务器
收到客户端消息:你好我是服务器
收到客户端消息:你好我是服务器
收到客户端消息:你好我是服务器
收到客户端消息:你好我是服务器
收到客户端消息:你好我是服务器
收到客户端消息:你好我是服务器
收到客户端消息:你好我是服务器
收到客户端消息:你好我是服务器
收到客户端消息:你好我是服务器
收到客户端消息:你好我是服务器

  

 

 

 

 

 

 


 

posted @ 2026-06-27 14:13  烟雨楼台,行云流水  阅读(4)  评论(0)    收藏  举报