topbit 3.2.2 → 3.2.3

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.
@@ -42,4 +42,4 @@ if (port_ind > 0 && port_ind < process.argv.length - 1) {
42
42
  port = 2022
43
43
  }
44
44
 
45
- app.run(port)
45
+ app.printServInfo().run(port)
@@ -14,7 +14,7 @@ let app = new Topbit({
14
14
  if (app.isWorker) {
15
15
  let h2proxy = new Http2Proxy({
16
16
  config: {
17
- 'x.com': [
17
+ 'v.com': [
18
18
  {
19
19
  url: 'http://localhost:3001',
20
20
  weight: 10,
@@ -45,4 +45,4 @@ if (app.isWorker) {
45
45
  h2proxy.init(app)
46
46
  }
47
47
 
48
- app.daemon(1234, 2)
48
+ app.printServInfo().daemon(1234, 2)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "topbit",
3
- "version": "3.2.2",
3
+ "version": "3.2.3",
4
4
  "description": "A Server-side web framework support http/1.1 and http/2",
5
5
  "main": "src/topbit.js",
6
6
  "directories": {
@@ -29,7 +29,7 @@ let error_503_text = `<!DOCTYPE html><html>
29
29
  <p>此服务暂时不可用。</p>
30
30
  </div>
31
31
  </body>
32
- </html>`
32
+ </html>`
33
33
 
34
34
  function fmtpath(path) {
35
35
  path = path.trim()
@@ -83,6 +83,8 @@ let Http2Proxy = function (options = {}) {
83
83
 
84
84
  this.maxBody = 50000000
85
85
 
86
+ this.realIPHeader = 'x-real-ip'
87
+
86
88
  //是否启用全代理模式。
87
89
  this.full = false
88
90
 
@@ -103,8 +105,18 @@ let Http2Proxy = function (options = {}) {
103
105
  family: 4
104
106
  }
105
107
 
108
+ this.balancer = (options.balancer
109
+ && options.balancer.select
110
+ && typeof options.balancer.select === 'function')
111
+ ? options.balancer
112
+ : null
113
+
106
114
  for (let k in options) {
107
115
  switch (k) {
116
+ case 'realIPHeader':
117
+ this.realIPHeader = options[k]
118
+ break
119
+
108
120
  case 'config':
109
121
  this.config = options[k]
110
122
  break
@@ -323,9 +335,12 @@ Http2Proxy.prototype.checkAlive = function (pr) {
323
335
 
324
336
  Http2Proxy.prototype.getBackend = function (c, host) {
325
337
  let prlist = this.hostProxy[host][c.routepath]
326
-
327
338
  let pxybalance = this.proxyBalance[host][c.routepath]
328
339
 
340
+ if (this.balancer) {
341
+ return this.balancer.select(c, prlist, pxybalance)
342
+ }
343
+
329
344
  let pr
330
345
 
331
346
  if (prlist.length === 1) {
@@ -399,7 +414,7 @@ Http2Proxy.prototype.mid = function () {
399
414
 
400
415
  if (!self.hostProxy[host] || !self.hostProxy[host][c.routepath]) {
401
416
  if (self.full) {
402
- return c.status(502).to(error_502_text)
417
+ return c.status(502).to(error_502_text)
403
418
  }
404
419
 
405
420
  return await next(c)
@@ -408,10 +423,10 @@ Http2Proxy.prototype.mid = function () {
408
423
  let pr = self.getBackend(c, host)
409
424
  if (!pr) return c.status(503).to(error_503_text)
410
425
 
411
- if (self.addIP && c.headers['x-real-ip']) {
412
- c.headers['x-real-ip'] += `,${c.ip}`
426
+ if (self.addIP && c.headers[self.realIPHeader]) {
427
+ c.headers[self.realIPHeader] += `,${c.ip}`
413
428
  } else {
414
- c.headers['x-real-ip'] = c.ip
429
+ c.headers[self.realIPHeader] = c.ip
415
430
  }
416
431
 
417
432
  let hii = pr.h2Pool
@@ -40,6 +40,8 @@ class Proxy {
40
40
 
41
41
  this.methods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'HEAD', 'PATCH', 'TRACE']
42
42
 
43
+ this.realIPHeader = 'x-real-ip'
44
+
43
45
  this.hostProxy = {}
44
46
 
45
47
  this.proxyBalance = {}
@@ -104,8 +106,18 @@ class Proxy {
104
106
  options = {}
105
107
  }
106
108
 
109
+ this.balancer = (options.balancer
110
+ && options.balancer.select
111
+ && typeof options.balancer.select === 'function')
112
+ ? options.balancer
113
+ : null
114
+
107
115
  for (let k in options) {
108
116
  switch (k) {
117
+ case 'realIPHeader':
118
+ this.realIPHeader = options[k]
119
+ break
120
+
109
121
  case 'host':
110
122
  case 'config':
111
123
  this.config = options[k]
@@ -375,6 +387,10 @@ class Proxy {
375
387
  getBackend(c, host) {
376
388
  let prlist = this.hostProxy[host][c.routepath]
377
389
  let pb = this.proxyBalance[host][c.routepath]
390
+ if (this.balancer) {
391
+ return this.balancer.select(c, prlist, pxybalance)
392
+ }
393
+
378
394
  let pr
379
395
 
380
396
  if (prlist.length === 1) {
@@ -448,10 +464,10 @@ class Proxy {
448
464
  urlobj.headers = c.headers
449
465
  urlobj.method = c.method
450
466
 
451
- if (self.addIP && urlobj.headers['x-real-ip']) {
452
- urlobj.headers['x-real-ip'] += `,${c.ip}`
467
+ if (self.addIP && urlobj.headers[self.realIPHeader]) {
468
+ urlobj.headers[self.realIPHeader] += `,${c.ip}`
453
469
  } else {
454
- urlobj.headers['x-real-ip'] = c.ip
470
+ urlobj.headers[self.realIPHeader] = c.ip
455
471
  }
456
472
 
457
473
  let hci = urlobj.protocol == 'https:' ? https : http
@@ -0,0 +1,76 @@
1
+ 'use strict'
2
+
3
+ const crypto = require('node:crypto')
4
+
5
+ /**
6
+ * 确定性负载均衡器
7
+ * 支持基于用户 ID 的哈希分发
8
+ */
9
+ class ConsistentBalancer {
10
+ constructor(options = {}) {
11
+ // 定义如何从请求上下文 c 中提取唯一标识
12
+ // 默认尝试提取 header 中的 user-id,或者使用 IP
13
+ this.identityFn = options.identityFn || ((c) => {
14
+ return c.user ? c.user.id : c.ip
15
+ })
16
+
17
+ this.hashAlgorithm = options.hashAlgorithm || 'sha256'
18
+ }
19
+
20
+ /**
21
+ * 负载均衡选择算法
22
+ * @param {Object} c 请求上下文
23
+ * @param {Array} prlist 备选后端列表
24
+ * @param {Object} pxybalance 状态表
25
+ */
26
+ select(c, prlist, pxybalance) {
27
+ if (!prlist || prlist.length === 0) return null
28
+
29
+ if (prlist.length === 1) return prlist[0]
30
+
31
+ // 1. 提取标识符
32
+ const identity = this.identityFn(c)
33
+
34
+ if (!identity) {
35
+ // 如果没有标识符,退回到随机/轮询(或者直接用原逻辑)
36
+ return this.fallback(prlist, pxybalance)
37
+ }
38
+
39
+ // 2. 过滤健康的后端 (基于你原有的 checkAlive 逻辑)
40
+ // 注意:pr.h2Pool.ok() 是判断连接池是否正常的关键
41
+ const aliveBackends = prlist.filter(pr => pr.h2Pool && pr.h2Pool.ok())
42
+
43
+ const targets = aliveBackends.length > 0 ? aliveBackends : prlist
44
+
45
+ // 3. 确定性哈希计算 (Rendezvous Hashing)
46
+ let maxWeight = -1
47
+ let selected = targets[0]
48
+
49
+ for (let pr of targets) {
50
+ // 计算 Hash(identity + server_url)
51
+ let hash = crypto.createHash(this.hashAlgorithm)
52
+ .update(identity + pr.url)
53
+ .digest()
54
+ .readUInt32BE(0)
55
+
56
+ // 结合权重计算分值 (HRW Hashing 变体)
57
+ // 使用公式:Score = Hash * (Weight^(1/n)) 或者简单乘法
58
+ let score = hash * (pr.weight || 1)
59
+
60
+ if (score > maxWeight) {
61
+ maxWeight = score
62
+ selected = pr
63
+ }
64
+ }
65
+
66
+ return selected
67
+ }
68
+
69
+ // 兜底逻辑
70
+ fallback(prlist, pxybalance) {
71
+ if (pxybalance.stepIndex >= prlist.length) pxybalance.stepIndex = 0
72
+ return prlist[pxybalance.stepIndex++]
73
+ }
74
+ }
75
+
76
+ module.exports = ConsistentBalancer
package/src/topbit.js CHANGED
@@ -35,6 +35,7 @@ const TopbitExtends = require('./_loadExtends.js')
35
35
  const npargv = require('./lib/npargv.js')
36
36
  const zipdata = require('./lib/zipdata.js')
37
37
  const ErrorLog = require('./lib/errorlog.js')
38
+ const Balancer = require('./lib/balancer.js')
38
39
 
39
40
  let __instance__ = 0;
40
41
 
@@ -1335,5 +1336,6 @@ Topbit.npargv = npargv;
1335
1336
  Topbit.zipdata = zipdata;
1336
1337
  Topbit.ErrorLog = ErrorLog;
1337
1338
  Topbit.extensions = TopbitExtends;
1339
+ Topbit.ProxyBalancer = Balancer;
1338
1340
 
1339
1341
  module.exports = Topbit;