Java 文件下载/上传限流算法

2024-09-04 17:18
文章标签 java 算法 下载 上传 限流

本文主要是介绍Java 文件下载/上传限流算法,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

    • 一、算法思路
    • 二、限流的完整java代码实现
    • 三、注意点
    • 四、具体demo的github地址

在做文件下载功能时,为了避免下载功能将服务器的带宽打满,从而影响服务器的其他服务。我们可以设计一个限流器来限制下载的速率,从而限制下载服务所占用的带宽。

一、算法思路

定义一个数据块chunk(单位 bytes)以及允许的最大速率 maxRate(单位 KB/s)。通过maxRate我们可以算出,在maxRate的速率下,通过一个数据块大小的字节流所需要的时间 timeCostPerChunk

之后,在读取/写入字节时,我们维护已经读取/写入的字节量 bytesWillBeSentOrReceive

bytesWillBeSentOrReceive达到一个数据块的大小时,检查期间消耗的时间(nowNanoTime-lastPieceSentOrReceiveTick)

如果期间消耗的时间小于timeCostPerChunk的值,说明当前的速率已经超过了 maxRate的速率,这时候就需要休眠一会来限制流量

如果速率没超过或者休眠完后,将 bytesWillBeSentOrReceive=bytesWillBeSentOrReceive-chunkSize

之后在读取/写入数据时继续检查。

