9

架构师小飞传第三弹 - Golang的原生RPC与Node.js通讯

 3 years ago
source link: https://studygolang.com/articles/28700
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

背景

I7viYnn.jpg!web

621589303026_.pic_hd.jpg

小飞休息的时候也喜欢搞一些自己的小项目。花了几天的时间,小飞搞出了一套系统门户网站。其中包括广告管理,新闻管理,用户中心,统一登录,图片PaaS服务。

2aEVfmv.png!web

image.png

HTTP + RPC

小飞觉得,服务间通讯,Web和服务之间通讯都使用HTTP协议,未免觉得有点LOW。于是乎小飞决定WEB和服务间用HTTP通讯,而服务之间用Go的RPC协议通讯。这样写又面临着一个问题,一个服务要监听2个端口,一个是HTTP端口,另一个是RPC的端口。这让小飞无比的恼火。小飞找了一遍Go的Document,RPC包有这么几个方法吸引了小飞。HandleHTTP,DialHTTP。这说明什么,证明Golang的RPC包提供以HTTP协议为载体的方法。这样的话,就可以HTTP服务和RPC服务共用同一个接口了。看了下Golang RPC的代码是这样写的。

arith := new(Arith)
rpc.Register(arith)
rpc.HandleHTTP()
l, e := net.Listen("tcp", ":1234")
if e != nil {
    log.Fatal("listen error:", e)
}
go http.Serve(l, nil)

稍加改变,变成这样。

arith := new(Arith)
rpc.Register(arith)
rpc.HandleHTTP()
router := httprouter.New()
router.GET("/", GetIndex)
l, e := net.Listen("tcp", ":1234")
if e != nil {
    log.Fatal("listen error:", e)
}
go http.Serve(l, router)

用Postman测了一下,通了!完美解决。不过发现HTTP协议是通了,不过RPC协议的接口却返回404 Not Pages。看来方法不行,没办法开始Debug客户端代码,找到这个代码。

func DialHTTPPath(network, address, path string) (*Client, error) {
    var err error
    conn, err := net.Dial(network, address)
    ...
    io.WriteString(conn, "CONNECT "+path+" HTTP/1.0\n\n")
    ...
}

哇哦!原来是这样,DialHTTP会向服务器发出 CONNECT /_goRPC_ HTTP/1.0 这就有办法了,用http.Handle方法接收“CONNECT /_goRPC_” 请求。然后在接收函数里,把写管道中的连接(net.Connect)勾出来,再用conn创建rpc响应应该就可以了。

func RPC(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
    conn, _, err := w.(http.Hijacker).Hijack()
    if err != nil {
        log.Print("rpc hijacking ", r.RemoteAddr, ": ", err.Error())
        return
    }
    io.WriteString(conn, "HTTP/1.0 200 Connected to Go RPC\n\n")
    if err != nil {
        log.Print("rpc hijacking ", r.RemoteAddr, ": ", err.Error())
        return
    }
    rpc.ServeConn(conn)
}

func main() {
    wait := make(chan bool)
    arith := new(Arith)
    rpc.Register(arith)
    router := httprouter.New()
    router.Handle("CONNECT", rpc.DefaultRPCPath, RPC)
    l, _ := net.Listen("tcp", ":9999")
    go http.Serve(l, router)
    <- wait
}

完美解决,现在HTTP和RPC就共用同一个端口了。

就在调通的一刻,小飞发现rpc.Server实现了serveHTTP方法!也就是rpc.Server本身就是一个http.Handler。这么半天白费劲了!直接把代码改成如下就行。

// router.Handle("CONNECT", rpc.DefaultRPCPath, RPC)
router.Handler("CONNECT", rpc.DefaultRPCPath, rpc.DefaultServer)

项目就这样运行起来了。给每一个rpc服务都写个专门的SDK,把一些不需要访问数据库的服务去掉了,相互之间访问数据就需要用rpc来传输。

