自学内容网 自学内容网

GOLANG笔记第四周

什么是RPC,RPC的挑战是什么?

在这里插入图片描述

而对于远程过程面临的一些问题:

  1. 原本的本地函数放到另一个服务器上运行,但是引入了很多新问题
  2. Call 的id映射
  3. 序列化和反序列化
  4. 网络传输

Call 的id映射

当客户端发起一个远程调用时,它会为每个调用分配一个唯一的 Call ID。这个 Call ID 用于在服务器端准确地识别客户端请求的是哪个具体的函数调用,并且在服务器返回结果时,客户端也可以通过这个 Call ID 来确定返回的结果对应的是哪一个调用请求。

序列化和反序列化

序列化是将数据结构或对象转换为可以在网络上传输或者存储的格式(通常是字节流)的过程

与序列化相反,反序列化是将接收到的字节流重新转换为原来的数据结构或对象的过程。

package main

import "fmt"

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

type Company struct {
Name    string
Address string
}

type Employee struct {
Name    string
company Company
}

type PrintResult struct {
Info string
Err  error
}

func RpcPrintln(employee Employee) PrintResult {
/*
客户端
1.建立连接 tcp/http
2.将employee对象序列化成json字符串 - 序列化
3.发送json字符串 -- 调用成功后接受到的是二进制数据
4.等待服务器发送结果
5.将服务器返回的数据解析成PrintResult对象 -- 反序列化

服务端
1.监听网络端口 80
2.读取数据 -- 二进制的json数据
3.对数据反序列化成Employee
4.开始处理业务逻辑
5.将处理的结果PrintResult序列化成json二进制数据
6.将数据返回
序列化和反序列化是可以选择的,不一定是json
而在rpc中,
*/

}

func main() {
//现在我们想把Add变成一个远程的函数调用,也就意味着需要把Add函数放在远程服务器上运行
//一定会牵扯到网络,做成一个web服务(gin,beego,net/httpserver)

/*
1.这个函数的调用参数如何传递-json(json是一种数据格式的协议)  xml/protobuf/msgpack
  现在网络调用有两个端 - 客户端,应该将数据传输到gin
             gin -- 服务端,服务端负责解析数据
*/
fmt.Println(Add(1, 2))

//将这个打印的工作放在另一台服务器上,我需要将本地的内存对象 struct,这样不行
//可行的方式就是将struct序列化成json对象
//远程的服务器需要将二进制对象反解成struct对象
fmt.Println(Employee{
Name: "tc",
company: Company{
Name:    "tc",
Address: "深圳",
},
})

}

那么,直接全部使用json去格式化不行吗?
这种做法在浏览器和gin服务之间是可行的,但如果你是一个大型的分布式系统,那么会很难维护,基本是不行的

在 RPC 中,数据需要在不同的进程或者机器之间传输。由于网络只能传输字节流,所以必须将对象或数据结构进行序列化才能进行传输。比如,一个客户端在调用远程服务器上的一个函数时,需要把函数的参数进行序列化后发送给服务器,这样服务器才能接收到这些参数。

网络传输

http1.x 和 http2.0协议

http协议底层使用的也是tcp,http现在主流的是1.x 这种协议有性能问题(一次性)
一旦结果返回,连接就断开
1.直接自己基于tcp/udp协议 myhttp,但是没用通用性 – http2.0既有http特性,也有长连接特性

demo:

服务端
package main

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

func main() {
// http//127.0.0.1:8000/add?a=1&b=2
//Callid 问题:r.URL.Path
//数据的传输协议 URL的协议
//网络传输协议 http
http.HandleFunc("/add", func(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm() //解析参数
fmt.Println("path: ", r.URL.Path)
a, _ := strconv.Atoi(r.Form["a"][0])
b, _ := strconv.Atoi(r.Form["b"][0])
//返回的格式化,json{"data":3}

w.Header().Set("Content-Type", "application/json")
jData, _ := json.Marshal(map[string]int{
"data": a + b,
})
_, _ = w.Write(jData)

})

_ = http.ListenAndServe(":8000", nil)

}

客户端
package main

import (
"encoding/json"
"fmt"
"github.com/kirinlabs/HttpRequest"
)
type ResponseData struct {
Data string `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","add",a,b))
body, _ := res.Body()

//fmt.Println(string(body))
rspData := ResponseData{}
_ = json.Unmarshal(body, &rspData)
return rspData.Data
}

func main() {
fmt.Println(Add(2, 2))

}

rpc开发四大要素

客户端,客户端存根,服务端,服务端存根

在这里插入图片描述

快速体验rpc开发

rpc三步走
1.实例化一个server
2.注册处理逻辑handler
3.启动服务

对应实例代码:

服务端代码

package main

import (
"net"
"net/rpc"
)

type HelloService struct{}

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

func main() {
//三步走
//1.实例化一个server
//监听端口
listener, _ := net.Listen("tcp", ":1234")

//2.注册处理逻辑handler
_ = rpc.RegisterName("HelloService", &HelloService{})

//3.启动服务

conn, _ := listener.Accept()
rpc.ServeConn(conn)
}

服务端代码运行无误后
运行客户端代码

package main

import (
"fmt"
"net/rpc"
)

func main() {
//1、建立连接
client, err := rpc.Dial("tcp", "localhost:1234")
if err != nil {
panic("连接失败")
}

var reply *string
err = client.Call("HelloService.Hello", "bobby", reply)

if err != nil {
panic("调用失败")
}

fmt.Println(reply)
//2、

}

打印“调用失败”

debug
在这里插入图片描述

错误是,我们初始化了一个指针变量 var reply *string 初始化的时候的nil的,然后我们client.Call("HelloService.Hello", "bobby", reply)把nil指针放到了服务器上去。
而服务器对没用指向任何地方的指针nil加值,当然是不可以的。

我们可以使用new来初始化, var reply *string = new(string)

在这里插入图片描述
成功

当然也可以直接使用string类型初始化reply,记得加&符

var reply string
err = client.Call("HelloService.Hello", "tc", &reply)

在这里插入图片描述
成功

问题:
在server中,我们监听用到是net.Listen("tcp", ":1234")
接受用的是listener.Accept()
是不是没有rpc也可以呢?

不是的
在rpc中有几个问题需要解决
1.call id
2.序列化和反序列化
这两个工作,其实net是没有完成的,都是我们的rpc完成的

问题又来了,我们使用远程服务器的函数hello时
使用的是client.Call("HelloService.Hello", "tc", &reply)

但是我们实际上想要的是和本地开发一样
直接 client.Hello("tc", &reply)
可以吗?
可以,我们需要自己封装

替换rpc的序列化协议为json

go 语言的rpc的序列化和反序列化协议是什么(Gob)

能否替换成常见的序列化?可以

把之前server中的

rpc.ServeConn(conn)

替换成

rpc.ServeCodec(jsonrpc.NewServerCodec(conn))

server端代码改成

package main

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

type HelloService struct{}

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

func main() {
//三步走
//1.实例化一个server
//监听端口
listener, _ := net.Listen("tcp", ":1234")

//2.注册处理逻辑handler
_ = rpc.RegisterName("HelloService", &HelloService{})

//3.启动服务
for {
conn, _ := listener.Accept()
rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
}
}

直接使用net监听

protobuf和json直观对比

package main

import (
"9_22_helloworld/helloworld/proto"
"encoding/json"
"fmt"
"github.com/golang/protobuf/proto"
)

type Hello struct {
Name string `json:"name"`
}

func main() {
req := helloworld.HelloRequest{
Name: "tc",
}

jsonStruct := Hello{Name: "tc"}
jsonRsp, _ := json.Marshal(jsonStruct)
fmt.Println(string(jsonRsp))
rsp, _ := proto.Marshal(&req)
fmt.Println(string(rsp))

}

在这里插入图片描述
长度比是16:7

使用更大的数据来测试比较
在这里插入图片描述

比较len()
在这里插入图片描述
可以看出差异仍然很大
protobuf的效率比json高出好多

检查完了传输效率,检查是否能正常解析unmarshal()

rsp, _ := proto.Marshal(&req)

newReq := &helloworld.HelloRequest{}
_ = proto.Unmarshal(rsp, newReq)

fmt.Println(newReq.Name, newReq.Age, newReq.Courses)

在这里插入图片描述
没问题

解析
protoc -I . helloworld.proto --go_out=. --go-grpc_out=.

-I 表示(INPUT)
"."表示当前目录之下
helloworld.proto 就是要解析的文件名
“- -go_out” 表示生成go的文件

rpc的四种模式

简单模式(simple rpc)

客户端发起一次请求,服务端响应一个数据,这和大家平时熟悉的RPC没什么太大的区别

服务端数据流模式(server-side streaming rpc)

客户端发起一次请求,服务端返回一段连续的数据流。典型的是客户端向服务端发送一个股票代码,服务端就把该股票的实时数据源源不断的返回给客户端。

订阅模式

客户端数据流模式(client-side streaming rpc)

与服务端相反,客户端源源不断的向服务端发送数据流,在发送结束后,服务端返回一个响应,典型的例子就是,物联网终端向服务器报送数据。

双向数据流模式(bidirectional streaming rpc)

客户端和服务端都可以向对方发送数据,这个时候双方的数据可以同时互相发送,实时交互。典型的例子就是聊天机器人。

使用proto创建项目

syntax = "proto3";

option go_package=".;proto";

service Greeter{
  rpc GetStream(StreamReqData) returns (stream StreamResData);//服务端流模式
  rpc PostStream(stream StreamReqData) returns (StreamResData);// 客户端流模式
  rpc AllStream(stream StreamReqData) returns (stream StreamResData);// 客户端流模式
}


message StreamReqData{
  string data = 1;
}

message StreamResData{
  string data = 1;
}

之后建立服务端

package server

import (
"context"
"google.golang.org/grpc"
"mygogongchengshi/stream_grpc_test/proto"
"net"
)

const PORT = ":50052"

type server struct{}

func (s *server) GetStream(ctx context.Context, request proto.StreamReqData) (*proto.StreamResData, error) {
return nil, nil
}

func main() {
lis, err := net.Listen("tcp", PORT)
if err != nil {
panic(err)
}

s := grpc.NewServer()
proto.RegisterGreeterServer(s, &server{})
}

会发现最后一行报错,
在这里插入图片描述
原因是我们的PostStream和AllStream没有完成

而当我们用同样的方法创建PostStream和AllStream之后
依然报错
在这里插入图片描述

因为参数实际上是错误的

修改后

package server

import (
"google.golang.org/grpc"
"mygogongchengshi/stream_grpc_test/proto"
"net"
)

const PORT = ":50052"

type server struct {
proto.UnimplementedGreeterServer
}

func (s *server) GetStream(req *proto.StreamReqData, res proto.Greeter_GetStreamServer) error {
return nil
}

func (s *server) PostStream(cliStr proto.Greeter_PostStreamServer) error {
return nil
}

func (s *server) AllStream(allStr proto.Greeter_AllStreamServer) error {
return nil
}

func main() {
lis, err := net.Listen("tcp", PORT)
if err != nil {
panic(err)
}

s := grpc.NewServer()
proto.RegisterGreeterServer(s, &server{})
}

注意:
在这里插入图片描述
新版go中需要额外实现一个接口UnimplementedGreeterServer()将他嵌入到结构体中

单向流,双向流代码实现

server总代码

package main

import (
"fmt"
"google.golang.org/grpc"
"mygogongchengshi/stream_grpc_test/proto"
"net"
"sync"
"time"
)

const PORT = ":50052"

type server struct {
proto.UnimplementedGreeterServer
}

func (s *server) GetStream(req *proto.StreamReqData, res proto.Greeter_GetStreamServer) error {
i := 0
for {
_ = res.Send(&proto.StreamResData{
Data: fmt.Sprintf("%v", time.Now().Unix()),
})

i++
time.Sleep(time.Second)

if i > 10 {
break
}
}

return nil
}

func (s *server) PostStream(cliStr proto.Greeter_PostStreamServer) 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 proto.Greeter_AllStreamServer) error {

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 main() {
lis, err := net.Listen("tcp", PORT)
if err != nil {
panic(err)
}

s := grpc.NewServer()
proto.RegisterGreeterServer(s, &server{})

err = s.Serve(lis)
if err != nil {
panic(err)
}
}

首先测试client为服务端数据流
客户端发起一次请求,服务端返回一段连续的数据流。我们就让服务端每个一秒,返回一个时间戳

client代码(服务端流模式)

package main

import (
"context"
"fmt"
"google.golang.org/grpc"
"mygogongchengshi/stream_grpc_test/proto"
)

func main() {
conn, err := grpc.Dial("localhost:50052", grpc.WithInsecure())
if err != nil {
panic(err)
}

defer conn.Close()

c := proto.NewGreeterClient(conn)

res, _ := c.GetStream(context.Background(), &proto.StreamReqData{Data: "mooc"})

for {
a, err := res.Recv() //socket编程 send,recv
if err != nil {

fmt.Println(err)
break

}
fmt.Println(a)
}

}

可以看到结果

在这里插入图片描述
接下来是client–(客户端流模式)

添加:
//客户端流模式
posts, _ := c.PostStream(context.Background())

i := 0
for {
i++
posts.Send(&proto.StreamReqData{
Data: fmt.Sprintf("mooc%d", i),
})

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

在客户端如之前一样输出了10次时间后
在这里插入图片描述
服务端开始输出
在这里插入图片描述
接下来是双向流模式(聊天模式)

package main

import (
"context"
"fmt"
"google.golang.org/grpc"
"mygogongchengshi/stream_grpc_test/proto"
"sync"
"time"
)

func main() {
conn, err := grpc.Dial("localhost:50052", grpc.WithInsecure())
if err != nil {
panic(err)
}

defer conn.Close()

c := proto.NewGreeterClient(conn)

res, _ := c.GetStream(context.Background(), &proto.StreamReqData{Data: "mooc"})

for {
a, err := res.Recv() //socket编程 send,recv
if err != nil {

fmt.Println(err)
break

}
fmt.Println(a)
}

//客户端流模式
posts, _ := c.PostStream(context.Background())

i := 0
for {
i++
posts.Send(&proto.StreamReqData{
Data: fmt.Sprintf("mooc%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.StreamReqData{Data: "你好,我是客户端mooc"})
time.Sleep(time.Second)
}
}()

wg.Wait()

}

在这里插入图片描述
在这里插入图片描述
可以看到,在完成之前的两个模式之后,进入双向流模式
两边同时开始,互发消息
完成


原文地址:https://blog.csdn.net/fcopy/article/details/143659365

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!