环境:
Golang:go1.18.2 linux/amd64
完整代码:
https://github.com/WanshanTian/GolangLearning
cd GolangLearning/RPC/httpRPC
1. 简介
前面两篇文章【Golang | RPC】Golang-RPC机制的理解,【Golang | RPC】利用json编解码器实现RPC介绍了基于socket连接,分别采用gob,json作编解码器实现RPC服务。本文基于http协议实现RPC服务,并简单分析原理
2. 实践
通过Golang自带的包/net/rpc
实现RPC服务。现有下面一种场景:服务端保存着用户的年龄信息,客户端输入姓名,经RPC后获得对应的年龄
2.1 服务端
2.1.1 首先新建项目httpRPC,并创建Server目录,新建main.go
[root@tudou workspace]# mkdir -p httpRPC/Server && cd httpRPC/Server && touch main.go
2.1.2 服务端使用map保存用户年龄信息,同时创建Query结构体,该结构体实现了GetAge方法
package main
import (
"fmt"
"log"
"net/http"
"net/rpc"
)
// 用户信息
var userinfo = map[string]int{
"foo": 18,
"bar": 20,
}
// 实现查询服务,结构体Query实现了GetAge方法
type Query struct {
}
func (q *Query) GetAge(req string, res *string) error {
*res = fmt.Sprintf("The age of %s is %d", req, userinfo[req])
return nil
}
2.1.3 使用rpc.RegisterName
注册服务方法,并指定服务名为QueryService
func main() {
// 注册服务方法
if err := rpc.RegisterName("QueryService", new(Query)); err != nil {
log.Println(err)
}
...
}
2.1.4 使用rpc.HandleHTTP
方法进行路由分发,即将pattern
和handler
进行绑定
func main() {
...
// 绑定pattern和handler
rpc.HandleHTTP()
...
}
2.1.5 使用http.ListenAndServe
开启监听
func main() {
...
// 开启监听
if err := http.ListenAndServe(":1234", nil); err != nil {
log.Panic(err)
}
}
2.1.6 运行服务
[root@tudou Server]# go build main.go && ./main
2.2 客户端
2.2.1 在httpRPC/Server同级目录下创建Client目录,新建main.go
[root@tudou workspace]# mkdir -p httpRPC/Client && cd httpRPC/Client && touch main.go
2.2.2 首先通过rpc.DialHTTP
返回rpc客户端,然后通过Call
远程调用GetAge
方法
package main
import (
"fmt"
"log"
"net/rpc"
)
func main() {
// 建立http连接
client, _ := rpc.DialHTTP("tcp", ":1234")
// 远程调用GetAge方法
var res string
err := client.Call("QueryService.GetAge", "foo", &res)
if err != nil {
log.Panicln(err)
}
fmt.Println(res)
_ = client.Close()
}
2.2.3 运行客户端,得到如下结果
[root@tudou Client]# go run main.go
The age of foo is 18
3 原理分析
这里主要从客户端的角度分析http的建立过程
先看方法rpc.DialHttp
,直接返回DialHTTPPath
,其http建立过程就在这个方法里,我们结合wireshark抓包具体分析代码流程:
先贴上对应源码和一次RPC调用对应的抓包截图
const DefaultRPCPath = "/_goRPC_"
func DialHTTP(network, address string) (*Client, error) {
return DialHTTPPath(network, address, DefaultRPCPath)
}
// DialHTTPPath connects to an HTTP RPC server
// at the specified network address and path.
func DialHTTPPath(network, address, path string) (*Client, error) {
conn, err := net.Dial(network, address)
if err != nil {
return nil, err
}
io.WriteString(conn, "CONNECT "+path+" HTTP/1.0\n\n")
// Require successful HTTP response
// before switching to RPC protocol.
resp, err := http.ReadResponse(bufio.NewReader(conn), &http.Request{Method: "CONNECT"})
if err == nil && resp.Status == connected {
return NewClient(conn), nil
}
...
}
3.1 函数DialHTTPPath
内先是通过net.Dial
建立socket连接,对应抓包截图里的阶段1(3次握手过程)
3.2 通过io.WriteString()
发送http请求起始行(包括了请求方法CONNECT
、请求路径DefaultRPCPath
和http协议HTTP/1.0
),服务端收到请求处理(这里服务端的处理过程大家可以自行阅读源码)后返回响应,通过http.ReadResponse
方法将返回结果解析到http.Response
结构体内,如果响应码正确,则通过NewClient
方法返回rpc客户端(注意这里将编解码器写死成gob
了),对应抓包截图的阶段2
func NewClient(conn io.ReadWriteCloser) *Client {
encBuf := bufio.NewWriter(conn)
client := &gobClientCodec{conn, gob.NewDecoder(conn), gob.NewEncoder(encBuf), encBuf}
return NewClientWithCodec(client)
}
3.3 抓包截图的阶段3对应着Call
的流程,可以看到在http建链后具体的rpc调用还是建立在socket基础上
3.4 抓包截图的阶段4对应着释放连接的过程(4次挥手)
4. 思考
使用net/rpc
实现的基于socket和http的RPC服务有啥区别呢?笔者总结了以下两点:
- 基于socket连接,可以减少网络开销,RPC的一次调用时延更短
- 基于http连接,可以方便作一些认证(比如grpc里的各种拦截器)
5. 总结
- 服务端通过
rpc.HandleHTTP()
进行路由分发然后利用http.ListenAndServe
进行监听,客户端通过rpc.DialHTTP
建立http连接 - 基于http连接的RPC,其编解码器写死成
gob