292 lines
8.1 KiB
Markdown
292 lines
8.1 KiB
Markdown
# 低代码的服务器端应用框架 基于 [ssgo/s](https://github.com/ssgo/s)
|
||
|
||
快速创建一个web服务,提供 http、https、http2、h2c、websocket 服务
|
||
支持作为静态文件服务器
|
||
支持 rewrite 和 proxy反向代理(可以代理到discover应用或其他http服务器)
|
||
支持服务发现 [ssgo/discover](https://github.com/ssgo/discover) 快速构建一个服务网络
|
||
|
||
## 快速开始
|
||
|
||
```javascript
|
||
import s from "apigo.cc/gojs/service"
|
||
|
||
func main() {
|
||
s.config({
|
||
listen: '8080',
|
||
})
|
||
s.register({ path: '/echo' }, ({ args }) => {
|
||
return args.name
|
||
})
|
||
s.start()
|
||
}
|
||
```
|
||
|
||
受Javascript虚拟机机制限制简单方式是单线程,大量耗时请求压力下可能难以胜任
|
||
|
||
## 使用对象池实现高并发
|
||
|
||
#### api/echo.js
|
||
|
||
```javascript
|
||
import s from "apigo.cc/gojs/service"
|
||
|
||
function main() {
|
||
service.register({ path: '/echo' }, ({ args }) => {
|
||
return args.name
|
||
})
|
||
}
|
||
```
|
||
|
||
#### main.js
|
||
|
||
```javascript
|
||
import s from "apigo.cc/gojs/service"
|
||
|
||
function main() {
|
||
s.load('api/echo.js', { min: 20, max: 1000, idle: 100 })
|
||
s.start()
|
||
}
|
||
```
|
||
|
||
这种模式下服务代码会脱离主线程,使用对象池实现高并发
|
||
如果不配置对象池参数则不约束虚拟机数量上限,可以达到最佳性能但是对CPU和内存有一定挑战
|
||
设置较小的max可以有效保护CPU和内存资源但是设置过小将无法发挥服务器的性能
|
||
|
||
## 注册到服务发现
|
||
|
||
#### 在 user 服务中配置注册中心地址和应用名称,并配置访问令牌
|
||
|
||
```javascript
|
||
s.config({
|
||
app: 'user',
|
||
registry: redis://:password@127.0.0.1:6379/15
|
||
accessTokens: {
|
||
aaaaaaaa: 1,
|
||
},
|
||
})
|
||
s.register({ path: '/getUserInfo', authLevel: 1 }, ({ session }) => {
|
||
return session.get('id', 'name')
|
||
})
|
||
```
|
||
|
||
#### 在 调用的服务中配置访问user服务使用的令牌
|
||
|
||
```javascript
|
||
s.config({
|
||
registry: redis://:password@127.0.0.1:6379/15
|
||
calls: {
|
||
user: 'aaaaaaaa',
|
||
},
|
||
})
|
||
s.register({ path: '/getUserInfo' }, ({ caller }) => {
|
||
return caller.get('user/getUserInfo').object()
|
||
})
|
||
```
|
||
|
||
authLevel 不设置则不进行校验,如果获得 authLevel 2 则允许访问所有 2及以下级别的接口
|
||
如果 user 服务不配置 listen 默认使用 h2c 协议随机端口
|
||
如果不使用 h2c 协议,调用方配置 calls 时需指定 'http:aaaaaaaa'
|
||
|
||
## Session
|
||
|
||
服务默认启用 session 和 device 功能,如果不希望使用可以在配置中设置 sessionKey 或 deviceKey 为空
|
||
如果配置了 sessionProvider,session 将会存储在 redis 中,否则存储在内存中
|
||
sessionID和deviceID 同时支持HTTP头和Cookie两种传输方式,HTTP头优先,如果客户端没有传递则服务器会自动分配
|
||
如需使用 session 只需要在接口中直接获取即可
|
||
|
||
#### 下面是一个使用 session 并且使用参数有效性验证和限流器的例子
|
||
|
||
```javascript
|
||
import service from "apigo.cc/gojs/service"
|
||
|
||
let verifies = {
|
||
id: v => { return /^\d+$/.test(v) },
|
||
name: /^[a-zA-Z0-9_-\u4e00-\u9fa5\u3400-\u4db5\u3000-\u303F\u3040-\u309F\u30A0-\u30FF\u1100-\u11FF\u3130-\u318F\uAC00-\uD7AF\uD82F\uD835\uD83C\uD83D\uD83E\uD83F\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872]+$/u,
|
||
}
|
||
|
||
function main() {
|
||
service.config({
|
||
userIdKey: 'id',
|
||
limitedMessage: { code: 429, message: "访问过于频繁" },
|
||
authFieldMessage: { code: 403, message: "身份验证失败 [{{USER_AUTHLEVEL}}/{{TARGET_AUTHLEVEL}}]" },
|
||
verifyFieldMessage: { code: 400, message: "参数 [{{FAILED_FIELDS}} 验证失败" },
|
||
limiters: {
|
||
ip1s: {
|
||
from: 'ip',
|
||
time: 1000,
|
||
times: 10
|
||
}
|
||
},
|
||
})
|
||
service.register({ method: 'POST', path: '/login', , limiters: ['ip1s'], verifies }, ({ args, session }) => {
|
||
session.set('id', args.id)
|
||
session.set('name', args.name)
|
||
session.setAuthLevel(1)
|
||
session.save()
|
||
return { code: 1 }
|
||
})
|
||
service.register({ method: 'GET', path: '/userInfo', authLevel: 1, limiters: ['ip1s'] }, ({ session }) => {
|
||
return { code: 1, data: session.get('id', 'name') }
|
||
})
|
||
}
|
||
```
|
||
|
||
session对象自动注入,无需任何其他操作。修改session后需要使用 session.save 来保存
|
||
调用 session.setAuthLevel 可以设置用户权限,当接口注册的 authLevel 大于0时可以基于 session 中的设置进行访问控制
|
||
配置了 userIdKey 后,会自动将 session 的用户ID记录在访问日志中,方便对用户的访问进行分析和统计
|
||
示例中创建了一个每个IP每秒允许10次请求的限流器并且在接口中使用了这个限流器
|
||
login接口配置了 id 和 name 两个参数的有效性验证规则
|
||
参数有效性验证配置可以支持以下类型:
|
||
|
||
- value为string或RegExp对象时进行正则表达式校验
|
||
- value为number时表示 字符串长度
|
||
- value为boolean时表示 必须存在
|
||
- value为数组时表示 必须是数组中的值
|
||
- value为函数时调用函数进行校验
|
||
|
||
|
||
## 配置文件
|
||
|
||
除了在代码中使用 s.config 外可以有三种配置方式
|
||
|
||
1、在当前目录下创建 service.yml 或 service.json 文件
|
||
|
||
```yaml
|
||
listen: 80|443
|
||
ssl:
|
||
yourdomain.com:
|
||
certfile: /path/yourdomain.pem
|
||
keyfile: /path/yourdomain.pem
|
||
```
|
||
|
||
2、在环境配置文件 env.yml 或 env.json 中配置
|
||
|
||
```yaml
|
||
service:
|
||
listen: 80|443
|
||
```
|
||
|
||
3、在环境变量中配置(以docker为例)
|
||
|
||
```shell
|
||
docker run -e SERVICE_LISTEN=8080:8443
|
||
```
|
||
|
||
#### 所有配置方式的优先级为 s.config > 环境变量 > env.yml > service.yml
|
||
|
||
## 静态文件
|
||
|
||
```yaml
|
||
service:
|
||
static:
|
||
yourdomain.com: /path/www
|
||
yourdomain.com:8080: /path/www8080
|
||
yourdomain.com:80/abc: /path/abc
|
||
/def: /path/def
|
||
```
|
||
|
||
可以根据域名和路径配置静态文件
|
||
可以使用 file 模块将文件加载到内存中加速访问
|
||
|
||
```javascript
|
||
import file from "apigo.cc/gojs/file"
|
||
|
||
file.cache('/path/www', true)
|
||
```
|
||
|
||
## 反向代理和Rewrite
|
||
|
||
```yaml
|
||
service:
|
||
proxy:
|
||
yourdomain.com: serverA
|
||
/abc: http://HOST:PORT/PATH
|
||
yourdomain.com/def/(.*): http://127.0.0.1:8080/$1
|
||
rewrite:
|
||
yourdomain.com/001/(.*): /path/001/$1
|
||
yourdomain.com/002/(.*): http://127.0.0.1:8080/$1
|
||
http://yourdomain.com(.*): https://yourdomain.com$1
|
||
```
|
||
|
||
## websocket
|
||
|
||
```javascript
|
||
import service from "apigo.cc/gojs/service"
|
||
|
||
function main() {
|
||
service.register({
|
||
method: 'WS', path: '/ws',
|
||
onMessage: ({ client, type, data }) => {
|
||
// onMessage
|
||
client.writeMessage(type, data)
|
||
},
|
||
onClose: ({ client }) => {
|
||
// onClose
|
||
},
|
||
}, ({ client }) => {
|
||
// onOpen
|
||
client.write('Hello, World!')
|
||
})
|
||
}
|
||
```
|
||
|
||
注册接口时将 method 指定为 WS 即可创建 websocket 服务,配置 onMessage 来异步处理消息
|
||
|
||
## 后台任务
|
||
|
||
#### taskA.js
|
||
|
||
```javascript
|
||
import s from "apigo.cc/gojs/service"
|
||
|
||
// function onStart() {
|
||
// // TODO 任务启动时执行
|
||
// }
|
||
|
||
function onRun() {
|
||
// TODO 在指定间隔时间到达时被调用,根据需要对资源进行处理
|
||
if ( s.dataCount('websocketClients') > 0 ) {
|
||
let conns = s.dataFetch('websocketClients')
|
||
for ( let conn of conns ) {
|
||
conn.write('Hello, World!')
|
||
}
|
||
}
|
||
}
|
||
|
||
function onStop() {
|
||
// TODO 服务结束时被调用,用来收尾或释放资源
|
||
s.dataRemove('websocketClients')
|
||
}
|
||
```
|
||
|
||
#### main.js
|
||
|
||
```javascript
|
||
import s from "apigo.cc/gojs/service"
|
||
|
||
function main() {
|
||
s.task('task.js', 1000) // 每秒执行一次
|
||
s.register({method: 'WS', path: '/ws'}, ({ client }) => {
|
||
// 将连接放到资源池中供后台任务使用
|
||
s.dataSet('websocketClients', client.id, client)
|
||
})
|
||
s.start()
|
||
}
|
||
```
|
||
|
||
task 必须在单独的js文件中定义
|
||
每个 task 都会运行在单独的vm中
|
||
定义 task 必须在服务启动(s.start)之前
|
||
服务停止(s.stop)所有任务会被停止
|
||
|
||
#### 任务队列
|
||
|
||
```javascript
|
||
s.listPush('taskA', {})
|
||
s.listPop('taskA', {})
|
||
```
|
||
|
||
可以使用 list 相关操作实现基于队列的后台任务处理
|
||
|
||
## 完整的API参考 [service.ts](https://apigo.cc/gojs/service/src/branch/main/service.ts)
|