RPC教程 5.支持HTTP协议

1.HTTP的CONNECT方法

Web 开发中,我们经常使用 HTTP 协议中的 HEAD、GET、POST 等方式发送请求,等待响应。但 RPC 的消息格式与标准的 HTTP 协议并不兼容,在这种情况下,就需要一个协议的转换过程。HTTP 协议的 CONNECT 方法提供了这个能力,CONNECT 一般用于代理服务。

CONNECT请求是HTTP协议中的一种特殊请求方法,主要用于建立隧道连接。它允许客户端通过代理服务器与目标服务器建立一条直接的TCP连接,用于传输非HTTP协议的数据。

现在大多数浏览器与服务器之间都是 HTTPS 通信,其都是加密的,浏览器通过代理服务器发起 HTTPS 请求时,由于请求的站点地址和端口号都是加密保存在 HTTPS 请求报文头中的,代理服务器如何知道往哪里发送请求呢?

为了解决这个问题,浏览器通过 HTTP 明文形式向代理服务器发送一个 CONNECT 请求告诉代理服务器目标地址和端口,代理服务器接收到这个请求后,会在对应端口与目标站点建立一个 TCP 连接,连接建立成功后返回 HTTP 200 状态码告诉浏览器与该站点的加密通道已经完成。接下来代理服务器仅需透传浏览器和服务器之间的加密数据包即可,代理服务器无需解析 HTTPS 报文。

浏览器向代理服务器发送 CONNECT 请求的例子。

CONNECT www.microsoft.com:443 HTTP/1.0
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko
Host: www.microsoft.com
Content-Length: 0
DNT: 1
Connection: Keep-Alive
Pragma: no-cache

主要是三步:

1.浏览器向代理服务器发送 CONNECT 请求。

CONNECT www.baidu.com:443 HTTP/1.0

 2.代理服务器返回 HTTP 200 状态码表示连接已经建立。

HTTP/1.0 200 Connection Established

 3.之后浏览器和服务器开始 HTTPS 握手并交换加密数据,代理服务器只负责传输彼此的数据包,并不能读取具体数据内容(代理服务器也可以选择安装可信根证书解密 HTTPS 报文)。

客户端向服务端发起连接,就像第一步的浏览器向代理服务器发送 CONNECT 请求,所以客户端需要添加HTTP CONNECT 请求创建连接的逻辑。而服务端就需要将客户端的HTTP协议的消息转化成该rpc协议

2.服务端支持 HTTP 协议

这里默认读者对Go语言的http使用是相对熟悉的了,不会讲解太多基础内容。

那通信过程应该是这样的:

  1. 客户端发送CONNECT请求
  2. RPC 服务器返回 HTTP 200 状态码表示连接建立。
  3. 客户端使用创建好的连接发送 RPC 报文,先发送 Option,再发送 N 个请求报文,服务端处理 RPC 请求并响应。

 那服务端就需要添加返回HTTP200状态码给客户端的操作。那回顾下服务端的建立连接的操作。

func (server *Server) Accept(lis net.Listener) {
	for {
		conn, err := lis.Accept()
		// 拿到客户端的连接, 开启新协程异步去处理.
		go server.ServeConn(conn)
	}
}

 accept后就到了server.ServeConn(conn),所以后序http中我们会需要用到这个方法的。

//server.go
const (
	connected        = "200 Connected to RPC"
	defaultRPCPath   = "/myrpc"
	defaultDebugPath = "/debug/rpc"
)

// server HTTP部分,server实现了ServeHTTP方法,就是http.Handler接口了
func (server *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	if req.Method != "CONNECT" {
		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
		w.WriteHeader(http.StatusMethodNotAllowed)
		io.WriteString(w, "405 must CONNECT\n")
		return
	}

	conn, _, err := w.(http.Hijacker).Hijack()
	if err != nil {
		log.Print("rpc hijacking ", req.RemoteAddr, " :", err.Error())
	}

	io.WriteString(conn, "HTTP/1.0 "+connected+"\n\n")
	//server.ServeConn(conn)就回到了之前的accept后的那部分
	server.ServeConn(conn)
}

func (server *Server) HandleHTTP() {
	//方法原型func Handle(pattern string, handler Handler)
	http.Handle(defaultRPCPath, server)
}

func HandleHTTP() {
	DefaultServer.HandleHTTP()
}

 defaultDebugPath 是为后续 DEBUG 页面预留的地址。

Go语言实现http是比较容易的,只需要实现接口 Handler 即可作为一个 HTTP Handler 处理 HTTP 请求。接口 Handler 只定义了一个方法 ServeHTTP,实现该方法即可。

ServeHTTP方法中首先是判断HTTP请求方法是否是CONNECT。之后就到了w.(http.Hijacker).Hijack()

Hijacker

