分享workerman chat的index.html及start.php

回覆文章
Brave Ye
系統管理員
文章: 5547
註冊時間: 2025-11-08, 13:32
聯繫:

分享workerman chat的index.html及start.php

文章 Brave Ye »

先安裝composer
https://getcomposer.org/download/
2
apt-get update
apt-get install libevent-dev -y
3
建立mysql資料庫命名chat
mysql -p
CREATE DATABASE IF NOT EXISTS chat;

GRANT ALL PRIVILEGES ON chat.* TO 'phpmyadmin'@'localhost';

USE chat;
匯入

代碼: 選擇全部

CREATE TABLE chat_messages (
    id INT AUTO_INCREMENT PRIMARY KEY,
    room VARCHAR(32) NOT NULL DEFAULT 'general',
    sender VARCHAR(50) NOT NULL,
    message TEXT NOT NULL,
    is_private TINYINT(1) NOT NULL DEFAULT 0,
    private_to VARCHAR(50) DEFAULT NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_room_time (room, created_at),
    INDEX idx_private (is_private, private_to, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;   
mkdir /var/www/html/wm
chown www-data:www-data -R /var/www/html/wm
chmod 777 -R /var/www/html/wm
composer require workerman/workerman


然後 nano /var/www/html/wm/index.html

代碼: 選擇全部

<!DOCTYPE html>
<html lang="zh-TW">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>zine-tw.com 聊天室</title>
    <style>
        :root {
            --bg: #0d1117;
            --card: #161b22;
            --border: #21262d;
            --text: #c9d1d9;
            --text-dim: #8b949e;
            --accent: #58a6ff;
            --accent-hover: #388bfd;
            --input-bg: #0d1117;
            --input-border: #30363d;
            --green: #3fb950;
            --green-bg: #1a2e1a;
            --red: #f85149;
            --red-bg: #2d1a1a;
            --private-bg: #1c1432;
            --private-border: #3d2d6b;
        }
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            background: var(--bg);
            color: var(--text);
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            padding: 12px;
        }
        .container {
            background: var(--card);
            border: 1px solid var(--border);
            border-radius: 12px;
            width: 100%;
            max-width: 500px;
            display: flex;
            flex-direction: column;
            height: 88vh;
        }
        .header {
            padding: 14px 18px;
            border-bottom: 1px solid var(--border);
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .header h2 { font-size: 17px; }
        .status {
            font-size: 11px;
            padding: 3px 10px;
            border-radius: 12px;
            background: var(--border);
            color: var(--text-dim);
        }
        .status.connected { background: var(--green-bg); color: var(--green); }
        .status.disconnected { background: var(--red-bg); color: var(--red); }

        .name-bar {
            padding: 10px 18px;
            border-bottom: 1px solid var(--border);
            display: flex;
            align-items: center;
            gap: 8px;
        }
        .name-bar label { font-size: 12px; color: var(--text-dim); white-space: nowrap; }
        .name-bar input {
            flex: 1;
            padding: 6px 10px;
            border: 1px solid var(--input-border);
            border-radius: 6px;
            font-size: 13px;
            outline: none;
            background: var(--input-bg);
            color: var(--text);
        }
        .name-bar input:focus { border-color: var(--accent); }
        .name-bar button {
            padding: 6px 12px;
            background: var(--input-border);
            color: var(--text);
            border: none;
            border-radius: 6px;
            font-size: 12px;
            cursor: pointer;
        }
        .name-bar button:hover { background: var(--accent); color: #fff; }

        .tabs {
            display: flex;
            border-bottom: 1px solid var(--border);
            overflow-x: auto;
        }
        .tab {
            padding: 10px 16px;
            font-size: 13px;
            color: var(--text-dim);
            cursor: pointer;
            border-bottom: 2px solid transparent;
            white-space: nowrap;
            transition: color 0.2s;
        }
        .tab:hover { color: var(--text); }
        .tab.active { color: var(--accent); border-bottom-color: var(--accent); }

        .user-list {
            padding: 8px 18px;
            font-size: 12px;
            color: var(--text-dim);
            border-bottom: 1px solid var(--border);
            background: var(--bg);
        }
        .user-list span {
            cursor: pointer;
            color: var(--accent);
            margin-right: 6px;
        }
        .user-list span:hover { text-decoration: underline; }

        .chatbox {
            flex: 1;
            overflow-y: auto;
            padding: 14px 18px;
        }
        .chatbox::-webkit-scrollbar { width: 6px; }
        .chatbox::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }

        .msg { margin-bottom: 12px; line-height: 1.5; }
        .msg .meta { font-size: 12px; color: var(--text-dim); margin-bottom: 2px; }
        .msg .meta strong { color: var(--text); font-size: 13px; }
        .msg .meta .private-tag {
            font-size: 10px;
            padding: 1px 6px;
            border-radius: 4px;
            background: var(--private-bg);
            border: 1px solid var(--private-border);
            color: #a78bfa;
            margin-left: 6px;
        }
        .msg .text { font-size: 14px; word-break: break-word; }
        .msg.system { text-align: center; font-size: 12px; color: var(--text-dim); margin: 6px 0; }
        .msg.private-msg {
            padding: 8px 12px;
            background: var(--private-bg);
            border-left: 3px solid var(--private-border);
            border-radius: 4px;
        }

        .input-area {
            display: flex;
            padding: 12px 14px;
            border-top: 1px solid var(--border);
            gap: 8px;
        }
        .input-area input {
            flex: 1;
            padding: 10px 14px;
            border: 1px solid var(--input-border);
            border-radius: 8px;
            font-size: 14px;
            outline: none;
            background: var(--input-bg);
            color: var(--text);
            transition: border-color 0.2s;
        }
        .input-area input:focus { border-color: var(--accent); }
        .input-area input::placeholder { color: var(--text-dim); }
        .input-area button {
            padding: 10px 20px;
            background: var(--accent);
            color: #fff;
            border: none;
            border-radius: 8px;
            font-size: 14px;
            cursor: pointer;
            transition: background 0.2s;
        }
        .input-area button:hover { background: var(--accent-hover); }
        .input-area button:disabled { background: var(--input-border); cursor: not-allowed; }
    </style>
<script async custom-element="amp-ad" src="https://cdn.ampproject.org/v0/amp-ad-0.1.js"></script>
</head>
<body>
<div class="container">
    <div class="header">
        <h2>💬 聊天室</h2>
        <span class="status" id="status">連線中...</span>
    </div>
    <div class="name-bar">
        <label>暱稱:</label>
        <input type="text" id="nameInput" placeholder="輸入暱稱" maxlength="20">
        <button id="setNameBtn">設定</button>
    </div>
    <div class="tabs" id="tabs"></div>
    <div class="user-list" id="userList">目前沒有在線使用者</div>
    <div class="chatbox" id="chatbox"></div>
    <div class="input-area">
        <input type="text" id="msgInput" placeholder="輸入訊息..." autocomplete="off">
        <button id="sendBtn">發送</button>
    </div>
</div>

<script>
(function() {
    const chatbox    = document.getElementById('chatbox');
    const msgInput   = document.getElementById('msgInput');
    const sendBtn    = document.getElementById('sendBtn');
    const statusEl   = document.getElementById('status');
    const userListEl = document.getElementById('userList');
    const nameInput  = document.getElementById('nameInput');
    const setNameBtn = document.getElementById('setNameBtn');
    const tabsEl     = document.getElementById('tabs');

    let ws = null;
    let reconnectTimer = null;
    let reconnectDelay = 2000;
    let pingTimer = null;
    let pongTimeout = null;
    let currentRoom = 'general';
    let privateTarget = null;
    let allUsers = [];
    let allRooms = [];

    // 固定 clientId(防刷新重連重複)
    let clientId = localStorage.getItem('chat_client_id');
    if (!clientId) {
        clientId = 'c_' + Date.now() + '_' + Math.random().toString(36).substr(2, 8);
        localStorage.setItem('chat_client_id', clientId);
    }

    const savedName = localStorage.getItem('chat_name') || '';
    if (savedName) nameInput.value = savedName;

    function buildWsUrl() {
        const proto = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
        return proto + window.location.host + '/wss';
    }

    function connect() {
        setStatus('連線中...', '');
        ws = new WebSocket(buildWsUrl());

        ws.onopen = function() {
            setStatus('已連線', 'connected');
            reconnectDelay = 2000;
            startHeartbeat();
            // 發送 register
            const name = localStorage.getItem('chat_name') || '';
            ws.send(JSON.stringify({ type: 'register', clientId: clientId, name: name }));
        };

        ws.onmessage = function(e) {
            if (e.data === '__pong__') { clearTimeout(pongTimeout); return; }
            let data;
            try { data = JSON.parse(e.data); } catch { return; }

            switch (data.type) {
                case 'init':
                    allRooms = data.rooms || [];
                    allUsers = data.users || [];
                    renderTabs();
                    renderUserList();
                    addSystemMsg('已連線到聊天室');
                    break;
                case 'chat':
                    if (data.private) {
                        addChatMsg(data.from_id, data.message, data.time, true);
                    } else if (data.room === currentRoom) {
                        addChatMsg(data.from_id, data.message, data.time, false);
                    }
                    break;
                case 'user_joined':
                    addSystemMsg(data.user + ' 加入了聊天室');
                    if (!allUsers.find(u => u.name === data.user)) {
                        allUsers.push({ name: data.user, rooms: ['general'] });
                    }
                    renderUserList();
                    break;
                case 'user_left':
                    addSystemMsg(data.user + ' 離開了聊天室');
                    if (data.users) {
                        allUsers = data.users.map(n => typeof n === 'string' ? {name:n, rooms:['general']} : n);
                    }
                    renderUserList();
                    break;
                case 'name_changed':
                    addSystemMsg(data.from + ' 改名為 ' + data.to);
                    if (data.users) {
                        allUsers = data.users.map(n => typeof n === 'string' ? {name:n, rooms:['general']} : n);
                    }
                    renderUserList();
                    break;
            }
        };

        ws.onclose = function() {
            setStatus('已斷線', 'disconnected');
            sendBtn.disabled = true;
            stopHeartbeat();
            scheduleReconnect();
        };
        ws.onerror = function() {};
    }

    // ===== Tabs =====
    function renderTabs() {
        tabsEl.innerHTML = '';
        allRooms.forEach(function(room) {
            const div = document.createElement('div');
            div.className = 'tab' + (room === currentRoom && !privateTarget ? ' active' : '');
            div.textContent = room === 'general' ? '📢 大廳' : room;
            div.onclick = function() {
                currentRoom = room;
                privateTarget = null;
                msgInput.placeholder = '輸入訊息...';
                if (ws && ws.readyState === WebSocket.OPEN) {
                    ws.send(JSON.stringify({ type: 'join_room', room: room }));
                }
                renderTabs();
                chatbox.innerHTML = '';
                addSystemMsg('切換到 ' + room);
            };
            tabsEl.appendChild(div);
        });
        if (privateTarget) {
            const div = document.createElement('div');
            div.className = 'tab active';
            div.textContent = '🔒 ' + privateTarget;
            tabsEl.appendChild(div);
        }
    }

    function startPrivateChat(name) {
        privateTarget = name;
        currentRoom = null;
        msgInput.placeholder = '私訊 ' + name + '...';
        chatbox.innerHTML = '';
        addSystemMsg('開始私聊 ' + name);
        renderTabs();
    }

    // ===== User List =====
    function renderUserList() {
        if (!allUsers.length) {
            userListEl.textContent = '目前沒有在線使用者';
            return;
        }
        userListEl.innerHTML = '在線 (' + allUsers.length + '): ';
        allUsers.forEach(function(u) {
            const span = document.createElement('span');
            span.textContent = u.name;
            span.title = '點擊私聊';
            span.onclick = function() { startPrivateChat(u.name); };
            userListEl.appendChild(span);
        });
    }

    // ===== Heartbeat =====
    function startHeartbeat() {
        stopHeartbeat();
        pingTimer = setInterval(function() {
            if (ws && ws.readyState === WebSocket.OPEN) {
                ws.send('__ping__');
                pongTimeout = setTimeout(function() { if (ws) ws.close(); }, 10000);
            }
        }, 25000);
    }
    function stopHeartbeat() {
        if (pingTimer) { clearInterval(pingTimer); pingTimer = null; }
        if (pongTimeout) { clearTimeout(pongTimeout); pongTimeout = null; }
    }

    // ===== Reconnect =====
    function scheduleReconnect() {
        if (reconnectTimer) return;
        reconnectTimer = setTimeout(function() {
            reconnectTimer = null;
            reconnectDelay = Math.min(reconnectDelay * 1.5, 30000);
            connect();
        }, reconnectDelay);
    }

    // ===== UI =====
    function setStatus(text, cls) {
        statusEl.textContent = text;
        statusEl.className = 'status ' + cls;
    }

    function addSystemMsg(text) {
        const div = document.createElement('div');
        div.className = 'msg system';
        div.textContent = text;
        chatbox.appendChild(div);
        chatbox.scrollTop = chatbox.scrollHeight;
    }

    function addChatMsg(fromId, message, time, isPrivate) {
        const div = document.createElement('div');
        div.className = 'msg' + (isPrivate ? ' private-msg' : '');

        const meta = document.createElement('div');
        meta.className = 'meta';
        const strong = document.createElement('strong');
        strong.textContent = fromId;
        meta.appendChild(strong);
        if (isPrivate) {
            const tag = document.createElement('span');
            tag.className = 'private-tag';
            tag.textContent = '私訊';
            meta.appendChild(tag);
        }
        if (time) {
            const span = document.createElement('span');
            span.textContent = ' ' + time;
            meta.appendChild(span);
        }

        const textDiv = document.createElement('div');
        textDiv.className = 'text';
        textDiv.textContent = message;

        div.appendChild(meta);
        div.appendChild(textDiv);
        chatbox.appendChild(div);
        chatbox.scrollTop = chatbox.scrollHeight;
    }

    // ===== Actions =====
    function sendMessage() {
        const text = msgInput.value.trim();
        if (!text || !ws || ws.readyState !== WebSocket.OPEN) return;
        if (privateTarget) {
            ws.send(JSON.stringify({ type: 'chat', to: privateTarget, text: text }));
        } else {
            ws.send(JSON.stringify({ type: 'chat', room: currentRoom, text: text }));
        }
        msgInput.value = '';
    }

    function setName() {
        const name = nameInput.value.trim();
        if (!name) return;
        if (!ws || ws.readyState !== WebSocket.OPEN) {
            setNameBtn.textContent = '未連線';
            setTimeout(() => setNameBtn.textContent = '設定', 1500);
            return;
        }
        ws.send(JSON.stringify({ type: 'set_name', name: name }));
        localStorage.setItem('chat_name', name);
        setNameBtn.textContent = '✓ 已設定';
        setNameBtn.style.background = 'var(--green-bg)';
        setTimeout(() => {
            setNameBtn.textContent = '設定';
            setNameBtn.style.background = '';
        }, 2000);
    }

    sendBtn.addEventListener('click', sendMessage);
    msgInput.addEventListener('keydown', function(e) { if (e.key === 'Enter') sendMessage(); });
    setNameBtn.addEventListener('click', setName);
    nameInput.addEventListener('keydown', function(e) { if (e.key === 'Enter') setName(); });

    document.addEventListener('visibilitychange', function() {
        if (document.hidden) {
            stopHeartbeat();
        } else {
            if (ws && ws.readyState === WebSocket.OPEN) startHeartbeat();
            else if (ws && ws.readyState === WebSocket.CLOSED) connect();
        }
    });

    connect();
})();
</script>
</body>
</html>   
nano /var/www/html/wm/start.php

代碼: 選擇全部

<?php
use Workerman\Worker;
use Workerman\Connection\TcpConnection;

require_once __DIR__ . '/vendor/autoload.php';
// ===== MySQL =====這個地方要改你mysql_user的phpmyadmin的密碼
$pdo = new PDO('mysql:host=127.0.0.1;dbname=chat;charset=utf8mb4', 'phpmyadmin', 'db_password', [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_SILENT,
]);
// ===== 結束 =====
$ws_worker = new Worker("websocket://0.0.0.0:8282");
$ws_worker->count = 1;

$ROOMS = ['general', 'gaming', 'music'];

$users = [];

$ws_worker->onConnect = function (TcpConnection $connection) use ($ws_worker, &$users, $ROOMS) {
    $users[$connection->id] = [
        'name'  => '用戶' . $connection->id,
        'rooms' => ['general'],
    ];

    $connection->send(json_encode([
        'type'   => 'init',
        'users'  => array_values(array_map(function($u) {
            return ['name' => $u['name'], 'rooms' => $u['rooms']];
        }, $users)),
        'rooms'  => $ROOMS,
    ]));

    $payload = json_encode([
        'type' => 'user_joined',
        'user' => $users[$connection->id]['name'],
    ]);
    foreach ($ws_worker->connections as $conn) {
        if ($conn !== $connection) {
            try { $conn->send($payload); } catch (\Exception $e) {}
        }
    }
};

$ws_worker->onMessage = function (TcpConnection $connection, $data) use ($ws_worker, &$users, $ROOMS) {   

    if ($data === '__ping__') {
        $connection->send('__pong__');
        return;
    }

    $decoded = json_decode($data, true);
    if (!is_array($decoded)) {
        $decoded = ['type' => 'chat', 'room' => 'general', 'text' => $data];
    }

    // 忽略 register(前端新版會發,舊版不需要處理)
    if (($decoded['type'] ?? '') === 'register') return;

    $myName = $users[$connection->id]['name'] ?? 'unknown';
    $time   = date('H:i:s');

    switch ($decoded['type'] ?? '') {

        case 'set_name':
            $newName = trim($decoded['name'] ?? '');
            if ($newName !== '') {
                $oldName = $users[$connection->id]['name'];
                $users[$connection->id]['name'] = mb_substr($newName, 0, 20);
                $payload = json_encode([
                    'type'  => 'name_changed',
                    'from'  => $oldName,
                    'to'    => $users[$connection->id]['name'],
                    'users' => array_map(function($u) { return $u['name']; }, $users),
                ]);
                foreach ($ws_worker->connections as $conn) {
                    try { $conn->send($payload); } catch (\Exception $e) {}
                }
            }
            break;

        case 'join_room':
            $room = $decoded['room'] ?? '';
            if (in_array($room, $ROOMS) && !in_array($room, $users[$connection->id]['rooms'])) {
                $users[$connection->id]['rooms'][] = $room;
            }
            break;

        case 'chat':
            if (isset($decoded['to'])) {
                $targetName = $decoded['to'];
// ✅ 存 DB
        try {
            $stmt = $pdo->prepare("INSERT INTO chat_messages (room, sender, message, is_private, private_to) VALUES ('general', ?, ?, 1, ?)");
            $stmt->execute([$myName, $decoded['text'] ?? '', $targetName]);
        } catch (\Exception $e) {}

        // 你原本的 send 邏輯        
        $msg = [
                    'type'    => 'chat',
                    'private' => true,
                    'from_id' => $myName,
                    'message' => $decoded['text'] ?? '',
                    'time'    => $time,
                ];
                $connection->send(json_encode($msg));
                foreach ($users as $cid => $info) {
                    if ($info['name'] === $targetName) {
                        $targetConn = $ws_worker->connections[$cid] ?? null;
                        if ($targetConn) {
                            try { $targetConn->send(json_encode($msg)); } catch (\Exception $e) {}
                        }
                    }
                }
            } else {
                $room = $decoded['room'] ?? 'general';

// ✅ 存 DB
        try {
            $stmt = $pdo->prepare("INSERT INTO chat_messages (room, sender, message, is_private) VALUES (?, ?, ?, 0)");
            $stmt->execute([$room, $myName, $decoded['text'] ?? '']);
        } catch (\Exception $e) {}

        // 你原本的 send 邏輯

                    $payload = json_encode([
                    'type'    => 'chat',
                    'room'    => $room,
                    'from_id' => $myName,
                    'message' => $decoded['text'] ?? '',
                    'time'    => $time,
                ]);
                foreach ($users as $cid => $info) {
                    if (in_array($room, $info['rooms'])) {
                        $conn = $ws_worker->connections[$cid] ?? null;
                        if ($conn) {
                            try { $conn->send($payload); } catch (\Exception $e) {}
                        }
                    }
                }
            }
            break;
    }
};

$ws_worker->onClose = function (TcpConnection $connection) use ($ws_worker, &$users) {
    // ✅ Guard:如果已經不在(重複觸發),直接回
    if (!isset($users[$connection->id])) return;

    $name = $users[$connection->id]['name'];
    unset($users[$connection->id]);


    $payload = json_encode([
        'type'  => 'user_left',
        'user'  => $name,
        'users' => array_map(function($u) { return $u['name']; }, $users),
    ]);
    foreach ($ws_worker->connections as $conn) {
        try { $conn->send($payload); } catch (\Exception $e) {}
    }
};

// 清理殘留 PID 檔
$pidFile = __DIR__ . '/start.php.pid';
if (file_exists($pidFile)) {
    $oldPid = (int)file_get_contents($pidFile);
    if ($oldPid <= 0 || !posix_kill($oldPid, 0)) {
        @unlink($pidFile);
    }
}   

Worker::runAll();   
回覆文章

誰在線上

正在瀏覽這個版面的使用者:沒有註冊會員 和 0 位訪客