24. 从零用Rust编写正反向代理,细说HTTP行为中的几种定时器
wmproxy
wmproxy已用Rust实现http/https代理, socks5代理, 反向代理, 静态文件服务器,四层TCP/UDP转发,内网穿透,后续将实现websocket代理等,会将实现过程分享出来,感兴趣的可以一起造个轮子
项目地址
国内: https://gitee.com/tickbh/wmproxy
github: https://github.com/tickbh/wmproxy
敏感的时间
现实生活中大家都对时间有着概念,比如“快上班了,要不然要迟到了。”、“这班怎么这么久,怎么还没下班?”、“啊?已经晚上12点啦,等我这把游戏玩完。”、“叮叮叮,起床闹钟一直在催着你起床了。”
闹钟、自然变化、生物钟为我们提供着时间的保证。而计算机的世界里,就靠着硬件定时器,控制着时间的流逝,如果哪一天本地时间和别人的时间不一致了,此时需要找别人对时,这也就是经典的网络时间协议(NTP)。
现实的生活中,通常以分钟或者小时乃至天去和别人约定时间,而在计算机的世界里,在我们看来那就是朝生暮死的蚍蜉一般,他们生命较短,所以对他们来说,通常用s或者ms来乃至μs做通知,所以需要严格的遵守时间约定,绝对不允许有赖床的行为。
HTTP行为中的定时器
HTTP/HTTPS/WebSocket的访问撑起了互联网总流量的半臂江山,日常接触中APP或者游戏这种对外服务的基本上均是通过HTTP及HTTPS进行服务,长链接由于兼容小程序这类的大部分游戏直接也从普通的Socket转为WebSocket直接做全网的兼容。WebSocket的基础协议是由HTTP升级而来,所以也归于HTTP协议。
主要有以下列行为定时器
- 连接超时定时器
- 读操作超时定时器
- 写操作超时定时器
- 读/写操作超时定时器(在规则的时间内同时完成读和写)
- keep-alive超时定时器(连接保持的最长时间)
定时器类的定义
相关的超时数据均存放在该类里,接下来类型进行相应的处理
#[derive(Debug)]
pub struct TimeoutLayer {
pub connect_timeout: Option<Duration>,
pub read_timeout: Option<Duration>,
pub write_timeout: Option<Duration>,
pub timeout: Option<Duration>,
/// keep alive 超时时长
pub ka_timeout: Option<Duration>,
read_timeout_sleep: Option<Pin<Box<Sleep>>>,
write_timeout_sleep: Option<Pin<Box<Sleep>>>,
timeout_sleep: Option<Pin<Box<Sleep>>>,
ka_timeout_sleep: Option<Pin<Box<Sleep>>>,
}
连接超时定时器
此时约定的是客户端向服务端请求TCP连接建立的最长时间,如果没有约定,将由系统连接超时的或者确认不可达的时候才返回失败。
如果没有连接超时,那么以下我们的型业务场景模拟:
在弱网的环境下,比如手机信号不好的地方,一次连接请求建立可能会花费10秒左右才能返回,那么如果我们没有连接超时,用户在等待了8秒,客户端的界面都没有办法给出任何的响应,就会认为服务端出现了问题或者频繁的重启客户端应用不断的进行重试而得不到预期的反馈。在App设计中,对这种情况的用户体验极差,需要在指定的时间内给用户反馈出当前网络无法访问。
客户端的设计模型中,如果有定时请求或者埋点数据之类,如果没有超时机制,容易出现短时间内打开的socket过多出现资源耗尽的情况,其次客户端不知道是否将数据已经进行发送,不确认是否需要将该条数据进行缓存以便下一次推送。
在设计模型中,连接超时必不可少,因为各种业务场景的不同,需要要不同的时间内得到预期的反馈。以下是连接超时在Rust中的实现,因为只存在于客户端主动连接服务端,所以连接超时只在客户端实现。
async fn inner_connect<A: ToSocketAddrs>(&self, addr: A) -> ProtResult<TcpStream> {
if self.inner.timeout.is_some() {
// 获取是否配置了连接超时, 如果有连接超时那么指定timeout
if let Some(connect) = &self.inner.timeout.as_ref().unwrap().connect_timeout {
match tokio::time::timeout(*connect, TcpStream::connect(addr)).await {
Ok(v) => {
return Ok(v?)
}
Err(_) => return Err(ProtError::Extension("connect timeout")),
}
}
}
let tcp = TcpStream::connect(addr).await?;
Ok(tcp)
}
通过指定超时时间来对连接的建立监听。
读操作超时定时器
大部分HTTP请求,只有得到完整的数据才能进行处理,少部分如文件上传这种可以边上传边操作,而是否能开始操作关系到请求的响应时间。
读超时大概有以下的可能:
- 服务器处理请求的时间太长,导致客户端等待超时。
- 服务器返回的数据量过大,导致客户端读取数据的时间超过了规定的时间。
- 网络延迟或网络不稳定,导致客户端无法在规定的时间内读取完数据。
这造成客户端无法及时处理数据,可以报错好让客户端换备用线路或者备用服务器等以便及时的处理数据。
读操作我们不管服务端或者客户端,不管http/1.1或者http/2均由is_read_end字段来判定,在未读完当前请求的数据前,判断是否超时。
pub fn poll_ready(
&mut self,
cx: &mut Context<'_>,
ready_time: Instant,
is_read_end: bool,
is_write_end: bool,
is_idle: bool,
) -> ProtResult<()> {
let now = Instant::now();
if !is_read_end {
if let Some(read) = &self.read_timeout {
let next = ready_time + *read;
if now >= next {
return Err(crate::ProtError::Extension("read timeout"));
}
if self.read_timeout_sleep.is_some() {
self.read_timeout_sleep.as_mut().unwrap().as_mut().set(tokio::time::sleep_until(next.into()));
} else {
self.read_timeout_sleep = Some(Box::pin(tokio::time::sleep_until(next.into())));
}
let _ = Pin::new(self.read_timeout_sleep.as_mut().unwrap()).poll(cx);
}
}
Ok(())
}
其中比较复杂的是如何判断is_read_end,因为需要区分服务端客户端或者是http/1.1及http/2,这个源码实现 http2核心,http1.1核心
写操作超时定时器
对于客户端的写,就是将请求发送到服务端,而服务端的写,刚好是将返回发送给客户端。这是数据处理的重要的一环。
在异步的处理socket中,都会将socket设置成非阻塞,也就是nonblocking,此时我们会得到一个默认的内核缓冲区大小。如果在该缓冲区未满前,我们写入数据将是0等待,该缓冲区的数据将由系统进行数据发送给另一端。
如果对方不将我们的缓冲区数据读走,那么我们此时是无法在写入到该缓冲区的,如果远程端不读,我们的传输速度将会无限的接近于0KB/S。
此时如果是服务端,有数千上万个这种该连接,每分钟只读走几个字节的数据,那么没有写入操作,我们将要保持上万的空闲连接,而默认的端口连接数为65535,那么客户端将会耗尽我们的服务资源。此种为 [慢速攻击]。
该操作是通过is_write_end来判断是否写入完成。监听方式和读的一致,核心代码在 timeout,此处不再赘述
读/写操作超时定时器
该定时器是由读和写需要共同来完成的,在很多场景中,我们只关心该请求需要耗时多少时间来完成,此时我们不关心是读的时间或者写的时间,所以此时表示请求完成的需要在这个时间下完成
就比如HTTP/1.1中的Slow headers,也就是慢速头攻击。正常来说,我们的http/1.1的头类似如下:
GET / HTTP/1.1\r\n
Host : wm-proxy.com\r\n
Connection: keep-alive\r\n
Keep-Alive: 900\r\n
Content-Length: 100000000\r\n
Content_Type: application/x-www-form-urlencoded\r\n
Accept: *.*\r\n
\r\n
整个报文的结束将有一个空白的\r\n,如果没有收到该标记,服务端无法得到完整的头信息,也无法正确的把数据转成Request,那么此时客户端可以占用了大量的连接,从而使服务端拒绝服务。
此定时器判断由is_read_end及is_write_end有一方为false,监听方法略。
keep-alive操作超时定时器
在http/1.1中,端口是可以复用的,从而减少socket的反复建立关闭,并可以一定程度上快速的响应,这是端口请求完成后,又没有后续的请求,此时当前socket线路为空闲,即当前空闲线路的保持时间。
keep-alive为保持连接,此参数用的好可以极大的加速服务的访问,此参数设置不好的时候,如设置成9999s,那么客户端将会保持当前的空闲socket很久,从而另一个程度上造成了拒绝服务了。
此判定由is_idle来判定,判断当前是否空闲,也就是上一个请求已经读写均已完成,后一个请求还未进来,此时就为空闲时间。监听方法雷同,略。
测试客户端
let url = "http://www.baidu.com";
let req = Request::builder().method("GET").header(HeaderName::ACCEPT_ENCODING, "gzip").url(url).body("").unwrap(); Instant::now());
let client = Client::builder()
// 是否支持HTTP2
// .http2(false)
// 是否仅使用HTTP2
.http2_only(true)
// 连接使时时间3秒
.connect_timeout(Duration::new(5, 0))
// 设置keep-alive时间10秒
.ka_timeout(Duration::new(10, 0))
// 设置读超时5秒
// .read_timeout(Duration::new(5, 0))
// 设置写超时5秒
.write_timeout(Duration::new(5, 0))
.connect(url).await.unwrap();
//发起请求
let (mut recv, sender) = client.send2(req.into_type()).await?;
//接收请求
let mut res = recv.recv().await.unwrap();
//接收所有body数据
res.body_mut().wait_all().await;
测试服务端
let mut server = Server::new(stream, Some(addr));
// 设置读操作5秒
server.set_read_timeout(Some(Duration::new(5, 0)));
// 设置写超时5秒
server.set_write_timeout(Some(Duration::new(5, 0)));
// 设置读写超时5秒
server.set_timeout(Some(Duration::new(5, 0)));
async fn operate(req: Request<RecvStream>) -> ProtResult<Response<String>> {
let response = Response::builder()
.version(req.version().clone())
.body("Hello World\r\n".to_string())?;
Ok(response)
}
let _ = server.incoming(operate).await;
结语
时间的尺度越小,那么对时间的敏感度越高,定时器是约束,也是保护,保护不受攻击。感谢定时器给我们构建一个更加稳定的互联网世界。
点击 [关注],[在看],[点赞] 是对作者最大的支持
24. 从零用Rust编写正反向代理,细说HTTP行为中的几种定时器的更多相关文章
- bloom-server 基于 rust 编写的 rest api cache 中间件
bloom-server 基于 rust 编写的 rest api cache 中间件,他位于lb 与api worker 之间,使用redis 作为缓存内容存储, 我们需要做的就是配置proxy,同 ...
- 正反向代理、负载均衡、Nginx配置实现
一.正反向代理 1.前提 我们曾经使用翻墙软件,访问google:使用了代理软件时,需要在浏览器选项中配置代理的地址,我们仅仅有代理这个概念,并不清楚代理还有正向和反向之分. 2.正向代理(代替客户端 ...
- SSM-Spring-11:Spring中使用代理工厂Bean实现aop的四种增强
------------吾亦无他,唯手熟尔,谦卑若愚,好学若饥------------- 说说那四种增强:前置增强,后置增强,环绕增强,异常增强 那什么是代理工厂bean呢? org.springfr ...
- 程序一 用记事本建立文件src.dat,其中存放若干字符。编写程序,从文件src.dat中读取数据,统计其中的大写字母、小写字母、数字、其它字符的个数,并将这些数据写入到文件test.dat中。
用记事本建立文件src.dat,其中存放若干字符.编写程序,从文件src.dat中读取数据,统计其中的大写字母.小写字母.数字.其它字符的个数,并将这些数据写入到文件test.dat中. #inclu ...
- 详解 Java 中的三种代理模式
代理模式 代理(Proxy)是一种设计模式,提供了对目标对象另外的访问方式;即通过代理对象访问目标对象.这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能. 这里使用 ...
- 【Linux】windows下编写的脚本文件,放到Linux中无法识别格式
注意:我启动的时候遇到脚本错误 » sh startup.sh -m standalone tanghuang@bogon : command not found : command not foun ...
- 23、有一个字符串,包含n个字符,编写一函数,将此字符串中从第m个字符开始的全部字符串复制成另一个字符串
/* 有一个字符串,包含n个字符,编写一函数,将此字符串中从第m个字符开始的全部字符串复制成另一个字符串 */ #include <stdio.h> #include <stdlib ...
- 使用Squid部署代理缓存服务(标准正向、透明正反向代理)
正向代理让用户可以通过Squid服务程序获取网站页面等数据,具体工作形式又分为标准代理模式与透明代理模式.标准正向代理模式: 将网站的数据缓存在服务器本地,提高数据资源被再次访问时的效率,但用户必需在 ...
- Nginx正反向代理、负载均衡等功能实现配置
版权声明:本文为博主原创文章,未经博主允许不得转载. 目录(?)[+] 系统环境: VirtualBox Manager Centos6.4 nginx1.10.0 IP对应的机器名: IP ...
- UA池和代理池在scrapy中的应用
一.下载中间件 下载中间件(Downloader Middlewares) 位于scrapy引擎和下载器之间的一层组件. - 作用: (1)引擎将请求传递给下载器过程中, 下载中间件可以对请求进行一系 ...
随机推荐
- 结合ReentrantLock来看AQS的原理
AQS的定义 队列同步器 AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个 int 成员变量表示同步状态,通过内置的 F ...
- Verilog实现奇分频电路
在FPGA中,计数器电路用途很广,一般计数器电路都可作为分频电路.实现占空比为50的偶分频电路很好实现.但实现占空比为50的奇分频电路有点难度.下面给出一个简单例子,记录学习奇分频电路的过程. 实现占 ...
- Mapbox—geocoder搜索地点error eaching the server
Mapbox-geocoder搜索地点error eaching the server --There was an errorr eaching the server 环境说明: vue3.3.4 ...
- wget: 未找到命令
输入以下命令: yum -y install wget
- JSONP前端流程
JSONP前端流程 JSONP总体思路 后端先给前端一个接口文档. 如https://www.baidu.com/sugrec?prod=pc&wd=用户输入的文字&cb=后端要调用的 ...
- 【工具】-Reverse-DIE(Detect-It-Easy)
关于 Detect It Easy,或缩写为"DIE"是一个用于确定文件类型的程序.Detect It Easy 是一个多功能的 PE 检测工具,基于 QT 平台编写,主要用于 P ...
- 如何实现Excel中的多级数据联动
摘要:本文由葡萄城技术团队于博客园原创并首发.转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 前言 在类Excel表格应用中,常用的需求场景是根据单元格之间 ...
- MySql Workbench 迁移工具 migration 提示缺少pyodbc 2.1.8 的解决方法
想把公司的数据库转到MySQL,所以想装个MySQL测试,发现新版的MySQL(8.0.34)默认安装还是有不少问题, 一.譬如表.字段大小写的问题: lower_case_table_names=0 ...
- 【译】通过 GitHub Copilot Chat 简化代码优化和调试(AI 辅助编程)
今年3月,我们宣布了 Visual Studio 2022 的 GitHub Copilot Chat.通过 Chat, Copilot 已经超越了代码补全,提供了对代码工作原理的深入分析和解释.它支 ...
- Jmeter+Ant+Jenkins接口自动化框架(续)
前段时间给公司内部项目搭建了一套接口自动化框架,基于实际使用,需要配置自动发送邮件功能,将 执行结果发送给相关负责人.Jenkins本身也提供了一个邮件通知功能,但在提供详细的邮件内容.自定义邮 件格 ...