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 protocolreturn 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   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: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 := <-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​​​​​​​ 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/653058.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

在Java中,IO主要分为两种:同步阻塞IO(BIO)和NIO(New IO,也称为Non-blocking IO)。

IO&#xff08;Input/Output&#xff09;是指输入和输出&#xff0c;是程序与外部世界或者程序与程序之间进行数据交换的一种方式。在Java中&#xff0c;IO主要分为两种&#xff1a;同步阻塞IO&#xff08;BIO&#xff09;和NIO&#xff08;New IO&#xff0c;也称为Non-blocki…

51单片机-4G模块

51单片机-4G模块 4G控制LED #include "reg52.h" #include "intrins.h" #include <string.h>#define SIZE 12 sfr AUXR 0x8E; sbit D5 P3^7; char cmd[SIZE];void UartInit(void) //9600bps11.0592MHz {AUXR 0x01;SCON 0x50; //配置串口工作方…

MybatisPlus二级映射和关联对象ResultMap

文章目录 一、业务背景1. 数据库表结构2. 需求 二、使用映射直接得到指定结构三、其他文件1. Mapper2. Service3. Controller 四、概念理解一级映射二级映射聚合 五、标签使用1. \<collection\> 标签2. \<association\> 标签 在我们的教程中&#xff0c;我们设计了…

flask框架制作前端网页作为GUI

一、语法和原理 &#xff08;一&#xff09;、文件目录结构 需要注意的问题&#xff1a;启动文件命名必须是app.py。 一个典型的Flask应用通常包含以下几个基本文件和文件夹&#xff1a; app.py&#xff1a;应用的入口文件&#xff0c;包含了应用的初始化和配置。 requirem…

MySQL中四种索引类型

FULLTEXT &#xff1a;即为全文索引&#xff0c;目前只有MyISAM引擎支持。其可以在CREATE TABLE &#xff0c;ALTER TABLE &#xff0c;CREATE INDEX 使用&#xff0c;不过目前只有 CHAR、VARCHAR &#xff0c;TEXT 列上可以创建全文索引&#xff0c;需要注意的是MySQL5.6以后支…

Controller的部分注解

目录 1.增加 用到注解 1.1RequestBody注解解析&#xff1a; 2.查询方法当中参数不用注解&#xff01; 3.起售停售用到注解 3.1PathVariable解析 4.删除菜品注解 4.1RequestParam 5.修改用到的注解 5.1修改分两步 用到两个注解 6&#xff1a;总结 1.增加 用到注解…

【DeepLearning-8】MobileViT模块配置

完整代码&#xff1a; import torch import torch.nn as nn from einops import rearrange def conv_1x1_bn(inp, oup):return nn.Sequential(nn.Conv2d(inp, oup, 1, 1, 0, biasFalse),nn.BatchNorm2d(oup),nn.SiLU()) def conv_nxn_bn(inp, oup, kernal_size3, stride1):re…

Java基础知识-异常

资料来自黑马程序员 异常 异常&#xff0c;就是不正常的意思。在生活中:医生说,你的身体某个部位有异常,该部位和正常相比有点不同,该部位的功能将受影响.在程序中的意思就是&#xff1a; 异常 &#xff1a;指的是程序在执行过程中&#xff0c;出现的非正常的情况&#xff0c;…

深入理解HarmonyOS UIAbility:生命周期、WindowStage与启动模式探析

UIAbility组件概述 UIAbility组件是HarmonyOS中一种包含UI界面的应用组件&#xff0c;主要用于与用户进行交互。每个UIAbility组件实例对应最近任务列表中的一个任务&#xff0c;可以包含多个页面来实现不同功能模块。 声明配置 为了使用UIAbility&#xff0c;首先需要在mod…

学习了解 Vue3 的 nextTick() 方法

学习了解 Vue3 的 nextTick() 方法 Vue.js 3 引入了一系列新的特性和改进&#xff0c;其中之一是 nextTick() 方法的优化和变化。nextTick() 方法在 Vue 中用于在 DOM 更新后执行回调函数&#xff0c;确保在更新之后获得最新的 DOM 状态。 1. Vue 3 中的 nextTick() 方法 在 …