http.ResponseWriter是接口类型,w.(http.Hijacker)是将w转化成http.Hijacker类型。

这里是接管 HTTP 连接,其指接管了 HTTP 的 TCP 连接,也就是说 Golang 的内置 HTTP 库和 HTTPServer 库将不会管理这个 TCP 连接的生命周期,这个生命周期已经划给 Hijacker 了。

Hijack()可以将HTTP对应的TCP连接取出,连接在Hijack()之后,HTTP的相关操作就会受到影响,调用方需要负责去关闭连接。

之前已经分析了,要把HTTP协议的转换成自定义的RPC协议,所以就可以使用Hijack()。一般在创建连接阶段使用HTTP连接,后续自己完全处理connection,那就符合了我们想把HTTP协议的转换成自定义的RPC协议的做法。

来看看和正常的HTTP请求的区别

func main() {
	http.HandleFunc("/hijack", func(w http.ResponseWriter, r *http.Request) {
		conn, buf, _ := w.(http.Hijacker).Hijack()
		defer conn.Close()

		buf.WriteString("hello hijack\n")
		buf.Flush()
	})
	http.HandleFunc("/htt", func(writer http.ResponseWriter, request *http.Request) {
		io.WriteString(writer, "hello htt\n")
	})

	http.ListenAndServe("localhost:10000", nil)
}

 首先我们能看到使用Hijack的请求返回没有响应头信息。这里我们要明白的是,Hijack之后虽然能正常输出数据,但完全没有遵守http协议。这里net/http源码里做的一些处理,这里就不展开说了。

总结:Hijack的使用场景:当不想使用内置服务器的HTTP协议实现时,请使用Hijack。一般在创建连接阶段使用HTTP连接,后续自己完全处理connection的情况。

3.客户端支持 HTTP 协议

客户端要做的,发起 CONNECT 请求,检查返回状态码即可成功建立连接。

//client.go
// HTTP部分
func NewHTTPClient(conn net.Conn, opt *Option) (*Client, error) {
	io.WriteString(conn, fmt.Sprintf("CONNECT %s HTTP/1.0\n\n", defaultRPCPath))

	resp, err := http.ReadResponse(bufio.NewReader(conn), &http.Request{Method: "CONNECT"})
	if err == nil && resp.Status == connected {
		return NewClient(conn, opt)
	}
	if err != nil {
		err = errors.New("unexpected HTTP response: " + resp.Status)
	}
	return nil, err
}

func DialHTTP(network, address string, opts ...*Option) (*Client, error) {
	opt, err := parseOptions(opts...)
	if err != nil {
		return nil, err
	}
	return dialTimeout(NewHTTPClient, network, address, opt)
}

上一节的newClientFunc类型在这里就派上用场了,我们编写一个建立HTTP连接的函数,把该函数传给dialTimeout即可。

在NewHTTPClient函数中,通过 HTTP CONNECT 请求建立连接之后,后续的通信过程就交给 NewClient 了。

为了简化调用,提供了一个统一入口 XDial

// 统一的建立rpc客户端的接口
// rpcAddr格式 http@10.0.0.1:34232,tpc@10.0.0.1:10000
func XDial(rpcAddr string, opts ...*Option) (*Client, error) {
	parts := strings.Split(rpcAddr, "@")
	if len(parts) != 2 {
		return nil, fmt.Errorf("rpc client err: wrong format '%s', expect protocol@addr", rpcAddr)
	}
	protocol, addr := parts[0], parts[1]

	switch protocol {
	case "http":
		return DialHTTP("tcp", addr, opts...)
	default:
		// tcp, unix or other transport protocol
		return Dail(protocol, addr, opts...)
	}
}

4.实现简单的 DEBUG 页面

支持 HTTP 协议的好处在于,RPC 服务仅仅使用了监听端口的 /myrpc 路径,在其他路径上我们可以提供诸如日志、统计等更为丰富的功能。接下来我们在 /debug/rpc 上展示服务的调用统计视图。

//debug.go
//debugText不需要关注过多
const debugText = `<html>
	<body>
	<title>GeeRPC Services</title>
	{
  {range .}}
	<hr>
	Service {
  {.Name}}
	<hr>
		<table>
		<th align=center>Method</th><th align=center>Calls</th>
		{
  {range $name, $mtype := .Method}}
			<tr>
			<td align=left font=fixed>{
  {$name}}({
  {$mtype.ArgType}}, {
  {$mtype.ReplyType}}) error</td>
			<td align=center>{
  {$mtype.NumCalls}}</td>
			</tr>
		{
  {end}}
		</table>
	{
  {end}}
	</body>
	</html>`

var debug = template.Must(template.New("RPC debug").Parse(debugText))

type debugHTTP struct {
	*Server //继承做法
}

type debugService struct {
	Name   string
	Method map[string]*methodType
}

