本文目录导读:

我来详细介绍如何用PHP搭建IM即时通讯后端系统:
技术选型方案
核心组件
- WebSocket服务器: Swoole / Workerman
- 消息队列: Redis (Pub/Sub)
- 数据库: MySQL + Redis
- 协议支持: WebSocket + HTTP
推荐框架
// 1. Workerman (更易上手) composer require workerman/workerman // 2. Swoole (性能更强) composer require swoole/swoole // 3. 集成方案 - TPIM composer require topthink/think-im
基础架构设计
1 完整目录结构
im-system/
├── server/
│ ├── websocket.php # WebSocket服务
│ ├── http.php # HTTP API服务
│ └── worker.php # 消息处理worker
├── app/
│ ├── controller/
│ │ ├── Auth.php # 认证
│ │ ├── Message.php # 消息处理
│ │ └── Friend.php # 好友关系
│ ├── model/
│ │ ├── User.php
│ │ ├── Message.php
│ │ └── Friend.php
│ ├── service/
│ │ ├── RedisService.php
│ │ ├── MessageService.php
│ │ └── UserService.php
│ └── helper/
│ └── function.php
├── config/
│ ├── app.php
│ └── database.php
├── vendor/
├── composer.json
└── start.php
核心实现代码
1 WebSocket服务端 (Workerman示例)
server/websocket.php
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use Workerman\Worker;
use Workerman\Lib\Timer;
// 创建WebSocket服务器
$ws_worker = new Worker('websocket://0.0.0.0:8282');
$ws_worker->count = 4; // 进程数
// 全局用户连接映射表
$userConnections = [];
$ws_worker->onConnect = function($connection) {
echo "New connection: {$connection->id}\n";
};
$ws_worker->onMessage = function($connection, $data) {
global $userConnections;
$message = json_decode($data, true);
if (!$message) return;
switch ($message['type']) {
case 'auth': // 用户认证
handleAuth($connection, $message, $userConnections);
break;
case 'chat': // 发送消息
handleChat($connection, $message, $userConnections);
break;
case 'heartbeat': // 心跳
$connection->send(json_encode(['type' => 'heartbeat_ack']));
break;
case 'offline': // 离线消息获取
getOfflineMessages($connection, $message);
break;
}
};
$ws_worker->onClose = function($connection) {
global $userConnections;
// 移除用户连接
foreach ($userConnections as $uid => $conn) {
if ($conn->id === $connection->id) {
unset($userConnections[$uid]);
// 广播离线状态
broadcastStatus($uid, 'offline');
break;
}
}
echo "Connection {$connection->id} closed\n";
};
// 用户认证处理
function handleAuth($connection, $message, &$userConnections) {
$token = $message['token'] ?? '';
$userId = validateToken($token);
if ($userId) {
$userConnections[$userId] = $connection;
$connection->uid = $userId;
// 发送认证成功响应
$connection->send(json_encode([
'type' => 'auth_success',
'user_id' => $userId
]));
// 同步离线消息
syncOfflineMessages($userId, $connection);
// 广播在线状态
broadcastStatus($userId, 'online');
} else {
$connection->send(json_encode([
'type' => 'auth_failed',
'message' => 'Invalid token'
]));
$connection->close();
}
}
// 处理聊天消息
function handleChat($connection, $message, $userConnections) {
$fromUid = $connection->uid;
$toUid = $message['to_user_id'];
$content = $message['content'];
$msgType = $message['msg_type'] ?? 'text';
// 生成消息ID和时间戳
$msgId = uniqid('msg_', true);
$timestamp = time();
// 保存消息到数据库
$msgData = [
'msg_id' => $msgId,
'from_uid' => $fromUid,
'to_uid' => $toUid,
'content' => $content,
'msg_type' => $msgType,
'timestamp' => $timestamp,
'status' => 0 // 未读
];
saveMessage($msgData);
// 判断目标是否在线
if (isset($userConnections[$toUid])) {
// 在线,直接发送
$userConnections[$toUid]->send(json_encode([
'type' => 'new_message',
'data' => $msgData
]));
// 更新消息状态为已送达
updateMessageStatus($msgId, 1);
// 发送送达回执
$connection->send(json_encode([
'type' => 'message_delivered',
'msg_id' => $msgId,
'timestamp' => $timestamp
]));
} else {
// 离线,消息已保存到数据库
$connection->send(json_encode([
'type' => 'message_stored',
'msg_id' => $msgId,
'timestamp' => $timestamp
]));
}
}
// 启动服务
Worker::runAll();
2 REST API服务 (消息管理)
server/http.php
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use Workerman\Worker;
use Workerman\Protocols\Http;
$http_worker = new Worker('http://0.0.0.0:8280');
$http_worker->count = 4;
$http_worker->onMessage = function($connection, $request) {
$router = $request->path();
$method = $request->method();
// CORS头
$connection->sendHeader('Access-Control-Allow-Origin', '*');
$connection->sendHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
$connection->sendHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if ($method === 'OPTIONS') {
$connection->send('');
return;
}
$response = [];
switch (true) {
case $router === '/api/login' && $method === 'POST':
$response = handleLogin($request);
break;
case $router === '/api/register' && $method === 'POST':
$response = handleRegister($request);
break;
case $router === '/api/messages/history' && $method === 'GET':
$response = getMessageHistory($request);
break;
case $router === '/api/messages/offline' && $method === 'GET':
$response = getOfflineMessages($request);
break;
case preg_match('/\/api\/users\/(\d+)/', $router, $matches) && $method === 'GET':
$response = getUserInfo($matches[1]);
break;
default:
$response = ['code' => 404, 'message' => 'Not Found'];
}
$connection->send(json_encode($response));
};
// 登录处理
function handleLogin($request) {
$data = json_decode($request->rawBody(), true);
$username = $data['username'] ?? '';
$password = $data['password'] ?? '';
// 验证用户
$user = verifyUser($username, $password);
if ($user) {
// 生成token
$token = generateToken($user['id']);
// 返回WebSocket连接信息
return [
'code' => 200,
'data' => [
'token' => $token,
'user_id' => $user['id'],
'username' => $user['username'],
'avatar' => $user['avatar'],
'ws_url' => 'ws://127.0.0.1:8282'
]
];
}
return ['code' => 401, 'message' => 'Login failed'];
}
Worker::runAll();
3 消息服务模块
app/service/MessageService.php
<?php
namespace app\service;
use Redis;
use PDO;
class MessageService {
private $redis;
private $db;
public function __construct() {
$this->redis = new Redis();
$this->redis->connect('127.0.0.1', 6379);
$this->db = new PDO('mysql:host=localhost;dbname=im_db', 'root', 'password');
}
// 保存消息
public function saveMessage($msgData) {
// 保存到MySQL
$sql = "INSERT INTO messages (msg_id, from_uid, to_uid, content, msg_type, timestamp, status)
VALUES (?, ?, ?, ?, ?, ?, ?)";
$stmt = $this->db->prepare($sql);
$stmt->execute([
$msgData['msg_id'],
$msgData['from_uid'],
$msgData['to_uid'],
$msgData['content'],
$msgData['msg_type'],
$msgData['timestamp'],
$msgData['status']
]);
// 保存到Redis (最近消息缓存)
$cacheKey = "recent_messages:{$msgData['from_uid']}:{$msgData['to_uid']}";
$this->redis->lPush($cacheKey, json_encode($msgData));
$this->redis->lTrim($cacheKey, 0, 99); // 只保留最近100条
// 如果是离线消息,保存到离线队列
$this->addOfflineMessage($msgData['to_uid'], $msgData);
}
// 添加离线消息
public function addOfflineMessage($userId, $msgData) {
$key = "offline_messages:{$userId}";
$this->redis->rPush($key, json_encode($msgData));
$this->redis->expire($key, 86400); // 24小时过期
}
// 获取离线消息
public function getOfflineMessages($userId) {
$key = "offline_messages:{$userId}";
$messages = [];
while ($msg = $this->redis->lPop($key)) {
$messages[] = json_decode($msg, true);
}
return $messages;
}
// 获取聊天历史
public function getChatHistory($userId, $friendId, $limit = 20, $offset = 0) {
// 先从Redis获取缓存
$cacheKey = "recent_messages:{$userId}:{$friendId}";
$cached = $this->redis->lRange($cacheKey, $offset, $offset + $limit - 1);
if (!empty($cached)) {
return array_map('json_decode', $cached);
}
// Redis无缓存,从MySQL获取
$sql = "SELECT * FROM messages
WHERE (from_uid = ? AND to_uid = ?) OR (from_uid = ? AND to_uid = ?)
ORDER BY timestamp DESC
LIMIT ? OFFSET ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$userId, $friendId, $friendId, $userId, $limit, $offset]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// 更新消息状态
public function updateMessageStatus($msgId, $status) {
$sql = "UPDATE messages SET status = ? WHERE msg_id = ?";
$stmt = $this->db->prepare($sql);
return $stmt->execute([$status, $msgId]);
}
}
数据库设计
1 MySQL表结构
-- 用户表
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password` varchar(255) NOT NULL,
`avatar` varchar(255) DEFAULT NULL,
`status` tinyint(1) DEFAULT '0' COMMENT '0离线 1在线',
`last_login` timestamp NULL,
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 消息表
CREATE TABLE `messages` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`msg_id` varchar(50) NOT NULL,
`from_uid` int(11) NOT NULL,
`to_uid` int(11) NOT NULL,
`content` text NOT NULL,
`msg_type` varchar(20) DEFAULT 'text' COMMENT 'text/image/file',
`timestamp` int(11) NOT NULL,
`status` tinyint(1) DEFAULT '0' COMMENT '0未读 1已送达 2已读',
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX `idx_from_to` (`from_uid`, `to_uid`),
INDEX `idx_msg_id` (`msg_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 好友关系表
CREATE TABLE `friends` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`friend_id` int(11) NOT NULL,
`status` tinyint(1) DEFAULT '0' COMMENT '0待确认 1已确认',
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_friend` (`user_id`, `friend_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2 Redis数据结构
// 用户在线状态
$redis->hSet('online_users', $userId, $connectionId);
$redis->hDel('online_users', $userId);
// 用户会话信息
$redis->set("user_session:{$userId}", json_encode([
'token' => $token,
'expire' => time() + 86400
]));
$redis->expire("user_session:{$userId}", 86400);
// 离线消息队列
$redis->rPush("offline_messages:{$userId}", json_encode($msgData));
$redis->expire("offline_messages:{$userId}", 86400);
// 最近聊天缓存
$redis->lPush("recent_chat:{$userId}:{$friendId}", json_encode($msgData));
$redis->lTrim("recent_chat:{$userId}:{$friendId}", 0, 99);
启动服务
1 服务启动脚本
start.php
<?php
require_once __DIR__ . '/vendor/autoload.php';
// 启动WebSocket服务
$wsProcess = popen('php server/websocket.php start -d', 'r');
// 启动HTTP服务
$httpProcess = popen('php server/http.php start -d', 'r');
echo "IM Server started successfully!\n";
echo "WebSocket: ws://0.0.0.0:8282\n";
echo "HTTP API: http://0.0.0.0:8280\n";
2 生产环境部署
# 使用Supervisor管理进程 sudo apt-get install supervisor # 配置文件 /etc/supervisor/conf.d/im-server.conf [program:im-websocket] command=php /path/to/server/websocket.php start numprocs=1 autostart=true autorestart=true user=www-data [program:im-http] command=php /path/to/server/http.php start numprocs=1 autostart=true autorestart=true user=www-data # 启动 sudo supervisorctl reload sudo supervisorctl start all
前端JS连接示例
// 连接WebSocket
const ws = new WebSocket('ws://127.0.0.1:8282');
// 认证
ws.onopen = function() {
ws.send(JSON.stringify({
type: 'auth',
token: 'your_token_here'
}));
};
// 接收消息
ws.onmessage = function(event) {
const data = JSON.parse(event.data);
switch(data.type) {
case 'new_message':
// 处理新消息
showMessage(data.data);
break;
case 'auth_success':
console.log('连接成功');
break;
case 'heartbeat_ack':
// 心跳响应
break;
}
};
// 发送消息
function sendMessage(toUserId, content) {
ws.send(JSON.stringify({
type: 'chat',
to_user_id: toUserId,
content: content,
msg_type: 'text'
}));
}
// 心跳保持
setInterval(() => {
ws.send(JSON.stringify({ type: 'heartbeat' }));
}, 30000);
性能优化建议
1 消息合并发送
// 批量处理消息
function batchSendMessages() {
$batchSize = 100;
$messages = [];
// 收集消息
while (count($messages) < $batchSize) {
$msg = getNextMessage();
if (!$msg) break;
$messages[] = $msg;
}
// 批量发送
foreach ($messages as $msg) {
sendToUser($msg['to_uid'], $msg);
}
}
2 连接池优化
// 使用Swoole连接池
$pool = new Swoole\Database\PDOPool(
new Swoole\Database\PDOConfig()
);
// 从池中获取连接
$pdo = $pool->get();
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$userId]);
$pool->put($pdo);
3 消息压缩
// 大消息压缩
function compressMessage($data) {
if (strlen($data) > 1024) {
return gzcompress($data, 6);
}
return $data;
}
这个PHP IM系统实现了:
- ✅ WebSocket实时通信
- ✅ 用户认证系统
- ✅ 消息存储和转发
- ✅ 离线消息处理
- ✅ 消息历史查询
- ✅ 心跳检测机制
- ✅ 分布式部署支持
可以根据实际需求扩展群聊、文件传输、消息回执等功能,对于高并发场景,建议使用Swoole替代Workerman以获得更好的性能。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播