基于HTTP协议,扩展X-Upgrade头部的多协议MVC执行库
<dependency>
<groupId>cn.xnatural</groupId>
<artifactId>xnet</artifactId>
<version>1.2.2</version>
</dependency>
maven
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<!-- 编译时保留方法参数名 -->
<parameters>true</parameters>
</configuration>
</plugin>
gradle
compileJava {
options.compilerArgs << '-parameters'
}
compileGroovy {
groovyOptions.parameters = true
}
XNet xNet = new XNet(":8080");
xNet.http().chain()
// 添加控制层
.resolve(
new TestCtrl(), new MainCtrl()
)
// 手动添加路由
.get("/a/b/{c}", ctx -> {
ctx.render("c=" + ctx.param("c"));
});
.post("/a/b/{c}", ctx -> {
ctx.render("c=" + ctx.param("c"));
});
xNet.start();
@Route(path = "test", protocol = "HTTP")
public class TestCtrl {
// 支持get,post,put多种方法, /test/cus?p1=aa&p2=bb
@Route(path = "cus")
ApiResp form(Integer p1, Integer p2, HttpContext ctx) {
return ApiResp.ok(ctx.request().getFormParams());
}
// 接收form 表单提交. /test/form?p1=aa&p2=bb
@Route(path = "form", consume = "application/x-www-form-urlencoded")
ApiResp form(Integer p1, String p2, HttpContext ctx) {
return ApiResp.ok(ctx.request().getFormParams());
}
// json 参数
@Route(path = "json", consume = "application/json", method = "post")
ApiResp json(HttpContext ctx) {
return ApiResp.ok(ctx.request().getJsonParams());
}
// 手动响应(方便异步处理)
@Route(path = "async")
void async(String p1, HttpContext ctx) {
ctx.render(
ApiResp.ok("p1: " + p1 + ", " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()))
);
}
// 文件上传
@Route(path = "upload", method = "post")
ApiResp upload(FileData file, String version) throws Exception {
if (file == null) return ApiResp.fail("文件未上传");
File uploadDir = new File("./upload");
uploadDir.mkdirs();
file.transferTo(uploadDir);
log.info("upload file: " + file);
return ApiResp.ok().attr("file", file.toString()).attr("version", version);
}
// 下载文件, 路径变量, 自定义响应头
@Route(path = "download/{fName}", method = "get")
File download(String fName, HttpContext ctx) throws IOException {
ctx.response.contentDisposition("attachment;filename=" + f.getName());
return new File("./upload/" + fName);
}
}
final Set<WebSocket> wss = ConcurrentHashMap.newKeySet();
public void wsBroadcast(String msg) {
wss.forEach(ws -> ws.send(msg));
}
@WS(path = "msg")
public void wsReceive(WebSocket ws) {
log.info("WS connect. {}, {}", ws.request.getCookie("sessionId"), ws.getSession().getRemoteAddress());
ws.listen(new WsListener() {
@Override
public void onClose(WebSocket wst) {
wss.remove(wst);
}
@Override
public void onText(String msg) {
log.info("test ws receive client msg: {}", msg);
}
@Override
public void onBinary(byte[] msg) {
}
});
wsBroadcast("上线: " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
wss.add(ws);
}
扩展协议 X-Upgrade: X-Cluster
// 节点b1
XNet xNet = new XNet(":6001")
.setAttr("cluster.master", ":5001") // master指向a
.setAttr("cluster.name", "b") // 节点名(必须)
.start();
// 节点b2
XNet xNet = new XNet(":6002")
.setAttr("cluster.master", ":5001") // master指向a
.setAttr("cluster.name", "b") // 节点名(必须)
.start();
// 节点a1
XNet xNet = new XNet(":5001")
.setAttr("cluster.name", "a") // 节点名(必须)
.start();
Threaad.sleep(5 * 1000);
xnet.cluster().http("b", "/user/1").get(); // 会随机选择b的一个节点请求
cluster.name: 节点名(必须)
- 同名多实例节点自动组成集群
cluster.master: 集群master
- 配置后节点会自动与master所持有的应用组成集群
- 不定时向master上传自己的信息, 并获取集群中其它暴露给自己的节点信息
- 支持格式例子(多个以逗号分割 :6000,:7000)
- :6000
- localhost:6000
- https://:6000
cluster.exposeTo: 暴露给哪些应用
应用与应用之间互通和隔离配置
支持的格式(多个以逗号分割)
- * : 表示匹配任意节点名
- a1 : 匹配 a1
- *a1: 匹配以 a1结尾的节点名
- a1*: 匹配以 a1开头的节点名
默认 * 暴露给所有节点
本地开发环境可以配个不存在的应用名(比如 - ), 这样就可以让应用既获取集群其他应用节点, 又隔离于其他应用节点
cluster.proxyHp: 由额外的ip端口代理到本节点
- 一般用于由nginx加域名隐藏后边的节点, 让集群中其他节点通过proxyHp访问到本节点
cluster.syncToMe: 要求master是否主动同步我(默认true)
- 其他节点每次有更新都立即通知我
- 会主动调用来请求节点的接口节点更新
cluster.backFeed: 要求master是在我每次注册的时候同步所有节点给我(默认false)
- 会主动调用来请求节点的接口节点更新
公共请求头部:
header名 | 是否必须 | header说明 |
---|---|---|
X-Upgrade | 是 | 固定值:X-Cluster |
Content-Type | 是 | application/json 或 application/x-www-form-urlencoded |
X-Request-ID | 否 | 请求id |
被请求的是请求者的master
接口路由:/nodeUp
请求方法:POST
参数说明:
参数名 | 是否必须 | 参数说明 |
---|---|---|
name | 是 | 节点名 |
id | 是 | 节点id(保证同名节点下唯一id) |
hp | 是 | 请求节点暴露的访问ip和端口 例: 192.168.1.1:8080 或 xxx.com 或 https://xxx.com |
exposeTo | 否 | 暴露给哪些节点名 多个以逗号分割 例子: a,b 默认为*暴露给所有节点 |
syncToMe | 否 | 其他节点有更新是否主动同步我 会主动调用来请求节点的接口节点更新 |
backFeed | 否 | 立即同步所有节点给我 会主动调用来请求节点的接口节点更新 |
返回示例:
{
"code": "00",
"id": "b:laHi69J3On:1", // 请求id(可用X-Request-ID的值替代)
"msg": null,
"data": null,
"mark": null // 参数传什么就返回什么
}
如果syncToMe为true: master会主动通过此接口同步变化
接口路由:/nodeUpdate
请求方法:POST
参数说明:
参数名 | 是否必须 | 参数说明 |
---|---|---|
name | 是 | 节点名 |
id | 是 | 同名节点下唯一id |
hp | 是 | 节点暴露的访问ip和端口 例: 192.168.1.1:8080 或 xxx.com 或 https://xxx.com |
exposeTo | 否 | 暴露给哪些节点名 多个以逗号分割 例子: a,b 默认为*暴露给所有节点 |
syncToMe | 否 | 有更新是否主动同步我 |
返回示例:
{
"code": "00",
"id": "b:laHi69J3On:1", // 请求id(可用X-Request-ID的值替代)
"msg": null,
"data": null,
"mark": null // 参数传什么就返回什么
}
接口路由:/nodeDown/{infect}
请求方法:POST
参数说明:
参数名 | 是否必须 | 参数说明 |
---|---|---|
infect | 是 | true: 通知所有 false: 只下线一个节点 |
name | 是 | 节点名 |
id | 否 | 节点唯一id. 不传则匹配所有name的节点 |
返回示例:
{
"code": "00",
"id": "b:laHi69J3On:1", // 请求id(可用X-Request-ID的值替代)
"msg": null,
"data": null,
"mark": null // 参数传什么就返回什么
}
接口路由:/nodes/{appName}
请求方法:GET
参数说明:
参数名 | 是否必须 | 参数说明 |
---|---|---|
appName | 是 | 节点名 |
返回示例:
{
"code": "00",
"data": [
{
"_master": false,
"_uptime": 1686462464267,
"exposeTo": [
"*"
],
"hp": {
"host": "192.168.1.9",
"port": 6000,
"secure": false
},
"id": "b1",
"name": "b",
"syncToMe": true
},
{
"_master": false,
"_uptime": 1686462467716,
"exposeTo": [
"*"
],
"hp": {
"host": "192.168.1.9",
"port": 6001,
"secure": false
},
"id": "b2",
"name": "b",
"syncToMe": true
}
],
"id": "HgFGXbqlvoTvOCOnj7XMu",
"mark": null,
"msg": null
}
{
"code": "00",
"data": [
"a",
"b",
"c"
],
"id": "lrzu64KGHuXKGcKhJUfHp",
"mark": null,
"msg": null
}
{
"code": "00",
"data": {
"a": [
{
"_master": true,
"_uptime": 1686462823298,
"exposeTo": [
"*"
],
"hp": {
"host": "192.168.1.9",
"port": 5000,
"secure": false
},
"id": "RgWPXGaqW8fLfDRLDmOUY",
"name": "a",
"syncToMe": true
}
]
},
"id": "aDy5cvl9NhtfU64XCEB5X",
"mark": null,
"msg": null
}
{
"code": "00",
"data": {
"_master": false,
"_uptime": 1686791259583,
"exposeTo": [
"*"
],
"hp": {
"host": "172.17.0.1",
"port": 5000,
"secure": false
},
"id": "UzTaQHCIbpW940nCgAjza",
"name": "a",
"syncToMe": true
},
"id": "hDMznYyPiyzNZTts3I57o",
"mark": null,
"msg": null
}
扩展协议 X-Upgrade: X-ap
一般用于同名多节点有内存缓存,缓存更改后和其他节点之间的数据同步通知
@Route(path = "user", protocol = AP.DATA_VERSION)
void ap_user(String dataKey, Long version) {
// 处理同名应用其他节点的用户数据更改通知
}
ap.update("user", "user1", System.currentTimeMillis());
扩展协议 X-Upgrade: X-PieceFile
// 分片上传文件函数
function pieceUpload(file) {
const uploading = { // 分片上传对象
file: file,
pause: false, // 是否暂停
pieceSize: 1024 * 1024 * 5, // 每个分片的大小
pos: 0, // 当前切片的位置
progress: 0, // 上传的进度 0~100
url: null, // 上传完成后的完整下载地址
error: '',
uploadId: (((1+Math.random())*0x10000)|0).toString(16) + (((1+Math.random())*0x10000)|0).toString(16) + '_' + new Date().getTime()
};
doUpload(uploading)
}
// 分片上传执行函数
function doUpload(uploading) {
const fd = new FormData();
const endIndex = Math.min(uploading.pos + uploading.pieceSize, uploading.file.size);
// 分割当前上传的数据段
fd.append('file', uploading.file.slice(uploading.pos, endIndex));
// 配置http header
const headers = {
"Content-Type": "multipart/form-data",
"X-Upgrade": "X-PieceFile",
"x-pieceupload-id": uploading.uploadId,
}
if (uploading.pos === 0) { // 第一片必传参数
headers['x-pieceupload-filename'] = uploading.file.name
headers['x-pieceupload-length'] = uploading.file.size
}
// axios 执行上传动作
axios.post('test/upload', fd, {type: 'post', headers,}).then(resp => {
if (resp.data.code === '00') {
uploading.pos += uploading.pieceSize
// 判断所有分片是否已经上传完
if (uploading.pos >= uploading.file.size) {
uploading.progress = 100;
uploading.url = '//' + window.location.host + '/file/' +resp.data.data.fileId
}
// 判断是否暂停
else if (!uploading.pause) {
uploading.progress = Math.floor((uploading.pos / uploading.file.size) * 100);
const wait = resp.data.data.leftRead % uploading.pieceSize
if (wait) { // 服务器处理慢,稍等上传
setTimeout(() => doUpload(uploading), wait * 200)
} else {
doUpload(uploading)
}
}
} else {
// 上传错误处理
uploading.error = resp.data.msg;
}
})
}
// 获取上传的文件
const fd = new FormData(document.getElementById("form"));
const file = fd.get('file');
pieceUpload(file)
{
"uploadId": "上传id",
"fileId": "服务生成的文件id",
"end": false, // 是否处理结束
"leftRead": 1024 // 后端在已有的数据还剩多少没读取处理
}
@Route(path = "upload", method = "post", protocol = PieceFileHandler.X_PIECE_FILE)
void upload(FileData file) throws Exception {
if (file == null) return ApiResp.fail("文件未上传");
File uploadDir = new File("./upload");
uploadDir.mkdirs();
file.transferTo(uploadDir);
log.info("upload file: " + file);
}
TODO
TODO
TODO
TODO
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。