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协议。

主要有以下列行为定时器

  1. 连接超时定时器
  2. 读操作超时定时器
  3. 写操作超时定时器
  4. 读/写操作超时定时器(在规则的时间内同时完成读和写)
  5. 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连接建立的最长时间,如果没有约定,将由系统连接超时的或者确认不可达的时候才返回失败。

  如果没有连接超时,那么以下我们的型业务场景模拟:

  1. 在弱网的环境下,比如手机信号不好的地方,一次连接请求建立可能会花费10秒左右才能返回,那么如果我们没有连接超时,用户在等待了8秒,客户端的界面都没有办法给出任何的响应,就会认为服务端出现了问题或者频繁的重启客户端应用不断的进行重试而得不到预期的反馈。在App设计中,对这种情况的用户体验极差,需要在指定的时间内给用户反馈出当前网络无法访问。

  2. 客户端的设计模型中,如果有定时请求或者埋点数据之类,如果没有超时机制,容易出现短时间内打开的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请求,只有得到完整的数据才能进行处理,少部分如文件上传这种可以边上传边操作,而是否能开始操作关系到请求的响应时间。

读超时大概有以下的可能:

  1. 服务器处理请求的时间太长,导致客户端等待超时。
  2. 服务器返回的数据量过大,导致客户端读取数据的时间超过了规定的时间。
  3. 网络延迟或网络不稳定,导致客户端无法在规定的时间内读取完数据。

这造成客户端无法及时处理数据,可以报错好让客户端换备用线路或者备用服务器等以便及时的处理数据。

读操作我们不管服务端或者客户端,不管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_endis_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行为中的几种定时器的更多相关文章

  1. bloom-server 基于 rust 编写的 rest api cache 中间件

    bloom-server 基于 rust 编写的 rest api cache 中间件,他位于lb 与api worker 之间,使用redis 作为缓存内容存储, 我们需要做的就是配置proxy,同 ...

  2. 正反向代理、负载均衡、Nginx配置实现

    一.正反向代理 1.前提 我们曾经使用翻墙软件,访问google:使用了代理软件时,需要在浏览器选项中配置代理的地址,我们仅仅有代理这个概念,并不清楚代理还有正向和反向之分. 2.正向代理(代替客户端 ...

  3. SSM-Spring-11:Spring中使用代理工厂Bean实现aop的四种增强

    ------------吾亦无他,唯手熟尔,谦卑若愚,好学若饥------------- 说说那四种增强:前置增强,后置增强,环绕增强,异常增强 那什么是代理工厂bean呢? org.springfr ...

  4. 程序一 用记事本建立文件src.dat,其中存放若干字符。编写程序,从文件src.dat中读取数据,统计其中的大写字母、小写字母、数字、其它字符的个数,并将这些数据写入到文件test.dat中。

    用记事本建立文件src.dat,其中存放若干字符.编写程序,从文件src.dat中读取数据,统计其中的大写字母.小写字母.数字.其它字符的个数,并将这些数据写入到文件test.dat中. #inclu ...

  5. 详解 Java 中的三种代理模式

    代理模式 代理(Proxy)是一种设计模式,提供了对目标对象另外的访问方式;即通过代理对象访问目标对象.这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能. 这里使用 ...

  6. 【Linux】windows下编写的脚本文件,放到Linux中无法识别格式

    注意:我启动的时候遇到脚本错误 » sh startup.sh -m standalone tanghuang@bogon : command not found : command not foun ...

  7. 23、有一个字符串,包含n个字符,编写一函数,将此字符串中从第m个字符开始的全部字符串复制成另一个字符串

    /* 有一个字符串,包含n个字符,编写一函数,将此字符串中从第m个字符开始的全部字符串复制成另一个字符串 */ #include <stdio.h> #include <stdlib ...

  8. 使用Squid部署代理缓存服务(标准正向、透明正反向代理)

    正向代理让用户可以通过Squid服务程序获取网站页面等数据,具体工作形式又分为标准代理模式与透明代理模式.标准正向代理模式: 将网站的数据缓存在服务器本地,提高数据资源被再次访问时的效率,但用户必需在 ...

  9. Nginx正反向代理、负载均衡等功能实现配置

    版权声明:本文为博主原创文章,未经博主允许不得转载.   目录(?)[+]   系统环境: VirtualBox Manager Centos6.4 nginx1.10.0 IP对应的机器名: IP ...

  10. UA池和代理池在scrapy中的应用

    一.下载中间件 下载中间件(Downloader Middlewares) 位于scrapy引擎和下载器之间的一层组件. - 作用: (1)引擎将请求传递给下载器过程中, 下载中间件可以对请求进行一系 ...

随机推荐

  1. mysql注释的方法

    单行注释:"#", "--", 多行注释:/**/ 参考链接:https://www.cnblogs.com/JiangLe/articles/6897403. ...

  2. 小白也能搞定!Windows10上CUDA9.0+CUDNN7.0.5的完美安装教程

    前言: 为什么要在本地电脑安装 CUDA,CUDA 是什么的,用来做什么?我想,点击标题进来的小伙伴,应该都清楚这些.不管你是用来做什么,或者跟我一样为了跑 Tensorflow 的 Object D ...

  3. nodejs中如何使用http创建一个服务

    http模块是nodejs中非常重要的一部分,用于开启一个服务,我们可以用它自定义接口供客户端使用.   开启服务的方式也比较简单,几行代码就可以搞定 const http = require('ht ...

  4. 使用logrotate定期切割nginx日志

    前言 默认情况下,nginx的日志都会写到access.log文件中,访问流量大的话,日志文件很快就会膨胀到几十G,不方便分析处理,也占用硬盘空间.借助linux自带的logrotate工具可以很方便 ...

  5. 清理MySQL的binlog历史文件

    前言 系统版本:centos 7 MySQL版本:5.7 mysql的binlog文件最好不要手动删,避免删错导致bin log同步异常. 步骤 查看当前的binlog文件 show binary l ...

  6. TensorRT 模型加密杂谈

    在大多数项目交付场景中,经常需要对部署模型进行加密.模型加密一方面可以防止泄密,一方面可以便于模型跟踪管理,防止混淆. 由于博主使用的部署模型多为TensorRT格式,这里以TensorRT模型为例, ...

  7. AVR汇编(六):分支指令

    AVR汇编(六):分支指令 分支指令用于改变程序的执行流,分为无条件分支和条件分支两类. 无条件分支指令 JMP JMP 指令用于无条件跳转,类似于C中的 goto 关键字, JMP 指令的跳转范围为 ...

  8. 开机自动打开termux以及启动termux的服务

    ps:因为我们的服务是安装在平板上面的termux,客户不想维护麻烦,如果平板重启之后还需要手动启动ternux,还要开启命令启动服务,这样比较麻烦,所以研究如下操作 1.安装macroDroid 直 ...

  9. 《Linux基础》02. 目录结构 · vi、vim · 关机 · 重启

    @ 目录 1:目录结构 2:vi.vim快速入门 2.1:vi 和 vim 的三种模式 2.1.1:一般模式 2.1.2:编辑模式 2.1.3:命令模式 2.2:常用快捷键 2.2.1:一般模式 2. ...

  10. role

    角色权限管理改造方案 #   为什么需要角色 现有的权限方案 .net后台权限管理 在后台类中配置,权限 = 一级菜单:二级菜单:三级菜单: 通过在view模板中判断是否有权限显示菜单 后端通过权限配 ...