topbit 3.1.0 → 3.1.2
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.
- package/README.cn.md +1 -1
- package/README.md +1 -1
- package/demo/group-api.js +1 -1
- package/demo/group-api2.js +1 -1
- package/demo/monitor.js +6 -3
- package/package.json +1 -1
- package/src/extends/Http2Pool.js +143 -309
- package/src/extends/http2proxy.js +39 -141
- package/src/extends/proxy.js +16 -17
- package/src/lib/npargv.js +224 -280
- package/test/test-route-sort.js +1 -1
- package/test/test-route2.js +1 -1
package/README.cn.md
CHANGED
package/README.md
CHANGED
package/demo/group-api.js
CHANGED
package/demo/group-api2.js
CHANGED
package/demo/monitor.js
CHANGED
|
@@ -12,11 +12,14 @@ let {args} = npargv({
|
|
|
12
12
|
'--loadstdio': {
|
|
13
13
|
name: 'loadstdio',
|
|
14
14
|
default: false
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
'--http2': {
|
|
18
|
+
name: 'http2',
|
|
19
|
+
default: false
|
|
15
20
|
}
|
|
16
21
|
})
|
|
17
22
|
|
|
18
|
-
console.log(args)
|
|
19
|
-
|
|
20
23
|
const app = new Topbit({
|
|
21
24
|
debug : true,
|
|
22
25
|
allow : new Set(['127.0.0.1']),
|
|
@@ -24,7 +27,7 @@ const app = new Topbit({
|
|
|
24
27
|
unitTime: 10,
|
|
25
28
|
useLimit: true,
|
|
26
29
|
maxConn: 2000,
|
|
27
|
-
|
|
30
|
+
http2: args.http2,
|
|
28
31
|
loadMonitor: true,
|
|
29
32
|
loadInfoType : args.loadtype,
|
|
30
33
|
globalLog : false,
|
package/package.json
CHANGED
package/src/extends/Http2Pool.js
CHANGED
|
@@ -6,360 +6,194 @@ const crypto = require('node:crypto')
|
|
|
6
6
|
class Http2Pool {
|
|
7
7
|
|
|
8
8
|
constructor(options = {}) {
|
|
9
|
-
// 存储session的Map
|
|
10
9
|
if (!options || typeof options !== 'object') options = {}
|
|
11
|
-
if (!options.connectOptions) options.connectOptions = {}
|
|
12
|
-
|
|
13
|
-
this.pool = new Map()
|
|
14
10
|
|
|
15
|
-
|
|
16
|
-
this.
|
|
17
|
-
this.
|
|
18
|
-
|
|
19
|
-
this.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
this.
|
|
24
|
-
this.
|
|
25
|
-
|
|
26
|
-
this.max = (options.max && !isNaN(options.max) && options.max > 0) ? options.max : 50
|
|
27
|
-
this.poolMax = Math.floor(this.max * 1.5 + 0.5)
|
|
28
|
-
this.maxConnect = (options.maxConnect && !isNaN(options.maxConnect) && options.maxConnect > 0)
|
|
29
|
-
? options.maxConnect
|
|
30
|
-
: this.poolMax + 500
|
|
31
|
-
|
|
32
|
-
this.url = options.url || ''
|
|
33
|
-
this.debug = options.debug || false
|
|
34
|
-
// 连接选项
|
|
11
|
+
// 配置初始化
|
|
12
|
+
this.maxConnect = options.maxConnect || 100 // 最大物理连接数
|
|
13
|
+
this.maxAliveStreams = options.maxAliveStreams || 100 // 单连接最大并发流
|
|
14
|
+
this.url = options.url
|
|
15
|
+
this.debug = !!options.debug
|
|
16
|
+
this.reconnDelay = options.reconnDelay || 500
|
|
17
|
+
|
|
18
|
+
// 核心数据结构
|
|
19
|
+
this.sessions = [] // 使用数组代替Map,利用索引做 Round-Robin
|
|
20
|
+
this.cursor = 0 // 轮询指针
|
|
21
|
+
|
|
35
22
|
this.connectOptions = {
|
|
36
23
|
rejectUnauthorized: false,
|
|
37
|
-
|
|
38
|
-
peerMaxConcurrentStreams: 100,
|
|
39
|
-
timeout: this.timeout,
|
|
24
|
+
timeout: options.timeout || 30000,
|
|
40
25
|
...options.connectOptions
|
|
41
26
|
}
|
|
27
|
+
|
|
28
|
+
this.waitQueue = [] // 等待可用连接的队列
|
|
29
|
+
}
|
|
42
30
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (options.parent && typeof options.parent === 'object') {
|
|
51
|
-
this.parent = options.parent
|
|
31
|
+
/**
|
|
32
|
+
* 初始化连接池 (预热)
|
|
33
|
+
*/
|
|
34
|
+
createPool(initialSize = 5) {
|
|
35
|
+
for (let i = 0; i < initialSize; i++) {
|
|
36
|
+
this._createConnection()
|
|
52
37
|
}
|
|
53
|
-
|
|
54
|
-
this.maxAliveStreams = options.maxAliveStreams || 100
|
|
55
|
-
|
|
56
|
-
this.quiet = false
|
|
57
|
-
if (options.quiet)
|
|
58
|
-
this.quiet = !!options.quiet
|
|
59
38
|
}
|
|
60
39
|
|
|
61
40
|
/**
|
|
62
|
-
*
|
|
41
|
+
* 内部建立连接
|
|
63
42
|
*/
|
|
64
|
-
|
|
65
|
-
if (this.
|
|
66
|
-
return {
|
|
67
|
-
deny: true,
|
|
68
|
-
error: `超出最大连接限制:${this.maxConnect}`
|
|
69
|
-
}
|
|
70
|
-
}
|
|
43
|
+
_createConnection() {
|
|
44
|
+
if (this.sessions.length >= this.maxConnect) return null
|
|
71
45
|
|
|
72
46
|
const session = http2.connect(this.url, this.connectOptions)
|
|
73
|
-
|
|
74
|
-
// 生成唯一session id
|
|
75
|
-
const sessionId = crypto.randomBytes(16).toString('hex')
|
|
76
47
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
id: sessionId,
|
|
81
|
-
session,
|
|
82
|
-
streamCount: 0,
|
|
83
|
-
url: this.url,
|
|
48
|
+
const wrapper = {
|
|
49
|
+
id: crypto.randomBytes(8).toString('hex'),
|
|
50
|
+
session: session,
|
|
84
51
|
connected: false,
|
|
85
|
-
|
|
86
|
-
|
|
52
|
+
aliveStreams: 0, // 当前并发数
|
|
53
|
+
weight: 1 // 预留权重字段
|
|
87
54
|
}
|
|
88
55
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
let resolved = false
|
|
96
|
-
let rejected = false
|
|
97
|
-
|
|
98
|
-
await new Promise((resolve, reject) => {
|
|
99
|
-
session.once('connect', () => {
|
|
100
|
-
if (timeout_timer) {
|
|
101
|
-
clearTimeout(timeout_timer)
|
|
102
|
-
timeout_timer = null
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (this.failedCount > 0) {
|
|
106
|
-
this.failedCount--
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
this.innerConnectDelay = 0
|
|
110
|
-
|
|
111
|
-
sessionState.connected = true
|
|
112
|
-
this.parent && !this.parent.alive && (this.parent.alive = true)
|
|
113
|
-
|
|
114
|
-
resolve()
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
session.once('error', err => {
|
|
118
|
-
if (timeout_timer) {
|
|
119
|
-
clearTimeout(timeout_timer)
|
|
120
|
-
timeout_timer = null
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (this.pool.size < 1) {
|
|
124
|
-
this.parent && (this.parent.alive = false)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
this.failedCount++
|
|
128
|
-
|
|
129
|
-
if (this.failedCount < 10) {
|
|
130
|
-
this.innerConnectDelay = this.failedCount
|
|
131
|
-
} if (this.failedCount < 60000) {
|
|
132
|
-
this.innerConnectDelay = Math.floor(this.failedCount / 10)
|
|
133
|
-
} else { this.innerConnectDelay = 6000 }
|
|
134
|
-
|
|
135
|
-
!rejected && (rejected = true) && reject(err)
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
session.once('goaway', err => {
|
|
139
|
-
if (timeout_timer) {
|
|
140
|
-
clearTimeout(timeout_timer)
|
|
141
|
-
timeout_timer = null
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
!rejected && (rejected = true) && reject(err||new Error('goaway'))
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
session.once('frameError', err => {
|
|
148
|
-
if (timeout_timer) {
|
|
149
|
-
clearTimeout(timeout_timer)
|
|
150
|
-
timeout_timer = null
|
|
151
|
-
}
|
|
152
|
-
!rejected && (rejected = true) && reject(err)
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
if (!timeout_timer) {
|
|
156
|
-
timeout_timer = setTimeout(() => {
|
|
157
|
-
timeout_timer = null
|
|
158
|
-
!session.destroyed && session.destroy()
|
|
159
|
-
!rejected && (rejected = true) && reject(new Error('connect timeout'))
|
|
160
|
-
}, this.connectTimeout)
|
|
161
|
-
}
|
|
162
|
-
})
|
|
163
|
-
} catch (err) {
|
|
164
|
-
sessionState.error = err
|
|
165
|
-
sessionState.session = null
|
|
166
|
-
this.debug && console.error(err)
|
|
167
|
-
} finally {
|
|
168
|
-
this.reconnecting = false
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (this.pool.size < this.poolMax && sessionState.connected) {
|
|
172
|
-
this.pool.set(sessionId, sessionState)
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return sessionState
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
createPool(max=0) {
|
|
179
|
-
if (max <= 0) max = this.max
|
|
180
|
-
|
|
181
|
-
for (let i = 0; i < max; i++) {
|
|
182
|
-
this.connect()
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
delayConnect() {
|
|
187
|
-
if (this.reconnecting) return false
|
|
188
|
-
|
|
189
|
-
let delay_time = this.reconnDelay + this.innerConnectDelay
|
|
190
|
-
|
|
191
|
-
if (delay_time > 0) {
|
|
192
|
-
if (!this.delayTimer) {
|
|
193
|
-
this.reconnecting = true
|
|
194
|
-
this.delayTimer = setTimeout(() => {
|
|
195
|
-
this.delayTimer = null
|
|
196
|
-
this.connect()
|
|
197
|
-
}, delay_time)
|
|
198
|
-
}
|
|
199
|
-
} else {
|
|
200
|
-
this.reconnecting = true
|
|
201
|
-
this.connect()
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* 处理session的各种事件
|
|
207
|
-
*/
|
|
208
|
-
_handleSessionEvents(sessionState) {
|
|
209
|
-
const { session, id } = sessionState
|
|
210
|
-
|
|
211
|
-
session.on('close', () => {
|
|
212
|
-
// session关闭时从pool中移除
|
|
213
|
-
this.pool.delete(id)
|
|
214
|
-
|
|
215
|
-
if (this.pool.size < 1) {
|
|
216
|
-
this.delayConnect()
|
|
56
|
+
session.once('connect', () => {
|
|
57
|
+
wrapper.connected = true
|
|
58
|
+
// 触发队列中的等待者
|
|
59
|
+
while (this.waitQueue.length > 0 && wrapper.aliveStreams < this.maxAliveStreams) {
|
|
60
|
+
const resolve = this.waitQueue.shift()
|
|
61
|
+
resolve(wrapper)
|
|
217
62
|
}
|
|
218
63
|
})
|
|
219
64
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
65
|
+
// 错误处理与清理
|
|
66
|
+
const cleanup = () => {
|
|
67
|
+
if (wrapper._destroyed) return
|
|
68
|
+
wrapper._destroyed = true
|
|
69
|
+
|
|
70
|
+
// 从池中移除
|
|
71
|
+
const idx = this.sessions.indexOf(wrapper)
|
|
72
|
+
if (idx !== -1) {
|
|
73
|
+
this.sessions.splice(idx, 1)
|
|
74
|
+
// 修正指针,防止跳过
|
|
75
|
+
if (this.cursor >= idx && this.cursor > 0) this.cursor--
|
|
76
|
+
}
|
|
225
77
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
78
|
+
if (!session.destroyed) session.destroy()
|
|
79
|
+
|
|
80
|
+
// 自动补充连接 (维持最小连接数,可选)
|
|
81
|
+
if (this.sessions.length < 2) {
|
|
82
|
+
// 防止雪崩,延迟重连
|
|
83
|
+
setTimeout(() => this._createConnection(), this.reconnDelay)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
230
86
|
|
|
231
|
-
session.on('
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
87
|
+
session.on('close', cleanup)
|
|
88
|
+
session.on('error', (err) => {
|
|
89
|
+
if(this.debug) console.error(`[H2Pool] Session Error ${this.url}:`, err.message)
|
|
90
|
+
cleanup()
|
|
235
91
|
})
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
92
|
+
session.on('goaway', cleanup)
|
|
93
|
+
|
|
94
|
+
// 超时保活策略:只有完全空闲才销毁,否则发送 ping
|
|
95
|
+
session.setTimeout(this.connectOptions.timeout, () => {
|
|
96
|
+
if (wrapper.aliveStreams > 0) {
|
|
97
|
+
// 还有流量,不销毁,尝试 ping 保持活跃
|
|
98
|
+
session.ping && session.ping(() => {})
|
|
99
|
+
} else {
|
|
100
|
+
session.close() // 优雅关闭
|
|
101
|
+
}
|
|
244
102
|
})
|
|
245
|
-
}
|
|
246
103
|
|
|
247
|
-
|
|
248
|
-
return
|
|
249
|
-
&& !session.destroyed
|
|
250
|
-
&& !session.closed
|
|
251
|
-
&& session.socket
|
|
252
|
-
&& !session.socket.destroyed
|
|
104
|
+
this.sessions.push(wrapper)
|
|
105
|
+
return wrapper
|
|
253
106
|
}
|
|
254
107
|
|
|
255
108
|
/**
|
|
256
|
-
*
|
|
109
|
+
* 获取最佳可用 Session (Round-Robin)
|
|
257
110
|
*/
|
|
258
111
|
async getSession() {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
for (const [id, state] of items) {
|
|
262
|
-
if (state.connected
|
|
263
|
-
&& state.streamCount < this.maxStreamId
|
|
264
|
-
&& this.isSessionHealthy(state.session))
|
|
265
|
-
{
|
|
266
|
-
if (state.aliveStreams < this.maxAliveStreams) {
|
|
267
|
-
return state
|
|
268
|
-
}
|
|
269
|
-
} else {
|
|
270
|
-
state.connected = false
|
|
271
|
-
if (!state.session.destroyed) {
|
|
272
|
-
state.session.close()
|
|
112
|
+
let tried = 0
|
|
113
|
+
const len = this.sessions.length
|
|
273
114
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
let sess = state.session
|
|
279
|
-
setTimeout(() => {
|
|
280
|
-
!sess.destroyed && sess.destroy()
|
|
281
|
-
sess = null
|
|
282
|
-
}, this.timeout + 5000)
|
|
283
|
-
} */
|
|
284
|
-
}
|
|
115
|
+
// 1. 尝试轮询获取可用连接
|
|
116
|
+
while (tried < len) {
|
|
117
|
+
this.cursor = (this.cursor + 1) % len
|
|
118
|
+
const wrapper = this.sessions[this.cursor]
|
|
285
119
|
|
|
286
|
-
|
|
287
|
-
|
|
120
|
+
if (wrapper && wrapper.connected && !wrapper.session.destroyed && wrapper.aliveStreams < this.maxAliveStreams) {
|
|
121
|
+
return wrapper
|
|
122
|
+
}
|
|
123
|
+
tried++
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 2. 如果没有可用连接,且未达上限,创建新连接
|
|
127
|
+
if (this.sessions.length < this.maxConnect) {
|
|
128
|
+
const newWrapper = this._createConnection()
|
|
129
|
+
if (newWrapper) {
|
|
130
|
+
// 等待连接建立 (设置一个短超时)
|
|
131
|
+
return new Promise((resolve, reject) => {
|
|
132
|
+
const timer = setTimeout(() => {
|
|
133
|
+
reject(new Error('Connection timeout'))
|
|
134
|
+
}, 5000)
|
|
135
|
+
|
|
136
|
+
const onConnect = () => {
|
|
137
|
+
clearTimeout(timer)
|
|
138
|
+
newWrapper.session.removeListener('connect', onConnect)
|
|
139
|
+
resolve(newWrapper)
|
|
140
|
+
}
|
|
141
|
+
newWrapper.session.once('connect', onConnect)
|
|
142
|
+
newWrapper.session.once('error', (err) => {
|
|
143
|
+
clearTimeout(timer)
|
|
144
|
+
reject(err)
|
|
145
|
+
})
|
|
146
|
+
})
|
|
288
147
|
}
|
|
289
148
|
}
|
|
290
149
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
//创建请求stream
|
|
308
|
-
return sessionState.session.request(headers)
|
|
150
|
+
// 3. 还是没有,进入队列等待 (削峰填谷)
|
|
151
|
+
return new Promise((resolve, reject) => {
|
|
152
|
+
// 3秒后还没拿到连接就报错
|
|
153
|
+
const timer = setTimeout(() => {
|
|
154
|
+
const idx = this.waitQueue.indexOf(resolve)
|
|
155
|
+
if (idx !== -1) this.waitQueue.splice(idx, 1)
|
|
156
|
+
reject(new Error('No available h2 session (Queued timeout)'))
|
|
157
|
+
}, 3000)
|
|
158
|
+
|
|
159
|
+
// 包装 resolve 以清理 timer
|
|
160
|
+
this.waitQueue.push((wrapper) => {
|
|
161
|
+
clearTimeout(timer)
|
|
162
|
+
resolve(wrapper)
|
|
163
|
+
})
|
|
164
|
+
})
|
|
309
165
|
}
|
|
310
166
|
|
|
311
167
|
/**
|
|
312
|
-
*
|
|
168
|
+
* 发起请求 (核心入口)
|
|
313
169
|
*/
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
state.session.close()
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
this.pool.clear()
|
|
322
|
-
}
|
|
170
|
+
async request(headers) {
|
|
171
|
+
const wrapper = await this.getSession()
|
|
172
|
+
wrapper.aliveStreams++
|
|
323
173
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
174
|
+
try {
|
|
175
|
+
const stream = wrapper.session.request(headers)
|
|
176
|
+
|
|
177
|
+
// 监听流关闭,减少计数
|
|
178
|
+
stream.once('close', () => {
|
|
179
|
+
wrapper.aliveStreams--
|
|
180
|
+
// 如果有等待队列,唤醒一个
|
|
181
|
+
if (this.waitQueue.length > 0) {
|
|
182
|
+
const resolve = this.waitQueue.shift()
|
|
183
|
+
resolve(wrapper)
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
return stream
|
|
188
|
+
} catch (e) {
|
|
189
|
+
wrapper.aliveStreams--
|
|
190
|
+
throw e
|
|
327
191
|
}
|
|
328
|
-
|
|
329
|
-
return this.ok()
|
|
330
192
|
}
|
|
331
193
|
|
|
332
194
|
ok() {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
for (const [id, state] of items) {
|
|
336
|
-
if (state.connected) return true
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
return false
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* 获取当前pool状态
|
|
344
|
-
*/
|
|
345
|
-
status() {
|
|
346
|
-
const status = {
|
|
347
|
-
total: this.pool.size,
|
|
348
|
-
sessions: []
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
let items = this.pool.entries()
|
|
352
|
-
|
|
353
|
-
for (const [id, state] of items) {
|
|
354
|
-
status.sessions.push({
|
|
355
|
-
id: state.id,
|
|
356
|
-
streamCount: state.streamCount,
|
|
357
|
-
connected: state.connected
|
|
358
|
-
})
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
return status
|
|
195
|
+
return this.sessions.some(s => s.connected && !s.session.destroyed)
|
|
362
196
|
}
|
|
363
197
|
}
|
|
364
198
|
|
|
365
|
-
module.exports = Http2Pool
|
|
199
|
+
module.exports = Http2Pool
|