17. 从零用Rust编写正反向代理, Rust中一些功能的实现

wmproxy

wmproxy是由Rust编写,已实现http/https代理,socks5代理, 反向代理,静态文件服务器,内网穿透,配置热更新等, 后续将实现websocket代理等,同时会将实现过程分享出来, 感兴趣的可以一起造个轮子法

项目地址

gite: https://gitee.com/tickbh/wmproxy

github: https://github.com/tickbh/wmproxy

日志功能

为了更容易理解程序中发生的情况,我们可能想要添加一些日志语句。通常在编写应用程序时这很容易。「在某种程度上,日志记录与使用 println! 相同,只是你可以指定消息的重要性」。
在rust中定义的日志级别有5种分别为errorwarninfodebugtrace
定义日志的级别是表示只关系这级别的日志及更高级别的日志:

定义log,则包含所有的级别
定义warn,则只会显示error或者warn的消息

要向应用程序添加日志记录,你需要两样东西:

  1. log crate,rust官方指定的日志级别库
  2. 一个实际将日志输出写到有用位置的适配器

当下我们选用的是流行的根据环境变量指定的适配器env_logger,它会根据环境变量中配置的值,日志等级,或者只开启指定的库等功能,或者不同的库分配不同的等级等。

Linux或者MacOs上开启功能

env RUST_LOG=debug cargo run 

Windows PowerShell上开启功能

$env:RUST_LOG="debug"
cargo run

Windows CMD上开启功能

set RUST_LOG="debug"
cargo run

如果我们指定库等级可以设置

RUST_LOG="info,wenmeng=warn,webparse=warn"

这样就可以减少第三方库打日志给程序带来的干扰

需要在Cargo.toml中引用

[dependencies]
log = "0.4.20"
env_logger = "0.10.0"

以下是示意代码

use log::{info, warn};
fn main() {env_logger::init();info!("欢迎使用软件wmproxy");warn!("现在已经成功启动");
}

println!将会直接输出到stdout,当日志数据多的时候,无法进行关闭,做为第三方库,就不能干扰引用库的正常看日志,所以这只能调试的时候使用,或者少量的关键地方使用。

多个TcpListener的Accept

因为当前支持多个端口绑定,或者配置没有配置,存在None的情况,我们需要同时在一个线程中await所有的TcpListener。
在这里我们先用的是tokio::select!对多个TcpListener同时进行await。
如果此时我们没有绑定proxy的绑定地址,此时listener为None,但我们需要进行判断才知道他是否为None,如果我们用以下写法:

