Go微服务(一)——RPC详细入门

本文涉及的产品
注册配置 MSE Nacos/ZooKeeper,118元/月
云原生网关 MSE Higress,422元/月
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介: Go微服务(一)——RPC详细入门

篇幅可能较长,可以先收藏,方便后续观看。

文章名称 地址
Go微服务(一)——RPC详细入门 前往
Go微服务(二)——Protobuf详细入门 前往
Go微服务(三)——gRPC详细入门 前往

RPC⼊⻔

RPC代指 远程过程调用(Remote Procedure Call)

RPC是远程过程调⽤的简称,是分布式系统中不同节点间流⾏的通信⽅式。在互联⽹时代,RPC已经 和 IPC⼀样成为⼀个不可或缺的基础构件。因此Go语⾔的标准库也提供了⼀个简单的RPC实现,我们 将以此为⼊⼝学习RPC的各种⽤法。

1. RPC版"Hello, World"

Go语⾔的RPC包的路径为net/rpc,也就是放在了net包⽬录下⾯。因此我们可以猜测该RPC包是建⽴ 在net包基础之上的。在第⼀章“Hello, World”⾰命⼀节最后,我们基于http实现了⼀个打印例⼦。下⾯ 我们尝试基于rpc实现⼀个类似的例⼦。

server/main.go

type HelloService struct {}
// Hello的逻辑就是将对方发送的消息前面添加一个Hello然后返还给对方
// 由于我们是一个rpc服务,因此参数上面还是有约束:
// 第一个参数是请求
// 第二个参数是响应
// 可以类比Http handler
func (p *HelloService) Hello(request string, reply *string) error {
    *reply = "hello:" + request
    return nil
}

其中Hello⽅法必须满⾜Go语⾔的RPC规则:

⽅法只能有两个可序列化的参数,其中第⼆个参数是指针 类型,并且返回⼀个error类型,同时必须是公开的⽅法。

然后就可以将HelloService类型的对象注册为⼀个RPC服务:

func main() {
  // 把我们的对象注册成一个 rpc 的 receiver
  // 其中rpo Register函数调用会将对象类型中所有满足RPC规则的对象方法注册为RPC函数,
  // 所有注册的方法会放在"HelloService"服务空间之下
  rpc.RegisterName("HelloService", new(service.HelloService))
  // 然后我们建立一个唯一的TCP链接,监听1234端口
  listener, err := net.Listen("tcp", ":1234")
  if err != nil {
    log.Fatal("ListenTCP error:", err)
  }
  // 通过 rpc.ServeConn函数在该TCP链接上为对⽅提供RPC服务。
  // 每Accept一个请求,就创建一个 goroutie 进行处理
  for{
    // 从监听里获取一个连接
    conn, err := listener.Accept()
    if err != nil {
      log.Fatal("Accept error:", err)
    }
    // 将获取的连接交给RPC
    // 前面都是tcp的知识,到这个RPC就接管了
    // 因此,你可以认为 rpc 帮我们封装消息到函数调用的这个逻辑,
    // 提升了工作效率,逻辑比较简洁,可以看看他代码
    go rpc.ServeConn(conn)
  }
}

client/main.go

下⾯是客户端请求HelloService服务的代码:

func main() {
  // ⾸先是通过rpc.Dial拨号RPC服务,建立连接
  client, err := rpc.Dial("tcp", "localhost:1234")
  if err != nil {
    log.Fatal("dialing:", err)
  }
  var reply string
  // 然后通过client.Call调⽤具体的RPC⽅法
    // 在调⽤client.Call时:
    //    第⼀个参数是⽤点号链接的RPC服务名字和⽅法名字,
  //    第⼆和第三个参数分别我们定义RPC⽅法的两个参数:
    //      第二个参数:请求参数
    //      第三个参数:请求响应,必须是一个指针,由底层的rpc服务帮你赋值
  err = client.Call("HelloService.Hello", "hello", &reply)
  if err != nil {
    log.Fatal(err)
  }
  fmt.Println(reply)
}

由这个例⼦可以看出RPC的使⽤其实⾮常简单。

RPC的优点:

可以像使用本地函数一样使用远程服务

  • 简单
  • 高效

