首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP + MySQL 打造無刷新聊天室:框架隔離、輪詢機制與現代化 WebSocket 重構指南

     編輯:PHP 綜合知識

專案背景與設計思想

作者嘗試解決早期聊天室「每次刷新頁面體驗差、消息重複」的問題,提出多框架隔離 + 定時輪詢 + JavaScript 跨框架 DOM 注入的方案:

  • 主框架:chat.php 定義 4 個框架——消息顯示區 list.html、在線用戶區 username.php、消息獲取區 getmsg.php、發言輸入區 fayan.php。
  • 消息流向:用戶在 fayan.php 提交 → 寫入 MySQL chat 表 → getmsg.php 每 6 秒輪詢最新消息 → 通過 parent.frames[0].document.write() 直接注入 list.html 的 <div id="lin">。
  • 在線狀態:username.php 每 60 秒刷新,更新 chat_getmsg 表心跳,清理超時用戶與舊消息。
  • 歡迎流程:getmsg1.php 首次加載寫入歡迎語,6 秒後跳轉 getmsg.php。

核心代碼結構解析

1. 主框架 chat.php

<?php
if (!isset($username)) $username = "guest";
$conid = mysql_connect("localhost", "yourcounter", "password");
mysql_select_db("yourdadabase", $conid);
$dstr = date("YmdHis");
mysql_query("INSERT INTO chat_getmsg (username, shijian) VALUES ('$username', '$dstr')");
mysql_query("UPDATE chat_getmsg SET shijian='$dstr' WHERE username='$username'");
mysql_close($conid);
?>
<frameset rows="283,105">
  <frameset cols="520,113">
    <frame name="topFrame" src="list.html">           // 消息顯示
    <frame name="rightFrame" src="username.php">     // 在線列表
  </frameset>
  <frameset rows="17,90">
    <frame name="topFrame1" src="getmsg1.php?username=$username"> // 歡迎+轉輪詢
    <frame name="bottomFrame" src="fayan.php?username=$username"> // 輸入
  </frameset>
</frameset>

問題:直接使用 $username 變量(register_globals),密碼明文寫死,mysql_* 已移除(PHP 7+),frameset 已棄用。

2. 發言處理 fayan.php

<form method="post" action="fayan.php?username=$username&yanse=$yanse&objectname=$objectname">
  <select name="yanse">...顏色選項...</select>
  <input name="fayan" size=35>
  <input type="hidden" name="username" value="$username">
  <input type="button" value="發言" onclick="submitfayan()">
  <input name="objectname" size=10 value="$objectname"> // 私聊目標
</form>
<script>
function submitfayan() { if (fayanform.fayan.value!="") fayanform.submit(); else alert("不能發空信息"); fayanform.fayan.focus(); }
</script>
<?php
if (isset($username) && isset($fayan)) {
  $conid = mysql_connect(...);
  mysql_query("INSERT INTO chat (username, objectname, action, msg, color) VALUES ('$username','$objectname','NO','$fayan','$yanse')");
  mysql_close($conid);
}
?>

3. 消息輪詢與跨框架注入 getmsg.php

<?php
$conid = mysql_pconnect(...);
$fromdate = date("YmdHis", time()-6); // 最近 6 秒
$sql = "SELECT username, msg, action, objectname, shijian, color FROM chat WHERE shijian >= '$fromdate'";
$res = mysql_query($sql);
echo '<meta http-equiv="Refresh" content="6,url=getmsg.php?username=$username">';
echo '<script>';
echo 'var win=parent.frames[0]; var doc=win.document; var divlin=doc.body.all("lin");';
while (list($name,$msg,$action,$objectname,$shijian,$color)=mysql_fetch_row($res)) {
  $object = ($objectname!="大家"&&$objectname) ? "對 <a href=JavaScript:selectusername('$objectname')>$objectname</a> " : "";
  echo "divlin.innerHTML=divlin.innerHTML+"<font color=$color size=3><a href=JavaScript:selectusername('$name')>$name</a> $object 說: $msg </font><br>"";
}
echo "win.scrollBy(6000,6000);";
echo '</script>';
?>

關鍵技術:parent.frames[0] 獲取第一個框架(list.html),doc.body.all("lin") 為 IE 專有 API 獲取 id="lin" 元素,innerHTML 疊加消息,scrollBy 強制滾動到底部。

4. 在線用戶 username.php

<meta http-equiv="Refresh" content="60">
<?php
$fromdate = date("YmdHis", time()-200);
$sql = "SELECT username FROM chat_getmsg WHERE shijian >= '$fromdate'";
while (list($username)=mysql_fetch_row($res)) {
  echo "<a href=JavaScript:selectusername('$username')>$username</a><br>";
  // 順便清理舊消息
  $sql = "DELETE FROM chat WHERE shijian <= '$fromdate100'";
}
?>

