【Rust网络编程】开发一个图片代理和统计服务

最近我使用Rust开发了一个代理服务。可以用于代理和统计图片资源的访问

例如:

http://127.0.0.1:8100/image-public/0a1e65f4-7ced-4ef0-ba7d-12ec4d14a0d4.png
->http://xxx.com:45004/image-public/0a1e65f4-7ced-4ef0-ba7d-12ec4d14a0d4.png

项目特点

  • 高性能:使用Rust语言编写
  • 异步处理:基于Tokio运行时,实现高并发的异步I/O操作
  • 精确统计:准确记录目标图片的访问次数

技术栈

  • Rust 编程语言
  • Hyper:用于HTTP服务器和客户端的快速、安全框架。性能好,偏底层,应用广泛,知名的reqwest和axum等都使用了hyper,已成为Rust网络程序生态的重要基石之一
  • Tokio:异步运行时,提供高效的I/O操作

功能介绍

  1. HTTP 代理:

    • 监听本地端口(默认8100),接收 HTTP 请求
    • 访问目标图片(路径以/image-public/开头)将被代理,转发到配置的目标服务器
  2. 图片访问统计:

    • 精确统计目标图片的访问次数
  3. 请求日志:

    • 详细记录每个请求的方法、路径和头部信息
    • 输出响应状态码和图片访问计数
  4. 错误处理:

    • 对于错误图片的请求,返回404 Not Found响应

代码

核心代码如下:

#![deny(warnings)]

use std::net::SocketAddr;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::str::FromStr;

use bytes::Bytes;
use http_body_util::{combinators::BoxBody, BodyExt, Empty, Full};
use hyper::client::conn::http1::Builder;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::upgrade::Upgraded;
use hyper::{Method, Request, Response, StatusCode, Uri};

use tokio::net::{TcpListener, TcpStream};

#[path = "../benches/support/mod.rs"]
mod support;
use support::TokioIo;

// 图片下载计数器
static IMAGE_DOWNLOAD_COUNT: AtomicUsize = AtomicUsize::new(0);

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let addr = SocketAddr::from(([127, 0, 0, 1], 8100));

    let listener = TcpListener::bind(addr).await?;
    println!("正在监听 http://{}", addr);

    loop {
        let (stream, _) = listener.accept().await?;
        let io = TokioIo::new(stream);

        tokio::task::spawn(async move {
            if let Err(err) = http1::Builder::new()
                .preserve_header_case(true)
                .title_case_headers(true)
                .serve_connection(io, service_fn(proxy))
                .with_upgrades()
                .await
            {
                println!("服务连接失败: {:?}", err);
            }
        });
    }
}

