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"`
}