RPC和Protobuf(二)

 

跨语言的RPC

  标准库的RPC默认采用Go语言特有的Gob编码,因此从其他语言调用Go语言实现RPC服务将比较困难,在互联网的为服务时代,每个RPC以及服务的使用都可能采用不同的编码语言,因此跨语言是互联网时代RPC的一个首要

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

service.go代码如下:

package main

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

// 定义服务结构体
type HelloService struct {
}

// 定义服务空间名,之后要与客户端一致绑定
const HelloServiceName = "grpc.Server"

// 服务端接口要求方法实现
type HelloServiceInterface = interface {
	Hellos(request string, reply *string) error
}

// 将结构体注册到rpc的命名空间下,注意这里的命名空间可以是一个路径片段,隐式约束实现接口中的方法后才能注册
func RegisterHelloService(svc HelloServiceInterface) error {
	// 将注册一这个名字为空间的,可以通过映射到这个结构体下,在客户端通过.调用对应的方法
	return rpc.RegisterName(HelloServiceName, svc)
}

// 服务端实现方法,注意这个函数名要和client端的.名字保持一致
func (p *HelloService) Hellos(request string, reply *string) error {
	*reply = "hello :" + request
	return nil
}

func main() {
	// 将结构体注册到rpc中,前提式实现了方法
	RegisterHelloService(new(HelloService))
	listener, err := net.Listen("tcp", ":1234")
	if err != nil {
		log.Fatal("ListenTCP error: ", err)
	}

	for {
		fmt.Println("listenning...")
		// 监听服务
		conn, err := listener.Accept()
		if err != nil {
			log.Fatal("Accept error:", err)
		}
		// 将监听到的服务挂在在rpc上,这不过这里使用rpc.ServeCodec(),传入参数是针对服务端的JSON编解码器
		go rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
	}
}  

client.go代码如下:

package main

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

// 自定义命名空间,与服务注册的名字一致
const HelloServiceName = "grpc.Server"

// 使用一个接口来约束注册的结构体
type HelloServiceInterface = interface {
	Hello (request string, reply *string) error
}

// 将结构体注册到rpc的命名空间下,注意这里的命名空间可以是一个路径片段
func RegisterHelloService(svc HelloServiceInterface) error {
	return rpc.RegisterName(HelloServiceName, svc)
}


func main() {
	// 首先通过net建立tcp拨号,而不是rpc创建客户端了
	conn, err := net.Dial("tcp","localhost:1234")
	if err != nil {
		log.Fatal("dialing: ",err)
	}

	// 区别于之前,这里基于tcp连接,建立JSON的编解码器
	client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))

	var reply
	// 通过client.Call()时,第一个参数是用点号连接的RPC服务名字和方法名字,第二个和第三个参数分别是定义rpc方法的两个参数
	err = client.Call(HelloServiceName+".Hellos","hello",&reply)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(reply)
}

有了以上代码,我们可以测试哈,分别运行服务端和客户端代码,可以看到相应的结果。那和之前有什么差别?我们停止服务端的运行,运行一个监听tcp端口的命令: nc -l 1234 ;之后我们再次调用客户端时会出现如下结果:

{"method":"grpc.Server.Hellos","params":["hello"],"id":0}。 这里我们可以看到客户端通过json数据传输给服务端(也就是说只要能发送json就可以调用服务端的函数了),这里需要说明的是Json数据内部对应了两个结构体:客户端是clientRequest、服务端是serverRequest:

type  clientRequest struct {
    Method string   `json:"method"`
    Params [1]interface `json:"params"`
    Id  uint64 `json:"id"`
}

// 服务端类似

在获取到RPC调用的Json数据后,我们来核实下service是否可以根据json数据返回相应的结果,启动服务端,在终端中数据:echo -e '{"method":"grpc.Server.Hellos","params":["hello"],"id":0}' | nc localhost 1234, 返回的结果是{"id":0,"result":"hello :hello","error":null}。说明可以通过json来实现跨语言的交流了,这里忽略了服务端的返回结构体。

 


之前的版本使用的是Gob编码,并比支持http协议,并没有完全实现跨语言调用,下面的版本可以实现:

package main

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

// 创建一个函数的承载体
type HelloServer struct {
}

// 定义服务函数
// rpc规则: 必须含有两个参数,一个请求、一个相应,返回值为error类型,且方法名必须大写
func (p *HelloServer) Hello(request string, reply *string) error {
	*reply = "hello" + request
	return nil
}

func main() {
	// 将承载体的所有满足rpc规则的方法注册到HelloServer服务空间中
	rpc.RegisterName("HelloServer", new(HelloServer))

	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)
}

在终端数据如下命令: curl localhost:1234/jsonrpc -X POST --data '{"method":"HelloServer.Hello","params":["hello"],"id":0}' 就可以完成rpc调用

 

posted @ 2020-04-19 22:20  独角兕大王  阅读(383)  评论(0编辑  收藏  举报