wsTable; // 从查询字符串获取 user_id 和 token $userId = (int)($request->get['user_id'] ?? 0); $token = $request->get['token'] ?? ''; // 用户认证 if (!$userId || !$token) { $server->push($request->fd, json_encode([ 'type' => 'error', 'data' => [ 'message' => '认证失败:缺少 user_id 或 token', 'code' => 401 ] ])); $server->disconnect($request->fd); return; } // 验证 JWT token try { $payload = JWTAuth::setToken($token)->getPayload(); // 验证 token 中的用户 ID 是否匹配 $tokenUserId = $payload['sub'] ?? null; if ($tokenUserId != $userId) { Log::warning('WebSocket 认证失败:用户 ID 不匹配', [ 'fd' => $request->fd, 'token_user_id' => $tokenUserId, 'query_user_id' => $userId ]); $server->push($request->fd, json_encode([ 'type' => 'error', 'data' => [ 'message' => '认证失败:用户 ID 不匹配', 'code' => 401 ] ])); $server->disconnect($request->fd); return; } // 验证 token 是否过期 if (isset($payload['exp']) && $payload['exp'] < time()) { Log::warning('WebSocket 认证失败:token 已过期', [ 'fd' => $request->fd, 'user_id' => $userId, 'exp' => $payload['exp'], 'current_time' => time() ]); $server->push($request->fd, json_encode([ 'type' => 'error', 'data' => [ 'message' => '认证失败:token 已过期', 'code' => 401 ] ])); $server->disconnect($request->fd); return; } Log::info('WebSocket 认证成功', [ 'fd' => $request->fd, 'user_id' => $userId ]); } catch (\Exception $e) { Log::warning('WebSocket 认证失败:无效的 token', [ 'fd' => $request->fd, 'user_id' => $userId, 'error' => $e->getMessage() ]); $server->push($request->fd, json_encode([ 'type' => 'error', 'data' => [ 'message' => '认证失败:无效的 token', 'code' => 401 ] ])); $server->disconnect($request->fd); return; } // 存储连接映射:uid:{userId} -> fd $wsTable->set('uid:' . $userId, [ 'value' => $request->fd, 'expiry' => time() + 3600 // 1 小时过期 ]); // 存储反向映射:fd:{fd} -> userId $wsTable->set('fd:' . $request->fd, [ 'value' => $userId, 'expiry' => time() + 3600 ]); // 发送欢迎消息 $server->push($request->fd, json_encode([ 'type' => 'connected', 'data' => [ 'message' => '欢迎连接到 LaravelS WebSocket', 'user_id' => $userId, 'fd' => $request->fd, 'timestamp' => time() ] ])); Log::info('WebSocket 连接已打开', [ 'fd' => $request->fd, 'user_id' => $userId, 'ip' => $request->server['remote_addr'] ]); } catch (\Exception $e) { Log::error('WebSocket onOpen 错误', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), 'fd' => $request->fd ]); $server->push($request->fd, json_encode([ 'type' => 'error', 'data' => [ 'message' => '连接错误:' . $e->getMessage(), 'code' => 500 ] ])); $server->disconnect($request->fd); } } /** * 处理接收消息事件 * * @param Server $server WebSocket 服务器对象 * @param Frame $frame WebSocket 帧对象 * @return void */ public function onMessage(Server $server, Frame $frame): void { try { // 从服务器获取 wsTable $wsTable = $server->wsTable; // 从 fd 映射获取 user_id $fdInfo = $wsTable->get('fd:' . $frame->fd); if ($fdInfo === false) { $server->disconnect($frame->fd); return; } $userId = (int)$fdInfo['value']; // 解析消息 $message = json_decode($frame->data, true); if (!$message || !isset($message['type'])) { $server->push($frame->fd, json_encode([ 'type' => 'error', 'data' => [ 'message' => '无效的消息格式', 'code' => 400 ] ])); return; } $type = $message['type']; $data = $message['data'] ?? []; Log::info('收到 WebSocket 消息', [ 'fd' => $frame->fd, 'user_id' => $userId, 'type' => $type ]); // 处理不同类型的消息 switch ($type) { case 'ping': // 响应 ping $server->push($frame->fd, json_encode([ 'type' => 'pong', 'data' => $data ])); break; case 'heartbeat': // 心跳确认 $server->push($frame->fd, json_encode([ 'type' => 'heartbeat_ack', 'data' => array_merge($data, [ 'timestamp' => time() ]) ])); break; case 'chat': // 私聊消息 $this->handleChatMessage($server, $wsTable, $frame, $userId, $data); break; case 'broadcast': // 广播消息给所有用户 $this->handleBroadcast($server, $wsTable, $userId, $data); break; case 'subscribe': // 订阅频道 $this->handleSubscribe($server, $wsTable, $frame, $userId, $data); break; case 'unsubscribe': // 取消订阅频道 $this->handleUnsubscribe($server, $wsTable, $frame, $userId, $data); break; default: // 未知消息类型 $server->push($frame->fd, json_encode([ 'type' => 'error', 'data' => [ 'message' => '未知的消息类型:' . $type, 'code' => 400 ] ])); break; } } catch (\Exception $e) { Log::error('WebSocket onMessage 错误', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), 'fd' => $frame->fd ]); } } /** * 处理连接关闭事件 * * @param Server $server WebSocket 服务器对象 * @param int $fd 文件描述符 * @param int $reactorId 反应器 ID * @return void */ public function onClose(Server $server, $fd, $reactorId): void { try { // 从服务器获取 wsTable $wsTable = $server->wsTable; // 从 fd 映射获取 user_id $fdInfo = $wsTable->get('fd:' . $fd); if ($fdInfo !== false) { $userId = (int)$fdInfo['value']; // 删除 uid 映射 $wsTable->del('uid:' . $userId); // 删除该用户的所有频道订阅 $this->removeUserFromAllChannels($wsTable, $userId, $fd); Log::info('WebSocket 连接已关闭', [ 'fd' => $fd, 'user_id' => $userId, 'reactor_id' => $reactorId ]); } // 删除 fd 映射 $wsTable->del('fd:' . $fd); } catch (\Exception $e) { Log::error('WebSocket onClose 错误', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), 'fd' => $fd ]); } } /** * 处理私聊消息 * * @param Server $server WebSocket 服务器对象 * @param \Swoole\Table $wsTable WebSocket 表 * @param Frame $frame WebSocket 帧对象 * @param int $fromUserId 发送者用户 ID * @param array $data 消息数据 * @return void */ protected function handleChatMessage(Server $server, \Swoole\Table $wsTable, Frame $frame, int $fromUserId, array $data): void { $toUserId = $data['to_user_id'] ?? 0; if (!$toUserId) { $server->push($frame->fd, json_encode([ 'type' => 'error', 'data' => [ 'message' => '缺少 to_user_id', 'code' => 400 ] ])); return; } // 获取接收者的 fd $recipientInfo = $wsTable->get('uid:' . $toUserId); if ($recipientInfo === false) { $server->push($frame->fd, json_encode([ 'type' => 'error', 'data' => [ 'message' => '用户不在线', 'to_user_id' => $toUserId, 'code' => 404 ] ])); return; } $toFd = (int)$recipientInfo['value']; // 发送消息给接收者 $server->push($toFd, json_encode([ 'type' => 'chat', 'data' => array_merge($data, [ 'from_user_id' => $fromUserId, 'timestamp' => time() ]) ])); } /** * 处理广播消息 * * @param Server $server WebSocket 服务器对象 * @param \Swoole\Table $wsTable WebSocket 表 * @param int $userId 用户 ID * @param array $data 消息数据 * @return void */ protected function handleBroadcast(Server $server, \Swoole\Table $wsTable, int $userId, array $data): void { $excludeUserId = $data['exclude_user_id'] ?? null; $message = json_encode([ 'type' => 'broadcast', 'data' => array_merge($data, [ 'from_user_id' => $userId, 'timestamp' => time() ]) ]); // 发送消息给所有连接的用户 foreach ($wsTable as $key => $row) { if (strpos($key, 'uid:') === 0) { $targetUserId = (int)substr($key, 4); // 移除 'uid:' 前缀 $fd = (int)$row['value']; // 跳过排除的用户 if ($excludeUserId && $targetUserId == $excludeUserId) { continue; } if ($server->isEstablished($fd)) { $server->push($fd, $message); } } } } /** * 处理频道订阅 * * @param Server $server WebSocket 服务器对象 * @param \Swoole\Table $wsTable WebSocket 表 * @param Frame $frame WebSocket 帧对象 * @param int $userId 用户 ID * @param array $data 消息数据 * @return void */ protected function handleSubscribe(Server $server, \Swoole\Table $wsTable, Frame $frame, int $userId, array $data): void { $channel = $data['channel'] ?? ''; if (!$channel) { $server->push($frame->fd, json_encode([ 'type' => 'error', 'data' => [ 'message' => '缺少频道名称', 'code' => 400 ] ])); return; } // 存储频道订阅 $channelKey = 'channel:' . $channel . ':fd:' . $frame->fd; $wsTable->set($channelKey, [ 'value' => $userId, 'expiry' => time() + 3600 ]); $server->push($frame->fd, json_encode([ 'type' => 'subscribed', 'data' => [ 'channel' => $channel, 'message' => '成功订阅频道:' . $channel, 'timestamp' => time() ] ])); Log::info('用户订阅频道', [ 'user_id' => $userId, 'channel' => $channel, 'fd' => $frame->fd ]); } /** * 处理频道取消订阅 * * @param Server $server WebSocket 服务器对象 * @param \Swoole\Table $wsTable WebSocket 表 * @param Frame $frame WebSocket 帧对象 * @param int $userId 用户 ID * @param array $data 消息数据 * @return void */ protected function handleUnsubscribe(Server $server, \Swoole\Table $wsTable, Frame $frame, int $userId, array $data): void { $channel = $data['channel'] ?? ''; if (!$channel) { $server->push($frame->fd, json_encode([ 'type' => 'error', 'data' => [ 'message' => '缺少频道名称', 'code' => 400 ] ])); return; } // 删除频道订阅 $channelKey = 'channel:' . $channel . ':fd:' . $frame->fd; $wsTable->del($channelKey); $server->push($frame->fd, json_encode([ 'type' => 'unsubscribed', 'data' => [ 'channel' => $channel, 'message' => '成功取消订阅频道:' . $channel, 'timestamp' => time() ] ])); Log::info('用户取消订阅频道', [ 'user_id' => $userId, 'channel' => $channel, 'fd' => $frame->fd ]); } /** * 从所有频道中移除用户 * * @param \Swoole\Table $wsTable WebSocket 表 * @param int $userId 用户 ID * @param int $fd 文件描述符 * @return void */ protected function removeUserFromAllChannels(\Swoole\Table $wsTable, int $userId, int $fd): void { foreach ($wsTable as $key => $row) { if (strpos($key, 'channel:') === 0 && strpos($key, ':fd:' . $fd) !== false) { $wsTable->del($key); } } } }