java 下载限速_Java 文件下载限流算法

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

一、算法思路

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

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

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

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

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

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

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

public synchronized void limitNextBytes(int len) {

//累计bytesWillBeSentOrReceive

this.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。设置一个块的大小为1M

private static final Long CHUNK_LENGTH = 1024 * 1024L;

//已经发送/读取的字节数

private int bytesWillBeSentOrReceive = 0;

//上一次接收到字节流的时间戳——单位纳秒

private long lastPieceSentOrReceiveTick = System.nanoTime();

//允许的最大速率,默认为 1024KB/s

private 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;

}

@Override

public int read() throws IOException {

if (bandwidthLimiter != null) {

bandwidthLimiter.limitNextBytes();

}

return inputStream.read();

}

@Override

public int read(byte[] b, int off, int len) throws IOException {

if (bandwidthLimiter != null) {

bandwidthLimiter.limitNextBytes(len);

}

return inputStream.read(b, off, len);

}

@Override

public 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地址

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

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

相关文章

【学习笔记】JAVA基础——异常处理部分

文章目录前言简介一. try、catch与finally① try && catch② finally③ throws补充&#xff1a;JVM 相关二. 异常的分类① 分类解释与思维导图三. 自定义异常例子&#xff1a;Hero类的attack方法的isDeadException。四. 上传代码到GIT① 首先在github新建一个仓库Java_…

java 反射 json_java 反射机制构建JSON字符串

java 反射机制构建JSON字符串。接着上一篇文章、今天继续学习利用java 反射机制构建JSON字符串。JSON的格式跟使用的方式方法就不讲了、这个百度一下就有……好了今天心情很糟糕、直接上代码吧&#xff01;[java]view plaincopyprint?import java.lang.reflect.Field;import j…

【学习笔记】数据链路层的差错控制——检错编码与纠错编码(海明码、奇偶检验码与CRC循环冗余码)

文章目录前言一. 差错控制简介二.补充知识三. 检错编码&#xff08;1&#xff09;奇偶检验码组成&#xff1a;构造方法&#xff1a;以奇检验码为例。举个例子&#xff1a;检验码求法&#xff1a;错误检测方法&#xff1a;特点&#xff08;2&#xff09;CRC循环冗余检验码三要素…

【学习笔记】数据链路层——流量控制:停止等待协议、后退N帧协议(GBN)、选择重传协议(SR)

文章目录一. 流量控制① 必要性② 数据链路层 VS 传输层③ 定义④ 方法1&#xff09;停止等待协议2&#xff09;滑动窗口协议关系&#xff1a;包括&#xff1a;3&#xff09;协议对比二. 停止-等待协议必要性应用情况① 无差错情况② 有差错情况1&#xff09;数据帧丢失&#x…

java线程唤醒与等待_Java线程的等待与唤醒

生产者和消费者必须使用同步代码块包裹起来&#xff0c;保证等待和唤醒只能有一个执行&#xff0c;同步使用的锁对象必须保证唯一Thread中重要方法void wait() 在其他线程调用此对象的notify()方法或notifyall()方法前&#xff0c;导致当前线程等待void notify() 唤醒在此对象监…

【学习笔记】数据链路层——信道划分访问控制(FDM、TDM、STDM、WDM、CDM CDMA)

PPT截自王道考研B站教程 一. 铺垫知识 ① 传输数据使用的两种链路 星型、总线型都是广播式结构。 星型更有容错率&#xff0c;总线型断一个则全断。 ② 介质访问控制 定义 采取一定措施&#xff0c;使得两对节点之间的通信不会发生互相干扰的情况。 分类 多路复用&…

stream of java_java8新特性之强大的Stream API

Stream APIStream是Java8中处理集合的关键抽象概念&#xff0c;它可以指定你希望对集合进行的操作&#xff0c;可以执行非常复杂的查找、过滤和映射数据等操作。使用Stream API 对集合数据进行操作&#xff0c;就类似于使用 SQL 执行的数 据库查询。也可以使用Stream API来并行…

【学习笔记】数据链路层——随机访问介质访问控制(ALOHA、CSMA、CSMA/CD、CSMA/CA),截断二进制指数规避算法

文章目录小前言一. ALOHA协议纯ALOHA协议时隙ALOHA协议ALOHA对比CSMA协议定义与分类① 1-坚持CSMA② 非坚持CSMA③ p-坚持CSMA总结CSMA/CD协议传播时延对载波监听的影响确定重传时机&#xff1a;截断二进制指数规避算法最小帧长问题CSMA/CA协议工作原理CSMA/CD 与 CSMA/CA的对比…

pca算法介绍及java实现_PCA算法原理及实现

众所周知&#xff0c;PCA(principal component analysis)是一种数据降维的方式&#xff0c;能够有效的将高维数据转换为低维数据&#xff0c;进而降低模型训练所需要的计算资源。以上是比较官方的说法&#xff0c;下面是人话(正常人讲的话)版。pca就是一种能够有效压缩数据的方…

