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、2序列化 -
通过网络发送给服务器
-
服务器执行
Add() -
返回结果
3 -
客户端收到结果
对程序员来说:
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 生态很火的微服务框架。
特点:
-
内置 RPC
-
服务发现
-
熔断
-
限流
-
链路追踪
定义:
service user {
rpc getUser(GetUserReq)
returns(GetUserResp);
}
生成:
goctl rpc protoc user.proto --go_out=. --go-grpc_out=.
4. Kratos
由 Bilibili 开源。
特点:
-
gRPC + HTTP 双协议
-
云原生友好
-
企业级
5. Dubbo-go
来自 Apache Software Foundation 生态。
特点:
-
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调用的代码

公共代码
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

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(解耦)

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 JSON | gRPC |
|---|---|
| 文本(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/rpc | gRPC |
|---|---|---|
| 官方支持 | 已基本停止演进 | 官方重点维护 |
| 协议 | 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 插件安装

编辑proto
syntax = "proto3";
package helloworld;
option go_package = "./;helloworld";
message HelloRequest {
string name = 1;//编号不是值
//int32 age = 不支持int类型必须是int32或int64
}
进入所在文件目录

执行生成命令
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 快?
主要有四个原因:
-
二进制编码:不需要解析大量文本字符。
-
字段编号代替字段名:减少传输的数据量。
-
生成的代码直接读写:不依赖运行时反射去解析字段。
-
编码规则紧凑:整数等类型采用可变长度编码(Varint),小整数占用的字节更少。
例如整数:
1
JSON:
"1"
Protobuf:
00000001
像 1、10、100 这类较小的数字,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 的入门

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
服务器收到后再重新组装成消息。
流模式

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 收到客户端消息:你好我是服务器 收到客户端消息:你好我是服务器 收到客户端消息:你好我是服务器 收到客户端消息:你好我是服务器 收到客户端消息:你好我是服务器 收到客户端消息:你好我是服务器 收到客户端消息:你好我是服务器 收到客户端消息:你好我是服务器 收到客户端消息:你好我是服务器 收到客户端消息:你好我是服务器 收到客户端消息:你好我是服务器 收到客户端消息:你好我是服务器 收到客户端消息:你好我是服务器 收到客户端消息:你好我是服务器 收到客户端消息:你好我是服务器 收到客户端消息:你好我是服务器 收到客户端消息:你好我是服务器 收到客户端消息:你好我是服务器 收到客户端消息:你好我是服务器 收到客户端消息:你好我是服务器

浙公网安备 33010602011771号