// Runs at /debug/rpc, 调用的是debugHTTP的ServeHTTP,不是server结构体的ServeHTTP
func (server debugHTTP) ServerHTTP(w http.ResponseWriter, rep *http.Request) {
	var services []debugService
	//sync.Map遍历,Range方法并配合一个回调函数进行遍历操作。通过回调函数返回遍历出来的键值对。
	server.serviceMap.Range(func(namei, svci any) bool {
		svc := svci.(*service) //转换成*service类型
		services = append(services, debugService{
			Name:   namei.(string),
			Method: svc.method,
		})
		return true //当需要继续迭代遍历时,Range参数中回调函数返回true;否则返回false
	})

	err := debug.Execute(w, services)
	if err != nil {
		fmt.Fprintln(w, "rpc: error executing template:", err.Error())
	}
}

在这里,我们将返回一个 HTML 报文,这个报文将展示注册所有的 service 的每一个方法的调用情况。

将 debugHTTP 实例绑定到地址 /debug/rpc,需要在server.go文件的func (server *Server) HandleHTTP()方法中继续添加。

func (server *Server) HandleHTTP() {
	//方法原型func Handle(pattern string, handler Handler)
	http.Handle(defaultRPCPath, server)
    
	http.Handle(defaultDebugPath, debugHTTP{Server: server}) //这个是新添加的,处理debug的
}

5.测试

debugHTTP做好后,就可以进行测试了。使用HTTP协议的rpc用法和之前的是稍微有点不同。

服务端中的变化是将 startServer 中的 geerpc.Accept() 替换为了 geerpc.HandleHTTP(),之后就是使用http.ListenAndServe()

type My int

type Args struct{ Num1, Num2 int }

func (m *My) Sum(args Args, reply *int) error {
	*reply = args.Num1 + args.Num2
	// time.Sleep(time.Second * 3)
	return nil
}

func startServer(addrCh chan string) {
	var myServie My
	//这里一定要用&myServie,因为前面Sum方法的接受者是*My;若接受者是My,myServie或者&myServie都可以
	if err := geerpc.Register(&myServie); err != nil {
		slog.Error("register error:", err) //slog是Go官方的日志库
		os.Exit(1)
	}
	geerpc.HandleHTTP()
	addrCh <- "127.0.0.1:10000"
	log.Fatal(http.ListenAndServe("127.0.0.1:10000", nil))
    
    //之前的写法
	// l, err := net.Listen("tcp", "localhost:10000")
	// geerpc.Accept(l)
}

客户端将 Dial 替换为 DialHTTP,其余地方没有发生改变。

func clientCall(addrCh chan string) {
	addr := <-addrCh
	fmt.Println(addr)
	client, err := geerpc.DialHTTP("tcp", addr)
	if err != nil {
		panic(err)
	}
	defer client.Close()

	num := 5
	var wg sync.WaitGroup
	wg.Add(num)

	for i := 0; i < num; i++ {
		go func(i int) {
			defer wg.Done()
			args := &Args{Num1: i, Num2: i * i}
			var reply int = 1324
			ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
			defer cancel()
			if err := client.Call(ctx, "My.Sum", args, &reply); err != nil {
				log.Println("call Foo.Sum error:", err)
			}
			fmt.Println("reply: ", reply)
		}(i)
	}
	wg.Wait()
}

func main() {
	ch := make(chan string)
	go clientCall(ch)
	startServer(ch)
}

效果如下

若是在浏览器输入http://localhost:10000/debug/rpc,出现如下效果

完整代码:https://github.com/liwook/Go-projects/tree/main/geerpc/5-http-debug​​​​​​​ 

相关推荐

  1. <span style='color:red;'>RPC</span><span style='color:red;'>协议</span>

    RPC协议

    2024-01-28 06:54:03      8 阅读
  2. SpringBoot项目中同时支持httpshttp协议

    2024-01-28 06:54:03       24 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-01-28 06:54:03       16 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-01-28 06:54:03       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-01-28 06:54:03       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-01-28 06:54:03       18 阅读

热门阅读

  1. 通信协议的TCP/IP模型

    2024-01-28 06:54:03       30 阅读
  2. 最新2024年项目基金撰写与技巧及GPT融合应用

    2024-01-28 06:54:03       35 阅读
  3. WPF的ViewBox控件

    2024-01-28 06:54:03       34 阅读
  4. docker-compose离线安装

    2024-01-28 06:54:03       33 阅读
  5. Debian 12.x apt方式快速部署LNMP

    2024-01-28 06:54:03       25 阅读
  6. 03 创建图像窗口的几种方式

    2024-01-28 06:54:03       34 阅读
  7. LeetCode-题目整理【12】:N皇后问题--回溯算法

    2024-01-28 06:54:03       38 阅读
  8. 【从浅到深的算法技巧】初级排序算法 上

    2024-01-28 06:54:03       35 阅读