Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

强类型声明 #41

Merged
merged 1 commit into from
Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading