@@ -7,34 +7,53 @@ namespace HttpAgent;
7
7
/// <summary>
8
8
/// 带应用速率限制的流
9
9
/// </summary>
10
+ /// <remarks>
11
+ /// <para>基于令牌桶算法(Token Bucket Algorithm)实现流量控制和速率限制。</para>
12
+ /// <para>参考文献:https://baike.baidu.com/item/令牌桶算法/6597000。</para>
13
+ /// </remarks>
10
14
public sealed class RateLimitedStream : Stream
11
15
{
12
16
/// <summary>
13
- /// 每秒允许的最大字节数
17
+ /// 单次读取或写入操作中处理的最大数据块大小
14
18
/// </summary>
15
- private readonly int _bytesPerSecond ;
19
+ internal const int CHUNK_SIZE = 4096 ;
20
+
21
+ /// <summary>
22
+ /// 每秒允许传输的最大字节数
23
+ /// </summary>
24
+ internal readonly double _bytesPerSecond ;
16
25
17
26
/// <inheritdoc cref="Stream" />
18
27
internal readonly Stream _innerStream ;
19
28
20
29
/// <summary>
21
- /// 用于精确计时的 <see cref="Stopwatch" /> 实例
30
+ /// 用于同步访问的锁对象
22
31
/// </summary>
23
- internal readonly Stopwatch _stopwatch = new ( ) ;
32
+ internal readonly object _lockObject = new ( ) ;
24
33
25
34
/// <summary>
26
- /// 到目前为止已读取或写入的总字节数
35
+ /// 用来计算时间间隔的计时器
27
36
/// </summary>
28
- internal long _totalBytesProcessed ;
37
+ internal readonly Stopwatch _stopwatch ;
38
+
39
+ /// <summary>
40
+ /// 当前可用的令牌数量(字节数)
41
+ /// </summary>
42
+ internal double _availableTokens ;
43
+
44
+ /// <summary>
45
+ /// 上次令牌补充的时间戳
46
+ /// </summary>
47
+ internal long _lastTokenRefillTime ;
29
48
30
49
/// <summary>
31
50
/// <inheritdoc cref="RateLimitedStream" />
32
51
/// </summary>
33
52
/// <param name="innerStream">
34
53
/// <see cref="Stream" />
35
54
/// </param>
36
- /// <param name="bytesPerSecond">每秒允许的最大字节数 </param>
37
- public RateLimitedStream ( Stream innerStream , int bytesPerSecond )
55
+ /// <param name="bytesPerSecond">每秒允许传输的最大字节数 </param>
56
+ public RateLimitedStream ( Stream innerStream , double bytesPerSecond )
38
57
{
39
58
// 空检查
40
59
ArgumentNullException . ThrowIfNull ( innerStream ) ;
@@ -49,8 +68,14 @@ public RateLimitedStream(Stream innerStream, int bytesPerSecond)
49
68
_innerStream = innerStream ;
50
69
_bytesPerSecond = bytesPerSecond ;
51
70
52
- // 启动 Stopwatch 来开始计时
53
- _stopwatch . Start ( ) ;
71
+ // 开始计时
72
+ _stopwatch = Stopwatch . StartNew ( ) ;
73
+
74
+ // 记录初始时间
75
+ _lastTokenRefillTime = _stopwatch . ElapsedMilliseconds ;
76
+
77
+ // 初始化可用令牌数
78
+ _availableTokens = bytesPerSecond ;
54
79
}
55
80
56
81
/// <inheritdoc />
@@ -81,11 +106,14 @@ public override long Position
81
106
/// <inheritdoc />
82
107
public override int Read ( byte [ ] buffer , int offset , int count )
83
108
{
84
- // 根据设定的速率限制调整读写操作的速度
85
- ApplyRateLimitAsync ( count ) . GetAwaiter ( ) . GetResult ( ) ;
109
+ // 确保单次读取不会超过预设的数据块大小
110
+ var adjustedCount = Math . Min ( count , CHUNK_SIZE ) ;
111
+
112
+ // 等待直到有足够令牌可用
113
+ WaitForTokens ( adjustedCount ) ;
86
114
87
115
// 从内部流读取数据到缓冲区
88
- return _innerStream . Read ( buffer , offset , count ) ;
116
+ return _innerStream . Read ( buffer , offset , adjustedCount ) ;
89
117
}
90
118
91
119
/// <inheritdoc />
@@ -97,11 +125,14 @@ public override int Read(byte[] buffer, int offset, int count)
97
125
/// <inheritdoc />
98
126
public override void Write ( byte [ ] buffer , int offset , int count )
99
127
{
100
- // 向内部流写入数据
101
- _innerStream . Write ( buffer , offset , count ) ;
128
+ // 确保单次写入不会超过预设的数据块大小
129
+ var adjustedCount = Math . Min ( count , CHUNK_SIZE ) ;
102
130
103
- // 根据设定的速率限制调整读写操作的速度
104
- ApplyRateLimitAsync ( count ) . GetAwaiter ( ) . GetResult ( ) ;
131
+ // 等待直到有足够令牌可用
132
+ WaitForTokens ( adjustedCount ) ;
133
+
134
+ // 向内部流写入数据
135
+ _innerStream . Write ( buffer , offset , adjustedCount ) ;
105
136
}
106
137
107
138
/// <inheritdoc />
@@ -111,35 +142,78 @@ protected override void Dispose(bool disposing)
111
142
if ( disposing )
112
143
{
113
144
_innerStream . Dispose ( ) ;
145
+ _stopwatch . Stop ( ) ;
114
146
}
115
147
116
148
base . Dispose ( disposing ) ;
117
149
}
118
150
119
151
/// <summary>
120
- /// 根据设定的速率限制调整读写操作的速度
152
+ /// 补充令牌的方法
121
153
/// </summary>
122
- /// <param name="bytesToProcess">本次操作将处理的字节数</param>
123
- internal async Task ApplyRateLimitAsync ( int bytesToProcess )
154
+ internal void RefillTokens ( )
124
155
{
125
- // 自开始以来经过的时间(秒)
126
- var elapsedSeconds = _stopwatch . ElapsedMilliseconds / 1000.0 ;
156
+ // 获取当前计时器的时间
157
+ var now = _stopwatch . ElapsedMilliseconds ;
127
158
128
- // 根据速率预期应读取的字节数
129
- var totalBytesExpected = elapsedSeconds * _bytesPerSecond ;
159
+ // 计算自上次填充令牌以来经过的时间
160
+ var timePassed = now - _lastTokenRefillTime ;
130
161
131
- // 计算实际与预期之差
132
- var bytesOverLimit = _totalBytesProcessed + bytesToProcess - totalBytesExpected ;
133
-
134
- if ( bytesOverLimit > 0 )
162
+ // 如果时间没有流逝或者流逝时间不足以产生新的令牌,则直接返回
163
+ if ( timePassed <= 0 )
135
164
{
136
- // 如果实际操作超过预期,则计算需要等待的时间,并进行延迟
137
- var delayMilliseconds = ( int ) ( bytesOverLimit / _bytesPerSecond * 1000.0 ) ;
138
-
139
- await Task . Delay ( delayMilliseconds ) . ConfigureAwait ( false ) ;
165
+ return ;
140
166
}
141
167
142
- // 更新已处理的总字节数
143
- _totalBytesProcessed += bytesToProcess ;
168
+ // 据每秒允许的最大字节数以及经过的时间计算可以补充的令牌数量
169
+ var newTokens = _bytesPerSecond * timePassed / 1000.0 ;
170
+
171
+ // 更新可用令牌,但不超过每秒允许的最大值
172
+ _availableTokens = Math . Min ( _bytesPerSecond , _availableTokens + newTokens ) ;
173
+
174
+ // 更新最后一次填充令牌的时间戳
175
+ _lastTokenRefillTime = now ;
176
+ }
177
+
178
+ /// <summary>
179
+ /// 等待直到有足够令牌可用
180
+ /// </summary>
181
+ /// <param name="desiredTokens">需要等待的令牌数量</param>
182
+ internal void WaitForTokens ( int desiredTokens )
183
+ {
184
+ while ( true )
185
+ {
186
+ // 防止并发访问问题
187
+ lock ( _lockObject )
188
+ {
189
+ // 尝试补充令牌
190
+ RefillTokens ( ) ;
191
+
192
+ // 检查是否已有足够的令牌
193
+ if ( _availableTokens >= desiredTokens )
194
+ {
195
+ // 扣除所需的令牌数量
196
+ _availableTokens -= desiredTokens ;
197
+
198
+ // 如果有足够的令牌,退出循环
199
+ return ;
200
+ }
201
+ }
202
+
203
+ // 如果没有足够的令牌,计算还需要多少令牌
204
+ var requiredTokens = desiredTokens - _availableTokens ;
205
+
206
+ // 计算为了获得所需令牌需要等待的时间
207
+ var waitTime = ( int ) ( requiredTokens * 1000.0 / _bytesPerSecond ) ;
208
+
209
+ // 添加一点额外延迟用来确保精确性,具体是增加了 5% 的延迟
210
+ waitTime = ( int ) ( waitTime * 1.05 ) ;
211
+
212
+ // 确保不会一次性等待过长时间,最多等待 100 毫秒
213
+ if ( waitTime > 0 )
214
+ {
215
+ Thread . Sleep ( Math . Min ( 100 , waitTime ) ) ;
216
+ }
217
+ }
144
218
}
145
219
}
0 commit comments