init
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
data.db
|
||||||
|
package-lock.json
|
||||||
216
app.js
Normal file
216
app.js
Normal 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
23
package.json
Normal 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
219
static/index.html
Normal 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>
|
||||||
Reference in New Issue
Block a user