局域网聊天室单文件c++版
局域网聊天室单文件c++版
*此项目由AI生成
运行效果
控制台
网页
代码
直接上代码:
#if defined(_MSC_VER) #pragma execution_character_set("utf-8") #endif #include <winsock2.h> #include <ws2tcpip.h> #include <windows.h> #include <iostream> #include <fstream> #include <sstream> #include <string> #include <vector> #include <mutex> #include <thread> #include <ctime> #include <algorithm> #pragma comment(lib, "Ws2_32.lib") // ==================== 全局变量 ==================== std::mutex g_mtx; std::vector<std::string> g_messages; // 内存中的消息列表 const int PORT = 8080; const std::string HISTORY_FILE = "chat_history.txt"; // ==================== 工具函数 ==================== std::string getCurrentTime() { time_t now = time(nullptr); tm ltm; localtime_s(<m, &now); char buf[64]; sprintf_s(buf, "%04d-%02d-%02d %02d:%02d:%02d", 1900 + ltm.tm_year, 1 + ltm.tm_mon, ltm.tm_mday, ltm.tm_hour, ltm.tm_min, ltm.tm_sec); return std::string(buf); } void log(const std::string& msg) { std::lock_guard<std::mutex> lock(g_mtx); std::cout << "[" << getCurrentTime() << "] " << msg << std::endl; } // JSON 转义 std::string jsonEscape(const std::string& s) { std::string result; for (char c : s) { switch (c) { case '"': result += "\\\""; break; case '\\': result += "\\\\"; break; case '\n': result += "\\n"; break; case '\r': result += "\\r"; break; case '\t': result += "\\t"; break; default: result += c; break; } } return result; } // URL 解码 std::string urlDecode(const std::string& src) { std::string result; for (size_t i = 0; i < src.size(); ++i) { if (src[i] == '%' && i + 2 < src.size()) { int hex = 0; std::istringstream iss(src.substr(i + 1, 2)); if (iss >> std::hex >> hex) { result += static_cast<char>(hex); i += 2; } else { result += src[i]; } } else if (src[i] == '+') { result += ' '; } else { result += src[i]; } } return result; } // 从 JSON 中提取字段值(简易解析) std::string extractJsonField(const std::string& json, const std::string& field) { std::string key = "\"" + field + "\""; size_t pos = json.find(key); if (pos == std::string::npos) return ""; pos = json.find(':', pos + key.size()); if (pos == std::string::npos) return ""; pos = json.find('"', pos + 1); if (pos == std::string::npos) return ""; size_t end = json.find('"', pos + 1); while (end != std::string::npos && json[end - 1] == '\\') { end = json.find('"', end + 1); } if (end == std::string::npos) return ""; return json.substr(pos + 1, end - pos - 1); } // ==================== 消息存储 ==================== void saveMessageToFile(const std::string& line) { std::ofstream file(HISTORY_FILE, std::ios::app); if (file.is_open()) { file << line << std::endl; file.close(); } } void loadHistoryFromFile() { std::ifstream file(HISTORY_FILE); if (file.is_open()) { std::string line; while (std::getline(file, line)) { if (!line.empty()) { g_messages.push_back(line); } } file.close(); log("已从文件加载 " + std::to_string(g_messages.size()) + " 条历史消息"); } } // ==================== HTML 页面(内嵌) ==================== // ==================== HTML 页面(内嵌) ==================== std::string getHtmlPage() { return R"HTML(<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>局域网聊天室</title> <style> * { margin:0; padding:0; box-sizing:border-box; } body { font-family:'Segoe UI','Microsoft YaHei',sans-serif; background:#f0f2f5; height:100vh; display:flex; justify-content:center; align-items:center; } /* ===== 登录页 ===== */ .login-box { background:#fff; border-radius:12px; padding:40px; box-shadow:0 8px 30px rgba(0,0,0,.12); text-align:center; width:360px; } .login-box h2 { color:#07c160; margin-bottom:24px; font-size:22px; } .login-box input { width:100%; padding:12px 16px; border:1px solid #ddd; border-radius:8px; font-size:15px; outline:none; transition:border .2s; } .login-box input:focus { border-color:#07c160; } .login-box button { width:100%; padding:12px; margin-top:16px; background:#07c160; color:#fff; border:none; border-radius:8px; font-size:16px; cursor:pointer; transition:background .2s; } .login-box button:hover { background:#06ad56; } /* ===== 聊天页 ===== */ .chat-app { width:420px; height:640px; background:#fff; border-radius:12px; box-shadow:0 8px 30px rgba(0,0,0,.12); display:none; flex-direction:column; overflow:hidden; } .chat-header { background:#07c160; color:#fff; padding:14px 20px; display:flex; justify-content:space-between; align-items:center; } .chat-header h3 { font-size:17px; font-weight:600; } .header-right { display:flex; align-items:center; gap:12px; } .logout-btn { background:rgba(255,255,255,0.2); border:none; color:#fff; padding:4px 10px; border-radius:4px; font-size:12px; cursor:pointer; } .logout-btn:hover { background:rgba(255,255,255,0.4); } .notify-toggle { display:flex; align-items:center; gap:6px; font-size:13px; cursor:pointer; user-select:none; } .toggle-switch { width:36px; height:20px; background:#ccc; border-radius:10px; position:relative; transition:background .3s; } .toggle-switch.on { background:#fff; } .toggle-switch::after { content:''; position:absolute; width:16px; height:16px; background:#fff; border-radius:50%; top:2px; left:2px; transition:left .3s; box-shadow:0 1px 3px rgba(0,0,0,.3); } .toggle-switch.on::after { left:18px; background:#07c160; } .toggle-switch:not(.on)::after { background:#999; } /* 消息区 */ .msg-area { flex:1; overflow-y:auto; padding:16px; background:#f5f5f5; } .msg-row { display:flex; margin-bottom:14px; } .msg-row.self { flex-direction:row-reverse; } .msg-avatar { width:36px; height:36px; border-radius:6px; background:#07c160; color:#fff; display:flex; align-items:center; justify-content:center; font-size:14px; font-weight:bold; flex-shrink:0; } .msg-row.self .msg-avatar { background:#5b93d0; } .msg-body { max-width:65%; margin:0 10px; } .msg-info { font-size:11px; color:#999; margin-bottom:4px; } .msg-row.self .msg-info { text-align:right; } .msg-bubble { padding:10px 14px; border-radius:10px; font-size:14px; line-height:1.5; word-wrap:break-word; position:relative; } .msg-row:not(.self) .msg-bubble { background:#fff; border-top-left-radius:2px; } .msg-row.self .msg-bubble { background:#95ec69; border-top-right-radius:2px; } /* 输入区 */ .input-area { display:flex; padding:12px; border-top:1px solid #eee; background:#fff; } .input-area input { flex:1; padding:10px 14px; border:1px solid #ddd; border-radius:20px; font-size:14px; outline:none; } .input-area input:focus { border-color:#07c160; } .input-area button { margin-left:10px; padding:0 22px; background:#07c160; color:#fff; border:none; border-radius:20px; font-size:14px; cursor:pointer; } .input-area button:hover { background:#06ad56; } /* ===== Toast 提醒 ===== */ .toast-container { position:fixed; top:20px; right:20px; z-index:9999; display:flex; flex-direction:column; gap:8px; } .toast { background:#fff; border-left:4px solid #07c160; padding:14px 20px; border-radius:8px; box-shadow:0 4px 16px rgba(0,0,0,.15); min-width:260px; max-width:340px; animation:slideIn .3s ease; } .toast .toast-title { font-weight:600; font-size:14px; color:#333; margin-bottom:4px; } .toast .toast-body { font-size:13px; color:#666; } @keyframes slideIn { from{transform:translateX(100%);opacity:0} to{transform:translateX(0);opacity:1} } @keyframes slideOut { from{transform:translateX(0);opacity:1} to{transform:translateX(100%);opacity:0} } .toast.hide { animation:slideOut .3s ease forwards; } </style> </head> <body> <!-- 登录页 --> <div class="login-box" id="loginPage"> <h2>💬 局域网聊天室</h2> <input type="text" id="nameInput" placeholder="请输入你的昵称" maxlength="20" onkeydown="if(event.key==='Enter')doJoin()"> <button onclick="doJoin()">加入聊天</button> </div> <!-- 聊天页 --> <div class="chat-app" id="chatPage"> <div class="chat-header"> <h3>💬 聊天室</h3> <div class="header-right"> <div class="notify-toggle" onclick="toggleNotify()"> <span id="notifyLabel">提醒</span> <div class="toggle-switch" id="notifySwitch"></div> </div> <button class="logout-btn" onclick="doLogout()" title="清除缓存并退出">切换</button> </div> </div> <div class="msg-area" id="msgArea"></div> <div class="input-area"> <input type="text" id="msgInput" placeholder="输入消息,回车发送..." onkeydown="if(event.key==='Enter')sendMsg()"> <button onclick="sendMsg()">发送</button> </div> </div> <!-- Toast 容器 --> <div class="toast-container" id="toastContainer"></div> <script> let myName = ''; let lastCount = 0; let notifyOn = localStorage.getItem('notifyOn') === 'true'; let pollTimer = null; // ===== 页面加载时自动检查缓存 ===== window.addEventListener('DOMContentLoaded', () => { const savedName = localStorage.getItem('chat_username'); if (savedName) { // 有缓存,直接跳过登录页进入聊天 myName = savedName; enterChatRoom(); } // 请求浏览器通知权限 if ('Notification' in window && Notification.permission === 'default') { setTimeout(() => Notification.requestPermission(), 1000); } }); // ===== 进入聊天室(公共逻辑) ===== function enterChatRoom() { document.getElementById('loginPage').style.display = 'none'; document.getElementById('chatPage').style.display = 'flex'; initNotify(); loadMessages(); if (pollTimer) clearInterval(pollTimer); pollTimer = setInterval(loadMessages, 1500); } // ===== 退出/切换账号 ===== function doLogout() { if (confirm('确定要清除本地缓存并切换账号吗?')) { localStorage.removeItem('chat_username'); location.reload(); // 刷新页面回到登录 } } // ===== 初始化提醒开关 ===== function initNotify() { const sw = document.getElementById('notifySwitch'); const lb = document.getElementById('notifyLabel'); if (notifyOn) { sw.classList.add('on'); lb.textContent = '提醒开'; } else { sw.classList.remove('on'); lb.textContent = '提醒关'; } } function toggleNotify() { notifyOn = !notifyOn; localStorage.setItem('notifyOn', notifyOn); initNotify(); if (notifyOn && 'Notification' in window && Notification.permission === 'default') { Notification.requestPermission(); } } // ===== 登录 ===== function doJoin() { const name = document.getElementById('nameInput').value.trim(); if (!name) { alert('请输入昵称'); return; } myName = name; // 【核心改动】将名字存入浏览器本地缓存 localStorage.setItem('chat_username', name); enterChatRoom(); } // ===== 发送消息 ===== function sendMsg() { const input = document.getElementById('msgInput'); const text = input.value.trim(); if (!text) return; fetch('/api/send', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({sender: myName, content: text}) }); input.value = ''; input.focus(); } // ===== 加载消息 ===== function loadMessages() { fetch('/api/history') .then(r => r.json()) .then(msgs => { if (msgs.length === lastCount) return; const isNew = msgs.length > lastCount; const newMsgs = msgs.slice(lastCount); lastCount = msgs.length; renderMessages(msgs); if (isNew) { newMsgs.forEach(m => { if (m.sender !== myName) { showNotify(m.sender, m.content); } }); } }) .catch(() => {}); } // ===== 渲染消息 ===== function renderMessages(msgs) { const area = document.getElementById('msgArea'); area.innerHTML = ''; msgs.forEach(m => { const isSelf = m.sender === myName; const row = document.createElement('div'); row.className = 'msg-row' + (isSelf ? ' self' : ''); const initial = m.sender.charAt(0).toUpperCase(); row.innerHTML = '<div class="msg-avatar">' + initial + '</div>' + '<div class="msg-body">' + '<div class="msg-info">' + escapeHtml(m.sender) + ' · ' + m.time + '</div>' + '<div class="msg-bubble">' + escapeHtml(m.content) + '</div>' + '</div>'; area.appendChild(row); }); area.scrollTop = area.scrollHeight; } function escapeHtml(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; } // ===== 消息提醒 ===== function showNotify(sender, content) { if (!notifyOn) return; showToast(sender, content); if ('Notification' in window && Notification.permission === 'granted') { new Notification('💬 ' + sender, { body: content }); } } function showToast(title, body) { const container = document.getElementById('toastContainer'); const toast = document.createElement('div'); toast.className = 'toast'; toast.innerHTML = '<div class="toast-title">💬 ' + escapeHtml(title) + '</div>' + '<div class="toast-body">' + escapeHtml(body) + '</div>'; container.appendChild(toast); setTimeout(() => { toast.classList.add('hide'); setTimeout(() => toast.remove(), 300); }, 3000); } </script> </body> </html>)HTML"; } // ==================== HTTP 处理 ==================== void handleClient(SOCKET clientSock) { char buf[8192] = { 0 }; int totalRecv = 0; // 先接收头部 int n = recv(clientSock, buf, sizeof(buf) - 1, 0); if (n <= 0) { closesocket(clientSock); return; } totalRecv = n; std::string request(buf, n); // 如果有 Content-Length,继续接收 body std::string clHeader = "Content-Length:"; size_t clPos = request.find(clHeader); if (clPos == std::string::npos) clPos = request.find("content-length:"); if (clPos != std::string::npos) { size_t valStart = clPos + clHeader.size(); size_t valEnd = request.find("\r\n", valStart); int contentLen = atoi(request.substr(valStart, valEnd - valStart).c_str()); size_t bodyStart = request.find("\r\n\r\n"); if (bodyStart != std::string::npos) { bodyStart += 4; int bodyReceived = (int)(request.size() - bodyStart); while (bodyReceived < contentLen) { char tmp[4096]; int r = recv(clientSock, tmp, sizeof(tmp), 0); if (r <= 0) break; request.append(tmp, r); bodyReceived += r; } } } // 解析请求行 std::istringstream reqStream(request); std::string method, path, version; reqStream >> method >> path >> version; std::string responseBody; std::string contentType = "text/html; charset=utf-8"; int statusCode = 200; // ---- GET / ---- if (method == "GET" && path == "/") { responseBody = getHtmlPage(); log("页面请求: GET /"); } // ---- GET /api/history ---- else if (method == "GET" && path == "/api/history") { std::lock_guard<std::mutex> lock(g_mtx); responseBody = "["; for (size_t i = 0; i < g_messages.size(); ++i) { if (i > 0) responseBody += ","; responseBody += g_messages[i]; } responseBody += "]"; contentType = "application/json; charset=utf-8"; } // ---- POST /api/send ---- else if (method == "POST" && path == "/api/send") { size_t bodyPos = request.find("\r\n\r\n"); if (bodyPos != std::string::npos) { std::string body = request.substr(bodyPos + 4); std::string sender = extractJsonField(body, "sender"); std::string content = extractJsonField(body, "content"); if (!sender.empty() && !content.empty()) { std::string timeStr = getCurrentTime(); // 构建 JSON 对象 std::string jsonMsg = "{\"sender\":\"" + jsonEscape(sender) + "\",\"content\":\"" + jsonEscape(content) + "\",\"time\":\"" + timeStr + "\"}"; { std::lock_guard<std::mutex> lock(g_mtx); g_messages.push_back(jsonMsg); } // 保存到文件 saveMessageToFile(jsonMsg); log("[" + sender + "] 发送消息: " + content); responseBody = "{\"status\":\"ok\"}"; contentType = "application/json; charset=utf-8"; } else { statusCode = 400; responseBody = "{\"status\":\"error\",\"msg\":\"invalid data\"}"; contentType = "application/json; charset=utf-8"; log("错误: 收到无效消息数据"); } } } // ---- 404 ---- else { statusCode = 404; responseBody = "Not Found"; contentType = "text/plain; charset=utf-8"; log("警告: 404 未找到路径 " + path); } // 构建 HTTP 响应 std::ostringstream resp; resp << "HTTP/1.1 " << statusCode << " OK\r\n" << "Content-Type: " << contentType << "\r\n" << "Content-Length: " << responseBody.size() << "\r\n" << "Access-Control-Allow-Origin: *\r\n" << "Connection: close\r\n" << "\r\n" << responseBody; std::string respStr = resp.str(); send(clientSock, respStr.c_str(), (int)respStr.size(), 0); closesocket(clientSock); } // ==================== 主函数 ==================== int main() { system("chcp 65001"); // 设置控制台为 UTF-8") SetConsoleOutputCP(65001); // 支持中文输出 log("===== 局域网聊天室服务器 ====="); // 加载历史消息 loadHistoryFromFile(); // 初始化 Winsock WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { log("错误: Winsock 初始化失败"); return 1; } log("Winsock 初始化成功"); // 创建 Socket SOCKET serverSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (serverSock == INVALID_SOCKET) { log("错误: 创建 Socket 失败, 错误码: " + std::to_string(WSAGetLastError())); WSACleanup(); return 1; } // 允许端口重用 int opt = 1; setsockopt(serverSock, SOL_SOCKET, SO_REUSEADDR, (char*)&opt, sizeof(opt)); // 绑定 sockaddr_in addr{}; addr.sin_family = AF_INET; addr.sin_addr.s_addr = INADDR_ANY; addr.sin_port = htons(PORT); if (bind(serverSock, (sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR) { log("错误: 绑定端口 " + std::to_string(PORT) + " 失败, 错误码: " + std::to_string(WSAGetLastError())); closesocket(serverSock); WSACleanup(); return 1; } log("成功绑定端口 " + std::to_string(PORT)); // 监听 if (listen(serverSock, SOMAXCONN) == SOCKET_ERROR) { log("错误: 监听失败"); closesocket(serverSock); WSACleanup(); return 1; } log("开始监听..."); // 获取本机 IP char hostname[256] = { 0 }; gethostname(hostname, sizeof(hostname)); log("本机主机名: " + std::string(hostname)); struct addrinfo hints {}, * result = nullptr; hints.ai_family = AF_INET; if (getaddrinfo(hostname, nullptr, &hints, &result) == 0 && result) { char ipBuf[INET_ADDRSTRLEN]; sockaddr_in* sin = (sockaddr_in*)result->ai_addr; inet_ntop(AF_INET, &sin->sin_addr, ipBuf, sizeof(ipBuf)); log("本机局域网 IP: " + std::string(ipBuf)); log("其他电脑请访问: http://" + std::string(ipBuf) + ":" + std::to_string(PORT)); freeaddrinfo(result); } log("本机访问: http://127.0.0.1:" + std::to_string(PORT)); log("等待连接中...\n"); // 主循环 while (true) { SOCKET clientSock = accept(serverSock, nullptr, nullptr); if (clientSock != INVALID_SOCKET) { std::thread(handleClient, clientSock).detach(); } } closesocket(serverSock); WSACleanup(); return 0; }编译要求
VS选控制台应用
ISOC++20标准
编译后单文件就可以运行,不需要其他的,很方便
怎么自己加标签页的图标
如果控制台出现这一行,不用管他,是标签页上的图标没找到
如果在意,可以按以下步骤:
- 准备一张小图(png 格式,32×32 左右最好)
- 用在线工具(搜"图片转 base64")把它转成一串 base64 文本
getHtmlPage()的 HTML 中,<title>后面加上这一行:
<link rel="icon" type="image/png" href="data:image/png;base64,这里粘贴base64字符串">资源下载
自己不想编译的可以去我主页资源下载,也可以下载此文档绑定的资源
