mirror of
https://github.com/idrunk/dce-go.git
synced 2026-04-22 23:17:04 +08:00
483 lines
16 KiB
HTML
483 lines
16 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Websocket test</title>
|
|
<style>
|
|
body{width: 800px; margin: 50px auto}
|
|
.chat-box {display: flex; border: 1px solid #aaa; border-radius: 6px;}
|
|
.cb-left {flex: auto; border-right: 1px solid #aaa}
|
|
.chat-board {height: 500px; overflow-y: auto; border-bottom: 1px solid #aaa;}
|
|
.msg-bubble {padding: 10px;}
|
|
.msg-bubble.self {text-align: right}
|
|
.msg-head {font-size: 14px; color: #777;}
|
|
.msg-head .time-wrap {font-size: 12px; margin-left: 10px;}
|
|
.msg-bubble.self .time-wrap {margin: 0 10px 0 0;}
|
|
.msg-bubble p {margin: 0; padding: 10px 5px;}
|
|
.editor-wrap {display: flex;}
|
|
.editor-wrap #editor {flex: auto; border: 0; border-right: 1px solid #aaa; border-bottom-left-radius: 8px;}
|
|
.editor-wrap button {width: 80px; height: 48px; border: 0;}
|
|
.group-members {width: 160px; list-style: none; margin: 0; padding: 0;}
|
|
.group-members li {margin: 10px;}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>[<span class="online-state"></span>] <span class="session-user"></span>'s group chat</h1>
|
|
<div class="chat-box">
|
|
<div class="cb-left">
|
|
<div class="chat-board"></div>
|
|
<form class="editor-wrap">
|
|
<textarea id="editor"></textarea>
|
|
<button type="submit">Send</button>
|
|
</form>
|
|
</div>
|
|
<ul class="group-members"></ul>
|
|
</div>
|
|
<script>
|
|
class FlexWebsocketClient {
|
|
ws
|
|
sidHandler
|
|
requestMapping = {}
|
|
listenMapping = {}
|
|
|
|
constructor(address, sidHandler, onopen, onclose, onmessage) {
|
|
const self = this
|
|
this.sidHandler = sidHandler
|
|
this.ws = new WebSocket(address)
|
|
this.ws.binaryType = "arraybuffer"
|
|
this.ws.onopen = onopen
|
|
this.ws.onclose = onclose
|
|
this.ws.onmessage = function (ev) {
|
|
let fp
|
|
try {
|
|
fp = FlexPackage.deserialize(Array.from(new Uint8Array(ev.data)))
|
|
} catch (e) {
|
|
onmessage && onmessage(ev.data) || console.error(e)
|
|
return
|
|
}
|
|
self.sidHandler(fp)
|
|
if (fp.code || fp.message) {
|
|
console.warn(`Code: ${fp.code}, Message: ${fp.message}`)
|
|
}
|
|
if (fp.id in self.requestMapping) {
|
|
self.requestMapping[fp.id](fp)
|
|
} else if (fp.path in self.listenMapping) {
|
|
self.listenMapping[fp.path](fp)
|
|
} else if (onmessage) {
|
|
onmessage(fp, true)
|
|
}
|
|
}
|
|
}
|
|
|
|
bind(path, callback) {
|
|
this.listenMapping[path] = callback
|
|
}
|
|
|
|
sendText(content) {
|
|
this.ws.send(content)
|
|
}
|
|
|
|
send(path, content, id) {
|
|
const pkg = FlexPackage.new(path, content, this.sidHandler(), id)
|
|
this.sendText(pkg.serialize())
|
|
return pkg.id
|
|
}
|
|
|
|
async request(path, content) {
|
|
const reqId = this.send(path, content, -1)
|
|
const self = this
|
|
return new Promise(resolve => {
|
|
self.requestMapping[reqId] = function(fp) {
|
|
resolve(fp.body)
|
|
delete self.requestMapping[fp.id]
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
class PackageField {
|
|
constructor(field, kind) {
|
|
this.field = field
|
|
this.kind = kind
|
|
}
|
|
}
|
|
|
|
const baseFields = [
|
|
new PackageField("id", "uint"),
|
|
new PackageField("path", "string"),
|
|
new PackageField("numPath", "uint"),
|
|
new PackageField("sid", "string"),
|
|
new PackageField("code", "int"),
|
|
new PackageField("message", "string"),
|
|
new PackageField("body", "bytes"),
|
|
];
|
|
|
|
class FlexPackage {
|
|
static #reqId = 0
|
|
|
|
id
|
|
path
|
|
numPath
|
|
sid
|
|
code
|
|
message
|
|
body
|
|
|
|
static new(path, body, sid, id, numPath) {
|
|
if (id === -1) {
|
|
id = ++FlexPackage.#reqId
|
|
}
|
|
const fp = new FlexPackage
|
|
fp.id = id
|
|
fp.path = path
|
|
fp.sid = sid
|
|
fp.numPath = numPath
|
|
fp.body = body
|
|
return fp
|
|
}
|
|
|
|
serialize() {
|
|
let flag = 0;
|
|
const totalFields = baseFields.length;
|
|
const numHeadVec = [];
|
|
const textBuffer = [];
|
|
const bodyBuffer = [];
|
|
for (const [i, fc] of baseFields.entries()) {
|
|
let nh;
|
|
let textSeq;
|
|
const prop = this[fc.field];
|
|
if (prop === undefined) {
|
|
continue;
|
|
} else if (fc.kind === "int") {
|
|
nh = FlexNum.intPackHead(prop);
|
|
} else if (fc.kind === "uint") {
|
|
nh = FlexNum.uintPackHead(prop);
|
|
} else if (fc.kind === "string") {
|
|
textSeq = new TextEncoder().encode(prop);
|
|
nh = FlexNum.non0LenPackHead(textSeq.length);
|
|
} else if (fc.kind === "bytes") {
|
|
textSeq = new TextEncoder().encode(prop);
|
|
nh = FlexNum.non0LenPackHead(textSeq.length);
|
|
} else {
|
|
continue;
|
|
}
|
|
flag |= 1 << i;
|
|
numHeadVec.push(nh);
|
|
if (fc.kind === "bytes") {
|
|
textSeq && bodyBuffer.push(textSeq);
|
|
} else if (textSeq) {
|
|
textBuffer.push(textSeq);
|
|
}
|
|
}
|
|
|
|
const flagSeq = FlexNum.uintSerialize(flag);
|
|
const flagLen = flagSeq.length;
|
|
const buffer = [...flagSeq];
|
|
buffer[buffer.length + flagLen - 1] = undefined;
|
|
for (const [i, nh] of numHeadVec.entries()) {
|
|
buffer[flagLen+i] = nh[0];
|
|
buffer.push(...FlexNum.packBody(nh));
|
|
}
|
|
for (const part of textBuffer) {
|
|
buffer.push(...part);
|
|
}
|
|
for (const part of bodyBuffer) {
|
|
buffer.push(...part);
|
|
}
|
|
return Uint8Array.from(buffer);
|
|
}
|
|
|
|
static deserialize(seq) {
|
|
const fp = new FlexPackage;
|
|
const head = seq.splice(0, 1)?.[0];
|
|
if (!head) {
|
|
return fp;
|
|
}
|
|
const nh = FlexNum.parseHead(head, false);
|
|
let flag = nh[0];
|
|
if (nh[1] > 0) {
|
|
const flagBodySeq = seq.splice(0, nh[1]);
|
|
flagBodySeq.unshift(nh[3]);
|
|
flag = FlexNum.parse(flagBodySeq);
|
|
}
|
|
|
|
const onesCount = FlexNum.onesCount(flag);
|
|
const numHeadList = seq.splice(0, onesCount)
|
|
if (numHeadList.length < onesCount) {
|
|
return fp;
|
|
}
|
|
const numInfoList = new Array(onesCount).fill(0)
|
|
let nhi = 0;
|
|
let bitsLen = FlexNum.bitsLen(flag);
|
|
for (let i=0; i<bitsLen; i++) {
|
|
if (((1 << i) & flag) === 0) {
|
|
continue;
|
|
}
|
|
const nh = FlexNum.parseHead(numHeadList[nhi], true);
|
|
const numBodySeq = seq.splice(0, nh[1]);
|
|
numInfoList[nhi] = [i, nh, numBodySeq];
|
|
nhi ++;
|
|
}
|
|
|
|
if (bitsLen > baseFields.length) {
|
|
return fp;
|
|
}
|
|
for (const ni of numInfoList) {
|
|
const fc = baseFields[ni[0]];
|
|
if (fc.kind === "string" || fc.kind === "bytes") {
|
|
const len = FlexNum.non0LenParse([ni[1][3], ...ni[2]]);
|
|
const sq = seq.splice(0, len);
|
|
if (sq.length < len) {
|
|
return fp;
|
|
}
|
|
fp[fc.field] = new TextDecoder().decode(Uint8Array.from(sq));
|
|
} else if (fc.kind === "int") {
|
|
const val = FlexNum.intParse([ni[1][0], ...ni[2]], ni[1][2]);
|
|
fp[fc.field] = val;
|
|
} else if (fc.kind === "uint") {
|
|
const val = FlexNum.parse([ni[1][3], ...ni[2]]);
|
|
fp[fc.field] = val;
|
|
}
|
|
}
|
|
return fp
|
|
}
|
|
}
|
|
|
|
class FlexNum {
|
|
static uintSerialize(unsigned) {
|
|
return this.#serialize(...this.uintPackHead(unsigned));
|
|
}
|
|
|
|
static intSerialize(integer) {
|
|
return this.#serialize(...this.intPackHead(integer));
|
|
}
|
|
|
|
static non0LenPackHead(unsigned) {
|
|
return this.uintPackHead(unsigned - 1);
|
|
}
|
|
|
|
static uintPackHead(unsigned) {
|
|
const usize = Math.abs(unsigned);
|
|
const bitsLen = this.bitsLen(usize);
|
|
const [head, bytesLen] = this.#packHead(usize, bitsLen);
|
|
return [head, bytesLen, bitsLen, usize];
|
|
}
|
|
|
|
static intPackHead(integer) {
|
|
let unsigned = 0
|
|
if (integer < 0) {
|
|
unsigned = Math.abs(integer)
|
|
}
|
|
let bitsLen = FlexNum.bitsLen(unsigned)
|
|
let [head, bytesLen] = FlexNum.#packHead(unsigned, bitsLen)
|
|
if (integer < 0) {
|
|
let negative = 1
|
|
if (bytesLen < 7) {
|
|
negative = 1 << (6 - bytesLen)
|
|
}
|
|
head |= negative
|
|
}
|
|
return [head, bytesLen, bitsLen, unsigned]
|
|
}
|
|
|
|
static #packHead(unsigned, bitsLen) {
|
|
let bytesLen = Math.floor(bitsLen / 8)
|
|
let headMaskShift = 8 - bytesLen
|
|
let headBits = 0
|
|
if (bytesLen > 5) {
|
|
bytesLen = 8
|
|
headMaskShift = 2
|
|
} else if (bitsLen%8 > 7-bytesLen) {
|
|
bytesLen ++
|
|
headMaskShift --
|
|
} else {
|
|
headBits |= unsigned >> (bytesLen * 8)
|
|
}
|
|
return [255 << headMaskShift & 255 | headBits, bytesLen]
|
|
}
|
|
|
|
static #serialize(head, bytesLen, bitsLen, u64) {
|
|
let units = new Uint8Array(bytesLen + 1)
|
|
units[0] = head
|
|
units.set(this.packBody(u64, bitsLen, bytesLen), 1)
|
|
return units
|
|
}
|
|
|
|
static packBody(usize, bitsLen, bytesLen) {
|
|
let units = new Uint8Array(bytesLen)
|
|
for (let i=0; i<bytesLen && i*8 < bitsLen; i++) {
|
|
units[bytesLen-i-1] = usize >> (i * 8) & 255
|
|
}
|
|
return units
|
|
}
|
|
|
|
static uintDeserialize(seq) {
|
|
[seq[0]] = this.parseHead(seq[0], false)
|
|
return this.parse(seq)
|
|
}
|
|
|
|
static intDeserialize(seq) {
|
|
const [headBits, _, negative] = this.parseHead(seq[0], true)
|
|
seq[0] = headBits
|
|
return this.intParse(seq, negative)
|
|
}
|
|
|
|
static parseHead(head, sign) {
|
|
let unsignedBits = 0
|
|
let bytesLen = 0
|
|
let negative = false
|
|
let originalBits = 0
|
|
for (let i = 0; i<8; i ++) {
|
|
if ((128 >> i & head) === 0) {
|
|
if ((bytesLen = i) > 5) {
|
|
bytesLen = 8
|
|
originalBits = 1 & head
|
|
} else {
|
|
originalBits = 127 >> bytesLen & head
|
|
}
|
|
break
|
|
}
|
|
}
|
|
unsignedBits = originalBits
|
|
if (sign) {
|
|
if (bytesLen === 8) {
|
|
negative = (1 & head) === 1
|
|
} else {
|
|
let signShift = 0
|
|
if ((negative = (64 >> bytesLen & head) > 0)) {
|
|
signShift = 1
|
|
}
|
|
unsignedBits = 127 >> bytesLen >> signShift & head
|
|
}
|
|
}
|
|
return [unsignedBits, bytesLen, negative, originalBits]
|
|
}
|
|
|
|
static intParse(seq, negative) {
|
|
const u64 = this.parse(seq)
|
|
if (negative) {
|
|
return -u64
|
|
}
|
|
return u64
|
|
}
|
|
|
|
static non0LenParse(seq) {
|
|
return this.parse(seq) + 1
|
|
}
|
|
|
|
static parse(seq) {
|
|
let u64 = 0
|
|
for (let i = 0; i < seq.length; i++) {
|
|
if (seq[i] > 0) {
|
|
u64 |= seq[i] << ((seq.length -i - 1) * 8)
|
|
}
|
|
}
|
|
return u64
|
|
}
|
|
|
|
static bitsLen(num) {
|
|
let len = 0
|
|
do {
|
|
len++
|
|
num >>= 1
|
|
} while (num > 0)
|
|
return len
|
|
}
|
|
|
|
static onesCount(num) {
|
|
let count = 0
|
|
for (let i = this.bitsLen(num) - 1; i >=0; i --) {
|
|
if ((1 << i & num) > 0) {
|
|
count ++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
}
|
|
|
|
const sessionUser = {}
|
|
const sessionUserElem = document.querySelector(".session-user")
|
|
const chatBoard = document.querySelector(".chat-board")
|
|
const editorWrap = document.querySelector(".editor-wrap")
|
|
const editor = document.getElementById("editor")
|
|
const onlineState = document.querySelector(".online-state")
|
|
const groupMembers = document.querySelector(".group-members")
|
|
|
|
function sidHandler(fp) {
|
|
if (fp) {
|
|
if (fp?.sid && fp.sid.length > 0) {
|
|
sessionStorage.setItem("session-id", fp.sid)
|
|
}
|
|
} else {
|
|
// return location.pathname.replace(/^\/+/, "").replace(/^([^/]+).*/, "$1")
|
|
return sessionStorage.getItem("session-id")
|
|
}
|
|
}
|
|
|
|
const SWC = new FlexWebsocketClient(
|
|
"{{.ServerAddr}}" + (sidHandler() ? "/"+sidHandler() : ""),
|
|
sidHandler,
|
|
() => onlineState.textContent = "Online",
|
|
() => onlineState.textContent = "Offline",
|
|
)
|
|
|
|
SWC.bind("sync-user-list", fp => {
|
|
groupMembers.innerHTML = ""
|
|
const resp = JSON.parse(fp.body)
|
|
if (resp.sessionUser) {
|
|
Object.assign(sessionUser, resp.sessionUser)
|
|
sessionUserElem.textContent = sessionUser.nick
|
|
}
|
|
for (const user of resp.userList) {
|
|
const userElem = document.createElement("li")
|
|
userElem.textContent = user.nick
|
|
if (user.id === sessionUser.id) {
|
|
userElem.className = "self"
|
|
}
|
|
groupMembers.appendChild(userElem)
|
|
}
|
|
})
|
|
|
|
SWC.bind("sync-new-message", fp => {
|
|
const msgPkg = JSON.parse(fp.body)
|
|
showMsg(msgPkg)
|
|
})
|
|
|
|
function showMsg(msgPkg) {
|
|
const isSelf = msgPkg.uid === sessionUser.id
|
|
const msgBubble = chatBoard.appendChild(document.createElement("div"))
|
|
msgBubble.className = "msg-bubble " + (isSelf ? "self" : "")
|
|
const msgHead = msgBubble.appendChild(document.createElement("div"))
|
|
msgHead.className = "msg-head"
|
|
const userWrap = msgHead.appendChild(document.createElement("span"))
|
|
userWrap.className = "user-wrap"
|
|
userWrap.textContent = isSelf ? "ME" : msgPkg.nick
|
|
const timeWrap = document.createElement("span")
|
|
timeWrap.className = "time-wrap"
|
|
timeWrap.textContent = msgPkg.time
|
|
if (isSelf) {
|
|
msgHead.insertBefore(timeWrap, userWrap)
|
|
} else {
|
|
msgHead.appendChild(timeWrap)
|
|
}
|
|
const msgContent = msgBubble.appendChild(document.createElement("p"))
|
|
msgContent.textContent = msgPkg.msg
|
|
chatBoard.scrollTop = chatBoard.scrollHeight
|
|
}
|
|
|
|
editorWrap.onsubmit = function () {
|
|
sendMsg()
|
|
return false
|
|
}
|
|
|
|
async function sendMsg() {
|
|
if (editor.value === "") {
|
|
alert("empty message cannot send")
|
|
return
|
|
}
|
|
await SWC.request("send", editor.value)
|
|
editor.value = ""
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |