手写RPC框架-第五天 支持HTTP协议

Posted Harris-H

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手写RPC框架-第五天 支持HTTP协议相关的知识,希望对你有一定的参考价值。

手写RPC框架-第五天 支持HTTP协议

1.支持 HTTP 协议需要做什么?

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

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

举一个简单例子:

  1. 浏览器向代理服务器发送 CONNECT 请求。
CONNECT geektutu.com:443 HTTP/1.0
  1. 代理服务器返回 HTTP 200 状态码表示连接已经建立。
HTTP/1.0 200 Connection Established
  1. 之后浏览器和服务器开始 HTTPS 握手并交换加密数据,代理服务器只负责传输彼此的数据包,并不能读取具体数据内容(代理服务器也可以选择安装可信根证书解密 HTTPS 报文)。

事实上,这个过程其实是通过代理服务器将 HTTP 协议转换为 HTTPS 协议的过程。对 RPC 服务端来,需要做的是将 HTTP 协议转换为 RPC 协议,对客户端来说,需要新增通过 HTTP CONNECT 请求创建连接的逻辑。


2.服务端支持 HTTP 协议

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

1.客户端向 RPC 服务器发送 CONNECT 请求

CONNECT 10.0.0.1:9999/_geerpc_ HTTP/1.0

2.RPC 服务器返回 HTTP 200 状态码表示连接建立。

HTTP/1.0 200 Connected to Gee RPC

3.客户端使用创建好的连接发送 RPC 报文,先发送 Option,再发送 N 个请求报文,服务端处理 RPC 请求并响应。

const (
	connected        = "200 Connected to Gee RPC"
	defaultRPCPath   = "/_geeprc_"
	defaultDebugPath = "/debug/geerpc"
)

// ServeHTTP implements an http.Handler that answers RPC requests.
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())
		return
	
	_, _ = io.WriteString(conn, "HTTP/1.0 "+connected+"\\n\\n")
	server.ServeConn(conn)


// HandleHTTP registers an HTTP handler for RPC messages on rpcPath.
// It is still necessary to invoke http.Serve(), typically in a go statement.
func (server *Server) HandleHTTP() 
	http.Handle(defaultRPCPath, server)


// HandleHTTP is a convenient approach for default server to register HTTP handlers
func HandleHTTP() 
	DefaultServer.HandleHTTP()

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

在 Go 语言中处理 HTTP 请求是非常简单的一件事,Go 标准库中 http.Handle 的实现如下:

package http
// Handle registers the handler for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func Handle(pattern string, handler Handler)  DefaultServeMux.Handle(pattern, handler) 

第一个参数是支持通配的字符串 pattern,在这里,我们固定传入 /_geeprc_,第二个参数是 Handler 类型,Handler 是一个接口类型,定义如下:

type Handler interface 
    ServeHTTP(w ResponseWriter, r *Request)

也就是说,只需要实现接口 Handler 即可作为一个 HTTP Handler 处理 HTTP 请求。接口 Handler 只定义了一个方法 ServeHTTP,实现该方法即可。

3.客户端支持 HTTP 协议

服务端已经能够接受 CONNECT 请求,并返回了 200 状态码 HTTP/1.0 200 Connected to Gee RPC,客户端要做的,发起 CONNECT 请求,检查返回状态码即可成功建立连接。

// NewHTTPClient new a Client instance via HTTP as transport protocol
func NewHTTPClient(conn net.Conn, opt *Option) (*Client, error) 
	_, _ = io.WriteString(conn, fmt.Sprintf("CONNECT %s HTTP/1.0\\n\\n", defaultRPCPath))

	// Require successful HTTP response
	// before switching to RPC protocol.
	resp, err := http.ReadResponse(bufio.NewReader(conn), &http.RequestMethod: "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


// DialHTTP connects to an HTTP RPC server at the specified network address
// listening on the default HTTP RPC path.
func DialHTTP(network, address string, opts ...*Option) (*Client, error) 
	return dialTimeout(NewHTTPClient, network, address, opts...)

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

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

// XDial calls different functions to connect to a RPC server
// according the first parameter rpcAddr.
// rpcAddr is a general format (protocol@addr) to represent a rpc server
// eg, http@10.0.0.1:7001, tcp@10.0.0.1:9999, unix@/tmp/geerpc.sock
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 Dial(protocol, addr, opts...)
	


4.总结

通过http.Connect方法建立连接。后续的通信过程就交给 NewClient 了。

从而实现了支持HTTP协议。

一般来说,http 是基于 tcp的,未来可能会有不基于 tcp 实现的 http。所以客户端通过在 TCP 上包一层 HTTP 协议来支持 HTTP。使用 HTTP 有很多好处,比如监听一个 tcp 端口,可以使用不同的 PATH 支持不同的 HTTP 服务。

你可以把 HTTP 协议理解为一种协议协商的方式,客户端和服务端一侧做编码,一侧做解码就好了,除了 HTTP 协议头,后面的报文是没有变化的。

这里只是用http 协议完成了一个握手的过程,然后在使用自己的定义的协议(详见Day1 服务端与消息编码 中的通信过程)来进行双方的通讯。

以上是关于手写RPC框架-第五天 支持HTTP协议的主要内容,如果未能解决你的问题,请参考以下文章

是时候手写一个RPC框架了

手写基于 http 的RPC框架

带你手写基于 Spring 的可插拔式 RPC 框架通信协议模块

#yyds干货盘点# 基于Netty,20分钟手写一个RPC框架

带你手写基于 Spring 的可插拔式 RPC 框架整体结构

手写一个自己的 RPC 框架?