2. 更安全的RPC接⼝

在涉及RPC的应⽤中,作为开发⼈员⼀般⾄少有三种⻆⾊:⾸选是服务端实现RPC⽅法的开发⼈员, 其次是客户端调⽤RPC⽅法的⼈员,最后也是最重要的是制定服务端和客户端RPC接⼝规范的设计⼈ 员。

在前⾯的例⼦中我们为了简化将以上⼏种⻆⾊的⼯作全部放到了⼀起,虽然看似实现简单,但是 不利于后期的维护和⼯作的切割。

上面的RPC有一个显著的缺陷,就是我们可以看到Call的方法是这样一个结构:

// Call 调用指定函数,等待其完成,并返回其错误状态。
func (client *Client) Call(serviceMethod string, args interface{}, reply interface{}) error {
  call := <-client.Go(serviceMethod, args, reply, make(chan *Call, 1)).Done
  return call.Error
}

至于这个serviceMethod ,你作为一个RPC-Service服务的提供端,你可能要告诉调用者,这个服务的名字叫SeriviceA,方法叫Hello,如果他不知道这个信息,他就完全不知道怎么调用,你可能还需要根据这个信息写一个文档,这是不是又回到了之前的RESTful的矛盾点上了,又要写一大堆说明,又要写一大堆代码。

然后是这个请求的参数args它是一个interface,意味着什么都可以传,调用者也不知道你要他传的到底是什么,是一个string,还是一个int,还是一个你自定义的struct,调用者无从知晓。

因此我们如果直接使用底层的这一套RPC的方式,那么对调用者是非常不友好的,那么怎么办呢?

我们可以选择去包装一下我们的RPC,让他看起来更加友好。有没有什么方式或者方法,让它变得更加规范?

我们的解决办法是,为它定义一套接口:

定义接口

service/interface.go

定义Hello Service 的接口:

package service
const HelloServiceName = "HelloService"
type HelloService interface {
  Hello(request string,reply *string) error
}

相当于一种约束。

约束服务端和客户端

server/main.go

约束服务端:

type HelloServer struct{}
func (p *HelloServer) Hello(request string, reply *string) error {
  *reply = "hello:" + request
  return nil
}
// 通过接口约束HelloService服务
var _ service.HelloService = (*HelloServer)(nil)
func main() {
  rpc.RegisterName(service.HelloServiceName, new(HelloServer))
  listener, err := net.Listen("tcp", ":1234")
  if err != nil {
    log.Fatal("ListenTCP error:", err)
  }
  for {
    conn, err := listener.Accept()
    if err != nil {
      log.Fatal("Accept error:", err)
    }
    go rpc.ServeConn(conn)
  }
}

这行代码的意思是,声明一个service.HelloService类型的变量,声明后这个变量的实体,抛弃,不用内存来存,只是做一下静态检查,让编译器来帮我们把一些错误屏蔽掉,那么为什么我们要采用静态检查呢,他的核心点在(*HelloService)(nil),

如此的话,如果你在编写方法的时候,如果不满足实现接口的要求就会报错,从而必须按照规范实现接口。

client/main.go

约束客户端:

封装客户端,让其满足HelloService接口约束

type HelloServiceClient struct {
  *rpc.Client
}
func (p *HelloServiceClient) Hello(request string, reply *string) error {
  return p.Client.Call(service.HelloServiceName+".Hello", request, reply)
}
// 静态检查,同上面一样
var _ service.HelloService = (*HelloServiceClient)(nil)
// 通过rpc.Dial拨号RPC服务,建立连接,并将获取连接后的客户端返回
func DialHelloService(network, address string) (*HelloServiceClient, error) {
  client, err := rpc.Dial(network, address)
  if err != nil {
    return nil, err
  }
  return &HelloServiceClient{client}, nil
}

基于约束 后的客户端,使用起来就容易多了:

func main() {
  client, err := DialHelloService("tcp", "localhost:1234")
  if err != nil {
    log.Fatalln("dialing:", err)
  }
  var reply string
    // 在使用goland的时候就会提示
  err = client.Hello("hello", &reply)
  fmt.Println(reply)
}

现在客户端⽤户不⽤再担⼼RPC⽅法名字或参数类型不匹配等低级错误的发⽣。

3. 跨语⾔的RPC

标准库的RPC默认采⽤Go语⾔特有的gob编码,因此从其它语⾔调⽤Go语⾔实现的RPC服务将⽐较困难。

相比较与通用的JSON编码,每个语言都认识它,他就是一个跨语言的编码,但是gob不是跨语言的编码,所以我们要选择一个合适的编码。

常见的编解码:

MessagePack:高效的二进制序列化格式

JSON:文本编码(即肉眼可以看懂的)

XML:文本编码

ProtoBuf:二进制编码(即肉眼看不懂,需要按照他的规范去解码,才可以看懂)

在互联⽹的微服务时代,每个RPC以及服务的使⽤者都可能采⽤不同的编程语⾔,因此跨语⾔是 互联⽹时代RPC的⼀个⾸要条件。得益于RPC的框架设计,Go语⾔的RPC其实也是很容易实现跨语⾔⽀持的。

Go语⾔的RPC框架有两个⽐较有特⾊的设计:⼀个是RPC数据打包时可以通过插件实现⾃定义的编码 和解码;另⼀个是RPC建⽴在抽象的io.ReadWriteCloser接⼝之上的,我们可以将RPC架设在不同的通讯协议之上。

这⾥我们将尝试通过官⽅⾃带的net/rpc/jsonrpc扩展(JSON)实现⼀个跨语⾔的PPC。

JSON ON TCP

server/main.go

服务端:

...
func main() {
    rpc.RegisterName("HelloService", new(HelloService))
    listener, err := net.Listen("tcp", ":1234")
    if err != nil {
        log.Fatal("ListenTCP error:", err)
    }
    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Fatal("Accept error:", err)
        }
        // NewServerCodec: 在 conn 上使用 JSON-RPC 返回一个新的 rpc.ServerCodec
        // ServeCodec: ServeCodec 类似于 ServeConn,但使用指定的编解码器解码请求和编码响应。
        // Codec: 编解码器
        go rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
    }
}

代码中最⼤的变化是⽤rpc.ServeCodec函数替代了rpc.ServeConn函数,传⼊的参数是针对服务端的 json编解码器。

client/main.go

客户端:

func main() {
    // 建立链接
  conn, err := net.Dial("tcp", "localhost:1234")
  if err != nil {
    log.Fatal("net.Dial:", err)
  }
    // 基于该链接建立针对客户端的JSON编解码器
  client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
  var reply string
  err = client.Call("HelloService.Hello", "world", &reply)
  if err != nil {
    log.Fatal(err)
  }
  fmt.Println(reply)
}

先⼿⼯调⽤net.Dial函数建⽴TCP链接,然后基于该链接建⽴针对客户端的json编解码器。

在确保客户端可以正常调⽤RPC服务的⽅法之后,我们⽤⼀个普通的TCP服务代替Go语⾔版本的RPC 服务,这样可以查看客户端调⽤时发送的数据格式。

因此⽆论采⽤何种语⾔,只要遵循同样的json结构,以同样的流程就可以和Go语⾔编写的RPC服务进 ⾏通信。这样我们就实现了跨语⾔的RPC。

JSON ON HTTP

Go语⾔内在的RPC框架已经⽀持在Http协议上提供RPC服务。但是框架的http服务同样采⽤了内置的 gob协议,并且没有提供采⽤其它协议的接⼝,因此从其它语⾔依然⽆法访问的。

在前⾯的例⼦中,我 们已经实现了在TCP协议之上运⾏jsonrpc服务,并且通过nc命令⾏⼯具成功实现了RPC⽅法调⽤。现在我们尝试在http协议上提供jsonrpc服务。

新的RPC服务其实是⼀个类似REST规范的接⼝,接收请求并采⽤相应处理流程:

server/main.go

服务端

