场景:
当进行http请求获取大数据的情况,例如下载视频图片的情况,特别是云服务器会有带宽限制,超出带宽会出现掉包丢数据的情况,例如:云服务器限制25m/s的速度,当一个视频下载超过25m/s的速度的时候就会导致掉包,除了限速还需要注意下并发问题,多个线程同时下载也会超出带宽限制,可实现该代码并设置单独的线程池,控制同时并发的线程数来解决带宽问题.
逻辑:
我们可以使用一个缓冲区,每次从输入流读取数据后,先将写入文件的同时更新MD5,这样更高效,因为只需要处理一次数据。这样就不会增加额外的读取次数,避免影响性能。然后,带宽限流的逻辑需要确保在限流的同时处理这两个操作。每次读取数据块后,会计算总字节数和时间,判断是否需要暂停(使用Thread.sleep())。计算MD5的消耗应该相对较小,主要的时间还是在网络读取和文件写入上。
1. 核心公式
限速逻辑基于以下公式实现:
预期耗时(ms)=已下载总字节数×1000最大允许带宽(B/s)预期耗时(ms)=最大允许带宽(B/s)已下载总字节数×1000
其中 MAX_BPS = NMB/s = N × 1024 × 1024 B/s
2. 控制逻辑分步说明
步骤 | 代码片段 | 说明 |
---|---|---|
1. 记录开始时间 | long startTime = System.currentTimeMillis(); | 下载任务启动时记录初始时间戳 |
2. 累计下载量 | totalBytes += bytesRead; | 每次读取数据块后更新总字节数 |
3. 计算理论耗时 | expectedTime = (totalBytes * 1000) / MAX_BPS | 根据当前下载量计算理论应耗时 |
4. 对比实际耗时 | elapsed = now - startTime | 获取实际已耗时 |
5. 动态休眠补偿 | Thread.sleep(expectedTime - elapsed) | 强制等待至理论时间线 |
流程:
1. 打开网络连接,获取输入流。
2. 创建文件输出流和MessageDigest实例。
3. 循环读取数据到缓冲区,每次读取后写入文件,更新MD5(MD5可以边读取文件边计算,防止整个文件读取完再计算导致的内存压力),并进行限流计算。
4. 下载完成后,关闭流,并输出MD5结果。
实现:
public static String getMd5(String url, String type) {
CloseableHttpClient httpClient = HttpClients.createDefault();
final long MAX_BPS = 2 * 1024 * 1024; // 200Mbps = 25MB/s
MessageDigest md;
// 创建时间格式化器(线程安全)
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
try {
md = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("MD5 algorithm not found", e);
}
try (CloseableHttpResponse response = httpClient.execute(new HttpGet(url))) {
HttpEntity entity = response.getEntity();
if (entity == null) {
return "";
}
// 记录开始时间
long startTime = System.currentTimeMillis();
try (InputStream in = entity.getContent()) {
byte[] buffer = new byte[8192];
int bytesRead;
long totalBytes = 0;
while ((bytesRead = in.read(buffer)) != -1) {
md.update(buffer, 0, bytesRead);
totalBytes += bytesRead;
// 动态限速逻辑
long elapsed = System.currentTimeMillis() - startTime;
long expectedTime = (totalBytes * 1000) / MAX_BPS;
if (elapsed < expectedTime) {
Thread.sleep(expectedTime - elapsed);
}
}
// 计算最终统计信息
long endTime = System.currentTimeMillis();
double timeCost = DoubleUtils.formatDouble((endTime - startTime) / 1000.0);
String sizeMB = String.format("%.3f", DoubleUtils.formatDouble(totalBytes / (1024.0 * 1024)));
// 生成MD5
String md5 = DatatypeConverter.printHexBinary(md.digest()).toLowerCase();
// 完整日志输出
log.info("下载完成 | 类型: {} | MD5: {} | 大小: {}MB | 耗时: {}s",
type, md5, sizeMB, timeCost);
return md5;
}
} catch (Exception e) {
// 记录异常发生时间
String errorTime = LocalDateTime.now().format(formatter);
log.error("下载失败 | 类型: {} | 错误时间: {} | 原因: {}",
type, errorTime, e.getMessage());
return "";
}finally {
try {
httpClient.close();
}catch (Exception e) {
}
}
}