【Golang | RPC】使用包net/rpc实现基于http协议的RPC服务

环境:
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方法进行路由分发,即将patternhandler进行绑定

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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

田土豆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值