WebSocket 實現瀏覽器與服務器雙向通信,是構建即時應用的關鍵技術。本文提供基於原生 PHP Socket 實現的聊天室完整服務端代碼,涵蓋握手協議、數據幂編解碼、用戶管理、消息廣播等核心邏輯。
<?php
error_reporting(E_ALL);
ob_implicit_flush();
$server = new ChatServer('127.0.0.1', 8000);
$server->run();
class ChatServer {
public $sockets = [];
public $users = [];
public $master;
public function __construct($address, $port) {
$this->master = $this->createSocket($address, $port);
$this->sockets['master'] = $this->master;
}
public function run() {
while (true) {
$changes = $this->sockets;
socket_select($changes, $write = null, $except = null, null);
foreach ($changes as $sock) {
if ($sock === $this->master) {
$this->acceptConnection();
} else {
$this->handleMessage($sock);
}
}
}
}
// 接受新連接
private function acceptConnection() {
$client = socket_accept($this->master);
$this->sockets[] = $client;
$this->users[] = ['socket' => $client, 'handshaked' => false];
}
// 處理消息/握手
private function handleMessage($sock) {
$len = socket_recv($sock, $buffer, 2048, 0);
$index = $this->findUserIndex($sock);
if ($len < 7) { // 斷開連接
$this->disconnect($index);
return;
}
if (!$this->users[$index]['handshaked']) {
$this->doHandshake($index, $buffer);
} else {
$data = $this->decode($buffer);
$this->dispatch($index, $data);
}
}
// WebSocket 握手協議
private function doHandshake($index, $buffer) {
$key = $this->extractSecKey($buffer);
$accept = base64_encode(sha1($key . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true));
$response = "HTTP/1.1 101 Switching Protocols\r\n"
. "Upgrade: websocket\r\n"
. "Sec-WebSocket-Version: 13\r\n"
. "Connection: Upgrade\r\n"
. "Sec-WebSocket-Accept: $accept\r\n\r\n";
socket_write($this->users[$index]['socket'], $response);
$this->users[$index]['handshaked'] = true;
}
// 數據幂解碼 (RFC 6455)
private function decode($str) {
$mask = [];
$data = '';
$msg = unpack('H*', $str);
$head = substr($msg[1], 0, 2);
if (hexdec($head[1]) === 8) return false; // 關閉幂
if (hexdec($head[1]) === 1) { // 文本幂
$mask[] = hexdec(substr($msg[1], 4, 2));
$mask[] = hexdec(substr($msg[1], 6, 2));
$mask[] = hexdec(substr($msg[1], 8, 2));
$mask[] = hexdec(substr($msg[1], 10, 2));
$s = 12;
$e = strlen($msg[1]) - 2;
for ($i = $s, $n = 0; $i <= $e; $i += 2, $n++) {
$data .= chr($mask[$n % 4] ^ hexdec(substr($msg[1], $i, 2)));
}
}
return $data;
}
// 數據幂編碼
private function encode($msg) {
$frame = [chr(129)]; // FIN=1, OPCODE=1 (text)
$len = strlen($msg);
$frame[] = $len < 126 ? chr($len) : ($len < 65536 ? chr(126) . pack('n', $len) : chr(127) . pack('J', $len));
$frame[] = $msg;
return implode('', $frame);
}
// 消息分發
private function dispatch($index, $msg) {
parse_str($msg, $data);
if ($data['type'] === 'add') {
$this->users[$index]['name'] = $data['ming'];
$this->broadcast(['add' => true, 'msg' => "歡迎 {$data['ming']} 加入!", 'users' => $this->getUserList()]);
} elseif ($data['type'] === 'ltiao') {
$this->sendTo($data['key'], ['msg' => $data['nr']], $index);
}
}
// 廣播/私聊發送
private function broadcast($data, $exclude = null) {
$encoded = $this->encode(json_encode($data));
foreach ($this->users as $i => $user) {
if ($i !== $exclude) socket_write($user['socket'], $encoded);
}
}
private function sendTo($targetIndex, $data, $fromIndex) {
$encoded = $this->encode(json_encode($data));
socket_write($this->users[$targetIndex]['socket'], $encoded);
socket_write($this->users[$fromIndex]['socket'], $encoded); // 回顯給自己
}
// 工具方法
private function findUserIndex($sock) { /* ... */ }
private function disconnect($index) { /* ... */ }
private function getUserList() { /* ... */ }
private function createSocket($addr, $port) { /* ... */ }
private function extractSecKey($buffer) { /* ... */ }
}
Sec-WebSocket-Key258EAFA5-E914-47DA-95CA-C5AB0DC85B11,SHA1 加密後 Base640 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-------+-+-------------+-------------------------------+ |F|R|R|R| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | (if payload len==126/127) | | |1|2|3| |K| | | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + | Extended payload length continued, if payload len == 127 | + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | |Masking-key, if MASK set to 1 | +-------------------------------+-------------------------------+ | Masking-key (continued) | Payload Data | +---------------------------------------------------------------+
socket_select() 監聽多個連接,單進程處理高併發,避免多進程/線程開銷。
const ws = new WebSocket('ws://127.0.0.1:8000');
ws.onopen = () => ws.send('type=add&ming=' + encodeURIComponent(username));
ws.onmessage = (e) => { /* 渲染消息 */ };
ws.send('type=ltiao&nr=' + encodeURIComponent(msg) + '&key=' + targetId);
原生 Socket 實現適合學習協議細節,生產環境強烈建議使用 Swoole 或 Workerman 等成熟框架。