use tokio::net::TcpListener;
use std::io;#[tokio::main]
async fn main() -> io::Result<()> {let mut listener: Option<TcpListener> = None;tokio::select! {// 加了if条件判断是否有值Ok((conn, addr)) = listener.as_mut().unwrap().accept(), if listener.is_some() => {println!("accept addr = {:?}", addr);}}Ok(())
}

此时我们试运行,依然报以下错误:

thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', examples/udp.rs:9:46

也就是即使加了if条件我们也正确的执行我们的操作,因为tokio::select的每个分支必须返回Fut,此时如果为None,就不能返回Fut违反了该函数的定义,那么我们做以下封装:

async fn tcp_listen_work(listen: &Option<TcpListener>) -> Option<(TcpStream, SocketAddr)> {if listen.is_some() {match listen.as_ref().unwrap().accept().await {Ok((tcp, addr)) => Some((tcp, addr)),Err(_e) => None,}} else {// 如果为None的时候,就永远返回Poll::Pendinglet pend = std::future::pending();let () = pend.await;None}
}

如果为None的话,将其返回Poll::Pending,则该分支await的时候永远不会等到结果。
那么最终的的代码示意如下:

#[tokio::main]
async fn main() -> io::Result<()> {let listener: Option<TcpListener> = TcpListener::bind("127.0.0.1:8090").await.ok();tokio::select! {Some((conn, addr)) = tcp_listen_work(&listener) => {println!("accept addr = {:?}", addr);}}Ok(())
}

另一种在反向代理的时候因为server的数量是不定的,所以监听的TcpListener也是不定的,此时我们用Vec<TcpListener>来做表示,那么此时,我们如何通过tokio::select来一次性await所有的accept呢?
此时我们借助futures库中的select_all来监听,但是select_all又不允许空的Vec,因为他要返回一个Fut,空的无法返回一个Fut,所以此时我们也要对其进行封装:

async fn multi_tcp_listen_work(listens: &mut Vec<TcpListener>) -> (io::Result<(TcpStream, SocketAddr)>, usize) {if !listens.is_empty() {let (conn, index, _) = select_all(listens.iter_mut().map(|listener| listener.accept().boxed())).await;(conn, index)} else {let pend = std::future::pending();let () = pend.await;unreachable!()}
}

此时监听从8091-8099,我们的最终代码:

#[tokio::main]
async fn main() -> io::Result<()> {let listener: Option<TcpListener> = TcpListener::bind("127.0.0.1:8090").await.ok();let mut listeners = vec![];for i in 8091..8099 {listeners.push(TcpListener::bind(format!("127.0.0.1:{}", i)).await?);}tokio::select! {Some((conn, addr)) = tcp_listen_work(&listener) => {println!("accept addr = {:?}", addr);}(result, index) = multi_tcp_listen_work(&mut listeners) => {println!("index receiver = {:?}", index)}}Ok(())
}

如果此时我们用

telnet 127.0.0.1 8098

那么我们就可以看到输出:

index receiver = 7

表示代码已正确的执行。

Rust中数据在多个线程中的共享

Rust中每个对象的所有权都仅只能有一个对象拥有,那么我们数据在在多个地方共享的时候可以怎么办呢?
在单线程中,我们可以用use std::rc::Rc;

Rc的特点
  1. 单线程的引用计数
  2. 不可变引用
  3. 非线程安全,即仅能在单线程中使用
    Rc引用计数中还有一个弱引用称为Weak,弱引用表示持有对象的一个指针,但是不添加引用计数,也不会影响数据删除,不保证一定能取得到数据。
    因为其不能修改数据,所以也常用RefCell做配合,来做引用计数的修改。
    以下是一个父类子类用弱引用计数实现的方案:
use std::rc::Rc;
use std::rc::Weak;
use std::cell::RefCell;/// 父类拥有者
struct Owner {name: String,gadgets: RefCell<Vec<Weak<Gadget>>>,
}/// 子类对象
struct Gadget {id: i32,owner: Rc<Owner>,
}fn main() {let gadget_owner: Rc<Owner> = Rc::new(Owner {name: "wmproxy".to_string(),gadgets: RefCell::new(vec![]),});// 生成两个小工具let gadget1 = Rc::new(Gadget {id: 1,owner: Rc::clone(&gadget_owner),});let gadget2 = Rc::new(Gadget {id: 2,owner: Rc::clone(&gadget_owner),});{let mut gadgets = gadget_owner.gadgets.borrow_mut();gadgets.push(Rc::downgrade(&gadget1));gadgets.push(Rc::downgrade(&gadget2));}for gadget_weak in gadget_owner.gadgets.borrow().iter() {let gadget = gadget_weak.upgrade().unwrap();println!("小工具 {} 的拥有者:{}", gadget.id, gadget.owner.name);}
}

因为其并未实现Send函数,所以无法在多线程种传递。在多线程中,我们需要用Arc,但是在Arc获取可变对象的时候有限制,必须他是唯一引用的时候才能修改。

use std::sync::Arc;
fn main() {let mut x = Arc::new(3);*Arc::get_mut(&mut x).unwrap() = 4;assert_eq!(*x, 4);let _y = Arc::clone(&x);assert!(Arc::get_mut(&mut x).is_none());
}

所以我们在多线程中的引用需要修改的时候,通常会用Atomic或者Mutex来做数据的写入的唯一性。

#![allow(unused)]
fn main() {use std::sync::{Arc, Mutex};use std::thread;use std::sync::mpsc::channel;const N: usize = 10;let data = Arc::new(Mutex::new(0));let (tx, rx) = channel();for _ in 0..N {let (data, tx) = (Arc::clone(&data), tx.clone());thread::spawn(move || {// 共享数据data,保证在线程中只会同时有一个对象拥有修改权限,也相当于拥有所有权,10个线程,每个线程+1,最终结果必须等于10let mut data = data.lock().unwrap();*data += 1;if *data == N {tx.send(()).unwrap();}});}rx.recv().unwrap();assert!(*data.lock().unwrap() == 10);
}

结语

以上是三种编写Rust中常碰见的情况,也是在此项目中应用解决过的方案,在了解原理的情况下,解决问题可以有不同的思路。理解了原理,你就知道他设计的初衷,更好的帮助你学习相关的Rust知识。

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

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

相关文章

vite前端工具链,为开发提供极速响应

一、概念 Vite是一个高性能的分布式智能合约平台。它使用了一种名为“异步架构”的设计&#xff0c;能够支持高吞吐量和低延迟的交易处理。Vite采用了基于DAG&#xff08;有向无环图&#xff09;的账本结构&#xff0c;可以实现并行处理多个交易&#xff0c;并且具有快速确认的…

【Golang】十六进制字符串转二进制字符串

使用Go语言将十六进制转换为二进制 在计算机科学中&#xff0c;我们经常需要在不同进制的数字之间进行转换。Go语言提供了一系列的标准库来方便我们进行这些进制转换。本文将介绍如何使用Go语言将十六进制字符串转换为二进制字符串。 函数概述 首先&#xff0c;我们定义一个…

[Oracle][详细] Win完全卸载Oracle

前提准备 进入服务 找到Oracle开头的服务 将这些服务全部停止 Top 1 点击开始菜单找到Oracle,然后点击Oracle安装产品,再点击【Universal Installer】 Top 2 点击之后稍等一会然后会进入进入下图界面,点击卸载产品 Top 3 选中要删除的Oracle产品,然后点击【删除】 Top 4 进…

性能监控软件选择攻略

随着企业对应用程序性能的关注度不断增加&#xff0c;选择适当的性能监控软件变得至关重要。性能监控软件能够帮助企业实时追踪应用程序的性能指标&#xff0c;识别潜在问题并提高系统的稳定性。在选择性能监控软件时&#xff0c;以下攻略将有助于确保您的选择符合业务需求并能…

SpringIOC之support模块GenericApplicationContext

博主介绍:✌全网粉丝5W+,全栈开发工程师,从事多年软件开发,在大厂呆过。持有软件中级、六级等证书。可提供微服务项目搭建与毕业项目实战,博主也曾写过优秀论文,查重率极低,在这方面有丰富的经验✌ 博主作品:《Java项目案例》主要基于SpringBoot+MyBatis/MyBatis-plus+…

鸿蒙开发笔记(一):ArkTS概述及声明式UI的使用

ArkTS是HarmonyOS优选的主力应用开发语言。ArkTS围绕应用开发在TypeScript&#xff08;简称TS&#xff09;生态基础上做了进一步扩展&#xff0c;继承了TS的所有特性&#xff0c;是TS的超集。 ArkTS在TS的基础上主要扩展了如下能力&#xff1a; 基本语法&#xff1a;ArkTS定义…

Docker实战06|深入剖析Docker Run命令

前几篇文章中&#xff0c;重点讲解了Linux Namespace、Cgroups、AUFS的核心原理&#xff0c;同样也是Docker的底层原理实现。目录如下&#xff1a; • 《Docker实战01&#xff5c;容器与开发语言》 • 《Docker实战02&#xff5c;Namespace》 • 《Docker实战03&#xff5c;C…

软件测试公式之如何高质量的做BUG分析?

对于BUG分析&#xff0c;测试人员再熟悉不过了。但如果是面对大量的BUG&#xff0c;要如何有效的分析呢&#xff1f;有什么好的方案和行动项&#xff1f;今天聊聊这个话题。 01 BUG分析简单可以分为两类&#xff1a;宏观BUG分析 和 微观BUG分析。 宏观BUG分析&#xff1a;在…

响应式Web开发项目教程(HTML5+CSS3+Bootstrap)第2版 例3-5 CSS3 动画

代码 <!doctype html> <html> <head> <meta charset"utf-8"> <title>CSS3 动画</title> <style> .img {width: 150px; } keyframes rotate { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg);} } img…

【C++】入门与过渡

【C】入门与过渡 文章目录 【C】入门与过渡一、命名空间二、输入输出(cin/cout)三、缺省参数四、函数重载五、引用作用使用注指针和引用的区别 六、内联函数七、auto关键字八、空指针 注&#xff1a;本文为学习笔记&#xff0c;只记录了一些重点&#xff0c;有些比较简单的内容…

使用boost.hana在编译期加密字符串

在当今数字时代&#xff0c;软件安全问题愈发凸显&#xff0c;攻击者利用各种手段对应用程序进行破解和逆向工程的尝试也日益猖獗。其中&#xff0c;通过使用OllyDbg、IDA等软件加载应用程序&#xff0c;分析程序中的字符串&#xff0c;进而找到关键条件判断&#xff0c;实施软…

【python】进阶--->MySQL数据库(一)

一、mysql数据库 关系型数据库 &#xff1a; 一些相关的表和其他数据库对象的集合。 表是由行和列组成。列包含一组命名的属性(也称为字段)。 行包含一组记录&#xff0c;行和列的交集称为数据项(也叫字段值)。 数据库(database) : 存储数据的仓库&#xff0c;本质上就是一个文…

《C++ Primer》第14章 重载运算与类型转换(二)

参考资料&#xff1a; 《C Primer》第5版《C Primer 习题集》第5版 14.8 函数调用运算符&#xff08;P506&#xff09; 如果类重载了函数调用运算符&#xff0c;则我们可以像使用函数一样使用该类的对象。这样的类同时也能存储状态&#xff0c;所以它们比普通函数更加灵活。…

x-cmd pkg | czg - git commit 智能生成工具

目录 简介首次用户功能特点竞品和相关作品进一步探索 简介 czg 源于 commitizen/cz-cli 交互插件中 cz-git 的延伸项目&#xff0c;重新使用 TypeScript 编写的零依赖独立的 Node.js 命令行工具。旨在使用交互友好的方式&#xff0c;辅助用户生成规范的 git commit message 约…

游泳耳机哪种款式好?最值得入手的游泳耳机大全

在如今注重健康和娱乐的生活方式中&#xff0c;游泳作为一项全身性的运动备受欢迎。然而&#xff0c;对于热爱水中活动的人们来说&#xff0c;选择一款出色的游泳耳机至关重要。好的游泳耳机不仅能提供清晰的音质&#xff0c;还能有效防水&#xff0c;让您在水中尽情畅游的同时…

MySQL中约束是什么?

&#x1f389;欢迎您来到我的MySQL基础复习专栏 ☆* o(≧▽≦)o *☆哈喽~我是小小恶斯法克&#x1f379; ✨博客主页&#xff1a;小小恶斯法克的博客 &#x1f388;该系列文章专栏&#xff1a;重拾MySQL &#x1f379;文章作者技术和水平很有限&#xff0c;如果文中出现错误&am…

记录由客户端http请求原因引起的5xx响应问题排查过程

看到 http 状态码 5xx&#xff0c;很多开发者第一感觉就是服务端的问题&#xff0c;其实并不全是。下面我遇到的问题就是一个例外。 问题描述 最近在为反向代理 nginx 配置 auth_request 后&#xff0c;出现了请求504错误。 504状态码是HTTP协议中的一种服务器错误状态码。当…

Windows压缩包的MySQL安装方式

1.下载压缩包 https://cdn.mysql.com//Downloads/MySQL-8.0/mysql-8.0.35-winx64.zip 2.解压压缩包&#xff08;建议将解压到非C盘&#xff0c;路径不要出现特殊符号&#xff09; 3.在MySQL主目录下&#xff0c;创建my.ini空文件&#xff08;先创建一个txt文件&#xff0c;进…

如何改造现有文件为 CMD 模块

如何改造现有文件为 CMD 模块 经过一段考察&#xff0c;我们终于要在项目中引入模块机制和 Sea.js 了&#xff0c;那么如何将现有的文件改造成 CMD 模块就成了重要的问题。下面针对一些典型场景来说明包装的方式。 首先还是请大家详细了解下 CMD 模块定义规范&#xff0c;只要…

JavaScript删除数组中指定元素的5种方法

文章目录 目录 文章目录 前言 一、数组是什么&#xff1f; 二、讲解数组 总结 前言 在JavaScript开发中&#xff0c;处理数组是一项非常常见的任务。有时候我们需要从数组中删除特定的元素&#xff0c;以便对数组进行进一步操作或者满足特定的需求。幸运的是&#xff0c;JavaS…