编写下载服务器。第五部分:节流下载速度

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新项目:《从零手撸:仿小红书(微服务架构)》 正在持续爆肝中,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...点击查看项目介绍 ;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/ ;

截止目前, 星球 内专栏累计输出 63w+ 字,讲解图 2808+ 张,还在持续爆肝中.. 后续还会上新更多项目,目标是将 Java 领域典型的项目都整一波,如秒杀系统, 在线商城, IM 即时通讯,权限管理,Spring Cloud Alibaba 微服务等等,已有 2200+ 小伙伴加入学习 ,欢迎点击围观

在僵尸网络时代,你可以花几百美元租用并运行你自己的分布式拒绝服务攻击,拥有紧急开关可以选择性地关闭昂贵的功能或感激地降低性能是一个巨大的胜利。当您缓解问题时,您的应用程序仍在运行。当然,这种安全措施在高峰或营业时间也很有价值。适用于下载服务器的此类机制之一是动态限制下载速度。为了防止分布式拒绝服务攻击和过高的云发票,请考虑内置的下载限制,您可以在运行时启用和微调。这个想法是限制最大下载速度,全局或每个客户端(IP?连接?Cookie?用户代理?)。

我必须承认,我喜欢 java.io 设计,它有很多简单的 Input / OutputStream Reader / Writer 实现,每个实现只有一个职责。你想要缓冲? GZIPing?字符编码?文件系统编写?只需编写始终相互协作的所需类即可。好吧,它仍然是阻塞的,但它是在反应型潮人诞生之前设计的。没关系, java.io 也遵循 开闭原则 :可以简单地增强现有的 I/O 代码而无需触及内置类 - 但通过插入新的装饰器。所以我为 InputStream 创建了一个简单的装饰器,它减慢了我们这边的读取资源,以强制执行给定的下载速度。我正在使用 我最喜欢的 RateLimiter


 public class ThrottlingInputStream extends InputStream {
private final InputStream target;
private final RateLimiter maxBytesPerSecond;

public ThrottlingInputStream(InputStream target, RateLimiter maxBytesPerSecond) {
    this.target = target;
    this.maxBytesPerSecond = maxBytesPerSecond;
}

@Override
public int read() throws IOException {
    maxBytesPerSecond.acquire(1);
    return target.read();
}

@Override
public int read(byte[] b) throws IOException {
    maxBytesPerSecond.acquire(b.length);
    return target.read(b);
}

@Override
public int read(byte[] b, int off, int len) throws IOException {
    maxBytesPerSecond.acquire(len);
    return target.read(b, off, len);
}

//less important below...

@Override
public long skip(long n) throws IOException {
    return target.skip(n);
}

@Override
public int available() throws IOException {
    return target.available();
}

@Override
public synchronized void mark(int readlimit) {
    target.mark(readlimit);
}

@Override
public synchronized void reset() throws IOException {
    target.reset();
}

@Override
public boolean markSupported() {
    return target.markSupported();
}

@Override
public void close() throws IOException {
    target.close();
}

}


任意 InputStream 可以用 ThrottlingInputStream 包装,这样读取实际上会变慢。您可以为每个 ThrottlingInputStream 创建新的 RateLimiter ,也可以创建一个全局的,由所有下载共享。当然,有人可能会争辩说简单的 sleep() RateLimiter 在下面做了什么)会浪费大量资源,但让我们保持这个示例简单并避免非阻塞 I/O。现在我们可以轻松地插入装饰器:


 public class ThrottlingInputStream extends InputStream {
private final InputStream target;
private final RateLimiter maxBytesPerSecond;

public ThrottlingInputStream(InputStream target, RateLimiter maxBytesPerSecond) {
    this.target = target;
    this.maxBytesPerSecond = maxBytesPerSecond;
}

@Override
public int read() throws IOException {
    maxBytesPerSecond.acquire(1);
    return target.read();
}

@Override
public int read(byte[] b) throws IOException {
    maxBytesPerSecond.acquire(b.length);
    return target.read(b);
}

@Override
public int read(byte[] b, int off, int len) throws IOException {
    maxBytesPerSecond.acquire(len);
    return target.read(b, off, len);
}

//less important below...

@Override
public long skip(long n) throws IOException {
    return target.skip(n);
}

@Override
public int available() throws IOException {
    return target.available();
}

@Override
public synchronized void mark(int readlimit) {
    target.mark(readlimit);
}

@Override
public synchronized void reset() throws IOException {
    target.reset();
}

@Override
public boolean markSupported() {
    return target.markSupported();
}

@Override
public void close() throws IOException {
    target.close();
}

}

上面的示例将下载速度限制为 64 KiB/s - 显然在现实生活中您希望可以配置这样的数字,最好是在运行时。顺便说一句,我们已经讨论过 Content-Length 标头的重要性。如果您使用 pv 监控下载进度,它将正确估计剩余时间,这是一个不错的功能:


 public class ThrottlingInputStream extends InputStream {
private final InputStream target;
private final RateLimiter maxBytesPerSecond;

public ThrottlingInputStream(InputStream target, RateLimiter maxBytesPerSecond) {
    this.target = target;
    this.maxBytesPerSecond = maxBytesPerSecond;
}

@Override
public int read() throws IOException {
    maxBytesPerSecond.acquire(1);
    return target.read();
}

@Override
public int read(byte[] b) throws IOException {
    maxBytesPerSecond.acquire(b.length);
    return target.read(b);
}

@Override
public int read(byte[] b, int off, int len) throws IOException {
    maxBytesPerSecond.acquire(len);
    return target.read(b, off, len);
}

//less important below...

@Override
public long skip(long n) throws IOException {
    return target.skip(n);
}

@Override
public int available() throws IOException {
    return target.available();
}

@Override
public synchronized void mark(int readlimit) {
    target.mark(readlimit);
}

@Override
public synchronized void reset() throws IOException {
    target.reset();
}

@Override
public boolean markSupported() {
    return target.markSupported();
}

@Override
public void close() throws IOException {
    target.close();
}

}

作为额外的奖励, pv 证明了我们的节流效果(最后一栏)。现在没有 Content-Length pv 对实际进度一无所知:


 public class ThrottlingInputStream extends InputStream {
private final InputStream target;
private final RateLimiter maxBytesPerSecond;

public ThrottlingInputStream(InputStream target, RateLimiter maxBytesPerSecond) {
    this.target = target;
    this.maxBytesPerSecond = maxBytesPerSecond;
}

@Override
public int read() throws IOException {
    maxBytesPerSecond.acquire(1);
    return target.read();
}

@Override
public int read(byte[] b) throws IOException {
    maxBytesPerSecond.acquire(b.length);
    return target.read(b);
}

@Override
public int read(byte[] b, int off, int len) throws IOException {
    maxBytesPerSecond.acquire(len);
    return target.read(b, off, len);
}

//less important below...

@Override
public long skip(long n) throws IOException {
    return target.skip(n);
}

@Override
public int available() throws IOException {
    return target.available();
}

@Override
public synchronized void mark(int readlimit) {
    target.mark(readlimit);
}

@Override
public synchronized void reset() throws IOException {
    target.reset();
}

@Override
public boolean markSupported() {
    return target.markSupported();
}

@Override
public void close() throws IOException {
    target.close();
}

}


我们看到数据在流动,但不知道还剩下多少。因此 Content-Length 是一个非常重要的标头。


编写下载服务器

GitHub 上提供了贯穿这些文章开发的 示例应用程序


相关文章