zoukankan      html  css  js  c++  java
  • Golang RPC入门

    简介

    RPC是在分布式计算,远程过程调用(英语:Remote Procedure Call,缩写为 RPC)是一个计算机通信协议。在互联网时代,RPC已经和IPC一样成为一个不可或缺的基础构建。RPC是进程之间的通信方式(inter-process communication,IPC)不同的进程有不停的地址空间。

    如果client和server在同一台机器上,尽管物理地址空间是相同的,但是虚拟地址空间不同。

    如果他们在不同的主机上,物理地址空间不停,。RPC的实现的技术各不相同,也一定不兼容。

    一个正常的RPC过程可以分为下面几步:

    1. client调用client stub,这是一个过程调用
    2. client stub将参数打包成一个消息,然后发送这个消息,打包过程叫做marshalling
    3. client所在的系统将消息发送给server
    4. server的系统将收到的包传给server stub
    5. server stub解包得到参数。解包也被称作unmarshalling
    6. 最后server stub调用服务过程,返回结果按照相反的步骤传给client

    RPC只是描绘了 Client 与 Server 之间的点对点调用流程,包括 stub、通信、RPC 消息解析等部分,在实际应用中,还需要考虑服务的高可用、负载均衡等问题,所以产品级的 RPC 框架除了点对点的 RPC 协议的具体实现外,还应包括服务的发现与注销、提供服务的多台 Server 的负载均衡、服务的高可用等更多的功能。目前的 RPC 框架大致有两种不同的侧重方向,一种偏重于服务治理,另一种偏重于跨语言调用。

    服务治理型的 RPC 框架有 Dubbo、DubboX、Motan 等,这类的 RPC 框架的特点是功能丰富,提供高性能的远程调用以及服务发现及治理功能,适用于大型服务的微服务化拆分以及管理,对于特定语言(Java)的项目可以十分友好的透明化接入。但缺点是语言耦合度较高,跨语言支持难度较大。

    跨语言调用型的 RPC 框架有 Thrift、gRPC、Hessian、Hprose 等,这一类的 RPC 框架重点关注于服务的跨语言调用,能够支持大部分的语言进行语言无关的调用,非常适合于为不同语言提供通用远程服务的场景。但这类框架没有服务发现相关机制,实际使用时一般需要代理层进行请求转发和负载均衡策略控制。

    下面是一个基于HTTP的 JSON的 RPC:

    HelloWorld

    GO语言标准库net/rpc也提供了一个简单的RPC实现

    分别建立两个项目client、server

    server.go

    package main
    
    import (
    	"fmt"
    	"net"
    	"net/rpc"
    )
    
    type HelloService struct {}
    
    // Hello的逻辑 就是将对方发送的消息前面添加一个Hello 然后返还给对方
    // 由于是一个rpc服务, 因此参数上面还是有约束:
    // 		第一个参数是请求
    // 		第二个参数是响应
    // 可以类比Http handler
    func (p *HelloService) Hello(request string, reply *string) error {
    	*reply = "hello:" + request
    	return nil
    }
    
    func main() {
    	// 把对象注册成一个rpc的 receiver
    	// 其中rpc.Register函数调用会将对象类型中所有满足RPC规则的对象方法注册为RPC函数,
    	// 所有注册的方法会放在“HelloService”服务空间之下
    	rpc.RegisterName("HelloService", new(HelloService))
    
    	// 然后建立一个唯一的TCP链接,
    	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)
    		}
    
    		// 前面都是Tcp的知识, 到这个RPC就接管了,因此 你可以认为 rpc 封装消息到函数调用的这个逻辑,
    		// 提升了工作效率, 逻辑比较简洁
    		go rpc.ServeConn(conn)
    	}
    }
    

    client.go

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

    测试:

    启动服务端:

    go run server.go
    

    启动客户端:

    go run client.go
    
    >>>>>>>>>>>>>>>>>
    输出结果:
    hello wrold!
    

    rpc服务最多的优点就是可以像使用本地函数一样使用 远程服务上的函数, 因此有几个关键点:

    • 远程连接: 类似于pkg
    • 函数名称: 要表用的函数名称
    • 函数参数: 这个需要符合RPC服务的调用签名, 及第一个参数是请求,第二个参数是响应
    • 函数返回: rpc函数的返回是 连接异常信息, 真正的业务Response不能作为返回值

    基于接口的RPC服务

    // Call invokes the named function, waits for it to complete, and returns its error status.
    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
    }
    

    上面是client call 方法, 里面3个参数2个interface{}, 你再使用的时候 可能真不知道要传入什么, 这就好像你写了一个HTTP的服务, 没有接口文档, 容易调用错误

    为了避免这种情况, 可以对客户端进行一次封装, 使用接口当作文档, 明确参数类型

    定义hello service的接口

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

    约束服务端:

    // 通过接口约束HelloService服务
    var _ service.HelloService = (*HelloService)(nil)
    

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

    // 约束客户端
    var _ service.HelloService = (*HelloServiceClient)(nil)
    
    type HelloServiceClient struct {
    	*rpc.Client
    }
    
    func DialHelloService(network, address string) (*HelloServiceClient, error) {
    	c, err := rpc.Dial(network, address)
    	if err != nil {
    		return nil, err
    	}
    	return &HelloServiceClient{Client: c}, nil
    }
    
    func (p *HelloServiceClient) Hello(request string, reply *string) error {
    	return p.Client.Call(service.HelloServiceName+".Hello", request, reply)
    }
    

    基于接口约束后的客户端使用就要容易很多了

    func main() {
    	client, err := DialHelloService("tcp", "localhost:1234")
    	if err != nil {
    		log.Fatal("dialing:", err)
    	}
    
    	var reply string
    	err = client.Hello("hello", &reply)
    	if err != nil {
    		log.Fatal(err)
    	}
    	fmt.Println(reply)
    }
    

    Gob编码

    标准库的RPC默认采用Go语言特有的gob编码, 标准库gob是golang提供的“私有”的编解码方式,它的效率会比json,xml等更高,特别适合在Go语言程序间传递数据

    // ServeConn runs the server on a single connection.
    // ServeConn blocks, serving the connection until the client hangs up.
    // The caller typically invokes ServeConn in a go statement.
    // ServeConn uses the gob wire format (see package gob) on the
    // connection. To use an alternate codec, use ServeCodec.
    // See NewClient's comment for information about concurrent access.
    func (server *Server) ServeConn(conn io.ReadWriteCloser) {
    	buf := bufio.NewWriter(conn)
    	srv := &gobServerCodec{
    		rwc:    conn,
    		dec:    gob.NewDecoder(conn),
    		enc:    gob.NewEncoder(buf),
    		encBuf: buf,
    	}
    	server.ServeCodec(srv)
    }
    
    

    gob的使用很简单, 和使用base64编码理念一样, 有 Encoder和Decoder

    func GobEncode(val interface{}) ([]byte, error) {
    	buf := bytes.NewBuffer([]byte{})
    	encoder := gob.NewEncoder(buf)
    	if err := encoder.Encode(val); err != nil {
    		return []byte{}, err
    	}
    	return buf.Bytes(), nil
    }
    
    func GobDecode(data []byte, value interface{}) error {
    	reader := bytes.NewReader(data)
    	decoder := gob.NewDecoder(reader)
    	return decoder.Decode(value)
    }
    

    测试用例:

    func TestGobCode(t *testing.T) {
    	t1 := &TestStruct{"name", "value"}
    	resp, err := service.GobEncode(t1)
    	fmt.Println(resp, err)
    
    	t2 := &TestStruct{}
    	service.GobDecode(resp, t2)
    	fmt.Println(t2, err)
    }
    

    Json ON TCP

    gob是golang提供的“私有”的编解码方式,因此从其它语言调用Go语言实现的RPC服务将比较困难

    因此可以选用所有语言都支持的比较好的一些编码:

    • MessagePack: 高效的二进制序列化格式。它允许在多种语言(如JSON)之间交换数据。但它更快更小
    • JSON: 文本编码
    • XML:文本编码
    • Protobuf 二进制编码

    Go语言的RPC框架有两个比较有特色的设计:

    • RPC数据打包时可以通过插件实现自定义的编码和解码;
    • RPC建立在抽象的io.ReadWriteCloser接口之上的,可以将RPC架设在不同的通讯协议之上。

    尝试通过官方自带的net/rpc/jsonrpc扩展实现一个跨语言的RPC。

    server

    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)
            }
    
    		// 代码中最大的变化是用rpc.ServeCodec函数替代了rpc.ServeConn函数,
    		// 传入的参数是针对服务端的json编解码器
            go rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
        }
    }
    

    客户端

    func DialHelloService(network, address string) (*HelloServiceClient, error) {
    
    	// 建立链接
    	conn, err := net.Dial("tcp", "localhost:1234")
    	if err != nil {
    		log.Fatal("net.Dial:", err)
    	}
    
    	// 采用Json编解码的客户端
    	c := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
    	return &HelloServiceClient{Client: c}, nil
    }
    

    验证功能是否正常,由于没有合适的tcp工具, 比如nc, 可以下来自己验证

    echo -e '{"method":"HelloService.Hello","params":["hello"],"id":1}' | nc localhost 1234
    {"id":1,"result":"hello:hello","error":null}
    

    Json ON HTTP

    Go语言内在的RPC框架已经支持在Http协议上提供RPC服务, 为了支持跨语言,编码依然使用Json

    新的RPC服务其实是一个类似REST规范的接口,接收请求并采用相应处理流程

    首先依然要解决JSON编解码的问题,需要将HTTP接口的Handler参数传递给jsonrpc, 因此需要满足jsonrpc接口, 因此需要提前构建也给conn io.ReadWriteCloser, writer现成的 reader就是request的body, 直接内嵌就可以

    func NewRPCReadWriteCloserFromHTTP(w http.ResponseWriter, r *http.Request) *RPCReadWriteCloser {
    	return &RPCReadWriteCloser{w, r.Body}
    }
    
    type RPCReadWriteCloser struct {
    	io.Writer
    	io.ReadCloser
    }
    

    服务端:

    func main() {
    	rpc.RegisterName("HelloService", new(HelloService))
    
    	// RPC的服务架设在“/jsonrpc”路径,
    	// 在处理函数中基于http.ResponseWriter和http.Request类型的参数构造一个io.ReadWriteCloser类型的conn通道。
    	// 然后基于conn构建针对服务端的json编码解码器。
    	// 最后通过rpc.ServeRequest函数为每次请求处理一次RPC方法调用
    	http.HandleFunc("/jsonrpc", func(w http.ResponseWriter, r *http.Request) {
    		conn := NewRPCReadWriteCloserFromHTTP(w, r)
    		rpc.ServeRequest(jsonrpc.NewServerCodec(conn))
    	})
    
    	http.ListenAndServe(":1234", nil)
    }
    

    这种用法常见于你的rpc服务需要暴露多种协议的时候, 其他时候还是老老实实写Restful API

    参考:什么是RPC

  • 相关阅读:
    Java之泛型练习
    集合框架-Map练习-记录字母出现的次数
    集合框架Map之entrySet方法的使用
    集合框架Map之KeySet方法的使用
    Java集合框架之LinkedList-----用LinkedList模拟队列和堆栈
    springboot2.0+mycat实验读写分离
    mysql主从复制
    mycat读写分离
    kafka初探
    redis-List类型
  • 原文地址:https://www.cnblogs.com/remixnameless/p/15636748.html
Copyright © 2011-2022 走看看