环境:
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. 实践
/net/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
}
rpc.RegisterNameQueryService
func main() {
	// 注册服务方法
	if err := rpc.RegisterName("QueryService", new(Query)); err != nil {
		log.Println(err)
	}
	...
}
rpc.HandleHTTPpatternhandler
func main() {
	...
	// 绑定pattern和handler
	rpc.HandleHTTP()
	...
}
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
rpc.DialHTTPCallGetAge
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 原理分析
rpc.DialHttpDialHTTPPath
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
	}
	...
}
DialHTTPPathnet.Dialio.WriteString()CONNECTDefaultRPCPathHTTP/1.0http.ReadResponsehttp.ResponseNewClientgob
func NewClient(conn io.ReadWriteCloser) *Client {
	encBuf := bufio.NewWriter(conn)
	client := &gobClientCodec{conn, gob.NewDecoder(conn), gob.NewEncoder(encBuf), encBuf}
	return NewClientWithCodec(client)
}
Call
4. 思考
net/rpc
  • 基于socket连接,可以减少网络开销,RPC的一次调用时延更短
  • 基于http连接,可以方便作一些认证(比如grpc里的各种拦截器)
5. 总结
rpc.HandleHTTP()http.ListenAndServerpc.DialHTTPgob