17. 从零开始编写一个类nginx工具, Rust中一些功能的实现
wmproxy
wmproxy
将用Rust
实现http/https
代理, socks5
代理, 反向代理, 静态文件服务器,后续将实现websocket
代理, 内外网穿透等, 会将实现过程分享出来, 感兴趣的可以一起造个轮子法
项目地址
gite: https://gitee.com/tickbh/wmproxy
github: https://github.com/tickbh/wmproxy
日志功能
为了更容易理解程序中发生的情况,我们可能想要添加一些日志语句。通常在编写应用程序时这很容易。「在某种程度上,日志记录与使用 println! 相同,只是你可以指定消息的重要性」。
在rust中定义的日志级别有5种分别为error
、warn
、info
、debug
和 trace
定义日志的级别是表示只关系这级别的日志及更高级别的日志:
定义log,则包含所有的级别
定义warn,则只会显示error
或者warn
的消息
要向应用程序添加日志记录,你需要两样东西:
- log crate,rust官方指定的日志级别库
- 一个实际将日志输出写到有用位置的适配器
当下我们选用的是流行的根据环境变量指定的适配器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::Pending
let 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的特点
- 单线程的引用计数
- 不可变引用
- 非线程安全,即仅能在单线程中使用
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,最终结果必须等于10
let mut data = data.lock().unwrap();
*data += 1;
if *data == N {
tx.send(()).unwrap();
}
});
}
rx.recv().unwrap();
assert!(*data.lock().unwrap() == 10);
}
结语
以上是三种编写Rust中常碰见的情况,也是在此项目中应用解决过的方案,在了解原理的情况下,解决问题可以有不同的思路。理解了原理,你就知道他设计的初衷,更好的帮助你学习相关的Rust知识。
17. 从零开始编写一个类nginx工具, Rust中一些功能的实现的更多相关文章
- 从零开始编写一个BitTorrent下载器
从零开始编写一个BitTorrent下载器 BT协议 简介 BT协议Bit Torrent(BT)是一种通信协议,又是一种应用程序,广泛用于对等网络通信(P2P).曾经风靡一时,由于它引起了巨大的流量 ...
- 22.编写一个类A,该类创建的对象可以调用方法showA输出小写的英文字母表。然后再编写一个A类的子类B,子类B创建的对象不仅可以调用方法showA输出小写的英文字母表,而且可以调用子类新增的方法showB输出大写的英文字母表。最后编写主类C,在主类的main方法 中测试类A与类B。
22.编写一个类A,该类创建的对象可以调用方法showA输出小写的英文字母表.然后再编写一个A类的子类B,子类B创建的对象不仅可以调用方法showA输出小写的英文字母表,而且可以调用子类新增的方法sh ...
- 35.按要求编写Java程序: (1)编写一个接口:InterfaceA,只含有一个方法int method(int n); (2)编写一个类:ClassA来实现接口InterfaceA,实现int method(int n)接口方 法时,要求计算1到n的和; (3)编写另一个类:ClassB来实现接口InterfaceA,实现int method(int n)接口 方法时,要求计算n的阶乘(n
35.按要求编写Java程序: (1)编写一个接口:InterfaceA,只含有一个方法int method(int n): (2)编写一个类:ClassA来实现接口InterfaceA,实现in ...
- 编写一个类,其中包含一个排序的方法Sort(),当传入的是一串整数,就按照从小到大的顺序输出,如果传入的是一个字符串,就将字符串反序输出。
namespace test2 { class Program { /// <summary> /// 编写一个类,其中包含一个排序的方法Sort(),当传入的是一串整数,就按照从小到大的 ...
- 二、 编写一个类,用两个栈实现队列,支持队列的基本操作(add,poll,peek)
请指教交流! package com.it.hxs.c01; import java.util.Stack; /* 编写一个类,用两个栈实现队列,支持队列的基本操作(add,poll,peek) */ ...
- 如何编写一个SQL注入工具
0x01 前言 一直在思考如何编写一个自动化注入工具,这款工具不用太复杂,但是可以用最简单.最直接的方式来获取数据库信息,根据自定义构造的payload来绕过防护,这样子就可以. 0x02 SQL注 ...
- 从零开始编写一个vue插件
title: 从零开始编写一个vue插件 toc: true date: 2018-12-17 10:54:29 categories: Web tags: vue mathjax 写毕设的时候需要一 ...
- 题目一:编写一个类Computer,类中含有一个求n的阶乘的方法
作业:编写一个类Computer,类中含有一个求n的阶乘的方法.将该类打包,并在另一包中的Java文件App.java中引入包,在主类中定义Computer类的对象,调用求n的阶乘的方法(n值由参数决 ...
- .NET 编写一个可以异步等待循环中任何一个部分的 Awaiter
林德熙 小伙伴希望保存一个文件,并且希望如果出错了也要不断地重试.然而我认为如果一直错误则应该对外抛出异常让调用者知道为什么会一直错误. 这似乎是一个矛盾的要求.然而最终我想到了一个办法:让重试一直进 ...
- 已知一个字符串S 以及长度为n的字符数组a,编写一个函数,统计a中每个字符在字符串中的出现次数
import java.util.Scanner; /** * @author:(LiberHome) * @date:Created in 2019/3/6 21:04 * @description ...
随机推荐
- ASP.NET MVC4 学习笔记-2
渲染网页-Randering Web Pages 前面示例的输出结果不是HTML,而是一个"Hello World"的字符串.为了响应浏览器的请求产生一个HTML网页,我们需要创建 ...
- Elasticsearch日常开发
2020-08-12 14:51:37 每次遇到ES开发,一般都是查询es里面的数据,今天我教大家一个简单的es的查询.废话不多说,直接上代码. 在pom文件中引入 <dependency> ...
- 堆栈式 CMOS、背照式 CMOS 和传统 CMOS 传感器的区别
光电效应 光电效应的现象是赫兹(频率的单位就是以他命名的)发现的,但是是爱因斯坦正确解释的.简单说,光或某一些电磁波,照射在某些光敏物质会产生电子,这就是光电效应. 这就将光变为了电,光信号的改变会带 ...
- PerfView专题 (第十四篇): 洞察那些 C# 代码中的短命线程
一:背景 1. 讲故事 这篇文章源自于分析一些疑难dump的思考而产生的灵感,在dump分析中经常要寻找的一个答案就是如何找到死亡线程的生前都做了一些什么?参考如下输出: 0:001> !t T ...
- 痞子衡嵌入式:恩智浦i.MX RT1xxx系列MCU启动那些事(10)- 从Serial NAND启动
大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是恩智浦i.MXRT1xxx系列MCU的Serial NAND启动. 最近越来越多的客户在咨询 i.MXRT1xxx 从 Serial N ...
- 《CUDA编程:基础与实践》读书笔记(1):CUDA编程基础
1. GPU简介 GPU与CPU的主要区别在于: CPU拥有少数几个快速的计算核心,而GPU拥有成百上千个不那么快速的计算核心. CPU中有更多的晶体管用于数据缓存和流程控制,而GPU中有更多的晶体管 ...
- BUUCTF-RE-[BJDCTF2020]BJD hamburger competition
啊这,点进去康康 dnspy反编译的题,https://www.52pojie.cn/thread-495115-1-1.html 里面有详细介绍 然后文件很多,我不知道找哪一个下手 看其他师傅的wp ...
- lea指令调用
lea指令(Load Effective Address)在x86汇编语言中的作用是将一个有效地址(即一个内存地址或寄存器地址的偏移量)加载到目标寄存器中,而不是加载一个实际的内存值. lea指令的使 ...
- ESP32C3 LEDC_PWM
LEDC_PWM LED 控制器 (LEDC) 主要用于控制 LED,也可产生 PWM 信号用于其他设备的控制,ESP32C3有 6 路通道.设置 LEDC 通道分三步完成.与 ESP32 不同 ...
- CF-1860C Game on Permutation题解
题意:在一条数轴上,Alice可以跳到在你所在点前面且值比当前所在点小的点.每回合可以向任意符合要求的点跳一次.当轮到Alice的回合同时不存在符合要求的点,Alice就赢了.Alice可以选择一个点 ...