架構缺陷與安全隱患

隱患影響風險
SQL 注入遍地所有 $username、$fayan、$objectname 直接拼接 SQL🔴 嚴重
XSS 存儲型消息內容 $msg 直接拼接進 innerHTML,無轉義🔴 嚴重
依賴 register_globals變量來源不可控🔴 嚴重
mysql_* 已移除PHP 7+ 無法運行🔴 嚴重
frameset 已棄用HTML5 不合法,SEO/無障礙/收藏/打印全失效🟠 高
IE 專有 APIdoc.body.all() 非標準,現代瀏覽器不支持🟠 高
Meta Refresh 體驗差閃爍、滾動重置、無法實時、高併發壓力大🟠 高
明文密碼/無認證數據庫密碼寫死、用戶無密碼驗證🟠 高
消息無 ID 去重輪詢時間窗重疊導致重複消息🟡 中
編碼未指定MySQL 連接、HTML 頁面、JS 字符串編碼易亂碼🟡 中

現代化重構架構設計

數據模型

CREATE TABLE users (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  username VARCHAR(50) UNIQUE NOT NULL,
  pass_hash CHAR(60) NOT NULL,
  color VARCHAR(7) DEFAULT '#000000',
  last_active_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  is_online BOOLEAN DEFAULT FALSE,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE rooms (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(100) NOT NULL,
  is_lobby BOOLEAN DEFAULT FALSE,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE messages (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  room_id BIGINT REFERENCES rooms(id),
  from_id BIGINT REFERENCES users(id),
  to_id BIGINT DEFAULT 0,              -- 0=公開
  content TEXT NOT NULL,
  color VARCHAR(7) DEFAULT '#000000',
  is_system BOOLEAN DEFAULT FALSE,
  is_action BOOLEAN DEFAULT FALSE,     // /: 動作
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_room_time (room_id, created_at)
);

CREATE TABLE user_rooms (
  user_id BIGINT REFERENCES users(id),
  room_id BIGINT REFERENCES rooms(id),
  joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (user_id, room_id)
);

WebSocket 服務端

// Workerman 示例
use Workerman\Worker;
use Workerman\Connection\TcpConnection;

$ws = new Worker('websocket://0.0.0.0:2346');
$ws->count = 4; // 多進程

$ws->onConnect = function(TcpConnection $conn) {
  $conn->auth = false;
  $conn->userId = null;
  $conn->roomId = null;
};

$ws->onMessage = function(TcpConnection $conn, $data) {
  $msg = json_decode($data, true);
  if (!$msg) return;
  
  switch ($msg['type']) {
    case 'auth':
      $user = User::where('username', $msg['username'])->first();
      if ($user && password_verify($msg['password'], $user->pass_hash)) {
        $conn->auth = true;
        $conn->userId = $user->id;
        $user->update(['is_online'=>true, 'last_active_at'=>now()]);
        $conn->send(json_encode(['type'=>'auth_ok', 'user'=>$user->toArray()]));
        // 加入大廳
        joinRoom($conn, 1);
      } else {
        $conn->send(json_encode(['type'=>'auth_fail', 'error'=>'認證失敗']));
        $conn->close();
      }
      break;
      
    case 'join_room':
      if (!$conn->auth) return;
      joinRoom($conn, $msg['room_id'], $msg['password'] ?? null);
      break;
      
    case 'send':
      if (!$conn->auth) return;
      $message = MessageService::create(
        $conn->userId, $conn->roomId, $msg['content'], 
        $msg['color'] ?? '#000000', $msg['to_id'] ?? 0, $msg['is_action'] ?? false
      );
      broadcastToRoom($conn->roomId, ['type'=>'message', 'payload'=>$message->toArray()]);
      break;
      
    case 'heartbeat':
      if ($conn->auth) User::where('id', $conn->userId)->update(['last_active_at'=>now()]);
      break;
  }
};

$ws->onClose = function(TcpConnection $conn) {
  if ($conn->auth) {
    User::where('id', $conn->userId)->update(['is_online'=>false]);
    leaveRoom($conn);
  }
};

function joinRoom(TcpConnection $conn, int $roomId, ?string $password = null) {
  $room = Room::find($roomId);
  if (!$room) return $conn->send(json_encode(['type'=>'error', 'msg'=>'房間不存在']));
  if ($room->pass_hash && !password_verify($password, $room->pass_hash)) {
    return $conn->send(json_encode(['type'=>'error', 'msg'=>'密碼錯誤']));
  }
  // 離開舊房間
  if ($conn->roomId) leaveRoom($conn);
  $conn->roomId = $roomId;
  UserRoom::firstOrCreate(['user_id'=>$conn->userId, 'room_id'=>$roomId]);
  // 發送房間歷史消息(最近 50 條)
  $history = Message::where('room_id', $roomId)->latest()->limit(50)->get()->reverse()->values();
  $conn->send(json_encode(['type'=>'history', 'payload'=>$history]));
  // 廣播用戶加入
  broadcastToRoom($roomId, ['type'=>'user_joined', 'payload'=>['user_id'=>$conn->userId, 'username'=>User::find($conn->userId)->username]]);
  // 發送當前在線列表
  $members = User::whereHas('rooms', fn($q)=>$q->where('room_id',$roomId))->get(['id','username','color']);
  broadcastToRoom($roomId, ['type'=>'member_list', 'payload'=>$members]);
}

function leaveRoom(TcpConnection $conn) {
  if (!$conn->roomId) return;
  $roomId = $conn->roomId;
  UserRoom::where('user_id', $conn->userId)->where('room_id', $roomId)->delete();
  broadcastToRoom($roomId, ['type'=>'user_left', 'payload'=>['user_id'=>$conn->userId]]);
  $conn->roomId = null;
}

function broadcastToRoom(int $roomId, array $data) {
  global $ws;
  $json = json_encode($data);
  foreach ($ws->connections as $conn) {
    if (isset($conn->roomId) && $conn->roomId === $roomId && $conn->auth) {
      $conn->send($json);
    }
  }
}

Worker::runAll();

前端應用

<!-- Vue 3 + Pinia 核心邏輯 -->
const ws = ref(null)
const messages = ref([])
const members = ref([])
const currentRoom = ref(1)
const user = ref(null)
const messageInput = ref('')

function connect() {
  ws.value = new WebSocket('wss://chat.example.com/ws')
  ws.value.onopen = () => {
    ws.value.send(JSON.stringify({ type: 'auth', username: '...', password: '...' }))
  }
  ws.value.onmessage = (e) => {
    const data = JSON.parse(e.data)
    switch (data.type) {
      case 'auth_ok': user.value = data.user; break
      case 'history': messages.value = data.payload; scrollToBottom(); break
      case 'message': messages.value.push(data.payload); scrollToBottom(); break
      case 'member_list': members.value = data.payload; break
      case 'user_joined': members.value.push(data.payload); break
      case 'user_left': members.value = members.value.filter(m => m.id !== data.payload.user_id); break
    }
  }
  // 心跳
  setInterval(() => ws.value?.send(JSON.stringify({type:'heartbeat'})), 30000)
}

function send() {
  if (!messageInput.value.trim()) return
  ws.value.send(JSON.stringify({
    type: 'send',
    content: messageInput.value,
    color: user.value?.color || '#000000',
  }))
  messageInput.value = ''
}

function scrollToBottom() {
  nextTick(() => { document.querySelector('.messages')?.scrollTop = 99999 })
}

部署架構

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   Nginx     │────▶│  PHP-FPM    │────▶│   MySQL     │
│  (靜態/SSL) │     │  (API/SSR)  │     │  (持久化)   │
└─────────────┘     └─────────────┘     └─────────────┘
                           │
                           ▼
                    ┌─────────────┐
                    │  Workerman  │
                    │  (WebSocket)│
                    └─────────────┘
                           │
                           ▼
                    ┌─────────────┐
                    │    Redis    │
                    │ (發布/訂閱/   │
                    │  在線集合)   │
                    └─────────────┘
  • Nginx 反向代理 WebSocket /ws 到 Workerman 進程組。
  • PHP-FPM 處理登錄、註冊、歷史消息 API、靜態資源 SSR。
  • Workerman 維持長連接、廣播消息、管理在線狀態(可用 Redis PubSub 跨進程廣播)。
  • Redis 存儲在線用戶集合、消息離線隊列、頻率限制。

總結

原專案以「框架隔離 + Meta Refresh 輪詢 + 跨框架 DOM 注入」在無 Ajax/WebSocket 時代實現了「無刷新」聊天體驗,展現了極強的工程創造力。但其技術棧(mysql_*、register_globals、frameset、document.all、Meta Refresh)已全部過時且存在嚴重安全漏洞。現代重構應採用 WebSocket 長連接 + 結構化數據庫 + 前後端分離 + 標準 DOM API + 統一認證授權。本文提供的 Workerman + Vue 3 + MySQL + Redis 完整架構可直接用於生產部署,支撐萬級並發、實時推送、多房間、私聊、表情、指令等全功能,並具備良好的擴展性(如接入 Swoole、Kubernetes、消息隊列、語音視頻等)。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved