分享目前的workerman聊天室

Post Reply
Brave Ye
系統管理員
Posts: 5725
Joined: 2025-11-08, 13:32
Location: Taiwan
Contact:

分享目前的workerman聊天室

Post by Brave Ye »

sudo apt update && sudo apt upgrade -y

# 安裝 PHP 核心、CLI 模式、Composer 以及常駐進程必備的 pcntl 擴充
sudo apt install php-cli php-common php-xml php-mbstring php-curl php-zip php-pear unzip composer nginx -y

# 安裝 php-posix(Workerman 強烈建議,用於管理進程權限)
sudo apt install php-posix -y

mkdir -p /var/www/my-chat
cd /var/www/my-chat

composer require workerman/workerman
composer require workerman/channel
必須安裝php8.5 event 擴展
viewtopic.php?p=5737#p5737
nano /var/www/my-chat/start.php

Code: Select all

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

require_once __DIR__ . '/vendor/autoload.php';

// 1. 建立 Channel 服務
$channel_server = new Channel\Server('127.0.0.1', 2206);

// 2. 建立 WebSocket 服務
$ws_worker = new Worker("websocket://127.0.0.1:8282");
$ws_worker->count = 12; 

define('MAX_PROCESS_MEMORY', 128 * 1024 * 1024);
$ROOMS = ['大廳公聊', '電玩動漫', '音樂分享', '閒聊八卦'];

// 讓 Channel 服務內部維護一份萬能的名單
$channel_server->onWorkerStart = function() {
    global $global_online_users;
    $global_online_users = [];
};

// ==================== 修正後的 3. 工作進程啟動事件 ====================
$ws_worker->onWorkerStart = function($ws_worker) use ($ROOMS) {
    Client::connect('127.0.0.1', 2206);

    // 💡 只有 0 號進程初始化並負責維護全域使用者陣列,徹底解決進程記憶體隔離與集體覆寫問題
    global $global_online_users;
    if ($ws_worker->id === 0) {
        $global_online_users = [];
    } else {
        $global_online_users = null;
    }

    // 🌟 12 個進程都會收到總部名單更新,並立刻廣播給各自手上的連線
    Client::on('sync_full_user_list', function($event_data) use ($ws_worker) {
        $full_list = $event_data['users'] ?? [];
        $payload = json_encode([
            'type'  => 'user_list_update', 
            'users' => $full_list
        ]);
        
        foreach ($ws_worker->connections as $conn) {
            try { $conn->send($payload); } catch (\Exception $e) {}
        }
    });

    // 註冊全域廣播事件 (處理聊天訊息、進出場提示)
    Client::on('global_broadcast', function($event_data) use ($ws_worker) {
        $payload = $event_data['payload'] ?? '';
        $target_room = $event_data['room'] ?? null;

        foreach ($ws_worker->connections as $conn) {
            if ($target_room) {
                if (isset($conn->rooms) && in_array($target_room, $conn->rooms)) {
                    try { $conn->send($payload); } catch (\Exception $e) {}
                }
            } else {
                try { $conn->send($payload); } catch (\Exception $e) {}
            }
        }
    });

    // 註冊私訊事件
    Client::on('private_message', function($event_data) use ($ws_worker) {
        $target_name = $event_data['target_name'] ?? '';
        $payload = $event_data['payload'] ?? '';

        foreach ($ws_worker->connections as $conn) {
            if (($conn->user_name ?? '') === $target_name) {
                try { $conn->send($payload); } catch (\Exception $e) {}
            }
        }
    });

    // ─── 🌟 核心修正:指定只有 0 號子進程來做數據中轉中心 ───
    Client::on('internal_update_user', function($data) use ($ws_worker) {
        global $global_online_users;
        
        // 如果不是 0 號進程,直接不理會,避免 12 個進程重複運算或回傳空資料
        if ($ws_worker->id !== 0) {
            return;
        }
        
        $id = $data['g_id'];
        if ($data['action'] === 'add_or_update') {
            $global_online_users[$id] = [
                'name'  => $data['name'],
                'rooms' => $data['rooms']
            ];
        } elseif ($data['action'] === 'delete') {
            unset($global_online_users[$id]);
        } elseif ($data['action'] === 'request_sync') {
            // 新人進來時,單純要求 0 號進程觸發重播更新
        }

        // 由 0 號進程統一把最新名單分發給所有人(包含自己)
        Client::publish('sync_full_user_list', [
            'users' => array_values($global_online_users)
        ]);
    });
};

