文章目录
前言
上一博文体验了基于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
}