async fn proxy(
    req: Request<hyper::body::Incoming>,
) -> Result<Response<BoxBody<Bytes, hyper::Error>>, hyper::Error> {
    println!("收到请求: 方法={:?}, 路径={}, 头部={:?}", req.method(), req.uri().path(), req.headers());
    println!("请求: {:?}", req);

    if Method::CONNECT == req.method() {
        if let Some(addr) = host_addr(req.uri()) {
            tokio::task::spawn(async move {
                match hyper::upgrade::on(req).await {
                    Ok(upgraded) => {
                        if let Err(e) = tunnel(upgraded, addr).await {
                            eprintln!("服务器 IO 错误: {}", e);
                        };
                    }
                    Err(e) => eprintln!("升级错误: {}", e),
                }
            });

            Ok(Response::new(empty()))
        } else {
            eprintln!("CONNECT 主机不是 socket 地址: {:?}", req.uri());
            let mut resp = Response::new(full("CONNECT 必须连接到 socket 地址"));
            *resp.status_mut() = StatusCode::BAD_REQUEST;

            Ok(resp)
        }
    } else {
        // 检查是否是目标图片下载请求
        let is_target_image = req.uri().path().starts_with("/image-public/");

        if is_target_image {
            // 构建新的 URI
            let new_uri = format!("http://xxx.com:45004{}", req.uri().path());
            let new_uri = Uri::from_str(&new_uri).expect("无效的 URI");

            // 保存原始路径
            let original_path = req.uri().path().to_string();

            // 创建新的请求
            let (parts, body) = req.into_parts();
            let mut new_req = Request::new(body);
            *new_req.method_mut() = parts.method;
            *new_req.uri_mut() = new_uri;
            *new_req.version_mut() = parts.version;
            *new_req.headers_mut() = parts.headers;

            // 连接到实际的服务器
            let stream = TcpStream::connect(("xxx.com", 45004)).await.unwrap();
            let io = TokioIo::new(stream);

            let (mut sender, conn) = Builder::new()
                .preserve_header_case(true)
                .title_case_headers(true)
                .handshake(io)
                .await?;
            tokio::task::spawn(async move {
                if let Err(err) = conn.await {
                    println!("连接失败: {:?}", err);
                }
            });

            let resp = sender.send_request(new_req).await?;

            // 如果是目标图片请求,增加计数器
            if resp.status().is_success() || resp.status() == StatusCode::NOT_MODIFIED {
                let count = IMAGE_DOWNLOAD_COUNT.fetch_add(1, Ordering::SeqCst);
                println!("目标图片请求成功。状态码: {}. 总计数: {}", resp.status(), count + 1);
            } else if resp.status() == StatusCode::NOT_FOUND {
                println!("目标图片不存在。路径: {}", original_path);
                let mut not_found_resp = Response::new(full("Image Not Found"));
                *not_found_resp.status_mut() = StatusCode::NOT_FOUND;
                return Ok(not_found_resp);
            }

            Ok(resp.map(|b| b.boxed()))
        } else {
            // 对于非目标图片请求,返回 404 Not Found
            let mut resp = Response::new(full("Not Found"));
            *resp.status_mut() = StatusCode::NOT_FOUND;
            Ok(resp)
        }
    }
}

fn host_addr(uri: &http::Uri) -> Option<String> {
    uri.authority().and_then(|auth| Some(auth.to_string()))
}

fn empty() -> BoxBody<Bytes, hyper::Error> {
    Empty::<Bytes>::new()
        .map_err(|never| match never {})
        .boxed()
}

fn full<T: Into<Bytes>>(chunk: T) -> BoxBody<Bytes, hyper::Error> {
    Full::new(chunk.into())
        .map_err(|never| match never {})
        .boxed()
}

async fn tunnel(upgraded: Upgraded, addr: String) -> std::io::Result<()> {
    let mut server = TcpStream::connect(addr).await?;
    let mut upgraded = TokioIo::new(upgraded);

    let (from_client, from_server) =
        tokio::io::copy_bidirectional(&mut upgraded, &mut server).await?;

    println!(
        "客户端写入 {} 字节并接收 {} 字节",
        from_client, from_server
    );

    Ok(())
}

完整代码参考我的仓库:https://github.com/VinciYan/proxy_counter.git

运行效果