// ==================== 修正後的 4. 新連線事件 ====================
$ws_worker->onConnect = function (TcpConnection $connection) use ($ROOMS) {
    $connection->global_id = posix_getpid() . '_' . $connection->id; 
    $connection->user_name = '用戶_' . rand(100, 999);
    $connection->rooms = ['大廳公聊'];

    // 💡 修正初始化傳送的格式,讓前端第一時間繪製基本容器
    $connection->send(json_encode([
        'type'   => 'init',
        'rooms'  => $ROOMS,
        'my_name' => $connection->user_name
    ]));

    Client::publish('global_broadcast', [
        'payload' => json_encode([
            'type' => 'user_joined',
            'user' => $connection->user_name,
        ])
    ]);

    // 通知 0 號進程:有人加入了,請更新名單並廣播
    Client::publish('internal_update_user', [
        'action' => 'add_or_update',
        'g_id'   => $connection->global_id,
        'name'   => $connection->user_name,
        'rooms'  => $connection->rooms
    ]);
};

// 5. 訊息接收事件
$ws_worker->onMessage = function (TcpConnection $connection, $data) use ($ROOMS) {   
    if ($data === '__ping__') {
        $connection->send('__pong__');
        return;
    }

    $decoded = json_decode($data, true);
    if (!is_array($decoded)) return;

    $myName = $connection->user_name ?? 'unknown';
    $time   = date('H:i:s');

    switch ($decoded['type'] ?? '') {
        case 'set_name':
            $newName = trim($decoded['name'] ?? '');
            if ($newName !== '' && mb_strlen($newName) <= 20) {
                $oldName = $connection->user_name;
                $connection->user_name = $newName;
                
                Client::publish('global_broadcast', [
                    'payload' => json_encode([
                        'type'  => 'name_changed',
                        'from'  => $oldName,
                        'to'    => $connection->user_name,
                    ])
                ]);
                
                Client::publish('internal_update_user', [
                    'action' => 'add_or_update',
                    'g_id'   => $connection->global_id,
                    'name'   => $connection->user_name,
                    'rooms'  => $connection->rooms
                ]);
            }
            break;

        case 'join_room':
            $room = $decoded['room'] ?? '';
            if (in_array($room, $ROOMS) && !in_array($room, $connection->rooms)) {
                $connection->rooms[] = $room;
                
                Client::publish('internal_update_user', [
                    'action' => 'add_or_update',
                    'g_id'   => $connection->global_id,
                    'name'   => $connection->user_name,
                    'rooms'  => $connection->rooms
                ]);
            }
            break;

        case 'chat':
            $text = $decoded['text'] ?? '';
            if (empty($text)) break;

            if (isset($decoded['to'])) {
                $targetName = $decoded['to'];
                $msg = [
                    'type'    => 'chat',
                    'private' => true,
                    'from_id' => $myName,
                    'text'    => $text,
                    'time'    => $time,
                ];
                
                $connection->send(json_encode($msg));
                Client::publish('private_message', [
                    'target_name' => $targetName,
                    'payload'     => json_encode($msg)
                ]);
            } else {
                $room = $decoded['room'] ?? '大廳公聊';
                Client::publish('global_broadcast', [
                    'room'    => $room,
                    'payload' => json_encode([
                        'type'    => 'chat',
                        'room'    => $room,
                        'from_id' => $myName,
                        'text'    => $text,
                        'time'    => $time,
                    ])
                ]);
            }
            break;
    }
};

