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

2024-01-09 19:04

本文主要是介绍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知识。

这篇关于17. 从零用Rust编写正反向代理, Rust中一些功能的实现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/588109

相关文章

Java实现检查多个时间段是否有重合

《Java实现检查多个时间段是否有重合》这篇文章主要为大家详细介绍了如何使用Java实现检查多个时间段是否有重合,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录流程概述步骤详解China编程步骤1:定义时间段类步骤2:添加时间段步骤3:检查时间段是否有重合步骤4:输出结果示例代码结语作

使用C++实现链表元素的反转

《使用C++实现链表元素的反转》反转链表是链表操作中一个经典的问题,也是面试中常见的考题,本文将从思路到实现一步步地讲解如何实现链表的反转,帮助初学者理解这一操作,我们将使用C++代码演示具体实现,同... 目录问题定义思路分析代码实现带头节点的链表代码讲解其他实现方式时间和空间复杂度分析总结问题定义给定

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在

如何使用Java实现请求deepseek

《如何使用Java实现请求deepseek》这篇文章主要为大家详细介绍了如何使用Java实现请求deepseek功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1.deepseek的api创建2.Java实现请求deepseek2.1 pom文件2.2 json转化文件2.2

python使用fastapi实现多语言国际化的操作指南

《python使用fastapi实现多语言国际化的操作指南》本文介绍了使用Python和FastAPI实现多语言国际化的操作指南,包括多语言架构技术栈、翻译管理、前端本地化、语言切换机制以及常见陷阱和... 目录多语言国际化实现指南项目多语言架构技术栈目录结构翻译工作流1. 翻译数据存储2. 翻译生成脚本

如何通过Python实现一个消息队列

《如何通过Python实现一个消息队列》这篇文章主要为大家详细介绍了如何通过Python实现一个简单的消息队列,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录如何通过 python 实现消息队列如何把 http 请求放在队列中执行1. 使用 queue.Queue 和 reque

Python如何实现PDF隐私信息检测

《Python如何实现PDF隐私信息检测》随着越来越多的个人信息以电子形式存储和传输,确保这些信息的安全至关重要,本文将介绍如何使用Python检测PDF文件中的隐私信息,需要的可以参考下... 目录项目背景技术栈代码解析功能说明运行结php果在当今,数据隐私保护变得尤为重要。随着越来越多的个人信息以电子形

使用 sql-research-assistant进行 SQL 数据库研究的实战指南(代码实现演示)

《使用sql-research-assistant进行SQL数据库研究的实战指南(代码实现演示)》本文介绍了sql-research-assistant工具,该工具基于LangChain框架,集... 目录技术背景介绍核心原理解析代码实现演示安装和配置项目集成LangSmith 配置(可选)启动服务应用场景

使用Python快速实现链接转word文档

《使用Python快速实现链接转word文档》这篇文章主要为大家详细介绍了如何使用Python快速实现链接转word文档功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 演示代码展示from newspaper import Articlefrom docx import

前端原生js实现拖拽排课效果实例

《前端原生js实现拖拽排课效果实例》:本文主要介绍如何实现一个简单的课程表拖拽功能,通过HTML、CSS和JavaScript的配合,我们实现了课程项的拖拽、放置和显示功能,文中通过实例代码介绍的... 目录1. 效果展示2. 效果分析2.1 关键点2.2 实现方法3. 代码实现3.1 html部分3.2