不过下一个问题很快就到了,单点登录需要用户中心,而用户中心的访问权就需要单点登录。Golang还有个问题就是循环引用。解决这个循环登录的问题,小飞想到的就是“前后分离”。

前后分离

架构图变成了现在这样。

B7BjInY.png!web

image.png

前端都有前端的服务,不访问数据库。对Web提供HTTP服务,而后端对前端的服务提供RPC服务。但是很快一个心痒难耐的事儿就发生了。做Web服务,Node.js语言可能要比Golang更合适。因为JavaScript动态语言灵活性,V8引擎的高性能。第三方库丰富到让其他语言开发者都眼馋。

rMbYf2Y.png!web

image.png

JSON-RPC

一波未平一波又起,Golang的问题有暴露出来了,就是他的原生RPC库只能支持同语言。主要还是因为Golang默认RPC用的序列化编码是gob,这样选择一个通用的编码方式就好了XML或者JSON都是比较好的序列化编码方式。最后小飞选择了JSON。不为别的,Golang标准库中包含jsonrpc库。

3ue2EjE.png!web

image.png

HTTP和JSON-RPC

起初小飞想到的方法是,服务之间用HTTP作为载体,其中HTTP Body用JSON-RPC编码。于是乎代码变成了这个样子。

// server.go
func RPC(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
    conn, _, err := w.(http.Hijacker).Hijack()
    if err != nil {
        log.Print("rpc hijacking ", r.RemoteAddr, ": ", err.Error())
        return
    }
    io.WriteString(conn, "HTTP/1.0 200 Connected to Go RPC\n\n")
    if err != nil {
        log.Print("rpc hijacking ", r.RemoteAddr, ": ", err.Error())
        return
    }
    jsonrpc.ServeConn(conn)
}
// client
conn, err := net.Dial("tcp", "127.0.0.1:9999")
if err != nil {
    log.Fatal("dialing:", err)
}
io.WriteString(conn, "CONNECT " + rpc.DefaultRPCPath +" HTTP/1.0\n\n")
client := jsonrpc.NewClient(conn)

这样的话会出现一个问题,jsonrpc只能解码JSON数据。而HTTP返回数据中不只有数据体,而且还有HTTP头。这样默认的jsonrpc解码器就不起作用了,会出现超级恶心的'H'没法解码的问题。这时候需要做的就是,从新定义解码器。而既然已经前后分离,后端服务也没有必要非得用HTTP协议作为载体,那么干脆直接走原生jsonrpc来的痛快。于是乎代码编程了这样。

Node.js JSON-RPC

问题解决了,应该可以运行了吧。几行代码搞起个Node.js服务,再npm装个json-rpc库。F**K居然没有一个可以用的,都是八九年前的库。唯一看起来能用的居然是用采取了之前废掉的HTTP为载体数据体用JSON-RPC的方式玩的。这么看来,这些个库都是自嗨。根本就不能完成跨语言的调用。好像除了gRPC其他的框架真的不能完成跨语言调用吗?为了不给心爱的电脑添加更多的负担,坚决不用gRPC。

ymeENzb.png!web

image.png

自己写吧,因为通过之前的研究,Golang的rpc已经掌握的差不多了,有信心写一个rpc客户端。用net.Socket写了个go-rpc客户端。Cool!自此所有的服务都按部就班了。

const net = require('net')

var port = 9999;
var host = '127.0.0.1';
var client = new net.Socket();

client.connect(port, host, function() {
    const buf = JSON.stringify({"method":"Arith.Multiply","params":[{"A":7,"B":8}],"id":0})
    const buf2 = JSON.stringify({"method":"Arith.Multiply","params":[{"A":7,"B":8}],"id":1})
    client.write(`${buf}${buf2}`);
});

client.on('data', function(data){
    console.log(data.toString());
})

欢迎关注我们的微信公众号,每天学习Go知识

FveQFjN.jpg!web

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK