anytls 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +9 -3
- data/docs/ARCHITECTURE.md +12 -0
- data/docs/VALIDATION.md +1 -1
- data/lib/anytls/byte_buffer.rb +30 -1
- data/lib/anytls/cli/client_listener.rb +8 -0
- data/lib/anytls/inbound/http_proxy.rb +95 -10
- data/lib/anytls/io_helpers.rb +7 -0
- data/lib/anytls/server.rb +16 -6
- data/lib/anytls/session.rb +2 -6
- data/lib/anytls/session_pool.rb +8 -1
- data/lib/anytls/stream.rb +2 -2
- data/lib/anytls/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 614c54295c75168e63b13a6c1cb9703321cc70bbaa672a5a3d064ed630b014ad
|
|
4
|
+
data.tar.gz: 51446c2245624537d53fb9fcd8b49f51b3a57f3241bcfe5785eaa5206404c02e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1da8ddbc879a82a96263c991f27cbdaf8f9ae2b4ff90a112a811ad42ab63b102e09078af8fff6b8893c121c8b5683c68a65b942907181ffe464a0c8674e012f7
|
|
7
|
+
data.tar.gz: b3cece03e15e6593db595b7ca78e3291fecaf8eae3a1e4bc71cd73ba1b08ff22aab8a3cec5891e3eb8851e4af56b8cdfde760fc1c52197e88fa95cb79e58ddee
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.4 (2026-10-05)
|
|
4
|
+
|
|
5
|
+
- 临时接入网络错误后继续接受连接,保留已有代理会话。
|
|
6
|
+
- 修复 HTTP 上传完成后的响应超时,保留慢上传、提前拒绝、流式响应和 keep-alive;临时响应不重置超时预算。
|
|
7
|
+
- 接受 HTTP 字段值两端的合法空白,继续拒绝重复 Host、重复消息长度和冲突定界。
|
|
8
|
+
- 减少 HTTP 请求头写入、多流接收统计和频繁连接复用的处理开销。
|
|
9
|
+
|
|
3
10
|
## 0.1.3 (2026-10-05)
|
|
4
11
|
|
|
5
12
|
- 忽略 CI 的 Bundler 安装缓存,避免发布前工作区检查误判为未提交源文件。
|
data/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Ruby 3.4+ 的 AnyTLS gem 与命令行客户端、服务端。参考版本固定
|
|
|
4
4
|
[`anytls-go` 的 `fd6167a`](https://github.com/anytls/anytls-go/tree/fd6167acd6d73b9fa3e607659951847fbc9e6c50)。
|
|
5
5
|
核心代码不依赖 Rails。
|
|
6
6
|
|
|
7
|
-
当前版本为 `0.1.
|
|
7
|
+
当前版本为 `0.1.4`,Ruby 实现使用 [MIT 许可证](LICENSE)。项目及参考代码的许可边界见
|
|
8
8
|
[SOURCES](docs/SOURCES.md)。
|
|
9
9
|
|
|
10
10
|
## 使用
|
|
@@ -99,7 +99,8 @@ server.run # 阻塞;由其他正常线程调用 server.stop(grace: 5)
|
|
|
99
99
|
|
|
100
100
|
构造 Client 不拨号;首次连接时才启动会话池。`min_idle_sessions` 是回收时保留已有空闲
|
|
101
101
|
session 的数量,不会主动创建连接填满池。默认最多 128 个 session,空闲超时与检查间隔
|
|
102
|
-
各 30
|
|
102
|
+
各 30 秒;归还 session 不提前触发空闲扫描。客户端租用空闲 session;并发请求需要时
|
|
103
|
+
建立新的 session。服务端默认最多
|
|
103
104
|
256 个活动 stream。`Client#stats`、`Server#stats` 返回冻结的计数快照。
|
|
104
105
|
|
|
105
106
|
## 功能与边界
|
|
@@ -110,10 +111,15 @@ session 的数量,不会主动创建连接填满池。默认最多 128 个 ses
|
|
|
110
111
|
SOCKS UDP 分片和 UoT v1 不在当前接口范围。
|
|
111
112
|
|
|
112
113
|
HTTP 转发使用 `protocol-http1` 解析消息边界,拒绝冲突 Content-Length/Transfer-Encoding
|
|
113
|
-
及重复 Host
|
|
114
|
+
及重复 Host,接受字段值两端合法的空格和制表符。带请求体的 HTTP Upgrade 返回 400,
|
|
115
|
+
避免丢弃消息体;无体 Upgrade 继续支持。
|
|
116
|
+
普通 HTTP 上传完成后,最终响应头须在 `handshake_timeout` 内到达,否则返回 504;
|
|
117
|
+
上传过程及响应体传输不计入该响应头预算,临时响应不重置预算。目标提前拒绝上传时仍立即返回响应。
|
|
114
118
|
HTTPS 目标使用 CONNECT。代理客户端监听仅默认绑定回环地址;扩大监听
|
|
115
119
|
范围时由部署环境限制访问者。
|
|
116
120
|
|
|
121
|
+
服务端与 CLI 监听器会对待接入连接的暂时网络错误退避重试,保留已有连接;监听 fd 故障仍会报错退出。
|
|
122
|
+
|
|
117
123
|
每条 TLS 连接只有一个 owner Thread。协议接收按 stream/session 字节预算限流;慢消费者
|
|
118
124
|
超过预算会被关闭,避免无限增长或阻塞其他 stream。TLS/TCP 本身的队首阻塞仍然存在。
|
|
119
125
|
设计和状态边界见 [ARCHITECTURE](docs/ARCHITECTURE.md)。
|
data/docs/ARCHITECTURE.md
CHANGED
|
@@ -38,6 +38,7 @@ FIN 结束整个虚拟 stream,不提供 read/write 两侧独立关闭语义。
|
|
|
38
38
|
Client pool 同一时刻将一个 session 租给一条业务 stream,完成后将其归还并复用。
|
|
39
39
|
服务端 Session 解码器支持多条 stream 的帧,入站数量另受 session 和 server 限制。
|
|
40
40
|
旧空闲连接检测、心跳和 peer stream-ID 限制都在 session 生命周期中处理,不由应用代理层重试。
|
|
41
|
+
池容量等待与定时回收使用独立通知;租用、归还不触发全池回收扫描,关闭会立即唤醒两类等待者。
|
|
41
42
|
|
|
42
43
|
## 线程与 TLS 所有权
|
|
43
44
|
|
|
@@ -89,10 +90,19 @@ Session 状态锁只保护短操作;锁内不等待网络、不回调调用者
|
|
|
89
90
|
`ByteBuffer` 将输入合并为最多 16 KiB 的块,读取用头块偏移推进,避免小读后下一次写入
|
|
90
91
|
复制整个接收缓冲。容量按未读字节计算,消费完的块立即移除;小输入合并到尾块,避免
|
|
91
92
|
逐字节输入产生与字节数相同的队列对象。远端结束仍允许排空,显式关闭总会释放未读块。
|
|
93
|
+
活跃流共享 `ByteBuffer::Budget`:入队先预留,读取释放,结束时一次性退出共享计费,
|
|
94
|
+
因此每帧不再扫描所有 stream。锁顺序为 buffer → budget,不反向取 session/stream 锁。
|
|
95
|
+
与原先扫描活跃流的范围一致,远端结束后由调用者保留的尾部不计入 session 总量,
|
|
96
|
+
仍受单流容量限制;调用者应及时排空或关闭这些 Stream。
|
|
92
97
|
|
|
93
98
|
HTTP 提前拒绝上传时,先停止客户端上传读取并转发完整响应,再关闭上游和等待上传线程。
|
|
94
99
|
不能在转发响应前等待上传线程,因为该线程可能正在等待上游写入背压;也不能提前关闭
|
|
95
100
|
AnyTLS Stream,否则已接收的响应体会被丢弃。
|
|
101
|
+
上传线程完成写入后,以同一线程和条件变量守候最终响应头,避免新增定时线程或轮询。
|
|
102
|
+
预算从上传完成开始,到最终响应头结束;临时响应不延长预算,响应体不受此预算限制。
|
|
103
|
+
超时先记录原因再关闭上游,唤醒已经阻塞的读取者;提前响应与清理均解除等待并回收上传线程。
|
|
104
|
+
HTTP 字段值仅修剪 SP/HTAB,保持重复字段及定界冲突检查。头字段写入以最多 16 KiB
|
|
105
|
+
缓冲合并,保留依赖的校验和字段顺序,返回前发送;消息体仍直接流式转发。
|
|
96
106
|
|
|
97
107
|
同一 TCP 连接天然存在队首阻塞;有限缓冲只能限制 Ruby 自身的资源占用和慢消费者影响。
|
|
98
108
|
当前没有扩展协议增加 per-stream credit,不承诺无限慢消费者仍可无损持续接收。
|
|
@@ -108,6 +118,8 @@ Client 默认验证证书和目标名称;`insecure: true` 必须显式选择
|
|
|
108
118
|
`close` 可重复调用。`ensure` 负责 socket、session、stream 与线程回收,保留原始业务错误。
|
|
109
119
|
INT/TERM trap 只向 pipe 写字节;实际清理在正常线程执行。没有 gem publish、在线下载设备配置
|
|
110
120
|
或全局代理设置等隐藏副作用。
|
|
121
|
+
监听器仅对 accept 返回的待接入网络错误及资源不足退避重试,日志有界;已取得 socket
|
|
122
|
+
后的设置错误、失效监听 fd 等错误仍向外传播。退避可被停止请求唤醒,不影响已建立流。
|
|
111
123
|
|
|
112
124
|
服务端 `stop(grace:)` 先停止监听,允许已经建立的 stream 在宽限期继续传输;到期再关闭
|
|
113
125
|
剩余 session/目标 socket 并等待 worker 退出。并发 `stop` 服从第一次停止请求确定的边界,
|
data/docs/VALIDATION.md
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
CI 的 Ubuntu / Ruby 4.0 任务在验证通过后上传 `anytls-gem` artifact,包含 gem 和 `SHA256SUMS`。
|
|
27
27
|
发布前须确认同一提交的完整 Ruby 矩阵和 Go 互通任务均通过,下载该 artifact 并核对校验和。
|
|
28
|
-
运行 `bundle exec ruby script/verify_package.rb /path/to/anytls-0.1.
|
|
28
|
+
运行 `bundle exec ruby script/verify_package.rb /path/to/anytls-0.1.4.gem` 可验证已有包与当前源码
|
|
29
29
|
的文件内容、元数据和依赖一致,并隔离安装该包;提供路径时不会重新构建 gem。
|
|
30
30
|
GitHub Release 和 RubyGems 必须使用这一份验证过的包,发布后下载两处产物并核对 SHA256。
|
|
31
31
|
|
data/lib/anytls/byte_buffer.rb
CHANGED
|
@@ -4,11 +4,34 @@ module AnyTLS
|
|
|
4
4
|
# 有界二进制接收队列;生产者不阻塞,读取者可等待数据。
|
|
5
5
|
# 远端结束后先读完缓冲再报告 EOF 或异常;本地关闭则丢弃缓冲。
|
|
6
6
|
class ByteBuffer
|
|
7
|
+
# 多个活跃接收队列共用的字节预算。仅从 buffer 锁进入预算锁,
|
|
8
|
+
# 不反向获取队列、stream 或 session 锁,也不调用应用回调。
|
|
9
|
+
class Budget
|
|
10
|
+
def initialize(limit)
|
|
11
|
+
@limit = limit
|
|
12
|
+
@size = 0
|
|
13
|
+
@mutex = Mutex.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def reserve(size)
|
|
17
|
+
@mutex.synchronize do
|
|
18
|
+
raise BufferLimitExceeded, "session receive buffer limit exceeded" if @size + size > @limit
|
|
19
|
+
|
|
20
|
+
@size += size
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def release(size)
|
|
25
|
+
@mutex.synchronize { @size -= size }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
7
29
|
CHUNK_SIZE = 16_384
|
|
8
30
|
private_constant :CHUNK_SIZE
|
|
9
31
|
|
|
10
|
-
def initialize(limit: 262_144)
|
|
32
|
+
def initialize(limit: 262_144, budget: nil)
|
|
11
33
|
@limit = limit
|
|
34
|
+
@budget = budget
|
|
12
35
|
@chunks = []
|
|
13
36
|
@offset = 0
|
|
14
37
|
@size = 0
|
|
@@ -30,6 +53,7 @@ module AnyTLS
|
|
|
30
53
|
return if @finished
|
|
31
54
|
raise BufferLimitExceeded, "stream receive buffer limit exceeded" if @size + bytes.bytesize > @limit
|
|
32
55
|
|
|
56
|
+
@budget&.reserve(bytes.bytesize)
|
|
33
57
|
# 合并小片段,避免逐字节输入生成大量队列对象;只复制有界尾块。
|
|
34
58
|
# 已消费的前缀用偏移记录,不在每次小读后切出整个剩余缓冲。
|
|
35
59
|
cursor = 0
|
|
@@ -75,6 +99,7 @@ module AnyTLS
|
|
|
75
99
|
end
|
|
76
100
|
end
|
|
77
101
|
@size -= count
|
|
102
|
+
@budget&.release(count)
|
|
78
103
|
result
|
|
79
104
|
end
|
|
80
105
|
end
|
|
@@ -84,6 +109,10 @@ module AnyTLS
|
|
|
84
109
|
unless @finished
|
|
85
110
|
@error = error
|
|
86
111
|
@finished = true
|
|
112
|
+
# 流移出会话后退出共享计费;远端 FIN 保留的尾部仍受单流上限约束,
|
|
113
|
+
# 并可读到 EOF。之后的读、重复 finish/close 不得再次释放预算。
|
|
114
|
+
@budget&.release(@size)
|
|
115
|
+
@budget = nil
|
|
87
116
|
end
|
|
88
117
|
if discard
|
|
89
118
|
@chunks.clear
|
|
@@ -60,8 +60,16 @@ module AnyTLS
|
|
|
60
60
|
end
|
|
61
61
|
start_worker(socket)
|
|
62
62
|
@resource_limited = false
|
|
63
|
+
rescue *IOHelpers::ACCEPT_RETRY_ERRORS => error
|
|
64
|
+
raise if socket.is_a?(IO)
|
|
65
|
+
|
|
66
|
+
pause_accept(shutdown, error)
|
|
63
67
|
rescue Errno::EMFILE, Errno::ENFILE, ThreadError => error
|
|
64
68
|
socket.close if socket.is_a?(IO) && !socket.closed?
|
|
69
|
+
pause_accept(shutdown, error)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def pause_accept(shutdown, error)
|
|
65
73
|
Log.emit(@logger, :warn, "proxy admission paused (#{error.class})") unless @resource_limited
|
|
66
74
|
@resource_limited = true
|
|
67
75
|
IO.select([shutdown.reader], nil, nil, 0.05)
|
|
@@ -17,8 +17,8 @@ module AnyTLS
|
|
|
17
17
|
attr_reader :declared_content_length
|
|
18
18
|
|
|
19
19
|
def read_headers
|
|
20
|
-
|
|
21
|
-
fields =
|
|
20
|
+
# OWS 仅包含 SP/HTAB;保留重复字段,避免规范化掩盖消息定界冲突。
|
|
21
|
+
fields = super.to_a.map { |name, value| [name, value.gsub(/\A[ \t]+|[ \t]+\z/, "")] }
|
|
22
22
|
lengths = fields.select { |name, _| name.downcase == "content-length" }.map(&:last)
|
|
23
23
|
transfers = fields.select { |name, _| name.downcase == "transfer-encoding" }.map(&:last)
|
|
24
24
|
hosts = fields.count { |name, _| name.downcase == "host" }
|
|
@@ -36,7 +36,7 @@ module AnyTLS
|
|
|
36
36
|
fields.map(&:last).each do |value|
|
|
37
37
|
raise ProtocolError, "invalid HTTP header value" if value.match?(/[\x00-\x08\x0a-\x1f\x7f]/)
|
|
38
38
|
end
|
|
39
|
-
|
|
39
|
+
::Protocol::HTTP::Headers.new(fields)
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
def read_request_body(method, headers)
|
|
@@ -49,8 +49,85 @@ module AnyTLS
|
|
|
49
49
|
|
|
50
50
|
super
|
|
51
51
|
end
|
|
52
|
+
|
|
53
|
+
# 仅在同步写出头字段期间合并小写入;不缓存消息体,结束时即发送。
|
|
54
|
+
# 继续使用依赖的字段校验、顺序和重复字段语义。
|
|
55
|
+
def write_headers(headers)
|
|
56
|
+
@header_buffer = +"".b
|
|
57
|
+
super
|
|
58
|
+
bytes = @header_buffer
|
|
59
|
+
@header_buffer = nil
|
|
60
|
+
write_stream(bytes) unless bytes.empty?
|
|
61
|
+
ensure
|
|
62
|
+
@header_buffer = nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def write_stream(bytes)
|
|
68
|
+
return super unless @header_buffer
|
|
69
|
+
|
|
70
|
+
if @header_buffer.bytesize + bytes.bytesize > 16_384
|
|
71
|
+
super(@header_buffer) unless @header_buffer.empty?
|
|
72
|
+
@header_buffer.clear
|
|
73
|
+
end
|
|
74
|
+
if bytes.bytesize > 16_384
|
|
75
|
+
super
|
|
76
|
+
else
|
|
77
|
+
@header_buffer << bytes
|
|
78
|
+
end
|
|
79
|
+
end
|
|
52
80
|
end
|
|
53
81
|
|
|
82
|
+
# 复用上传线程守候最终响应头,不增加每请求的定时器线程。
|
|
83
|
+
# 上传结束才启动预算;提前响应和异常清理均唤醒此等待者。
|
|
84
|
+
class ResponseWait
|
|
85
|
+
def initialize(timeout)
|
|
86
|
+
@timeout = timeout
|
|
87
|
+
@mutex = Mutex.new
|
|
88
|
+
@changed = ConditionVariable.new
|
|
89
|
+
@uploading = true
|
|
90
|
+
@complete = false
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def uploading?
|
|
94
|
+
@mutex.synchronize { @uploading }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def error
|
|
98
|
+
@mutex.synchronize { @error }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def fail(error)
|
|
102
|
+
@mutex.synchronize do
|
|
103
|
+
return if @error
|
|
104
|
+
|
|
105
|
+
@error = error
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def uploaded
|
|
110
|
+
@mutex.synchronize do
|
|
111
|
+
@uploading = false
|
|
112
|
+
deadline = Deadline.new(@timeout)
|
|
113
|
+
deadline.wait(@changed, @mutex) until @complete
|
|
114
|
+
rescue TimeoutError => error
|
|
115
|
+
# 必须先发布超时再关闭上游,让被唤醒的读取者得到原始错误。
|
|
116
|
+
@error = error
|
|
117
|
+
raise
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def complete
|
|
122
|
+
@mutex.synchronize do
|
|
123
|
+
@complete = true
|
|
124
|
+
@changed.broadcast
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private_constant :ResponseWait
|
|
130
|
+
|
|
54
131
|
def initialize(client:, logger: nil, handshake_timeout: 5)
|
|
55
132
|
@client = client
|
|
56
133
|
@logger = logger
|
|
@@ -136,27 +213,33 @@ module AnyTLS
|
|
|
136
213
|
transport.write("\r\n")
|
|
137
214
|
elsif body
|
|
138
215
|
cancel_upload = false
|
|
216
|
+
response_wait = ResponseWait.new(@handshake_timeout)
|
|
139
217
|
upload = Thread.new do
|
|
140
218
|
upstream.write_body("HTTP/1.1", body)
|
|
219
|
+
response_wait.uploaded
|
|
141
220
|
nil
|
|
142
221
|
rescue StandardError => error
|
|
222
|
+
response_wait.fail(error)
|
|
143
223
|
stream.close unless cancel_upload || stream.closed?
|
|
144
224
|
error
|
|
145
225
|
end
|
|
146
226
|
else
|
|
147
227
|
upstream.write_body("HTTP/1.1", body)
|
|
148
228
|
end
|
|
229
|
+
transport.timeout = upload ? nil : @handshake_timeout
|
|
149
230
|
loop do
|
|
150
|
-
transport.timeout = upload&.alive? ? nil : @handshake_timeout
|
|
151
231
|
transport.begin_headers!
|
|
152
232
|
_response_version, status, reason, response_headers, response_body = upstream.read_response(method)
|
|
153
|
-
transport.timeout = nil
|
|
154
233
|
if status.between?(100, 199) && status != 101
|
|
155
234
|
downstream.write_interim_response(version, status, filtered_headers(response_headers), reason)
|
|
156
235
|
next
|
|
157
236
|
end
|
|
158
237
|
|
|
159
|
-
|
|
238
|
+
response_wait&.complete
|
|
239
|
+
raise response_wait.error if response_wait&.error
|
|
240
|
+
|
|
241
|
+
transport.timeout = nil
|
|
242
|
+
if response_wait&.uploading?
|
|
160
243
|
# 目标服务可能在读取完整请求体之前拒绝上传。
|
|
161
244
|
# 先停止本地上传读取并交付完整响应。上传也可能停在上游写入,
|
|
162
245
|
# 必须在 ensure 关闭上游后再 join,否则背压会阻止响应交付。
|
|
@@ -193,15 +276,17 @@ module AnyTLS
|
|
|
193
276
|
break
|
|
194
277
|
end
|
|
195
278
|
rescue StandardError => error
|
|
196
|
-
|
|
197
|
-
raise(upload_error || error)
|
|
279
|
+
raise(response_wait&.error || error)
|
|
198
280
|
ensure
|
|
281
|
+
response_wait&.complete
|
|
199
282
|
stream&.close
|
|
200
|
-
|
|
283
|
+
# 上传结束后线程可能仍在等待响应或退出;不能用 alive? 判断
|
|
284
|
+
# 客户端是否还有未读请求体,否则会误关 keep-alive 的读方向。
|
|
285
|
+
if response_wait&.uploading?
|
|
201
286
|
cancel_upload = true
|
|
202
287
|
io.close_read
|
|
203
|
-
upload.join
|
|
204
288
|
end
|
|
289
|
+
upload&.join
|
|
205
290
|
end
|
|
206
291
|
|
|
207
292
|
def filtered_headers(headers, upgrade: false)
|
data/lib/anytls/io_helpers.rb
CHANGED
|
@@ -4,6 +4,13 @@ module AnyTLS
|
|
|
4
4
|
# 为原生 socket、StringIO 和支持超时的流提供完整二进制读写。
|
|
5
5
|
# 部分进度消耗同一预算;辅助方法不重放失败写入。
|
|
6
6
|
module IOHelpers
|
|
7
|
+
# accept 可返回待接入连接的网络错误;它们不代表监听 fd 已失效。
|
|
8
|
+
# 常量按平台提供的 errno 构建,EBADF/EINVAL 等监听器故障仍向外传播。
|
|
9
|
+
ACCEPT_RETRY_ERRORS = %i[ECONNABORTED EPROTO ENETDOWN ENOPROTOOPT EHOSTDOWN ENONET
|
|
10
|
+
EHOSTUNREACH EOPNOTSUPP ENETUNREACH].filter_map do |name|
|
|
11
|
+
Errno.const_get(name) if Errno.const_defined?(name)
|
|
12
|
+
end.freeze
|
|
13
|
+
|
|
7
14
|
module_function
|
|
8
15
|
|
|
9
16
|
# Linux 上仅关闭描述符不能唤醒另一线程的 select;先终止 socket 通信。
|
data/lib/anytls/server.rb
CHANGED
|
@@ -59,14 +59,15 @@ module AnyTLS
|
|
|
59
59
|
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
|
60
60
|
accept_connection(socket)
|
|
61
61
|
socket = nil # 所有权已交给会话,或连接已被拒绝并关闭。
|
|
62
|
+
rescue *IOHelpers::ACCEPT_RETRY_ERRORS => error
|
|
63
|
+
# 仅重试 accept 本身;已接入 socket 的设置或会话构造错误不可吞掉。
|
|
64
|
+
raise if socket
|
|
65
|
+
|
|
66
|
+
pause_accept(error)
|
|
67
|
+
break if @mutex.synchronize { @stopping }
|
|
62
68
|
rescue Errno::EMFILE, Errno::ENFILE, ThreadError => error
|
|
63
69
|
socket&.close unless socket&.closed?
|
|
64
|
-
|
|
65
|
-
if !@resource_warning_at || now - @resource_warning_at >= 5
|
|
66
|
-
Log.emit(@logger, :warn, "accept capacity exhausted (#{error.class})")
|
|
67
|
-
@resource_warning_at = now
|
|
68
|
-
end
|
|
69
|
-
@mutex.synchronize { @changed.wait(@mutex, 0.05) unless @stopping }
|
|
70
|
+
pause_accept(error)
|
|
70
71
|
break if @mutex.synchronize { @stopping }
|
|
71
72
|
ensure
|
|
72
73
|
socket&.close unless socket&.closed?
|
|
@@ -130,6 +131,15 @@ module AnyTLS
|
|
|
130
131
|
|
|
131
132
|
private
|
|
132
133
|
|
|
134
|
+
def pause_accept(error)
|
|
135
|
+
now = Deadline.now
|
|
136
|
+
if !@resource_warning_at || now - @resource_warning_at >= 5
|
|
137
|
+
Log.emit(@logger, :warn, "accept paused (#{error.class})")
|
|
138
|
+
@resource_warning_at = now
|
|
139
|
+
end
|
|
140
|
+
@mutex.synchronize { @changed.wait(@mutex, 0.05) unless @stopping }
|
|
141
|
+
end
|
|
142
|
+
|
|
133
143
|
def accept_connection(socket)
|
|
134
144
|
@mutex.synchronize do
|
|
135
145
|
if @stopping || @sessions.size >= @max_sessions
|
data/lib/anytls/session.rb
CHANGED
|
@@ -53,7 +53,7 @@ module AnyTLS
|
|
|
53
53
|
@open_timeout = open_timeout
|
|
54
54
|
@max_streams = max_streams
|
|
55
55
|
@receive_limit = receive_limit
|
|
56
|
-
@
|
|
56
|
+
@receive_budget = ByteBuffer::Budget.new(session_receive_limit)
|
|
57
57
|
@send_limit = send_limit
|
|
58
58
|
@logger = logger
|
|
59
59
|
@on_stream = on_stream
|
|
@@ -507,10 +507,6 @@ module AnyTLS
|
|
|
507
507
|
stream = @streams[frame.stream_id]
|
|
508
508
|
return unless stream
|
|
509
509
|
|
|
510
|
-
if @streams.values.sum(&:buffered_bytes) + frame.payload.bytesize > @session_receive_limit
|
|
511
|
-
raise BufferLimitExceeded, "session receive buffer limit exceeded"
|
|
512
|
-
end
|
|
513
|
-
|
|
514
510
|
stream.receive(frame.payload)
|
|
515
511
|
rescue BufferLimitExceeded => error
|
|
516
512
|
remove_stream(frame.stream_id)&.finish(error)
|
|
@@ -546,7 +542,7 @@ module AnyTLS
|
|
|
546
542
|
end
|
|
547
543
|
|
|
548
544
|
def register_stream(id)
|
|
549
|
-
stream = Stream.new(self, id, receive_limit: @receive_limit)
|
|
545
|
+
stream = Stream.new(self, id, receive_limit: @receive_limit, receive_budget: @receive_budget)
|
|
550
546
|
@mutex.synchronize do
|
|
551
547
|
@streams[id] = stream
|
|
552
548
|
@active_count = @streams.size
|
data/lib/anytls/session_pool.rb
CHANGED
|
@@ -29,6 +29,7 @@ module AnyTLS
|
|
|
29
29
|
@factory = factory
|
|
30
30
|
@mutex = Mutex.new
|
|
31
31
|
@changed = ConditionVariable.new
|
|
32
|
+
@maintenance_changed = ConditionVariable.new
|
|
32
33
|
@sessions = {}
|
|
33
34
|
@idle = {}
|
|
34
35
|
@creating = 0
|
|
@@ -123,6 +124,7 @@ module AnyTLS
|
|
|
123
124
|
sessions = @mutex.synchronize do
|
|
124
125
|
@closed = true
|
|
125
126
|
@changed.broadcast
|
|
127
|
+
@maintenance_changed.broadcast
|
|
126
128
|
result = @sessions.keys
|
|
127
129
|
@sessions.clear
|
|
128
130
|
@idle.clear
|
|
@@ -143,7 +145,12 @@ module AnyTLS
|
|
|
143
145
|
@maintenance = Thread.new do
|
|
144
146
|
loop do
|
|
145
147
|
expired = @mutex.synchronize do
|
|
146
|
-
|
|
148
|
+
# 租用者的容量通知不触发全池扫描;关闭仍须立即唤醒维护线程。
|
|
149
|
+
# 绝对时间防止虚假唤醒提前扫描或重新消耗完整间隔。
|
|
150
|
+
check_at = Deadline.now + @interval
|
|
151
|
+
while !@closed && (remaining = check_at - Deadline.now).positive?
|
|
152
|
+
@maintenance_changed.wait(@mutex, remaining)
|
|
153
|
+
end
|
|
147
154
|
break if @closed
|
|
148
155
|
|
|
149
156
|
retained = @idle.keys.sort_by { |session| -session.sequence }.first(@min_idle_sessions)
|
data/lib/anytls/stream.rb
CHANGED
|
@@ -7,10 +7,10 @@ module AnyTLS
|
|
|
7
7
|
class Stream
|
|
8
8
|
attr_reader :id
|
|
9
9
|
|
|
10
|
-
def initialize(session, id, receive_limit: 262_144)
|
|
10
|
+
def initialize(session, id, receive_limit: 262_144, receive_budget: nil)
|
|
11
11
|
@session = session
|
|
12
12
|
@id = id
|
|
13
|
-
@buffer = ByteBuffer.new(limit: receive_limit)
|
|
13
|
+
@buffer = ByteBuffer.new(limit: receive_limit, budget: receive_budget)
|
|
14
14
|
@mutex = Mutex.new
|
|
15
15
|
@changed = ConditionVariable.new
|
|
16
16
|
@closed = false
|
data/lib/anytls/version.rb
CHANGED