Skip to content

Commit

Permalink
强类型声明
Browse files Browse the repository at this point in the history
  • Loading branch information
hongweipeng committed Apr 15, 2024
1 parent 327f519 commit c4362f8
Show file tree
Hide file tree
Showing 15 changed files with 125 additions and 108 deletions.
1 change: 0 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
}
}
},
"minimum-stability": "dev",
"require-dev": {
"phpunit/phpunit": "^9.5",
"guzzlehttp/guzzle": "^7.7",
Expand Down
57 changes: 24 additions & 33 deletions src/Throttle.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use think\middleware\throttle\ThrottleAbstract;
use think\Request;
use think\Response;
use TypeError;
use function sprintf;

/**
Expand All @@ -26,7 +27,7 @@ class Throttle
* 默认配置参数
* @var array
*/
public static $default_config = [
public static array $default_config = [
'prefix' => 'throttle_', // 缓存键前缀,防止键与其他应用冲突
'key' => true, // 节流规则 true为自动规则
'visit_method' => ['GET', 'HEAD'], // 要被限制的请求类型
Expand All @@ -38,7 +39,7 @@ class Throttle
'driver_name' => CounterFixed::class, // 限流算法驱动
];

public static $duration = [
public static array $duration = [
's' => 1,
'm' => 60,
'h' => 3600,
Expand All @@ -49,20 +50,19 @@ class Throttle
* 缓存对象
* @var CacheInterface
*/
protected $cache;
protected CacheInterface $cache;

/**
* 配置参数
* @var array
*/
protected $config = [];
protected array $config = [];

protected $key = null; // 解析后的标识
protected $wait_seconds = 0; // 下次合法请求还有多少秒
protected $now = 0; // 当前时间戳
protected $max_requests = 0; // 规定时间内允许的最大请求次数
protected $expire = 0; // 规定时间
protected $remaining = 0; // 规定时间内还能请求的次数
protected int $wait_seconds = 0; // 下次合法请求还有多少秒
protected int $now = 0; // 当前时间戳
protected int $max_requests = 0; // 规定时间内允许的最大请求次数
protected int $expire = 0; // 规定时间
protected int $remaining = 0; // 规定时间内还能请求的次数

/**
* Throttle constructor.
Expand Down Expand Up @@ -93,17 +93,17 @@ protected function allowRequest(Request $request): bool
}
[$max_requests, $duration] = $this->parseRate($this->config['visit_rate']);

$micronow = microtime(true);
$micro_now = microtime(true); // float

$driver = Container::getInstance()->invokeClass($this->config['driver_name']);
if (!($driver instanceof ThrottleAbstract)) {
throw new \TypeError('The throttle driver must extends ' . ThrottleAbstract::class);
throw new TypeError('The throttle driver must extends ' . ThrottleAbstract::class);
}
$allow = $driver->allowRequest($key, $micronow, $max_requests, $duration, $this->cache);
$allow = $driver->allowRequest($key, $micro_now, $max_requests, $duration, $this->cache);

if ($allow) {
// 允许访问
$this->now = (int) $micronow;
$this->now = (int) $micro_now;
$this->expire = $duration;
$this->max_requests = $max_requests;
$this->remaining = $max_requests - $driver->getCurRequests();
Expand Down Expand Up @@ -135,7 +135,11 @@ public function handle(Request $request, Closure $next, array $params=[]): Respo
$response = $next($request);
if (200 <= $response->getCode() && 300 > $response->getCode() && $this->config['visit_enable_show_rate_limit']) {
// 将速率限制 headers 添加到响应中
$response->header($this->getRateLimitHeaders());
$response->header([
'X-Rate-Limit-Limit' => $this->max_requests,
'X-Rate-Limit-Remaining' => max($this->remaining, 0),
'X-Rate-Limit-Reset' => $this->now + $this->expire,
]);
}
return $response;
}
Expand All @@ -149,7 +153,7 @@ protected function getCacheKey(Request $request): ?string
{
$key = $this->config['key'];

if ($key instanceof \Closure) {
if ($key instanceof Closure) {
$key = Container::getInstance()->invokeFunction($key, [$this, $request]);
}

Expand All @@ -160,7 +164,7 @@ protected function getCacheKey(Request $request): ?string

if ($key === true) {
$key = $request->ip();
} elseif (is_string($key) && false !== strpos($key, '__')) {
} elseif (is_string($key) && str_contains($key, '__')) {
$key = str_replace(['__CONTROLLER__', '__ACTION__', '__IP__'], [$request->controller(), $request->action(), $request->ip()], $key);
}

Expand All @@ -172,7 +176,7 @@ protected function getCacheKey(Request $request): ?string
* @param string $rate
* @return int[]
*/
protected function parseRate($rate): array
protected function parseRate(string $rate): array
{
[$num, $period] = explode("/", $rate);
$max_requests = (int) $num;
Expand Down Expand Up @@ -213,19 +217,6 @@ public function setDriverClass(string $class_name): self
return $this;
}

/**
* 获取速率限制头
* @return array
*/
public function getRateLimitHeaders(): array
{
return [
'X-Rate-Limit-Limit' => $this->max_requests,
'X-Rate-Limit-Remaining' => $this->remaining < 0 ? 0 : $this->remaining,
'X-Rate-Limit-Reset' => $this->now + $this->expire,
];
}

/**
* 构建 Response Exception
* @param int $wait_seconds
Expand All @@ -234,10 +225,10 @@ public function getRateLimitHeaders(): array
*/
public function buildLimitException(int $wait_seconds, Request $request): HttpResponseException {
$visitFail = $this->config['visit_fail_response'] ?? null;
if ($visitFail instanceof \Closure) {
if ($visitFail instanceof Closure) {
$response = Container::getInstance()->invokeFunction($visitFail, [$this, $request, $wait_seconds]);
if (!$response instanceof Response) {
throw new \TypeError(sprintf('The closure must return %s instance', Response::class));
throw new TypeError(sprintf('The closure must return %s instance', Response::class));
}
} else {
$content = str_replace('__WAIT__', (string) $wait_seconds, $this->config['visit_fail_text']);
Expand Down
8 changes: 6 additions & 2 deletions src/throttle/CounterFixed.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
namespace think\middleware\throttle;

use Psr\SimpleCache\CacheInterface;
use Psr\SimpleCache\InvalidArgumentException;

/**
* 计数器固定窗口算法
Expand All @@ -13,10 +14,13 @@
class CounterFixed extends ThrottleAbstract
{

public function allowRequest(string $key, float $micronow, int $max_requests, int $duration, CacheInterface $cache): bool
/**
* @throws InvalidArgumentException
*/
public function allowRequest(string $key, float $micro_now, int $max_requests, int $duration, CacheInterface $cache): bool
{
$cur_requests = (int) $cache->get($key, 0);
$now = (int) $micronow;
$now = (int) $micro_now;
$wait_reset_seconds = $duration - $now % $duration; // 距离下次重置还有n秒时间
$this->wait_seconds = $wait_reset_seconds % $duration + 1;
$this->cur_requests = $cur_requests;
Expand Down
12 changes: 8 additions & 4 deletions src/throttle/CounterSlider.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,22 @@
namespace think\middleware\throttle;

use Psr\SimpleCache\CacheInterface;
use Psr\SimpleCache\InvalidArgumentException;

/**
* 计数器滑动窗口算法
* Class CouterSlider
* Class CounterSlider
* @package think\middleware\throttle
*/
class CounterSlider extends ThrottleAbstract
{
public function allowRequest(string $key, float $micronow, int $max_requests, int $duration, CacheInterface $cache): bool
/**
* @throws InvalidArgumentException
*/
public function allowRequest(string $key, float $micro_now, int $max_requests, int $duration, CacheInterface $cache): bool
{
$history = $cache->get($key, []);
$now = (int) $micronow;
$now = (int) $micro_now;
// 移除过期的请求的记录
$history = array_values(array_filter($history, function ($val) use ($now, $duration) {
return $val >= $now - $duration;
Expand All @@ -31,7 +35,7 @@ public function allowRequest(string $key, float $micronow, int $max_requests, in

if ($history) {
$wait_seconds = $duration - ($now - $history[0]) + 1;
$this->wait_seconds = $wait_seconds > 0 ? $wait_seconds : 0;
$this->wait_seconds = max($wait_seconds, 0);
}

return false;
Expand Down
12 changes: 8 additions & 4 deletions src/throttle/LeakyBucket.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
namespace think\middleware\throttle;

use Psr\SimpleCache\CacheInterface;
use Psr\SimpleCache\InvalidArgumentException;

/**
* 漏桶算法
Expand All @@ -13,19 +14,22 @@
class LeakyBucket extends ThrottleAbstract
{

public function allowRequest(string $key, float $micronow, int $max_requests, int $duration, CacheInterface $cache): bool
/**
* @throws InvalidArgumentException
*/
public function allowRequest(string $key, float $micro_now, int $max_requests, int $duration, CacheInterface $cache): bool
{
if ($max_requests <= 0) return false;

$last_time = (float) $cache->get($key, 0); // 最近一次请求
$rate = (float) $duration / $max_requests; // 平均 n 秒一个请求
if ($micronow - $last_time < $rate) {
if ($micro_now - $last_time < $rate) {
$this->cur_requests = 1;
$this->wait_seconds = ceil($rate - ($micronow - $last_time));
$this->wait_seconds = (int) ceil($rate - ($micro_now - $last_time));
return false;
}

$cache->set($key, $micronow, $duration);
$cache->set($key, $micro_now, $duration);
return true;
}
}
12 changes: 6 additions & 6 deletions src/throttle/ThrottleAbstract.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,28 @@
abstract class ThrottleAbstract
{
/** @var int */
protected $cur_requests = 0; // 当前已有的请求数
protected int $cur_requests = 0; // 当前已有的请求数
/** @var int */
protected $wait_seconds = 0; // 距离下次合法请求还有多少秒
protected int $wait_seconds = 0; // 距离下次合法请求还有多少秒

/**
* 是否允许访问
* @param string $key 缓存键
* @param float $micronow 当前时间戳,可含毫秒
* @param float $micro_now 当前时间戳,可含毫秒
* @param int $max_requests 允许最大请求数
* @param int $duration 限流时长
* @param CacheInterface $cache 缓存对象
* @return bool
*/
abstract public function allowRequest(string $key, float $micronow, int $max_requests, int $duration, CacheInterface $cache): bool;
abstract public function allowRequest(string $key, float $micro_now, int $max_requests, int $duration, CacheInterface $cache): bool;

/**
* 计算距离下次合法请求还有多少秒
* @return int
*/
public function getWaitSeconds(): int
{
return (int) $this->wait_seconds;
return $this->wait_seconds;
}

/**
Expand All @@ -39,7 +39,7 @@ public function getWaitSeconds(): int
*/
public function getCurRequests(): int
{
return (int) $this->cur_requests;
return $this->cur_requests;
}

}
20 changes: 12 additions & 8 deletions src/throttle/TokenBucket.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
namespace think\middleware\throttle;

use Psr\SimpleCache\CacheInterface;
use Psr\SimpleCache\InvalidArgumentException;

/**
* 令牌桶算法
Expand All @@ -12,32 +13,35 @@
*/
class TokenBucket extends ThrottleAbstract
{
public function allowRequest(string $key, float $micronow, int $max_requests, int $duration, CacheInterface $cache): bool
/**
* @throws InvalidArgumentException
*/
public function allowRequest(string $key, float $micro_now, int $max_requests, int $duration, CacheInterface $cache): bool
{
if ($max_requests <= 0 || $duration <= 0) return false;

$assist_key = $key . 'store_num'; // 辅助缓存
$rate = (float) $max_requests / $duration; // 平均一秒生成 n 个 token

$last_time = $cache->get($key, null);
$store_num = $cache->get($assist_key, null);
$last_time = $cache->get($key, 0);
$store_num = $cache->get($assist_key, 0);

if ($last_time === null || $store_num === null) { // 首次访问
$cache->set($key, $micronow, $duration);
if ($last_time === 0 || $store_num === 0) { // 首次访问
$cache->set($key, $micro_now, $duration);
$cache->set($assist_key, $max_requests - 1, $duration);
return true;
}

$create_num = floor(($micronow - $last_time) * $rate); // 推算生成的 token 数
$create_num = floor(($micro_now - $last_time) * $rate); // 推算生成的 token 数
$token_left = (int) min($max_requests, $store_num + $create_num); //当前剩余 tokens 数量

if ($token_left < 1) {
$tmp = (int) ceil($duration / $max_requests);
$this->wait_seconds = $tmp - intval(($micronow - $last_time)) % $tmp;
$this->wait_seconds = $tmp - intval(($micro_now - $last_time)) % $tmp;
return false;
}
$this->cur_requests = $max_requests - $token_left;
$cache->set($key, $micronow, $duration);
$cache->set($key, $micro_now, $duration);
$cache->set($assist_key, $token_left - 1, $duration);
return true;
}
Expand Down
Loading

0 comments on commit c4362f8

Please sign in to comment.