天地心公司做网站怎样,建行个人账户查询,湖北百度关键词排名软件,中核二三公司最新招聘1.HTTP的CONNECT方法
Web 开发中#xff0c;我们经常使用 HTTP 协议中的 HEAD、GET、POST 等方式发送请求#xff0c;等待响应。但 RPC 的消息格式与标准的 HTTP 协议并不兼容#xff0c;在这种情况下#xff0c;就需要一个协议的转换过程。HTTP 协议的 CONNECT 方法提供了…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使用是相对熟悉的了不会讲解太多基础内容。
那通信过程应该是这样的
客户端发送CONNECT请求RPC 服务器返回 HTTP 200 状态码表示连接建立。客户端使用创建好的连接发送 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 RPCdefaultRPCPath /myrpcdefaultDebugPath /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; charsetutf-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格式 http10.0.0.1:34232,tpc10.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 protocoladdr, rpcAddr)}protocol, addr : parts[0], parts[1]switch protocol {case http:return DialHTTP(tcp, addr, opts...)default:// tcp, unix or other transport protocolreturn Dail(protocol, addr, opts...)}
}
4.实现简单的 DEBUG 页面
支持 HTTP 协议的好处在于RPC 服务仅仅使用了监听端口的 /myrpc 路径在其他路径上我们可以提供诸如日志、统计等更为丰富的功能。接下来我们在 /debug/rpc 上展示服务的调用统计视图。
//debug.go
//debugText不需要关注过多
const debugText htmlbodytitleGeeRPC Services/title{{range .}}hrService {{.Name}}hrtableth aligncenterMethod/thth aligncenterCalls/th{{range $name, $mtype : .Method}}trtd alignleft fontfixed{{$name}}({{$mtype.ArgType}}, {{$mtype.ReplyType}}) error/tdtd aligncenter{{$mtype.NumCalls}}/td/tr{{end}}/table{{end}}/body/htmlvar debug template.Must(template.New(RPC debug).Parse(debugText))type debugHTTP struct {*Server //继承做法
}type debugService struct {Name stringMethod 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 inttype 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:10000log.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 : -addrChfmt.Println(addr)client, err : geerpc.DialHTTP(tcp, addr)if err ! nil {panic(err)}defer client.Close()num : 5var wg sync.WaitGroupwg.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 1324ctx, 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