type HelloService struct{}
func (p *HelloService) Hello(request string, reply *string) error {..}
// 内嵌了io.Writer,io.ReaderCloser
// 即实现了这俩接口的所有方法,也即实现了io.ReadWriterCloser接口
type RPCReadWriterCloser struct {
  io.Writer     // Writer 是现成的w
  io.ReadCloser // ReadCloser就是r的Body
}
func NewRPCReadWriterCloserFromHTTP(w http.ResponseWriter, r *http.Request) *RPCReadWriterCloser {
  return &RPCReadWriterCloser{w, r.Body}
}
func main() {
  rpc.RegisterName("HelloService", new(HelloService))
  http.HandleFunc("/jsonrpc", func(w http.ResponseWriter, r *http.Request) {
    var conn io.ReadWriteCloser = NewRPCReadWriterCloserFromHTTP(w, r)
    rpc.ServeRequest(jsonrpc.NewServerCodec(conn))
  })
  http.ListenAndServe(":1234", nil)
}

RPC的服务架设在“/jsonrpc”路径,在处理函数中基于http.ResponseWriter和http.Request类型的参数 构造⼀个io.ReadWriteCloser类型的conn通道。然后基于conn构建针对服务端的json编码解码器。最后通过rpc.ServeRequest函数为每次请求处理⼀次RPC⽅法调⽤。

运行之后,我们使用postman进行测试向该链接发送⼀个json字符串:

GET: http://localhost:1234/jsonrpc
{
    "method":"HelloService.Hello",
    "params":[
        "hyy"
    ],
    "id":0
}

返回的结果依然是json字符串:

{
    "id": 0,
    "result": "hello:hyy",
    "error": null
}

这样就可以很⽅便地从不同语⾔中访问RPC服务了。

当你后端写了很多接口,但又不想写RESTful风格那样的API,去不断重复的写很多接口的时候,你可以将你所有的功能都封装到一个

案例:

通过jsonrpc 实现简单的go-web http业务

model/user.go

type User struct {
  UserName string `json:"user_name"`
  Password string `json:"password"`
}
func (p *User)Post(user User,reply *bool) error {
  fmt.Println(user)
  *reply = true
  return nil
}
func (p *User)Delete(UID int,reply *bool) error {
  fmt.Println(UID)
  *reply = true
  return nil
}
func (p *User)Get(UID int,reply *User) error {
  fmt.Println(UID)
  *reply = User{UserName: "111",Password: "222"}
  return nil
}
func (p *User)Update(user User,reply *bool) error {
  fmt.Println(user)
  *reply = true
  return nil
}

server/main.go

type RPCReadWriterCloser struct {
  io.Writer
  io.ReadCloser
}
func NewRPCReadWriterCloserFromHTTP(w http.ResponseWriter, r *http.Request) *RPCReadWriterCloser {
  return &RPCReadWriterCloser{Writer: w, ReadCloser: r.Body}
}
func main() {
  rpc.RegisterName("user", new(models.User))
  http.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) {
    var conn io.ReadWriteCloser = NewRPCReadWriterCloserFromHTTP(w, r)
    rpc.ServeRequest(jsonrpc.NewServerCodec(conn))
  })
  http.ListenAndServe(":8000", nil)
}

postman测试:

