增加websocket支持

This commit is contained in:
Dev User
2026-08-04 20:07:32 +08:00
parent 176eb29ef9
commit db95627dd8
19 changed files with 803 additions and 161 deletions

14
www/api/sha1.lua Normal file
View File

@@ -0,0 +1,14 @@
-- GET /api/sha1.lua 测试 WebSocket Accept 的 SHA1/Base64 算法
-- RFC6455 样例:
-- Key = dGhlIHNhbXBsZSBub25jZQ==
-- Accept = s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
--
-- 可选参数:?key=你的Sec-WebSocket-Key
local request = require("fastweb.request")
local response = require("fastweb.response")
local codec = require("fastweb.codec")
local base64 = require("base64")
local utils = require("utils")
local cjson = require("cjson")
print("headers:",cjson.encode(request.headers()))

77
www/api/ws.lua Normal file
View File

@@ -0,0 +1,77 @@
local request = require("fastweb.request")
local response = require("fastweb.response")
local ws = require("fastweb.websocket")
local codec = require("fastweb.codec")
local base64 = require("base64")
local utils = require("utils")
if not ws.valid() then
response.response_done()
return
end
if ws.type() == ws.UPGRADE then
print("[WS_UPGRADE] CONNID:" .. ws.connid())
local accept_key = base64.encode(utils.hex_to_bytes(codec.sha1(ws.sec_websocket_key() .. "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")))
response.header("Sec-WebSocket-Accept", accept_key)
response.header("Connection", "Upgrade")
response.header("Upgrade", "websocket")
response.send_header(101, "Switching Protocols")
elseif ws.type() == ws.MESSAGE_HEADER then
local opcode = ws.opcode()
local log = "[WS_HEADER] CONNID:" .. ws.connid()
if opcode == ws.OPCODE_TEXT then
log = log .. " [TEXT]"
elseif opcode == ws.OPCODE_BINARY then
log = log .. " [BINARY]"
elseif opcode == ws.OPCODE_CLOSE then
log = log .. " [CLOSE]"
elseif opcode == ws.OPCODE_PING then
log = log .. " [PING]"
elseif opcode == ws.OPCODE_PONG then
log = log .. " [PONG]"
else
log = log .. " [OPCODE:" .. opcode .. "]"
end
log = log .. " [FINAL:" .. tostring(ws.final()) .. "] [LENGTH:" .. ws.length() .. "]"
print(log)
elseif ws.type() == ws.MESSAGE_BODY then
local connid = ws.connid()
local opcode = ws.opcode()
local body = request.body()
local log = "[WS_BODY] CONNID:" .. connid
.. " [OPCODE:" .. tostring(opcode) .. "]"
.. " [LENGTH:" .. tostring(#body) .. "]"
print(log)
if opcode == ws.OPCODE_CLOSE then
-- 客户端关闭:回 Close 帧(可带回原载荷),完成握手
print("[WS_BODY] reply CLOSE")
response.send_ws(body, ws.OPCODE_CLOSE)
elseif opcode == ws.OPCODE_PING then
-- Ping 必须回 Pong载荷原样带回
print("[WS_BODY] reply PONG")
response.send_ws(body, ws.OPCODE_PONG)
elseif opcode == ws.OPCODE_PONG then
-- Pong 无需回复
print("[WS_BODY] PONG ignored")
elseif opcode == ws.OPCODE_TEXT or opcode == ws.OPCODE_BINARY then
-- 文本/二进制:原样回显
response.send_ws(body, opcode)
else
print("[WS_BODY] unhandled opcode:" .. tostring(opcode))
end
elseif ws.type() == ws.CLOSE then
-- TCP 已断开,只做清理,不要再发帧
print("[WS_CLOSE] CONNID:" .. ws.connid())
return
else
print("UNKNOWN")
end
response.response_done()

View File

@@ -1,113 +1,345 @@
<!DOCTYPE html>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户登录</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f0f0f0;
margin: 0;
}
.login-container {
background-color: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
text-align: center;
width: 350px;
}
.login-container h1 {
margin-bottom: 20px;
font-size: 24px;
}
.login-container input[type="text"], .login-container input[type="password"] {
width: 100%;
padding-top: 15px;
padding-bottom: 15px;
margin-bottom: 20px;
border: 1px solid #ccc;
border-radius: 5px;
}
.login-container button {
width: 100%;
padding: 15px;
background-color: #007BFF;
border: none;
border-radius: 5px;
color: white;
font-size: 16px;
cursor: pointer;
}
.login-container button:hover {
background-color: #0056b3;
}
.error-message {
color: red;
margin-top: 10px;
}
</style>
<script src="/js/jquery-3.4.1.min.js"></script>
<script>
var token = ""
function login() {
const username = $('#username').val();
const password = $('#password').val();
if (!username || !password) {
alert('请输入用户名和密码');
return;
}
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>WebSocket 测试</title>
<style>
:root {
--bg: #0f1419;
--surface: #1a2332;
--border: #2d3a4d;
--text: #e7ecf3;
--muted: #8b9bb4;
--accent: #3d9cf0;
--accent-hover: #5aadf5;
--ok: #3ecf8e;
--err: #f07178;
--warn: #e6c07b;
}
$.ajax({
url: '/api/user.lua?action=login',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ username: username, password: password }),
success: function(response) {
if (response.code === 200) {
token = response.data.token;
fetchUserInfo();
} else {
$('#errorMessage').text(response.msg || '登录失败,请重试');
}
}
});
}
* { box-sizing: border-box; margin: 0; padding: 0; }
function fetchUserInfo() {
$.ajax({
url: '/api/user.lua?action=getinfo',
type: 'GET',
headers:{
"token":token
},
success: function(response) {
if (response.code === 200) {
const userInfo = response.data;
alert(`用户信息:\n登录时间: ${userInfo.login_time}`);
} else {
alert('获取用户信息失败,请重试');
}
},
error: function() {
alert('获取用户信息失败,请重试');
}
});
}
</script>
body {
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
background:
radial-gradient(ellipse 80% 50% at 20% -10%, #1a3050 0%, transparent 55%),
radial-gradient(ellipse 60% 40% at 90% 100%, #152030 0%, transparent 50%),
var(--bg);
color: var(--text);
min-height: 100vh;
padding: 2rem 1.25rem 3rem;
line-height: 1.5;
}
main {
max-width: 720px;
margin: 0 auto;
}
h1 {
font-family: "IBM Plex Mono", "Cascadia Code", monospace;
font-size: 1.35rem;
font-weight: 600;
letter-spacing: -0.02em;
margin-bottom: 0.35rem;
}
.sub {
color: var(--muted);
font-size: 0.9rem;
margin-bottom: 1.75rem;
}
.sub code {
font-family: "IBM Plex Mono", monospace;
font-size: 0.85em;
color: var(--accent);
}
.row {
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
align-items: center;
margin-bottom: 1rem;
}
.status {
display: inline-flex;
align-items: center;
gap: 0.45rem;
font-size: 0.85rem;
color: var(--muted);
margin-left: auto;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--muted);
}
.dot.open { background: var(--ok); box-shadow: 0 0 8px var(--ok); }
.dot.connecting { background: var(--warn); }
.dot.closed, .dot.error { background: var(--err); }
button, input {
font: inherit;
}
button {
background: var(--accent);
color: #0a1220;
border: none;
padding: 0.55rem 1.1rem;
font-weight: 600;
font-size: 0.875rem;
cursor: pointer;
transition: background 0.15s;
}
button:hover:not(:disabled) { background: var(--accent-hover); }
button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
button.ghost {
background: transparent;
color: var(--text);
border: 1px solid var(--border);
}
button.ghost:hover:not(:disabled) {
border-color: var(--muted);
background: var(--surface);
}
.url {
width: 100%;
background: var(--surface);
border: 1px solid var(--border);
color: var(--text);
padding: 0.65rem 0.85rem;
font-family: "IBM Plex Mono", monospace;
font-size: 0.85rem;
margin-bottom: 1rem;
}
.url:focus {
outline: none;
border-color: var(--accent);
}
.send-row {
display: flex;
gap: 0.6rem;
margin-bottom: 1rem;
}
.send-row input {
flex: 1;
background: var(--surface);
border: 1px solid var(--border);
color: var(--text);
padding: 0.65rem 0.85rem;
font-size: 0.9rem;
}
.send-row input:focus {
outline: none;
border-color: var(--accent);
}
.log {
background: var(--surface);
border: 1px solid var(--border);
height: min(52vh, 420px);
overflow-y: auto;
padding: 0.75rem;
font-family: "IBM Plex Mono", monospace;
font-size: 0.8rem;
}
.log .line {
padding: 0.2rem 0;
border-bottom: 1px solid transparent;
word-break: break-all;
white-space: pre-wrap;
}
.log .line .t { color: var(--muted); margin-right: 0.5rem; }
.log .line.sys { color: var(--muted); }
.log .line.out { color: var(--accent); }
.log .line.in { color: var(--ok); }
.log .line.err { color: var(--err); }
</style>
</head>
<body>
<div class="login-container">
<h1>用户登录</h1>
<input type="text" id="username" placeholder="请输入用户名">
<input type="password" id="password" placeholder="请输入密码">
<button onclick="login()">确认</button>
<div id="errorMessage" class="error-message"></div>
<main>
<h1>WebSocket 测试</h1>
<p class="sub">连接到当前服务器 <code id="pathHint">/api/ws.lua</code></p>
<input class="url" id="url" type="text" spellcheck="false" />
<div class="row">
<button id="btnConnect" type="button">连接</button>
<button id="btnDisconnect" class="ghost" type="button" disabled>断开</button>
<button id="btnClear" class="ghost" type="button">清空日志</button>
<span class="status">
<span class="dot" id="dot"></span>
<span id="statusText">未连接</span>
</span>
</div>
<div class="send-row">
<input id="msg" type="text" placeholder="输入要发送的消息…" disabled />
<button id="btnSend" type="button" disabled>发送</button>
</div>
<div class="log" id="log" aria-live="polite"></div>
</main>
<script>
(function () {
var path = "/api/ws.lua";
var proto = location.protocol === "https:" ? "wss:" : "ws:";
var defaultUrl = proto + "//" + location.host + path;
var urlEl = document.getElementById("url");
var msgEl = document.getElementById("msg");
var logEl = document.getElementById("log");
var dotEl = document.getElementById("dot");
var statusEl = document.getElementById("statusText");
var btnConnect = document.getElementById("btnConnect");
var btnDisconnect = document.getElementById("btnDisconnect");
var btnSend = document.getElementById("btnSend");
var btnClear = document.getElementById("btnClear");
urlEl.value = defaultUrl;
var ws = null;
function now() {
var d = new Date();
return (
String(d.getHours()).padStart(2, "0") + ":" +
String(d.getMinutes()).padStart(2, "0") + ":" +
String(d.getSeconds()).padStart(2, "0") + "." +
String(d.getMilliseconds()).padStart(3, "0")
);
}
function log(kind, text) {
var line = document.createElement("div");
line.className = "line " + kind;
var t = document.createElement("span");
t.className = "t";
t.textContent = now();
line.appendChild(t);
line.appendChild(document.createTextNode(text));
logEl.appendChild(line);
logEl.scrollTop = logEl.scrollHeight;
}
function setState(state, label) {
dotEl.className = "dot " + state;
statusEl.textContent = label;
var open = state === "open";
var connecting = state === "connecting";
btnConnect.disabled = open || connecting;
btnDisconnect.disabled = !open && !connecting;
btnSend.disabled = !open;
msgEl.disabled = !open;
urlEl.disabled = open || connecting;
}
function connect() {
var url = urlEl.value.trim();
if (!url) return;
if (ws) {
try { ws.close(); } catch (e) {}
ws = null;
}
setState("connecting", "连接中…");
log("sys", "→ 连接 " + url);
try {
ws = new WebSocket(url);
} catch (e) {
setState("error", "错误");
log("err", "创建失败: " + e.message);
return;
}
ws.onopen = function () {
setState("open", "已连接");
log("sys", "✓ 连接已建立 (readyState=" + ws.readyState + ")");
msgEl.focus();
};
ws.onmessage = function (ev) {
var data = ev.data;
if (data instanceof Blob) {
data.arrayBuffer().then(function (buf) {
log("in", "← binary " + buf.byteLength + " bytes");
});
} else if (data instanceof ArrayBuffer) {
log("in", "← binary " + data.byteLength + " bytes");
} else {
log("in", "← " + data);
}
};
ws.onerror = function () {
log("err", "WebSocket error");
setState("error", "错误");
};
ws.onclose = function (ev) {
var reason = ev.reason ? " — " + ev.reason : "";
log("sys", "× 已关闭 (code=" + ev.code + reason + ", clean=" + ev.wasClean + ")");
setState("closed", "已断开");
ws = null;
};
}
function disconnect() {
if (!ws) return;
log("sys", "→ 主动关闭");
ws.close(1000, "client close");
}
function send() {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
var text = msgEl.value;
ws.send(text);
log("out", "→ " + text);
msgEl.value = "";
msgEl.focus();
}
btnConnect.addEventListener("click", connect);
btnDisconnect.addEventListener("click", disconnect);
btnSend.addEventListener("click", send);
btnClear.addEventListener("click", function () {
logEl.innerHTML = "";
});
msgEl.addEventListener("keydown", function (e) {
if (e.key === "Enter") send();
});
urlEl.addEventListener("keydown", function (e) {
if (e.key === "Enter") connect();
});
setState("closed", "未连接");
})();
</script>
</body>
</html>