跟着cherno手搓游戏引擎【10】使用glm窗口特性

修改ImGui层架构&#xff1a; 创建&#xff1a; ImGuiBuild.cpp&#xff1a;引入ImGui #include"ytpch.h" #define IMGUI_IMPL_OPENGL_LOADER_GLAD//opengl的头文件需要的定义&#xff0c;说明使用的是gald #include "backends/imgui_impl_opengl3.cpp" …

03_Opencv简单实例演示效果和基本介绍

视频处理 视频分解图片 在后面我们要学习的机器学习中,我们需要大量的图片训练样本,这些图片训练样本如果我们全都使用相机拍照的方式去获取的话,工作量会非常巨大, 通常的做法是我们通过录制视频,然后提取视频中的每一帧即可! 接下来,我们就来学习如何从视频中获取信息 ubun…

@Autowired和@Resource区别

目录 前言 一、Autowired 二、Resource 三、区别 前言 在Java的Spring框架中&#xff0c;依赖注入&#xff08;Dependency Injection, DI&#xff09;是一种核心的技术&#xff0c;它允许我们将所依赖的对象或属性以外部化的方式提供给一个对象&#xff0c;而不是在对象内部…

c#之构值类型和引用类型

值类型:(整数/bool/struct/char/小数) 引用类型:(string/ 数组 / 自定义的类 / 内置的类) 值类型只需要一段单独的内存,用于存储实际的数据 引用类型需要两段内存(第一段存储实际的数据,他总是位于 堆中第二段是一个引用,指向数据在堆中的存放位置) 当使用引用类型赋值的时…

C++:类 的简单介绍(一)

目录 类的引用&#xff1a; 类的定义&#xff1a; 类的两种定义方式&#xff1a; 成员变量命名规则的建议&#xff1a; 类的访问限定符及封装&#xff1a; 访问限定符 【访问限定符说明】 封装 class与struct的区别&#xff1a; 类的作用域&#xff1a; 类的实例化…

前端大厂面试题探索编辑部——第三期

目录 题目 单选题1 题解 关于浏览器缓存 Last-Modified/If-Modified-Since ETag/If-None-Match 关于浏览器删除缓存数据 单选题2 题解 跨域问题 用document.domain解决的问题 题目 单选题1 1.关于浏览器缓存&#xff0c;以下哪个选项是不正确的&#xff08;&#…

centos下安装mongo C C++ 驱动

安装mongo-cxx-driver-r3.4.0 cmake的时候报错: 报错&#xff1a; CMake Error at src/mongocxx/CMakeLists.txt:54 (find_package):By not providing "Findlibmongoc-1.0.cmake" in CMAKE_MODULE_PATH thisproject has asked CMake to find a package configura…

ubuntu 安装node和npm

ubuntu 安装node 一、前言 在ubuntu中经常需要用到node ,npm&#xff0c;因为npm基本会和node同时安装&#xff0c;所以只需要安装node即可。 可以使用 nvm&#xff08;Node Version Manager&#xff09;来管理你的 Node.js 版本 二、具体步骤 1、nvm的安装 首先&#xf…

嵌入式——直接存储器存取(DMA)补充

目录 一、认识 DMA 二、DMA结构 1. DMA请求 2. 通道DMA 补&#xff1a;通道配置过程。 3. 仲裁器 三、DMA数据配置 1. 从哪里来&#xff0c;到哪里去 &#xff08;1&#xff09;从外设到存储器 &#xff08;2&#xff09;从存储器到外设 &#xff08;3&#xff09;从…

React 组件生命周期-概述、生命周期钩子函数 - 挂载时、生命周期钩子函数 - 更新时、生命周期钩子函数 - 卸载时

React 组件生命周期-概述 学习目标&#xff1a; 能够说出组件的生命周期一共几个阶段 组件的生命周期是指组件从被创建到挂在到页面中运行&#xff0c;在到组件不用时卸载组件 注意&#xff1a;只有类组件才有生命周期&#xff0c;函数组件没有生命周期(类组件需要实例化&…