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,一经查实,立即删除!

相关文章

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…

【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…

跟着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…

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

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

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

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

嵌入式——直接存储器存取(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;函数组件没有生命周期(类组件需要实例化&…

LeetCode344反转字符串(java实现)

今天我们来分享的题目是leetcode344反转字符串。题目描述如下&#xff1a; 我们观察题目发现&#xff0c;题目要求使用O(1)的空间解决这一问题。那么我们就不能进行使用开辟新的数组进行反转了。 解题思路&#xff1a;那么该题的我得思路是使用双指针的方法进行题解&#xff0…

2024獬豸杯

2024.1.28上午9-12时&#xff0c;返乡大学生边帮姐带娃边做&#xff0c;有几题没交上 解压密码&#xff1a;都考100分 手机备份包 手机基本信息 1、IOS手机备份包是什么时候开始备份的。&#xff08;标准格式&#xff1a;2024-01-20.12:12:12) 2024-01-15.14.19.44 2、请分…

Docker 安装与基本操作

目录 一、Docker 概述 1、Docker 简述 2、Docker 的优势 3、Docker与虚拟机的区别 4、Docker 的核心概念 1&#xff09;镜像 2&#xff09;容器 3&#xff09;仓库 二、Docker 安装 1、命令&#xff1a; 2、实操&#xff1a; 三、Docker 镜像操作 1、命令&#xff1…

centos7 挂载windows共享文件夹报错提示写保护

centos7挂载windows共享时&#xff0c;提示被共享的位置写保护&#xff0c;只能以只读方式挂载&#xff0c;紧接着就是以只读方式挂载失败 原因是组件少装了 yum install cifs-utils 安装完后&#xff0c;正常挂载使用。 下载离线安装包 下载离线包下载工具 下载离线安装包…

SpringBoot系列之MybatisPlus实现分组查询

SpringBoot系列之MybatisPlus实现分组查询 我之前博主曾记写过一篇介绍SpringBoot2.0项目怎么集成MybatisPlus的教程&#xff0c;不过之前的博客只是介绍了怎么集成&#xff0c;并没有做详细的描述各种业务场景&#xff0c;本篇博客是对之前博客的补充&#xff0c;介绍在mybat…

2024/1/27 备战蓝桥杯 1-1

目录 求和 0求和 - 蓝桥云课 (lanqiao.cn) 成绩分析 0成绩分析 - 蓝桥云课 (lanqiao.cn) 合法日期 0合法日期 - 蓝桥云课 (lanqiao.cn) 时间加法 0时间加法 - 蓝桥云课 (lanqiao.cn) 扫雷 0扫雷 - 蓝桥云课 (lanqiao.cn) 大写 0大写 - 蓝桥云课 (lanqiao.cn) 标题…

【SpringSpringBoot】概述

Spring&SpringBoot专题 【注】&#xff1a; 本专题围绕框架核心概念展开&#xff0c;渐进式深入总结学习、面试、开发经验&#xff0c;集中整理便于回顾 持续补充与施工中~~~~ 1.发展史 2.基本架构 Spring框架的基本架构是一个分层架构&#xff0c;包括多个模块&#x…

2024三掌柜赠书活动第六期:人人都离不开的算法——图解算法应用

目录 前言算法概念图解算法应用算法的价值和挑战关于《人人都离不开的算法——图解算法应用》编辑推荐内容简介作者简介图书目录书中前言/序言书摘插画《人人都离不开的算法——图解算法应用》全书速览结束语 前言 作为开发者想必都知道&#xff0c;算法是现代社会中无处不在…

day33_js

今日内容 0 复习昨日 1 JS概述 2 JS的引入方式 3 JS语法 3.1 变量 3.2 基本数据类型 3.3 引用类型 3.4 数组类型 3.5 日期类型 3.6 运算符(算术运算,逻辑,关系运算,三目运算) 3.7 分支 3.8 循环 3.9 函数(重点) 3 常见弹窗函数 alter,confirm,prompt 0 复习昨日 1 盒子模型 对d…