// 6. 斷線事件
$ws_worker->onClose = function (TcpConnection $connection) {
    if (!isset($connection->user_name)) return;

    Client::publish('global_broadcast', [
        'payload' => json_encode([
            'type'  => 'user_left',
            'user'  => $connection->user_name,
        ])
    ]);
    
    Client::publish('internal_update_user', [
        'action' => 'delete',
        'g_id'   => $connection->global_id
    ]);
};

Worker::runAll();
nano /var/www/my-chat/public/index.html

Code: Select all

<!DOCTYPE html>
<html lang="zh-TW">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <title>Zine-TW 聊天室 (行動省電版)</title>
<style>
    * { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
    body { 
        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 
        background-color: #121212; 
        color: #e0e0e0; 
        height: 100vh;
        height: -webkit-fill-available; 
        padding: 10px; 
    }
    .chat-container { 
        display: flex; 
        max-width: 1200px; 
        height: 90vh; 
        margin: 0 auto; 
        background: #1e1e1e; 
        border-radius: 12px; 
        box-shadow: 0 4px 30px rgba(0,0,0,0.5); 
        overflow: hidden; 
    }
    .chat-sidebar { 
        width: 280px; 
        background-color: #181818; 
        border-right: 1px solid #2c2c2c; 
        padding: 20px; 
        display: flex; 
        flex-direction: column; 
        z-index: 99;
    }
    .sidebar-close-btn { display: none; }
    .status-badge { font-weight: bold; margin-bottom: 15px; font-size: 13px; padding: 6px 12px; border-radius: 20px; display: inline-block; }
    .status-badge.online { color: #81c784; background: rgba(129, 199, 132, 0.1); }
    .status-badge.offline { color: #e57373; background: rgba(229, 115, 115, 0.1); }
    .user-profile { 
        font-size: 14px; background: #242424; padding: 12px; border-radius: 8px; 
        border: 1px solid #333; margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; 
    }
    .profile-info { display: flex; flex-direction: column; gap: 2px; }
    .user-profile .label { color: #888; font-size: 12px; }
    #my-name-display { font-weight: bold; color: #64b5f6; font-size: 15px; word-break: break-all; padding-right: 5px; }
    h4 { font-size: 13px; color: #aaa; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 1px; }
    .scroll-list { background: #141414; border: 1px solid #2c2c2c; border-radius: 8px; flex: 1; min-height: 120px; overflow-y: auto; list-style: none; margin-bottom: 20px; padding: 5px; }
    .scroll-list li { padding: 12px 14px; border-bottom: 1px solid #1f1f1f; font-size: 14px; cursor: pointer; border-radius: 6px; transition: background 0.2s; color: #ccc; }
    .scroll-list li:hover { background-color: #242424; color: #fff; }
    .scroll-list li.active-room { font-weight: bold; color: #64b5f6; background-color: rgba(100, 181, 246, 0.15); border-left: 4px solid #64b5f6; }
    .loading-text { color: #666; font-style: italic; text-align: center; padding-top: 20px !important; }
    .chat-main { flex: 1; padding: 20px; display: flex; flex-direction: column; background: #1e1e1e; }
    #current-room-title { font-size: 16px; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #2c2c2c; color: #fff; }
    #chat-window { flex: 1; border: 1px solid #2c2c2c; border-radius: 8px; background: #151515; overflow-y: auto; padding: 15px; margin-bottom: 15px; }
    .msg-line { margin-bottom: 12px; line-height: 1.6; font-size: 15px; word-break: break-all; color: #e0e0e0; }
    .msg-time { color: #555; font-size: 11px; margin-right: 8px; }
    .msg-user { font-weight: bold; color: #ffb74d; }
    .msg-system { color: #ffd54f; font-style: italic; background: rgba(255, 213, 79, 0.08); padding: 6px 12px; border-radius: 6px; display: inline-block; font-size: 13px; width: 100%; text-align: center; }
    .msg-private { color: #ba68c8; font-weight: bold; }
    .chat-input-area { display: flex; gap: 10px; }
    #message-input { 
        flex: 1; padding: 14px; background-color: #262626; border: 1px solid #3a3a3a; 
        border-radius: 8px; font-size: 15px; color: #fff; outline: none; transition: border-color 0.2s; 
    }
    #message-input:focus { border-color: #64b5f6; }
    #message-input::placeholder { color: #555; }
    button { border: none; border-radius: 6px; font-weight: bold; cursor: pointer; transition: background 0.2s, transform 0.1s; }
    button:active { transform: scale(0.98); }
    .btn-mini { background: #333; color: #ccc; padding: 5px 10px; font-size: 12px; }
    .btn-mini:hover { background: #444; color: #fff; }
    .btn-send { background: #1e88e5; color: #fff; padding: 0 24px; font-size: 15px; }
    .btn-send:hover { background: #1565c0; }
    .mobile-nav-bar { display: none; }

    @media (max-width: 768px) {
        body { padding: 0; }
        .chat-container { height: 100vh; width: 100vw; border-radius: 0; box-shadow: none; padding-top: 50px; }
        .mobile-nav-bar { 
            display: flex; align-items: center; justify-content: space-between; position: fixed; 
            top: 0; left: 0; right: 0; height: 50px; background: #181818; border-bottom: 1px solid #2c2c2c; padding: 0 15px; z-index: 100; 
        }
        .nav-toggle-btn { background: #262626; color: #fff; padding: 6px 12px; font-size: 13px; border: 1px solid #3a3a3a; }
        #mobile-room-title { font-weight: bold; font-size: 15px; color: #fff; }
        .status-dot { width: 10px; height: 10px; border-radius: 50%; }
        .status-dot.online { background-color: #81c784; box-shadow: 0 0 8px #81c784; }
        .status-dot.offline { background-color: #e57373; }
        .chat-sidebar { 
            position: fixed; top: 0; bottom: 0; left: -290px; width: 280px; 
            transition: left 0.3s ease; box-shadow: 5px 0 25px rgba(0,0,0,0.5); background: #151515;
        }
        .chat-sidebar.mobile-open { left: 0; }
        .sidebar-close-btn { display: block; background: #e57373; color: #fff; padding: 10px; margin-bottom: 15px; font-size: 13px; width: 100%; text-align: center; }
        .desktop-only-title { display: none; }
        .chat-main { padding: 12px; }
        #chat-window { padding: 10px; margin-bottom: 10px; border-radius: 6px; }
        .chat-input-area { gap: 6px; }
        #message-input { padding: 12px; font-size: 16px; }
        .btn-send { padding: 0 16px; font-size: 14px; }
    }
</style>
</head>
<body>

<div class="mobile-nav-bar">
    <button class="nav-toggle-btn" onclick="toggleSidebar()">☰ 頻道/成員</button>
    <div id="mobile-room-title">大廳公聊</div>
    <div id="mobile-conn-status" class="status-dot offline"></div>
</div>

<div class="chat-container">
    <div id="chat-sidebar" class="chat-sidebar">
        <button class="sidebar-close-btn" onclick="toggleSidebar()">✕ 關閉選單</button>
        <div id="conn-status" class="status-badge offline">🔴 連線中...</div>
        
        <div class="user-profile">
            <div class="profile-info">
                <span class="label">我的名字:</span>
                <span id="my-name-display">未設定</span>
            </div>
            <button onclick="changeMyName()" class="btn-mini">修改</button>
        </div>

        <h4>⚡ 群聊頻道</h4>
        <ul id="room-list-box" class="scroll-list"></ul>

        <h4>👥 在線成員 ( 點擊私訊)</h4>
        <ul id="user-list-box" class="scroll-list">
            <li class="loading-text">連線載入中...</li>
        </ul>
    </div>

    <div class="chat-main">
        <h4 id="current-room-title" class="desktop-only-title">當前頻道: 大廳公聊</h4>
        <div id="chat-window"></div>
        
        <div class="chat-input-area">
            <input type="text" id="message-input" placeholder="請輸入訊息..." onkeydown="if(event.key==='Enter') sendChatMessage()" enterkeyhint="send">
            <button onclick="sendChatMessage()" class="btn-send">發送</button>
        </div>
    </div>
</div>

<script>
    var ws;
    var currentRoom = '大廳公聊';
    var myName = '未設定';
    var privateTarget = null;

    function toggleSidebar() {
        var sidebar = document.getElementById('chat-sidebar');
        if (sidebar) { sidebar.classList.toggle('mobile-open'); }
    }
    function connectWebSocket() {
        // 🟢 絕殺防封鎖修正:智慧感應進站是 IP 還是域名,強制分流連線通道
        var wsUrl;
        var currentHostname = window.location.hostname;

        if (currentHostname === '123.241.40.195') {
            // 如果牆內用戶用純 IP 進站,強制走標準不加密的 ws 通道連向伺服器
            wsUrl = 'ws://123.241.40.195/wss';
        } else {
            // 如果是一般網域用戶,完美對接網域的 wss 安全加密通道
            wsUrl = 'wss://' + currentHostname + '/wss';
        }
        
        console.log("【WebSocket】正在連線至: " + wsUrl);
        ws = new WebSocket(wsUrl);

        ws.onopen = function() {
            console.log("【WebSocket】伺服器連線成功。");
            document.getElementById('conn-status').innerText = '🟢 已連線';
            document.getElementById('conn-status').className = 'status-badge online';
            document.getElementById('mobile-conn-status').className = 'status-dot online';
        };

        ws.onclose = function() {
            console.warn("【WebSocket】連線已中斷,3秒後嘗試自動重連...");
            document.getElementById('conn-status').innerText = '🔴 連線中斷,重連中...';
            document.getElementById('conn-status').className = 'status-badge offline';
            document.getElementById('mobile-conn-status').className = 'status-dot offline';
            document.getElementById('user-list-box').innerHTML = '<li class="loading-text">正在重新連線...</li>';
            setTimeout(connectWebSocket, 3000);
        };
        ws.onmessage = function(event) {
            if (event.data === '__ping__') return;

            var data;
            try { data = JSON.parse(event.data); } catch (err) { return; }

            switch(data.type) {
                case 'init':
                    myName = data.my_name || '用戶';
                    document.getElementById('my-name-display').innerText = myName;
                    renderRooms(data.rooms || []);
                    break;
                case 'user_list_update':
                    renderOnlineUsers(data.users || []);
                    break;
                case 'chat':
                    appendMessageToWindow(data);
                    break;
                case 'user_joined':
                    appendSystemNotice(data.user + " 進入了聊天室");
                    break;
                case 'user_left':
                    appendSystemNotice(data.user + " 離開了聊天室");
                    break;
                case 'name_changed':
                    appendSystemNotice(data.from + " 已改名為 " + data.to);
                    if (data.from === myName) {
                        myName = data.to;
                        document.getElementById('my-name-display').innerText = myName;
                    }
                    break;
            }
        };
    }
    function sendChatMessage() {
        var inputEl = document.getElementById('message-input');
        var text = inputEl.value.trim();
        if (!text) return;

        var payload = { type: 'chat', text: text };

        if (privateTarget) {
            payload.to = privateTarget;
        } else {
            payload.room = currentRoom;
        }

        ws.send(JSON.stringify(payload));
        inputEl.value = '';
        inputEl.focus();
    }

    function changeMyName() {
        var newName = prompt("請輸入您的新名字(最多20個字):", myName);
        if (newName && newName.trim() !== "" && newName.trim() !== myName) {
            ws.send(JSON.stringify({
                type: 'set_name',
                name: newName.trim()
            }));
        }
    }

    function selectRoom(roomName, allRooms) {
        privateTarget = null;
        currentRoom = roomName;
        
        document.getElementById('current-room-title').innerText = "當前頻道: " + roomName;
        document.getElementById('mobile-room-title').innerText = roomName;
        
        renderRooms(allRooms);

        ws.send(JSON.stringify({
            type: 'join_room',
            room: roomName
        }));

        if (window.innerWidth <= 768) {
            toggleSidebar();
        }
    }
    function togglePrivateTarget(targetName) {
        if (targetName === myName) {
            alert("溫馨提示:不需要跟自己講悄悄話哦!");
            return;
        }

        if (privateTarget === targetName) {
            privateTarget = null;
            document.getElementById('current-room-title').innerText = "當前頻道: " + currentRoom;
            document.getElementById('mobile-room-title').innerText = currentRoom;
        } else {
            privateTarget = targetName;
            document.getElementById('current-room-title').innerText = "正在私訊給: " + targetName + " 🔒";
            document.getElementById('mobile-room-title').innerText = "🔒 私訊:" + targetName;
        }

        var items = document.getElementById('user-list-box').getElementsByTagName('li');
        for(var i=0; i<items.length; i++){
            if(items[i].getAttribute('data-username') === targetName) {
                if (privateTarget) {
                    items[i].style.backgroundColor = 'rgba(186, 104, 200, 0.15)';
                    items[i].style.borderLeft = '4px solid #ba68c8';
                }
            } else {
                items[i].style.backgroundColor = '';
                items[i].style.borderLeft = '';
            }
        }

        if (window.innerWidth <= 768) {
            toggleSidebar();
        }
    }

    function renderRooms(rooms) {
        var roomBox = document.getElementById('room-list-box');
        var html = '';
        rooms.forEach(function(room) {
            var activeClass = (room === currentRoom && !privateTarget) ? 'class="active-room"' : '';
            var arrow = (room === currentRoom && !privateTarget) ? '▶ ' : '';
            var roomsJson = JSON.stringify(rooms).replace(/"/g, '&quot;');
            html += '<li ' + activeClass + ' onclick="selectRoom(\'' + room + '\', ' + roomsJson + ')">' + arrow + room + '</li>';
        });
        roomBox.innerHTML = html;
    }

    function renderOnlineUsers(users) {
        var userBox = document.getElementById('user-list-box');
        var html = '';

        if (!users || users.length === 0) {
            html = '<li class="loading-text">暫無其他成員在線</li>';
            userBox.innerHTML = html;
            return;
        }

        users.forEach(function(user) {
            var isMeMark = (user.name === myName) ? ' <span style="color:#666; font-size:11px;">(我)</span>' : '';
            var privateMark = (user.name === privateTarget) ? ' <span class="msg-private" style="font-size:11px;">[私訊中]</span>' : '';
            var activeStyle = (user.name === privateTarget) ? 'class="active-room" style="background-color: rgba(186, 104, 200, 0.15); border-left: 4px solid #ba68c8;"' : '';

            html += '<li ' + activeStyle + ' data-username="' + user.name + '" onclick="togglePrivateTarget(\'' + user.name + '\')">';
            html += '<span style="color: #81c784; margin-right: 6px;">🟢</span>' + user.name + isMeMark + privateMark;
            html += '</li>';
        });
        userBox.innerHTML = html;
    }

    function appendMessageToWindow(data) {
        var win = document.getElementById('chat-window');
        var line = document.createElement('div');
        line.className = 'msg-line';

        var timeSpan = '<span class="msg-time">' + (data.time || '') + '</span>';
        var userSpan = '<span class="msg-user">' + data.from_id + '</span>: ';
        
        var content = '';
        if (data.private) {
            content = '<span class="msg-private">[密語]</span> ' + data.text;
        } else {
            var roomLabel = data.room ? '<span style="color:#666; font-size:12px;">(' + data.room + ')</span> ' : '';
            content = roomLabel + data.text;
        }

        line.innerHTML = timeSpan + userSpan + content;
        win.appendChild(line);
        win.scrollTop = win.scrollHeight;
    }

    function appendSystemNotice(text) {
        var win = document.getElementById('chat-window');
        var line = document.createElement('div');
        line.className = 'msg-line';
        line.innerHTML = '<span class="msg-system">' + text + '</span>';
        win.appendChild(line);
        win.scrollTop = win.scrollHeight;
    }

    setInterval(function() {
        if (ws && ws.readyState === WebSocket.OPEN) {
            ws.send('__ping__');
        }
    }, 25000);

    window.onload = connectWebSocket;
</script>

</body>
</html>
Post Reply

Who is online

Users browsing this forum: No registered users and 28 guests