This commit is contained in:
2025-07-14 19:23:51 +08:00
commit 283fc7a4b9
4 changed files with 461 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
node_modules/
data.db
package-lock.json

216
app.js Normal file
View File

@@ -0,0 +1,216 @@
import express from "express";
import http from "http";
import log4js from "log4js";
import { Server } from "socket.io";
import { Sequelize, Model, DataTypes, Op } from "sequelize";
const PORT = process.env.PORT || 5691;
let dbReady = false;
const app = express();
const server = http.createServer(app);
const io = new Server(server, {
cors: {
origin: "*",
methods: ["GET", "POST"],
},
});
const sequelize = new Sequelize({
dialect: 'sqlite',
storage: './data.db'
})
const logger = log4js.getLogger("app");
app.use(express.static("static"))
logger.level = process.env.LOG_LEVEL || "debug";
// 初始化在线列表
let onlineList = [];
let clientMap = new Map();
class Response {
constructor(status, msg) {
this.status = status
this.msg = msg
}
}
class Messgae extends Model {}
Messgae.init({
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
},
sender: {
type: DataTypes.STRING,
allowNull: false
},
message: {
type: DataTypes.STRING,
allowNull: false
},
sendtime: {
type: DataTypes.INTEGER,
allowNull: false
}
}, {
sequelize,
modelName: "Message"
});
try {
await sequelize.authenticate();
logger.info("Database connection has been established successfully.");
dbReady = true;
} catch (err) {
logger.error("Unable to connect to the database:" + err);
}
try {
if(dbReady) {
await Messgae.sync();
logger.info("Table created successfully.");
}
} catch (error) {
logger.error("Unable to create table:" + error)
}
function saveToDatabase(sender, msg, time) {
const messageItem = Messgae.build({
sender: sender,
message: msg,
sendtime: time
});
messageItem.save().then(() => {
logger.debug(`message saved, sender: ${sender}, message: ${msg}`)
}).catch((err) => {
logger.error(`message save failed, sender: ${sender}, message: ${msg}, error: ${err}`)
})
}
function loadHistory(limit = 10) {
if (!dbReady) {
return [];
}
return Messgae.findAll({
limit: limit,
order: [['sendtime', 'DESC']],
attributes: ['id', 'sender', 'message', 'sendtime']
})
}
function queryHistory(start, end, limit, order) {
if (!dbReady) {
return [];
}
return Messgae.findAll({
limit: limit,
order: [['sendtime', order]],
attributes: ['id', 'sender', 'message', 'sendtime'],
where: {
sendtime: {
[Op.between]: [start, end]
}
}
});
}
app.get("/online", (req, res) => {
res.status(200).send({
online: onlineList.length,
user: onlineList
})
})
app.get("/history", (req, res) => {
if (!dbReady) {
res.status(500).send({
status: 500,
msg: "database is not ready",
data: null
})
}
if (!req.query.limit || isNaN(req.query.limit) || req.query.limit <= 0 || req.query.limit > 100) {
req.query.limit = 10;
}
loadHistory(req.query.limit).then((data) => {
res.status(200).send({
status: 200,
msg: "success",
data: data
})
}).catch((err) => {
logger.error(`database error: ${err}`)
res.status(500).send({
status: 500,
msg: "database error",
data: null
})
})
})
io.on("connection", (socket) => {
logger.info(`A client connected, id: ${socket.id}`)
socket.on("system", data => {
if (!data) {
socket.emit("system", JSON.stringify(new Response(false, "data payload is empty.")))
return;
}
if (clientMap.has(socket.id)) {
socket.emit("system", JSON.stringify(new Response(false, "you are already online.")))
return;
}
try {
const { opt, args } = JSON.parse(data);
console.log(opt, args)
if (opt == "signin") {
if (!args || !args.username) {
socket.emit("system", JSON.stringify(new Response(false, "username is empty.")))
return;
}
const { username } = args
if (onlineList.indexOf(username) != -1) {
socket.emit("system", JSON.stringify(new Response(false, "username is already taken.")))
return;
}
socket.username = username;
onlineList.push(username);
clientMap.set(socket.id, username);
socket.emit("system", JSON.stringify(new Response(true, `welcome, ${socket.username}`)))
io.emit("notice", JSON.stringify(new Response(true, `${socket.username} join the chat.`)))
logger.debug(`socket(${socket.id}) -- ${username} signed in.`)
}
} catch (err) {
logger.error(`socket(${socket.id}) -- ${err}.`)
socket.emit("system", JSON.stringify(new Response(false, "data payload is invaild.")))
return;
}
console.log(clientMap)
console.log(onlineList)
})
socket.on("msg", data => {
if (!socket.username) {
socket.emit("system", JSON.stringify(new Response(false, "you are not signed in.")));
return;
}
logger.debug(`socket(${socket.id}) -- ${data}`)
io.emit("msg", JSON.stringify({sender: socket.username, msg: data}))
saveToDatabase(socket.username, data, new Date().getTime())
})
socket.on("disconnect", (reason, description) => {
logger.info(`A client disconnected, id: ${socket.id}`)
logger.trace(`socket(${socket.id}) -- reason: ${reason}, description: ${description}`);
if (clientMap.has(socket.id)) clientMap.delete(socket.id);
// TODO: 这个算法有点不安全
if (onlineList.includes(socket.username)) onlineList.splice(onlineList.indexOf(socket.id));
io.emit("notice", JSON.stringify(new Response(true, `${socket.username} left the chat.`)));
})
});
server.listen(PORT, () => {
logger.info(`Server running on port ${PORT}`);
});

