【RPC】Golang的rpc体验


前言

上一博文体验了基于python的不同框架的rpc体验
这次用golang体验


一、go快速体验rpc开发

Go语言的RPC包的路径为net/rpc,也就是放在了net包目录下面。因此我们可以猜测该RPC包是建立在net包基础之上的。在上一章“Hello, World”革命一节最后,我们基于http实现了一个打印例子。下面我们尝试基于rpc实现一个类似的例子。

服务端:

package main

import (
	"net"
	"net/rpc"
)

type HelloService struct{}

func (s *HelloService) Hello(request string, reply *string) error {
	//返回值是通过修改reply的值
	*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)

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

其中Hello方法必须满足Go语言的RPC规则:方法只能有两个可序列化的参数,其中第二个参数是指针类型,并且返回一个error类型,同时必须是公开的方法。

然后就可以将HelloService类型的对象注册为一个RPC服务:(TCP RPC服务)。

其中rpc.Register函数调用会将对象类型中所有满足RPC规则的对象方法注册为RPC函数,所有注册的方法会放在“HelloService”服务空间之下。然后我们建立一个唯一的TCP链接,并且通过rpc.ServeConn函数在该TCP链接上为对方提供RPC服务。

客户端:

package main

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

func main() {
	//1. 建立连接
	conn, err := net.Dial("tcp", "localhost:1234")
	if err != nil {
		panic("连接失败")
	}
	var reply string //string有默认值
	client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
	err = client.Call("HelloService.Hello", "bobby", &reply)
	if err != nil {
		panic("调用失败")
	}
	fmt.Println(reply)
}

首先是通过rpc.Dial拨号RPC服务,然后通过client.Call调用具体的RPC方法。在调用client.Call时,第一个参数是用点号链接的RPC服务名字和方法名字,第二和第三个参数分别我们定义RPC方法的两个参数。

二、替换rpc的序列化协议为json

标准库的RPC默认采用Go语言特有的gob编码,因此从其它语言调用Go语言实现的RPC服务将比较困难。在互联网的微服务时代,每个RPC以及服务的使用者都可能采用不同的编程语言,因此跨语言是互联网时代RPC的一个首要条件。得益于RPC的框架设计,Go语言的RPC其实也是很容易实现跨语言支持的。

Go语言的RPC框架有两个比较有特色的设计:一个是RPC数据打包时可以通过插件实现自定义的编码和解码;另 一个是RPC建立在抽象的io.ReadWriteCloser接口之上的,我们可以将RPC架设在不同的通讯协议之上。这里我们将尝试通过官方自带的net/rpc/jsonrpc扩展实现一个跨语言的PPC。

首先是基于json编码重新实现RPC服务:

服务端:

package main

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

type HelloService struct{}

func (s *HelloService) Hello(request string, reply *string) error {
	//返回值是通过修改reply的值
	*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() //当一个新的连接进来的时候,
		go rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
	}
}

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

只要发送{"method":"HelloService.Hello","params":["hello"],"id":0}这种格式的json过去 那这个server端就能够解析

客户端:

package main

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

func main(){
    conn, err := net.Dial("tcp", "localhost:1234")
    if err != nil {
        panic("连接错误")
    }
    client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
    var reply string
    err = client.Call("HelloService.Hello", "imooc", &reply)
    if err != nil {
        panic("调用错误")
    }
    fmt.Println(reply)
}

python调用比较好理解

python调用的客户端:

import json
import socket

# socket编程:
request = {
    "id":0,
    "params":["bobby"],
    "method": "HelloService.Hello"
}

client = socket.create_connection(("localhost", 1234))
client.sendall(json.dumps(request).encode())

#获取服务器返回的数据
rsp = client.recv(1024)
rsp = json.loads(rsp.decode())

print(rsp["result"])

三、替换rpc的传输协议为http

package main

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

type HelloService struct{}

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

func main() {
	//1. 实例化一个server
	_ = rpc.RegisterName("HelloService", &HelloService{})
	// 这个http不是重点  因为有gin等轻量级框架  
	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调用的客户端:

request = {
    "id":0,
    "params":["bobby"],
    "method": "HelloService.Hello"
}

import requests
rsp = requests.post("http://localhost:1234/jsonrpc", json=request)
print(rsp.text)

四、进一步改造rpc调用的代码

前面的rpc调用虽然简单,但是和普通的http的调用差异不大,这次我们解决下面的问题:

1. serviceName统一和名称冲突的问题

  • server端和client端如何统一serviceName
  • 多个server的包中serviceName同名的问题

新建handler/handler.go文件内容如下: 为什么要新建一个文件? - 解耦

package handler

const HelloServiceName = "handler/HelloService"

服务端:


import (
    "net"
    "net/rpc"
    "start/rpc_ch01/handler"
)



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

func main(){
    _ = rpc.RegisterName(handler.HelloServiceName, &HelloService{})
    listener, err := net.Listen("tcp", ":1234")
    if err != nil {
        panic("监听端口失败")
    }
    conn, err := listener.Accept()
    if err != nil {
        panic("建立链接失败")
    }
    rpc.ServeConn(conn)

}

客户端:


import (
    "fmt"
    "net/rpc"
    "start/rpc_ch01/handler"
)

func main() {
    client, err := rpc.Dial("tcp", "localhost:1234")
    if err != nil {
        panic("连接到服务器失败")
    }

    var reply string
    err = client.Call(handler.HelloServiceName+".Hello", "imooc", &reply)
    if err != nil {
        panic("服务调用失败")
    }

    fmt.Println(reply)
}

2. 继续屏蔽HelloServiceName和Hello函数名称

只想写业务逻辑 不想关注每个函数的名称

在这里插入图片描述

handler源码:

package handler

type HelloService struct{}

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

服务端代理:

package server_proxy

import (
	"OldPackageTest/new_helloworld/hanlder"
	"net/rpc"
)

type HelloServicer interface {
	Hello(request string, reply *string) error
}

//如果做到解耦 - 我们关系的是函数 鸭子类型
func RegisterHelloService(srv HelloServicer) error {
	return rpc.RegisterName(hanlder.HelloServiceName, srv)
}

客户端代理:

package client_proxy

import (
	"OldPackageTest/new_helloworld/hanlder"
	"net/rpc"
)

type HelloServiceStub struct {
	*rpc.Client
}

//在go语言中没有类、对象 就意味着没有初始化方法
func NewHelloServiceClient(protcol, address string) HelloServiceStub {
	conn, err := rpc.Dial(protcol, address)
	if err != nil {
		panic("connect error!")
	}
	return HelloServiceStub{conn}
}

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

客户端:

package main

import (
	"OldPackageTest/new_helloworld/client_proxy"
	"fmt"
)

func main() {
	//1. 建立连接
	client := client_proxy.NewHelloServiceClient("tcp", "localhost:1234")
	//1. 只想写业务逻辑 不想关注每个函数的名称
	// 客户端部分
	var reply string //string有默认值
	err := client.Hello("bobby", &reply)
	if err != nil {
		panic("调用失败")
	}
	fmt.Println(reply)

	//1. 这些概念在grpc中都有对应
	//2. 发自灵魂的拷问: server_proxy 和 client_proxy能否自动生成啊 为多种语言生成
	//3. 都能满足 这个就是protobuf + grpc
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jzin

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值