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 CHANGED
@@ -1921,7 +1921,7 @@ let hxy = new Http2Proxy({
1921
1921
  weight: 10,
1922
1922
  path : '/',
1923
1923
  reconnDelay: 200, // 重连延迟
1924
- max: 2,
1924
+ maxConnect: 2,
1925
1925
  headers: {
1926
1926
  'x-test-key': `${Date.now()}`
1927
1927
  },
package/README.md CHANGED
@@ -1882,7 +1882,7 @@ let hxy = new Http2Proxy({
1882
1882
  weight: 10,
1883
1883
  path : '/',
1884
1884
  reconnDelay: 200, // Reconnection delay
1885
- max: 2,
1885
+ maxConnect: 2,
1886
1886
  headers: {
1887
1887
  'x-test-key': `${Date.now()}`
1888
1888
  },
package/demo/group-api.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const titbit = require('../lib/titbit.js')
3
+ const titbit = require('../src/topbit.js')
4
4
 
5
5
  const app = new titbit({
6
6
  debug: true,
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const Titbit = require('../lib/titbit.js')
3
+ const Titbit = require('../src/topbit.js')
4
4
 
5
5
  let app = new Titbit({
6
6
  debug: true,
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
- //http2: true,
30
+ http2: args.http2,
28
31
  loadMonitor: true,
29
32
  loadInfoType : args.loadtype,
30
33
  globalLog : false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "topbit",
3
- "version": "3.1.0",
3
+ "version": "3.1.2",
4
4
  "description": "A Server-side web framework support http/1.1 and http/2",
5
5
  "main": "src/topbit.js",
6
6
  "directories": {
@@ -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
- this.innerConnectDelay = 0
16
- this.failedCount = 0
17
- this.reconnecting = false
18
- // 配置项
19
- this.maxStreamId = !isNaN(options.maxStreamId) && options.maxStreamId > 1
20
- ? options.maxStreamId
21
- : 90000
22
-
23
- this.timeout = options.timeout || 30000
24
- this.connectTimeout = options.connectTimeout || 15000
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
- requestCert: false,
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
- this.reconnDelay = 100
44
- if (options.reconnDelay !== undefined && !isNaN(options.reconnDelay)) {
45
- this.reconnDelay = options.reconnDelay
46
- }
47
-
48
- this.parent = null
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
- * 创建新的session连接
41
+ * 内部建立连接
63
42
  */
64
- async connect() {
65
- if (this.pool.size > this.maxConnect) {
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
- // 初始化session相关计数器和状态
78
- const sessionState = {
79
- using: false,
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
- error: null,
86
- aliveStreams: 0
52
+ aliveStreams: 0, // 当前并发数
53
+ weight: 1 // 预留权重字段
87
54
  }
88
55
 
89
- // 处理session事件
90
- this._handleSessionEvents(sessionState)
91
-
92
- // 等待连接建立
93
- try {
94
- let timeout_timer = null
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
- session.on('error', err => {
221
- this.debug && console.error(err)
222
- !session.destroyed && session.destroy()
223
- this.pool.delete(id)
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
- session.on('frameError', err => {
227
- !session.destroyed && session.destroy()
228
- this.pool.delete(id)
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('goaway', err => {
232
- this.debug && err && console.error('..........goaway........', err)
233
- !session.destroyed && session.close()
234
- this.pool.delete(id)
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
- session.setTimeout(this.timeout, () => {
238
- this.debug && console.error('session.....time.....out......')
239
- if (!session.destroyed) {
240
- session.destroy()
241
- }
242
-
243
- this.pool.delete(id)
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
- isSessionHealthy(session) {
248
- return session
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
- * 获取可用的session,如果没有则创建新的
109
+ * 获取最佳可用 Session (Round-Robin)
257
110
  */
258
111
  async getSession() {
259
- if (this.pool.size > 0) {
260
- let items = this.pool.entries()
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
- if (state.aliveStreams < 1) {
275
- state.session.destroy()
276
- }
277
- /* else {
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
- this.pool.delete(state.id)
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
- return this.connect()
292
- }
293
-
294
- /**
295
- * 创建新的请求stream
296
- */
297
- async request(headers, sessionState=null) {
298
- !sessionState && (sessionState = await this.getSession())
299
-
300
- sessionState.streamCount++
301
-
302
- if (!sessionState.connected) {
303
- if (this.quiet) return null
304
- throw new Error('There is no connected')
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
- * 关闭所有session
168
+ * 发起请求 (核心入口)
313
169
  */
314
- closeAll() {
315
- for (const [id, state] of this.pool.entries()) {
316
- if (!state.session.destroyed) {
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
- async aok() {
325
- if (this.pool.size <= 0) {
326
- await this.connect()
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
- let items = this.pool.entries()
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