23
package.json Normal file
View File

@@ -0,0 +1,23 @@
{
"name": "chat-server",
"version": "3.0.0-alpha",
"description": "a simple chat server using socket.io",
"main": "app.js",
"type": "module",
"scripts": {
"test": "node app.js",
"debug": "nodemon --exec node app.js"
},
"author": "star",
"license": "MIT",
"dependencies": {
"express": "^5.1.0",
"log4js": "^6.9.1",
"sequelize": "^6.37.7",
"socket.io": "^4.8.1",
"sqlite3": "^5.1.7"
},
"devDependencies": {
"nodemon": "^3.1.10"
}
}

219
static/index.html Normal file
View File

@@ -0,0 +1,219 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
body {
font-family: 'HarmonyOS Sans SC Medium';
}
.title {
font-size: 24px;
font-weight: bolder;
}
.card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
background-color: #f9f9f9;
}
#container {
display: flex;
flex-direction: column;
gap: 5px;
}
#sysmsg {
display: flex;
flex-direction: column;
gap: 2px;
}
#usrmsg > span {
display: flex;
flex-direction: column;
gap: 2px;
}
#online {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 5px;
padding-top: 10px;
}
#online > span {
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
background-color: #f9f9f9;
padding: 15px;
text-align: center;
}
input[type="text"] {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
margin-right: 5px;
}
button, input[type="button"] {
padding: 8px 12px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #45a049;
}
</style>
</head>
<body>
<div id="container">
<div class="card">
<div class="title">在线列表(在线人数:<span id="online-number">NaN</span></div>
<div id="online"></div>
</div>
<div class="card">
<div class="title">系统消息</div>
<div id="sysmsg"></div>
</div>
<div class="card">
<div class="title">用户消息</div>
<div id="usrmsg"></div>
</div>
</div>
<div style="padding-top: 20px" id="control">
<div>
<input type="text" id="message" />
<button onclick="sendMsg()">Submit</button>
</div>
<div>
<div>Status: <span id="stat"></span></div>
<button onclick="connect()">Connect</button>
<button onclick="disconnect()">Disconnect</button>
</div>
<div>
<input type="text" id="username" />
<button onclick="register()">set Username</button>
</div>
</div>
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
<script>
window.onload = () => {
fetchHistory()
}
const socket = io("ws://localhost:5691");
socket.on("connect", () => {
console.log("Connected");
updateState(true)
writeLogSystem("You are connected to server", true, "client")
});
socket.on("system", data => {
console.log("system", data)
data = JSON.parse(data)
writeLogSystem(data.msg, data.status, "system")
})
socket.on("notice", data => {
console.log("notice", data)
data = JSON.parse(data)
writeLogSystem(data.msg, data.status, "notice")
})
socket.on("msg", data => {
console.log("user", data)
data = JSON.parse(data);
let msg = ` ${data.msg}`
writeLogUser(msg, data.sender)
})
socket.on("disconnect", () => {
updateState(false);
console.log("Disconnected")
writeLogSystem("You are disconnected from server", false, "client")
});
const connect = () => socket.connect()
const disconnect = () => socket.disconnect();
const register = () => socket.emit("system", JSON.stringify({ opt: "signin", args: { username: document.getElementById("username").value } }));
const sendMsg = () => socket.emit("msg", document.getElementById("message").value)
const updateState = (value) => {
document.getElementById('stat').textContent = value ? "connected" : "disconnected";
}
document.getElementById("message").addEventListener("keydown", (e) => {
if (e.key === "Enter") {
sendMsg();
}
});
function fetchHistory() {
fetch("/history")
.then(res => res.json())
.then(res => {
res.data.forEach(element => {
writeLogUser(element.message, element.sender)
});
})
.catch(err => {
console.log(err)
})
}
function fetchOnline() {
fetch("/online").then(res => res.json()).then(res => {
const c = document.getElementById('online');
// console.log(res)
document.getElementById('online-number').textContent = res.online;
c.innerHTML = "";
res.user.forEach(element => {
const u = document.createElement('span');
u.textContent = element;
c.append(u);
});
});
}
const onlineChecker = setInterval(() => {
fetchOnline();
}, 1000)
function writeLogSystem(msg, status = true, topic = "system") {
const c = document.getElementById('sysmsg');
const m = document.createElement('span');
m.style.color = status ? 'green' : 'red'
m.textContent = `[${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()}][${topic}] ${msg}`
c.append(m);
}
function writeLogUser(msg, sender) {
const c = document.getElementById('usrmsg');
const m = document.createElement('span');
m.textContent = `[${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()}][${sender}] ${msg}`
c.append(m);
}
document.getElementById("username").addEventListener("keydown", (e) => {
if (e.key === "Enter") {
register();
}
});
</script>
</body>
</html>