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)
}

RPC教程 5.支持HTTP协议_第1张图片

 首先我们能看到使用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格式 [email protected]:34232,[email protected]: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 = `
	
	GeeRPC Services
	{{range .}}
	
Service {{.Name}}
{{range $name, $mtype := .Method}} {{end}}
MethodCalls
{{$name}}({{$mtype.ArgType}}, {{$mtype.ReplyType}}) error {{$mtype.NumCalls}}
{{end}} ` 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)
}

效果如下

RPC教程 5.支持HTTP协议_第2张图片

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

RPC教程 5.支持HTTP协议_第3张图片

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

你可能感兴趣的:(#,Go实现rpc,http,网络,golang,rpc)