【学习笔记】数据链路层——轮询访问介质控制(轮询协议、令牌传递协议)

文章目录一. 轮询访问介质控制二. 轮询协议三. 令牌传递协议结束语PPT截自王道考研B站教程 一. 轮询访问介质控制 结合了前面的信道划分访问控制、随机访问MAC协议的优点&#xff1a; 既要不产生冲突&#xff0c;又要发送时占全部带宽。 二. 轮询协议 轮询开销&#xff1a;…

java boolean 多线程_JAVA多线程两个实用的辅助类(CountDownLatch和AtomicBoolean)

AtomicBoolean它允许一个线程等待一个线程完成任务&#xff0c;然后运行:A boolean value that may be updated atomically. See the java.util.concurrent.atomic package specification for description of the properties of atomic variables. An AtomicBoolean is used in…

【学习笔记】局域网基本概念和体系结构,以太网、无线局域网与PPP协议、HDLC协议

文章目录一. 局域网&#xff1a;特点与要素① 拓扑结构② 传输介质③ 介质访问控制方法④ 局域网的分类⑤ IEEE 802标准⑥ MAC子层和LLC子层二. 以太网① 概述② 提供无连接、不可靠的服务③ 传输介质与拓扑结构的发展④ 10BAST-T以太网⑤ 适配器与MAC地址⑥ 以太网MAC帧⑦ 高速…

java truevfs_Java-Apache Commons VFS:使用FTP

我正在尝试通过FTP使用Apache Commons VFS.在我的FTP上,具有文件和文件夹的下一个结构&#xff1a;//test/test/in/test/in/file1.txt/test/in/file2.txt我需要连接并从/ test / in文件夹中读取所有文件(它一直在变化).码&#xff1a;FileSystemManager fsManager null;FileSy…

【学习笔记】数据链路层——链路层设备:物理层拓展以太网、链路层拓展以太网与冲突域和广播域

文章目录一. 冲突域与广播域① 定义与对比图② 例子二. 物理层扩展以太网三. 链路层扩展以太网① 网桥定义透明网桥源路由网桥② 以太网交换机PPT截自B站王道考研教程 本文内容导图 一. 冲突域与广播域 可以先只是简单看看定义&#xff0c;然后看完二、三后再回来看对比图和…

java jui 正则表达式_常规正则表达式练习

PS&#xff1a;join()数组转串&#xff0c;split()串转数组1、test()&#xff1a;在字符串查找符合正则的内容&#xff0c;如果查找到返回true&#xff0c;反之返回false用法&#xff1a;正则.test(字符串)2、search()&#xff1a;在字符串搜索符合正则的内容&#xff0c;找到就…

【学习笔记】网络层——概述、数据交换方式:电路交换、报文交换与分组交换(数据报与虚电路)

文章目录一. 概述二. 数据交换方式① 电路交换② 报文交换③ 分组交换④ 分组交换 && 报文交换举例对比⑤ 三种数据交换方式比较总结三. 分组交换的两种方式① 定义传输单元名词辨析② 数据报③ 虚电路④ 数据报与虚电路的对比ppt截自王道考研B站教程 太不容易了&#…

java代码中 作用_Java利用开发中代码生成工具的作用

Java利用开发中代码生成工具的作用2010-6-5文字大小:大中小近来&#xff0c;随着各种代码生成工具的不断涌现(如SpringSource的Spring Roo、Skyway Builder Community Edition 6.3及Blu Age的M2Spring等)&#xff0c;人们又将留心力转移到了这些代码生成工具在企业级Java使用开…

【学习记录】网络层——IP数据报(格式与分片)

文章目录一. IP数据报格式二. IP数据报分片① 为什么要分片?② 标识、标志与片偏移③ 例题单位为nB小结PPT截自王道考研教程 tips&#xff1a;b是位&#xff0c;B是字节。 一. IP数据报格式 在本章节中&#xff0c;暂时不区分IP数据报与分组。 生存时间(Time To Live)&#xf…

java计算器 运算符优先级_跪求大神帮忙,怎样在java 计算器中实现,四则运算优先级;...

该楼层疑似违规已被系统折叠 隐藏此楼查看此楼代码如下&#xff0c;跪求大神补充import java.awt.*;import java.awt.event.*;import java.awt.image.BufferedImage;import java.io.File;import java.io.IOException;import javax.imageio.ImageIO;import javax.swing.JFrame;i…

自动驾驶学习笔记(二十二)——自动泊车算法

#Apollo开发者# 学习课程的传送门如下&#xff0c;当您也准备学习自动驾驶时&#xff0c;可以和我一同前往&#xff1a; 《自动驾驶新人之旅》免费课程—> 传送门 《Apollo开放平台9.0专项技术公开课》免费报名—>传送门 文章目录 前言 感知算法 定位算法 规划算法…