golang RPC入门

1. RPC简介

RPC是远程系统调用的缩写,通俗地讲就是调用远处的一个函数,可能是一个文件内的不同函数,也可能是一个机器上另一个进程的函数,也可能是远处机器上的函数。

RPC是分布式系统中不同节点之间的通信方式,Go的标准库也实现了一个简单的RPC。

 

2. RPC简单使用

首先构造一个HelloService类型,其中的Hello方法用于实现打印功能,Login实现简单的用户验证

其中RPC方法必须满足golang的RPC规则:

  • 方法只能有两个可序列化的参数,其中第二个参数是指针类型
  • 返回一个error
  • 必须是公开的方法,首字母大写
type HelloService struct {
	conn    net.Conn
	isLogin bool
}

// Hello:
func (p *HelloService) Hello(request string, reply *string) error {
	if !p.isLogin {
		return fmt.Errorf("please login")
	}
	*reply = "hello:" + request + ",from" + p.conn.RemoteAddr().String()
	return nil
}

// Login: 提供用户登录验证
func (p *HelloService) Login(request string, reply *string) error {
	if request != "user:password" {
		return fmt.Errorf("auth failed")
	}
	log.Println("login ok")
	p.isLogin = true
	return nil
}

 

然后我们可以将HelloService类型的对象注册为一个RPC服务:

func main() {
	// 开启监听
	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 func() {
			defer conn.Close()
			p := rpc.NewServer()

			// RegisterName调用会将对象类型中所有满足RPC规则的对象方法注册为RPC函数
			// 所有注册的方法会放在HelloService服务空间之下
			p.Register(&HelloService{conn: conn})

			// ServeConn函数在conn这个TCP连接上为对方提供RPC服务
			p.ServeConn(conn)
		}()
	}
}

 

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

func main() {

	// 拨号RPC服务
	client, err := rpc.Dial("tcp", "localhost:1234")
	if err != nil {
		log.Fatal("Dail error:", err)
	}

	// 通过Call()调用RPC的具体方法
	var reply string
	err = client.Call("HelloService.Login", "user:password", &reply)
	if err != nil {
		log.Fatal(err)
	}
	err = client.Call("HelloService.Hello", "client", &reply)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(reply)
}

 

我们在终端开启server和client,看看会发生什么:

go run server.go
go run client.go

##################
hello:client,from[::1]:56769

 

3. 跨语言的RPC

标准库的RPC默认采用go语言特有的Gob编码,因此从其他语言调用Go语言实现的RPC服务比较困难。

go语言的RPC框架有两个比较有特色的设计:一个是RPC数据打包时可以通过插件实现自定义的编码和解码;另一个是RPC建立在抽象的io.ReadWriteCloser接口之上,我们可以将RPC架设在不同的通信协议之上。

我们利用net/rpc/jsonrpc实现一个跨语言的RPC。

func main() {
	rpc.RegisterName("HelloService", new(HelloService))
	listener, err := net.Listen("tcp", ":1234")
	if err != nil {
		log.Fatal(err)
	}

	for {
		conn, err := listener.Accept()
		if err != nil {
			log.Fatal(err)
		}

		// 使用rpc.ServeCodec代替rpc.ServeConn函数
		go rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
	}
}

 

func main() {
	conn, err := net.Dial("tcp", "localhost:1234")
	if err != nil {
		log.Fatal(err)
	}

	client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))

	var reply string
	err = client.Call("HelloService.Hello", "hello", &reply)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(reply)
}

 

无论采用什么样的语言,只要遵循一致的json映射结构,以同样的流程就可以实现和go语言编写的RPC服务进行通信

 

4. HTTP上的RPC服务

我们尝试在HTTP协议上提供jsonrpc服务

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

type HelloService struct{}

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

func main() {
	rpc.RegisterName("HelloService", new(HelloService))
	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)
}

 

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

让我们启动RPC服务,并打开postman,测试我们的RPC服务:

 

可以清楚的看到我们POST请求的body和response信息,都是json格式的,其实这两种格式基本都是固定写法。

因为在内部都是使用类似的结构体来封装的:

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

 

type serverResponse struct {
	Id     *json.RawMessage `json:"id"`
	Result interface{}      `json:"result"`
	Error  interface{}      `json:"error"`
}

 

posted @ 2022-03-20 20:04  aganippe  阅读(257)  评论(0编辑  收藏  举报