GET: http://localhost:8000/user
{
    "method":"user.Get",
    "params":[
        1
    ],
    "id":0
}
{
    "method":"user.Post",
    "params":[
        {"user_name":"111","password":"222"}
    ],
    "id":0
}
{
    "method":"user.Delete",
    "params":[
        1
    ],
    "id":0
}
{
    "method":"user.Update",
    "params":[
        {"user_name":"111","password":"222"}
    ],
    "id":0
}
相关文章
|
29天前
|
JavaScript Java Go
探索Go语言在微服务架构中的优势
在微服务架构的浪潮中,Go语言以其简洁、高效和并发处理能力脱颖而出。本文将深入探讨Go语言在构建微服务时的性能优势,包括其在内存管理、网络编程、并发模型以及工具链支持方面的特点。通过对比其他流行语言,我们将揭示Go语言如何成为微服务架构中的一股清流。
118 53
|
12天前
|
Kubernetes Cloud Native 开发者
云原生入门:从容器到微服务
本文将带你走进云原生的世界,从容器技术开始,逐步深入到微服务架构。我们将通过实际代码示例,展示如何利用云原生技术构建和部署应用。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的信息和启示。
|
26天前
|
监控 Go API
Go语言在微服务架构中的应用实践
在微服务架构的浪潮中,Go语言以其简洁、高效和并发处理能力脱颖而出,成为构建微服务的理想选择。本文将探讨Go语言在微服务架构中的应用实践,包括Go语言的特性如何适应微服务架构的需求,以及在实际开发中如何利用Go语言的特性来提高服务的性能和可维护性。我们将通过一个具体的案例分析,展示Go语言在微服务开发中的优势,并讨论在实际应用中可能遇到的挑战和解决方案。
|
27天前
|
Go 数据处理 API
Go语言在微服务架构中的应用与优势
本文摘要采用问答形式,以期提供更直接的信息获取方式。 Q1: 为什么选择Go语言进行微服务开发? A1: Go语言的并发模型、简洁的语法和高效的编译速度使其成为微服务架构的理想选择。 Q2: Go语言在微服务架构中有哪些优势? A2: 主要优势包括高性能、高并发处理能力、简洁的代码和强大的标准库。 Q3: 文章将如何展示Go语言在微服务中的应用? A3: 通过对比其他语言和展示Go语言在实际项目中的应用案例,来说明其在微服务架构中的优势。
|
28天前
|
存储 设计模式 安全
Go语言中的并发编程:从入门到精通###
本文深入探讨了Go语言中并发编程的核心概念与实践技巧,旨在帮助读者从理论到实战全面掌握Go的并发机制。不同于传统的技术文章摘要,本部分将通过一系列生动的案例和代码示例,直观展示Go语言如何优雅地处理并发任务,提升程序性能与响应速度。无论你是Go语言初学者还是有一定经验的开发者,都能在本文中找到实用的知识与灵感。 ###
|
1月前
|
Serverless Go
Go语言中的并发编程:从入门到精通
本文将深入探讨Go语言中并发编程的核心概念和实践,包括goroutine、channel以及sync包等。通过实例演示如何利用这些工具实现高效的并发处理,同时避免常见的陷阱和错误。
|
1月前
|
Cloud Native 持续交付 云计算
云原生入门指南:从容器到微服务
【10月更文挑战第28天】在数字化转型的浪潮中,云原生技术成为推动现代软件开发的关键力量。本篇文章将带你了解云原生的基本概念,探索它如何通过容器化、微服务架构以及持续集成和持续部署(CI/CD)的实践来提升应用的可伸缩性、灵活性和可靠性。你将学习到如何利用这些技术构建和部署在云端高效运行的应用,并理解它们对DevOps文化的贡献。
52 2
|
1月前
|
Kubernetes 关系型数据库 MySQL
Kubernetes入门:搭建高可用微服务架构
【10月更文挑战第25天】在快速发展的云计算时代,微服务架构因其灵活性和可扩展性备受青睐。本文通过一个案例分析,展示了如何使用Kubernetes将传统Java Web应用迁移到Kubernetes平台并改造成微服务架构。通过定义Kubernetes服务、创建MySQL的Deployment/RC、改造Web应用以及部署Web应用,最终实现了高可用的微服务架构。Kubernetes不仅提供了服务发现和负载均衡的能力,还通过各种资源管理工具,提升了系统的可扩展性和容错性。
98 3
|
2月前
|
Cloud Native Go API
Go语言在微服务架构中的创新应用与实践
本文深入探讨了Go语言在构建高效、可扩展的微服务架构中的应用。Go语言以其轻量级协程(goroutine)和强大的并发处理能力,成为微服务开发的首选语言之一。通过实际案例分析,本文展示了如何利用Go语言的特性优化微服务的设计与实现,提高系统的响应速度和稳定性。文章还讨论了Go语言在微服务生态中的角色,以及面临的挑战和未来发展趋势。
|
2月前
|
安全 Go 开发者
破译Go语言中的并发模式:从入门到精通
在这篇技术性文章中,我们将跳过常规的摘要模式,直接带你进入Go语言的并发世界。你将不会看到枯燥的介绍,而是一段代码的旅程,从Go的并发基础构建块(goroutine和channel)开始,到高级模式的实践应用,我们共同探索如何高效地使用Go来处理并发任务。准备好,让Go带你飞。