下面该算法的Java代码实现:

    public synchronized void limitNextBytes(int len) {//累计bytesWillBeSentOrReceivethis.bytesWillBeSentOrReceive += len;//如果积累的bytesWillBeSentOrReceive达到一个chunk的大小,就进入语句块操作while (this.bytesWillBeSentOrReceive > CHUNK_LENGTH) {long nowTick = System.nanoTime();//计算积累数据期间消耗的时间long passTime = nowTick - this.lastPieceSentOrReceiveTick;//timeCostPerChunk表示单个块最多需要多少纳秒//如果missedTime大于0,说明此时流量进出的速率已经超过maxRate了,需要休眠来限制流量long missedTime = this.timeCostPerChunk - passTime;if (missedTime > 0) {try {Thread.sleep(missedTime / 1000000, (int) (missedTime % 1000000));} catch (InterruptedException e) {LOGGER.error(e.getMessage(), e);}}this.bytesWillBeSentOrReceive -= CHUNK_LENGTH;//重置最后一次检查时间this.lastPieceSentOrReceiveTick = nowTick + (missedTime > 0 ? missedTime : 0);}}

二、限流的完整java代码实现

限流器的实现

public class BandwidthLimiter {private static final Logger LOGGER = LoggerFactory.getLogger(BandwidthLimiter.class);//KB代表的字节数private static final Long KB = 1024L;//一个chunk的大小,单位byte。设置一个块的大小为1Mprivate static final Long CHUNK_LENGTH = 1024 * 1024L;//已经发送/读取的字节数private int bytesWillBeSentOrReceive = 0;//上一次接收到字节流的时间戳——单位纳秒private long lastPieceSentOrReceiveTick = System.nanoTime();//允许的最大速率,默认为 1024KB/sprivate int maxRate = 1024;//在maxRate的速率下,通过chunk大小的字节流要多少时间(纳秒)private long timeCostPerChunk = (1000000000L * CHUNK_LENGTH) / (this.maxRate * KB);public BandwidthLimiter(int maxRate) {this.setMaxRate(maxRate);}//动态调整最大速率public void setMaxRate(int maxRate) {if (maxRate < 0) {throw new IllegalArgumentException("maxRate can not less than 0");}this.maxRate = maxRate;if (maxRate == 0) {this.timeCostPerChunk = 0;} else {this.timeCostPerChunk = (1000000000L * CHUNK_LENGTH) / (this.maxRate * KB);}}public synchronized void limitNextBytes() {this.limitNextBytes(1);}public synchronized void limitNextBytes(int len) {this.bytesWillBeSentOrReceive += len;while (this.bytesWillBeSentOrReceive > CHUNK_LENGTH) {long nowTick = System.nanoTime();long passTime = nowTick - this.lastPieceSentOrReceiveTick;long missedTime = this.timeCostPerChunk - passTime;if (missedTime > 0) {try {Thread.sleep(missedTime / 1000000, (int) (missedTime % 1000000));} catch (InterruptedException e) {LOGGER.error(e.getMessage(), e);}}this.bytesWillBeSentOrReceive -= CHUNK_LENGTH;this.lastPieceSentOrReceiveTick = nowTick + (missedTime > 0 ? missedTime : 0);}}
}

有了限流器后,现在我们要对下载功能做限流。因为java的io流的设计是装饰器模式,因此我们可以方便的封装一个我们自己的InputStream

public class LimitInputStream extends InputStream {private InputStream inputStream;private BandwidthLimiter bandwidthLimiter;public LimitInputStream(InputStream inputStream, BandwidthLimiter bandwidthLimiter) {this.inputStream = inputStream;this.bandwidthLimiter = bandwidthLimiter;}@Overridepublic int read() throws IOException {if (bandwidthLimiter != null) {bandwidthLimiter.limitNextBytes();}return inputStream.read();}@Overridepublic int read(byte[] b, int off, int len) throws IOException {if (bandwidthLimiter != null) {bandwidthLimiter.limitNextBytes(len);}return inputStream.read(b, off, len);}@Overridepublic int read(byte[] b) throws IOException {if (bandwidthLimiter != null && b.length > 0) {bandwidthLimiter.limitNextBytes(b.length);}return inputStream.read(b);}
}

后面我们使用这个LimitInputStream来读取文件,每次读取一块数据,限流器都会检查当前的速率是否超过指定的最大速率。这样就能间接的达到限制下载速率的目的了。

附上SpringMVC的一个下载限流的demo:

    @GetMapping("/limit")public void limitDownloadFile(String file, HttpServletResponse response) throws IOException {LOGGER.info("download file");if (file == null) {file = "/tmp/test.txt";}File downloadFile = new File(file);FileInputStream fileInputStream = new FileInputStream(downloadFile);response.setContentType("application/x-msdownload;");response.setHeader("Content-disposition", "attachment; filename=" + new String(downloadFile.getName().getBytes("utf-8"), "ISO8859-1"));response.setHeader("Content-Length", String.valueOf(downloadFile.length()));ServletOutputStream outputStream = null;try {LimitInputStream limitInputStream = new LimitInputStream(fileInputStream, new BandwidthLimiter(1024));long beginTime = System.currentTimeMillis();outputStream = response.getOutputStream();byte[] bytes = new byte[1024];int read = limitInputStream.read(bytes, 0, 1024);while (read != -1) {outputStream.write(bytes);read = limitInputStream.read(bytes, 0, 1024);}LOGGER.info("download use {} ms", System.currentTimeMillis() - beginTime);} finally {fileInputStream.close();if (outputStream != null) {outputStream.close();}LOGGER.info("download success!");}}

三、注意点

使用这个算法要注意一个问题,就是chunk的块大小不能设置的太小,即CHUNK_LENGTH不能设置的太小。否则容易造成明明maxRate设置的很大,但是实际下载速率却很小的问题

假设CHUNK_LENGTH就设置为1024 bytes,每次读取的块大小也是1024 bytes,maxRate 为 64M/s。那么我们可以计算出timeCostPerChunk约等于15258纳秒。

再如果真正的速率是100M/s,也就是每秒差不多会调用limitNextBytes方法100000次,由于每次读取消耗的时间极短,因此每次进入该方法都要sleep 15258纳秒之后再读取下一个块的数据。**如果没有算上线程调度的时间,就算1秒内休眠100000次也完全没什么问题。**但是线程的休眠和唤醒都需要内核来进行,线程上下文切换的时间应该远大于15258纳秒,这时候频繁的休眠就会导致线程暂停运行的时间和我们预期的不符。由于休眠时间过长,最终导致实际的下载速率大大的低于maxRate。

因此,我们需要调大CHUNK_LENGTH,尽量让timeCostPerChunk的值远大于线程调度的时间,减少线程调度对限流造成的影响。

四、具体demo的github地址

https://github.com/kongtrio/download-limit

这篇关于Java 文件下载/上传限流算法的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

不懂推荐算法也能设计推荐系统

本文以商业化应用推荐为例,告诉我们不懂推荐算法的产品,也能从产品侧出发, 设计出一款不错的推荐系统。 相信很多新手产品,看到算法二字,多是懵圈的。 什么排序算法、最短路径等都是相对传统的算法(注:传统是指科班出身的产品都会接触过)。但对于推荐算法,多数产品对着网上搜到的资源,都会无从下手。特别当某些推荐算法 和 “AI”扯上关系后,更是加大了理解的难度。 但,不了解推荐算法,就无法做推荐系

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

康拓展开(hash算法中会用到)

康拓展开是一个全排列到一个自然数的双射(也就是某个全排列与某个自然数一一对应) 公式: X=a[n]*(n-1)!+a[n-1]*(n-2)!+...+a[i]*(i-1)!+...+a[1]*0! 其中,a[i]为整数,并且0<=a[i]<i,1<=i<=n。(a[i]在不同应用中的含义不同); 典型应用: 计算当前排列在所有由小到大全排列中的顺序,也就是说求当前排列是第