收到请求: 方法=GET, 路径=/image-public/0a1e65f4-7ced-4ef0-ba7d-12ec4d14a0d4.png, 头部={"content-type": "application/json", "user-agent": "PostmanRuntime/7.42.0", "accept": "*/*", "postman-token": "9fe5ee1a-ad8e-4e0d-8f65-e82090115795", "host": "127.0.0.1:8100", "accept-encoding": "gzip, deflate, br", "connection": "keep-alive", "content-length": "75"}     
请求: Request { method: GET, uri: /image-public/0a1e65f4-7ced-4ef0-ba7d-12ec4d14a0d4.png, version: HTTP/1.1, headers: {"content-type": "application/json", "user-agent": "PostmanRun
time/7.42.0", "accept": "*/*", "postman-token": "9fe5ee1a-ad8e-4e0d-8f65-e82090115795", "host": "127.0.0.1:8100", "accept-encoding": "gzip, deflate, br", "connection": "keep-alive", "content-length": "75"}, body: Body(Streaming) }
目标图片请求成功。状态码: 200 OK. 总计数: 3

参考

  • https://github.com/VinciYan/proxy_counter.git
  • Getting Started | hyper

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Arduino UNO R3自学笔记15 之 Arduino如何驱动数码管?

注意&#xff1a;学习和写作过程中&#xff0c;部分资料搜集于互联网&#xff0c;如有侵权请联系删除。 前言&#xff1a;学习使用数码管。 1.数码管介绍 数码管的一种是半导体发光器件&#xff0c;数码管可分为七段数码管和八段数码管&#xff0c;区别在于八段数码管比七段数…

L0-Linux-关卡材料提交

SSH全称Secure Shell&#xff0c;中文翻译为安全外壳&#xff0c;它是一种网络安全协议&#xff0c;通过加密和认证机制实现安全的访问和文件传输等业务。SSH 协议通过对网络数据进行加密和验证&#xff0c;在不安全的网络环境中提供了安全的网络服务。 SSH 是&#xff08;C/S…

QSqlDatabase在多线程中的使用

Qt中多线程使用数据库_qt数据库管理类支持多数据库,多线程-CSDN博客 1. 代码&#xff1a; #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QPushButton> #include <QSqlDatabase> #include <QSqlQuery> #include <QSqlError>…

【深度学习】05-Rnn循环神经网络-01- 自然语言处理概述/词嵌入层/循环网络/文本生成案例精讲

循环神经网络&#xff08;RNN&#xff09;主要用于自然语言处理的。 循环神经网络&#xff08;RNN&#xff09;、卷积神经网络&#xff08;CNN&#xff09;和全连接神经网络&#xff08;FCN&#xff09;是三种常见的神经网络类型&#xff0c;各自擅长处理不同类型的数据。下面…

RabbitMQ应用

RabbitMQ 共提供了7种⼯作模式, 进⾏消息传递 一、七种模式的概述 1、Simple(简单模式) P:生产者,就是发送消息的程序 C:消费者,就是接收消息的程序 Queue:消息队列,类似⼀个邮箱, 可以缓存消息; ⽣产者向其中投递消息, 消费者从其中取出消息 特点: ⼀个⽣产者P,⼀…

负载均衡--相关面试题(六)

在负载均衡的面试中&#xff0c;可能会遇到一系列涉及概念、原理、实践应用以及技术细节的问题。以下是一些常见的负载均衡面试题及其详细解答&#xff1a; 一、什么是负载均衡&#xff1f; 回答&#xff1a;负载均衡是一种将网络请求或数据传输工作分配给多个服务器或网络资源…

SpringSession微服务

一.在linux中确保启动起来redis和nacos 依赖记得别放<dependencyManagement></dependencyManagement>这个标签去了 1.首先查看已经启动的服务 docker ps 查看有没有安装redis和nacos 2.启动redis和nacos 发现没有启动redis和nacos,我们先来启动它。&#xff0c;…

计算机视觉学习路线:从基础到进阶

计算机视觉学习路线&#xff1a;从基础到进阶 计算机视觉&#xff08;Computer Vision&#xff09;是人工智能和机器学习领域中重要的分支&#xff0c;致力于让计算机能够理解和分析图像、视频等视觉信息。随着深度学习的发展&#xff0c;计算机视觉的应用变得越来越广泛&…

音视频入门基础:FLV专题(7)——Tag header简介

一、引言 从《音视频入门基础&#xff1a;FLV专题&#xff08;3&#xff09;——FLV header简介》中可以知道&#xff0c; 在FLV header之后&#xff0c;FLV文件剩下的部分应由PreviousTagSize和Tag组成。FLV文件 FLV header PreviousTagSize0 Tag1 PreviousTagSize1 Ta…

【C++】“list”的介绍和常用接口的模拟实现

【C】“list”的介绍和常用接口的模拟实现 一. list的介绍1. list常见的重要接口2. list的迭代器失效 二. list常用接口的模拟实现&#xff08;含注释&#xff09;三. list与vector的对比 一. list的介绍 list是可以在常数范围内在任意位置进行插入和删除的序列式容器&#xf…

2025 年 IT 前景:机遇与挑战并存,人工智能和云计算成重点

云计算de小白 投资人工智能&#xff1a;平衡潜力与实用性 到 2025 年&#xff0c;人工智能将成为 IT 支出的重要驱动力&#xff0c;尤其是在生成式人工智能领域。人工智能的前景在于它有可能彻底改变业务流程、增强决策能力并开辟新的收入来源。然而&#xff0c;现实情况更加微…

SpringCloud源码:服务端分析(二)- EurekaServer分析

背景 从昨日的两篇文章&#xff1a;SpringCloud源码&#xff1a;客户端分析&#xff08;一&#xff09;- SpringBootApplication注解类加载流程、SpringCloud源码&#xff1a;客户端分析&#xff08;二&#xff09;- 客户端源码分析。 我们理解了客户端的初始化&#xff0c;其实…

Python画笔案例-071 绘制闪闪的红星

1、绘制通闪闪的红星 通过 python 的turtle 库绘制 闪闪的红星,如下图: 2、实现代码 绘制闪闪的红星,以下为实现代码: """闪闪的红星.py """ import time import turtledef xsleep(n):"""防

通信工程学习:什么是MAC媒体接入控制

MAC&#xff1a;媒体接入控制 MAC&#xff08;Medium Access Control&#xff09;&#xff0c;即媒体接入控制&#xff0c;是计算机网络中数据链路层的一个重要组成部分&#xff0c;负责协调多个发送和接收站点对一个共享传输媒体的占用。以下是关于MAC的详细解释&#xff1a; …

系统架构设计师-知识产权与标准化

目录 一、保护范围与对象 二、保护期限 三、知识产权人确定 四、侵权判断 五、标准化 一、保护范围与对象 知识产权是权利人依法就下列课题享有的专有权利&#xff1a; &#xff08;一&#xff09;作品&#xff08;著作&#xff09; &#xff08;二&#xff09;发明、实用…

泰勒图 ——基于相关性与标准差的多模型评价指标可视化比较-XGBoost、sklearn

1、基于相关性与标准差的多模型评价指标可视化比较 # 数据读取并分割 import pandas as pd import numpy as np import matplotlib.pyplot as plt from sklearn.model_selection import train_test_split plt.rcParams[font.family] = Times New Roman plt.rcParams[axes.unic…

针对考研的C语言学习(2019链表大题)

题目解析&#xff1a; 【考】双指针算法&#xff0c;逆置法&#xff0c;归并法。 解析&#xff1a;因为题目要求空间复杂度为O(1)&#xff0c;即不能再开辟一条链表&#xff0c;因此我们只能用变量来整体挪动原链表。 第一步先找出中间节点 typedef NODE* Node; Node find_m…

Linux-基础篇-磁盘分区,挂载

Linux 分区 原理介绍 Linux 来说无论有几个分区&#xff0c;分给哪一目录使用&#xff0c;它归根结底就只有一个根目录&#xff0c;一个独立且唯一的文件结构 , Linux 中每个分区都是用来组成整个文件系统的一部分。 Linux 采用了一种叫 “ 载入 ” 的处理方法&#xff0c;…

为什么有必要由母语人士翻译应用程序界面

在当今技术已成为我们生活不可或缺的一部分的世界中&#xff0c;移动应用接口在我们与数字空间的互动中发挥着关键作用。然而&#xff0c;无论应用程序本身多么完美&#xff0c;它的有效性可能会因糟糕地翻译而大大降低。这就是为什么&#xff0c;为了翻译应用程序界面&#xf…

在线css像素px到Em的转换器

具体请前往&#xff1a;在线Px转Em工具--将绝对像素(px)长度单位转换为相对长度em