<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ThinkChat2.0新版上線,更智能更精彩,支持會話、畫圖、視頻、閱讀、搜索等,送10W Token,即刻開啟你的AI之旅 廣告
                [TOC] # 數據庫的設計 ```[sql] -- ---------------------------- -- Table structure for rank -- ---------------------------- DROP TABLE IF EXISTS `rank`; CREATE TABLE `rank` ( `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, `room_id` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '房間id', `uid` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'uid', `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '昵稱', `stars` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '星星數', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for room -- ---------------------------- DROP TABLE IF EXISTS `room`; CREATE TABLE `room` ( `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '房間名', `create_time` datetime(0) NULL DEFAULT NULL, `last_hours` float(10, 2) NULL DEFAULT 0.00 COMMENT '持續時長', `finish_time` datetime(0) NULL DEFAULT NULL COMMENT '結束時間', `close_time` datetime(0) NULL DEFAULT NULL COMMENT '完成時間', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for room_user_cards -- ---------------------------- DROP TABLE IF EXISTS `room_user_cards`; CREATE TABLE `room_user_cards` ( `room_id` int(11) UNSIGNED NOT NULL COMMENT '房間id', `uid` int(11) UNSIGNED NOT NULL DEFAULT 0, `compete_uid` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '對抗uid', `used_order` int(11) NOT NULL DEFAULT -1 COMMENT '使用順序', `type` enum('石頭','剪刀','布') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '石頭', `used` tinyint(1) UNSIGNED NULL DEFAULT 0 COMMENT '是否使用 0', `create_time` datetime(0) NULL DEFAULT NULL, `use_time` datetime(0) NULL DEFAULT NULL COMMENT '使用時間', PRIMARY KEY (`room_id`, `uid`, `compete_uid`, `used_order`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for room_users -- ---------------------------- DROP TABLE IF EXISTS `room_users`; CREATE TABLE `room_users` ( `room_id` int(11) UNSIGNED NOT NULL COMMENT '房間id', `uid` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '用戶id', `type` enum('human','ai') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'human' COMMENT '身份類型', `stars` int(2) UNSIGNED NULL DEFAULT 0 COMMENT '星星數', `left_cards` int(11) UNSIGNED NULL DEFAULT 12 COMMENT '剩余卡片數', `status` enum('unknown','win','lose','draw') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'unknown' COMMENT '狀態', `gaming` tinyint(1) UNSIGNED NULL DEFAULT 0 COMMENT '是否比賽中 1-是 0-否', PRIMARY KEY (`room_id`, `uid`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for user -- ---------------------------- DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, `win_round` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '贏的次數', `create_time` datetime(0) NULL DEFAULT NULL, `fd` int(11) UNSIGNED NULL DEFAULT 0 COMMENT 'fd', `type` enum('human','ai') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'human' COMMENT '類型', `online` tinyint(1) UNSIGNED NULL DEFAULT 1 COMMENT '1在線 0離線', `online_time` datetime(0) NULL DEFAULT NULL, `offline_time` datetime(0) NULL DEFAULT NULL COMMENT '離線時間', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; ``` 從數據產生來說吧 顯示用戶表,然后用戶創建房間,然后房間里玩家出牌。一開始考慮的是類似動物世界的那種,大房間里好多人,淘汰后換別人比賽,所以要創建房間 指定每個房間幾個人。持續多久。后來時間有限,先簡化成自動創建房間 房間里2個人。 ## 用戶表 user 用戶表主要是昵稱作為唯一判斷,沒搞密碼那套。type里區分human 和ai。開始只搞人機對戰。后面其實也支持人人對戰。 因為是webscoket 必然會存在 在線、離線問題。 按照以前做的思路,fd存 當前鏈接fd,搞一個登錄消息,來自動更新online 狀態和時間。 ## 房間表 room 主要是create_time + lasthours 對應 finish_time 應結束時間, close_time 是實際結束時間。 動物世界里是1天,我這邊默認1小時。 close_time 表示 最小對局, 2人一輪,12張牌打完、也可能3次就全贏 后的 實際結束時間。 房間里沒存多少人,因為創建房間時候已經指定好,并隨機添加了除創建者以外的人數的機器人。 ## 房間用戶表 room_users 記錄本輪用戶的星星、剩余卡牌 狀態 是否比賽中等字段。 默認unknown,結束后是win、draw、lose。 ## 出牌表 room_user_cards 先記錄房間 和比賽者信息、后記錄出牌順序、出牌類型type,出牌時間等。開始是想分配機器人時自動將12張牌一次性插入的。后來想了想沒必要全插,因為可能提前結束,多插記錄也沒必要。 # websocket實現 ## swoole的安裝 首先是swoole 擴展,linux 環境很好搞定 pecl install swoole 就行。問題這樣必須弄一臺服務器開發。為了提高效率,本來準備按官方手冊里用mingw 來搞的。后來發現,swoole支持 wsl。 ![](https://box.kancloud.cn/ee8d18a48eccc7585df4d856eb96583f_823x300.png) 于是本地 wsl里先裝擴展,發現各種依賴出問題。最后用通過重置應用后,裝上了。 > wsl 里與本地磁盤路徑的對應 /mnt/c 對應C盤 > 當前linux 路徑 對應 C:\Users<username>\\AppData\\Local\Packages\<group_name>\\LocalState > 如 C:\Users\jay\AppData\Local\Packages\CanonicalGroupLimited.Ubuntu18.04onWindows_79rhkp1fndgsc\LocalState\rootfs ## think-swoole的使用 首先在創建了一個tp5.1的項目,然后升級至最新版,聽說修好了數據庫斷線重連問題。然后composer require think-swoole 就好。 ### 配置服務控制器 在config目錄里建一個swoole_server.php 的文件,配置一個控制器來作為swoole控制器: ~~~ <?php return [ 'swoole_class' => 'app\index\controller\Guess', ]; ~~~ 具體內容為: ~~~ <?php namespace app\index\controller; use app\index\model\User; use Swoole\Process; use think\Db; use think\swoole\Server; use think\facade\Env; class Guess extends Server { protected $host = '0.0.0.0'; protected $port = 9502; protected $serverType = 'socket'; public $option = [ 'worker_num' => 4, 'task_worker_num' => 4, 'daemonize' => true, 'backlog' => 128, 'log_file' => './swoole.log', 'pid_file' => './master', ]; public $from_fd; public $server; /** * 架構函數 * @access public */ public function init() { $pid = $this->getMasterPid(); if (!$this->isRunning($pid)) { Env::set('pid_path', Env::get('root_path').'pid/'); $this->option['pid_file'] = str_ireplace('./', Env::get('pid_path'), $this->option['pid_file']); User::offline(0); $server = $this->swoole; $process = new \swoole_process(function ($process) use ($server) { new \console\InotifyReload($server); ptrace(realpath(__DIR__)); file_put_contents(__DIR__.'/../../../pid/inotify', $process->pid); $process->name('inotify'); }); } $this->swoole->addProcess($process); } /** * 判斷PID是否在運行 * @access protected * @param int $pid * @return bool */ protected function isRunning($pid) { if (empty($pid)) { return false; } Process::kill($pid, 0); return !swoole_errno(); } public function getMasterPid() { if (file_exists($this->option['pid_file'])) { return file_get_contents($this->option['pid_file']); } else { return 0; } } // 加密 public static function encode($arr) { $str = http_build_query($arr); return bin2hex($str); } // 解析 public static function decode($binary) { return hex2bin($binary); } public function pretty_json($arr) { return json_encode($arr, JSON_UNESCAPED_UNICODE); } public function onWorkerStart($server, $worker_id) { // secho('start', 'worker_start'); // secho('root_path', Env::get('root_path')); } public function onShutdown($server) { file_put_contents($this->option['pid_file'], '0'); secho('stop', 'stoped'); ptrace('swoole 服務停止了'); } public function onStart($server) { define('SUCCESS_MSG', ''); if (!isDarwin()) { cli_set_process_title('swoole'); } $managerPid = $server->manager_pid; $shString = <<<SH echo "Reloading..." kill -USR1 {$managerPid} echo "Reloaded" SH; $sh_file = './.reload_manager.sh'; file_put_contents($sh_file, $shString); } public function onConnect($server, $fd) { $time = datetime(); echo "server: handshake success with fd:{$fd} {$time}\n"; } public function onWorkerExit($server, $worker_id) { echo "work exist " . datetime(); } public function onClose($server, $fd) { User::offline_fd($fd); $time = datetime(); echo "client {$fd} closed {$time}\n"; } public function onFinish($server, $task_id, $data) { print_r('return_data'); print_r($data); return true; } // 異步耗時任務 public function onTask($server, $task_id, $src_worker_id, $data) { $decode_str = self::decode($data); secho('on_task', $decode_str); \mb_parse_str($decode_str, $data); $call = $data['op'] . '_task'; $message = new GuessMessage($server, $this->from_fd); if (isset($data['op']) && method_exists($message, $call)) { return $message->$call($server, $task_id, $src_worker_id, $data['data']); } else { secho('error task', sprintf('%s not exist', $call)); return '404'; } } public function onMessage($server, $frame) { $data = $frame->data; $this->from_fd = $frame->fd; $this->server = $server; secho('msg_in', sprintf('#%d 的%s發來消息為 %s', $this->from_fd, datetime(), $data)); $data_arr = json_decode($data, 1); $message = new GuessMessage($server, $this->from_fd); if (!$data_arr) { $message->error('wrong format!1'); } else { if (isset($data_arr['op']) && method_exists($message, $data_arr['op'])) { $call = $data_arr['op']; unset($data_arr['op']); Db::startTrans(); try { $message->$call($server, $frame, $data_arr); Db::commit(); } catch (\Exception $e) { $info = $e->getMessage(); if($info == '對手已結束游戲'){ $message->error($info); }else{ $message->error('server error', ['info'=>$e->getMessage()]); } Db::rollback(); echo $e->getMessage() . PHP_EOL; echo $e->getTraceAsString(); } } else { if (isset($data_arr['op'])) { $message->error('缺少op參數'); } else { $message->error("{$data_arr['op']}對應的方法不存在"); } } } return ''; } } ~~~ 里面有一些關鍵函數,在common.php里 ~~~ <?php // +---------------------------------------------------------------------- // | ThinkPHP [ WE CAN DO IT JUST THINK ] // +---------------------------------------------------------------------- // | Copyright (c) 2006-2016 http://thinkphp.cn All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- // | Author: 流年 <liu21st@gmail.com> // +---------------------------------------------------------------------- // 應用公共文件 if (!function_exists('datetime')) { // 方便生成當前日期函數 function datetime($str = 'now', $formart = 'Y-m-d H:i:s') { return @date($formart, strtotime($str)); } } /** * 是否是mac系統 * @return bool */ function isDarwin() { if (PHP_OS == 'Darwin') { return true; } return false; } function secho($title, $message) { ob_start(); if (is_string($message)) { $message = ltrim($message); $message = str_replace(PHP_EOL, '', $message); } print_r($message); $content = ob_get_contents(); ob_end_clean(); $content = explode("\n", $content); $send = ""; foreach ($content as $value) { if (!empty($value)) { $echo = "[{$title}] {$value}"; $send = $send . $echo; echo $send . PHP_EOL; // ob_end_clean(); } } } if (!function_exists('is_online')) { // 判斷是否線上環境 function is_online() { if (PHP_SAPI == 'cli') { return isset($_SERVER['LOGNAME']) && $_SERVER['LOGNAME'] != 'root'; } else { return stripos($_SERVER['HTTP_HOST'], '39.108.156.37') !== false; } } } /** * @param string $dev * @return string */ function getServerIp($dev = 'eth0') { if (isDarwin()) { return '0.0.0.0'; } return exec("ip -4 addr show $dev | grep inet | awk '{print $2}' | cut -d / -f 1"); } if (!function_exists('get_client_ip')) { /** * 獲取客戶端IP地址 * @param int $type 返回類型 0 返回IP地址 1 返回IPV4地址數字 * @param bool $adv 是否進行高級模式獲取(有可能被偽裝) * @return mixed */ function get_client_ip($type = 0, $adv = false) { $type = $type ? 1 : 0; static $ip = null; if ($ip !== null) { return $ip[$type]; } if ($adv) { if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { $arr = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); $pos = array_search('unknown', $arr); if (false !== $pos) { unset($arr[$pos]); } $ip = trim($arr[0]); } elseif (isset($_SERVER['HTTP_CLIENT_IP'])) { $ip = $_SERVER['HTTP_CLIENT_IP']; } elseif (isset($_SERVER['REMOTE_ADDR'])) { $ip = $_SERVER['REMOTE_ADDR']; } } elseif (isset($_SERVER['REMOTE_ADDR'])) { $ip = $_SERVER['REMOTE_ADDR']; } // IP地址合法驗證 $long = sprintf("%u", ip2long($ip)); $ip = $long ? array($ip, $long) : array('0.0.0.0', 0); return $ip[$type]; } } if (!function_exists('ptrace')) { function ptrace($msg, $channel = 'normal') { echo var_export($msg, true).PHP_EOL; return ; } } ~~~ ### swoole的一些優化 #### 通過InotifyReload 實現的監聽文件變化后自動重啟worker ~~~ /** * 架構函數 * @access public */ public function init() { $pid = $this->getMasterPid(); if (!$this->isRunning($pid)) { User::offline(0); $server = $this->swoole; $process = new \swoole_process(function ($process) use ($server) { new \console\InotifyReload($server); file_put_contents('./pid/inotify', $process->pid); $process->name('inotify'); }); } $this->swoole->addProcess($process); } ~~~ init初始化里判斷是否運行,沒運行就用swoole_process進程類加一個監聽的進程。 加了一個 isRunning 方法,參考tp里think\swoole\command\Swoole 類。 InotifyReload 類內容: ~~~ <?php namespace console; use think\facade\Env; class InotifyReload { const RELOAD_SIG = 'reload_sig'; // 監控的目錄,默認是src public $monitor_dir; public $inotifyFd; public $managePid; public $server; public function __construct($server) { secho("SYS", "已開啟代碼熱重載"); $this->server = $server; $root_path = Env::get('root_path'); $this->monitor_dir = realpath($root_path); $this->cmd_dir = realpath($root_path . '../'); if (!extension_loaded('inotify')) { \swoole_timer_after(1000, [$this, 'unUseInotify']); } else { $this->useInotify(); } } public function reload_queue() { return; $cmd = "/usr/bin/php {$this->cmd_dir}/think queue:restart"; secho('RELOAD queue', 'cmd: ' . $cmd); $output = @shell_exec($cmd); secho('RELOAD queue result', var_export($output, 1)); } public function useInotify() { global $monitor_files; // 初始化inotify句柄 $this->inotifyFd = inotify_init(); // 設置為非阻塞 stream_set_blocking($this->inotifyFd, 0); // 遞歸遍歷目錄里面的文件 $dir_iterator = new \RecursiveDirectoryIterator($this->monitor_dir); $iterator = new \RecursiveIteratorIterator($dir_iterator); foreach ($iterator as $file) { // 只監控php文件 if (pathinfo($file, PATHINFO_EXTENSION) != 'php') { continue; } // 把文件加入inotify監控,這里只監控了IN_MODIFY文件更新事件 $wd = inotify_add_watch($this->inotifyFd, $file, IN_MODIFY); $monitor_files[$wd] = $file; } // 監控inotify句柄可讀事件 \swoole_event_add($this->inotifyFd, function ($inotify_fd) { global $monitor_files; // 讀取有哪些文件事件 $events = inotify_read($inotify_fd); if ($events) { // 檢查哪些文件被更新了 foreach ($events as $ev) { // 更新的文件 if (!array_key_exists($ev['wd'], $monitor_files)) { continue; } $file = $monitor_files[$ev['wd']]; secho("RELOAD", $file . " update"); unset($monitor_files[$ev['wd']]); // 需要把文件重新加入監控 if (is_file($file)) { try { $wd = inotify_add_watch($inotify_fd, $file, IN_MODIFY); if (false != $wd) { $monitor_files[$wd] = $file; } } catch (\exception $e) { } } } $this->reload_queue(); $this->server->reload(); } }, null, SWOOLE_EVENT_READ); } public function unUseInotify() { secho("SYS", "非inotify模式,性能極低,不建議在正式環境啟用。請安裝inotify擴展"); if (isDarwin()) { secho("SYS", "mac開啟auto_reload可能會導致cpu占用過高。"); } \swoole_timer_tick(1, function () { global $last_mtime; // recursive traversal directory $dir_iterator = new \RecursiveDirectoryIterator($this->monitor_dir); $iterator = new \RecursiveIteratorIterator($dir_iterator); foreach ($iterator as $file) { // only check php files if (pathinfo($file, PATHINFO_EXTENSION) != 'php') { continue; } if (!isset($last_mtime)) { $last_mtime = $file->getMTime(); } // check mtime if ($last_mtime < $file->getMTime()) { secho("RELOAD", $file . " update, old_time:{$last_mtime}, new_time:" . $file->getMTime()); //reload $this->reload_queue(); $this->server->reload(); $last_mtime = $file->getMTime(); break; } } }); } } ~~~ 這個是參考easeswoole里的類。 ![](https://box.kancloud.cn/713c2401b8d512bded5cbe4ecee8a919_622x71.png) > 為了性能 最好 pecl install inotify 裝上擴展 為了調試業務重載也生效,把消息處理的一些單獨寫到一個控制器里了: ~~~ <?php namespace app\index\controller; use app\index\model\Room; use app\index\model\RoomUserCards; use app\index\model\RoomUsers; use app\index\model\User; use think\swoole\Server; class GuessMessage { public $from_fd; public $server; public $egg_nickname = ['houmuyu']; public $egg_name = '侯穆玉'; public function __construct($server, $from_fd) { $this->server = $server; $this->from_fd = $from_fd; } public function pretty_json($arr) { return json_encode($arr, JSON_UNESCAPED_UNICODE); } /** * 給客戶端發消息 * * @param string $msg * @param boolean $boardcast * @return boolean */ public function msg_out($msg, $boardcast = false) { if ($boardcast) { secho('msg_out', datetime() . ' boardcast'); foreach ($this->server->getClientList() as $fd) { $info = $this->server->getClientInfo($fd); secho('msg_out', sprintf('#%d 的待收消息為 %s', $fd, $msg)); if ($info !== false && isset($info['websocket_status'])) { $this->server->push($fd, $msg); } else { User::offline_fd($fd); secho('msg_out', sprintf('#%d客戶端離線', $fd)); } } return true; } else { $fd = $this->from_fd; secho('msg_out', datetime() . ' single'); secho('msg_out', sprintf('#%d 的待收消息為 %s', $fd, $msg)); $info = $this->server->getClientInfo($fd); if ($info !== false && isset($info['websocket_status'])) { return $this->server->push($fd, $msg); } else { secho('msg_out', sprintf('#%d客戶端離線', $fd)); User::offline_fd($fd); } return true; } } public function success($msg, $data = [], $boardcast = false) { $msg = $this->pretty_json(['code' => 1, 'msg' => $msg, 'data' => $data]); return $this->msg_out($msg, $boardcast); } public function error($msg, $data = [], $boardcast = false) { $msg = $this->pretty_json(['code' => 0, 'msg' => $msg, 'data' => $data]); return $this->msg_out($msg, $boardcast); } /** * 注冊用戶 * {"op":"reg_user", "nickname":"楊維杰"} * * @param object $server * @param object $frame * @param array $data * @return void */ public function reg_user($server, $frame, $data) { extract($data); if ($exist = User::where('nickname', $nickname)->find()) { if ($exist['online'] == 0) { $exist->online = 1; $exist->online_time = datetime(); $exist->fd = $frame->fd; $exist->save(); $this->success('after_reg_user', ['uid' => $exist['id'], 'fd'=>$frame->fd, 'nickname'=>$nickname]); } else { $this->error('nickname重復'); } } else { $ret = User::create([ 'nickname' => $nickname, 'fd' => $frame->fd, 'online_time' => datetime(), ]); $this->success('after_reg_user', ['uid' => $ret->id, 'fd'=>$frame->fd, 'nickname'=>$nickname]); } } /** * 創建房間 * {"op":"create_room", "name":"room1", "hours":1, "number":2, "uid":1} * * @param object $server * @param object $frame * @param array $data * @return void */ public function create_room($server, $frame, $data) { extract($data); $exist = Room::where('name', $name)->find(); if ($exist) { $this->error('房間名稱重復'); } else { $ret = Room::create([ 'name' => $name, 'last_hours' => $hours, ]); $room_id = $ret->id; RoomUsers::assign($room_id, $uid, $number); $this->success('after_create_room', ['room_id' => $room_id]); } } /** * 加入房間 * {"op":"join_game", "room_id":"1", "uid":1} * * @param object $server * @param object $frame * @param array $data * @return void */ public function join_game($server, $frame, $data) { extract($data); $exist = Room::get($room_id); if (!$exist) { $this->error('房間不存在'); } else { if (RoomUsers::left_human($room_id)) { $join = RoomUsers::join_human($room_id, $uid); if ($join) { $this->success(SUCCESS_MSG); } else { goto full; } } else { full: $this->error('房間已滿'); } } } /** * 開始 * {"op":"begin", "room_id":"1", "uid":1} * * @param object $server * @param object $frame * @param array $data * @return void */ public function begin($server, $frame, $data) { extract($data); $exist = Room::get($room_id); Room::where('id', $room_id)->update(['finish_time' => datetime("+{$exist->last_hours} hours")]); $compete_uid = RoomUsers::get_compete_uid($room_id, $uid); if ($compete_uid) { RoomUsers::where('room_id', $room_id) ->where('uid', 'in', [$uid, $compete_uid]) ->update(['gaming' => 1]); } else { $this->error('沒有可比賽的對手'); } $this->success('after_begin', [ 'user_name' => User::where('id', $uid)->value('nickname'), 'compete_name' => User::where('id', $compete_uid)->value('nickname'), 'compete_uid' => $compete_uid ]); } /** * 進入房間 * {"op":"enter_room", "room_id": 1} * * @param object $server * @param object $frame * @param array $data * @return void */ public function enter_room($server, $frame, $data){ extract($data); $count_down_cards = RoomUserCards::count_down_cards($data['room_id']); $this->success('after_enter_room', [ 'room_id' => $room_id, 'list' => RoomUsers::all(['room_id' => $room_id]), 'user_name' => User::where('id', $uid)->value('nickname'), 'compete_name' => User::where('id', $compete_uid)->value('nickname'), 'count_down_cards' => $count_down_cards, '石頭' => RoomUserCards::left_card('石頭', $room_id, $uid), '剪刀' => RoomUserCards::left_card('剪刀', $room_id, $uid), '布' => RoomUserCards::left_card('布', $room_id, $uid) ]); } /** * 測試任務 * {"op":"test_task"} * * @param object $server * @param object $frame * @param array $data * @return void */ public function test_task($server, $frame, $data) { extract($data); // $data['op'] = 'testin'; // $data['data'] = ['fd' => $this->from_fd]; // $server->task(Guess::encode($data)); $this->success('after_test_task2' . Guess::encode($data)); } public function testin_task($server, $task_id, $src_worker_id, $data) { $this->from_fd = $data['fd']; $this->success('in task ' . Guess::encode($data)); echo 'in task'; } /** * 猜 * {"op":"do_guess", "room_id":"1", "uid":1, "compete_uid": 3, "type": "石頭", "used": 1} * * @param object $server * @param object $frame * @param array $data * @return void */ public function do_guess($server, $frame, $data) { extract($data); $ret = RoomUserCards::create([ 'uid' => $uid, 'room_id' => $room_id, 'compete_uid' => $compete_uid, 'type' => $type, 'used' => 0, 'used_order' => RoomUserCards::where('room_id', $room_id)->where('uid', $uid)->where('used', 1)->count() + 1, 'use_time' => datetime(), ]); // 輪詢 // 數據上報 $data = $ret->toArray(); $this->judge($data); // 更新from 出牌記錄 // 更新對手出牌記錄 } public function judge($data) { echo print_r($data, true); $compaire_card = RoomUserCards::get_compare_cards($data); // ptrace('compaire_card'); // ptrace($compaire_card); if ($compaire_card) { $result = RoomUserCards::judge($data, $compaire_card); ptrace($result); switch ($result) { case 'win': $ret_1 = RoomUsers::win($data['room_id'], $data['uid'], $data['compete_uid']); break; case 'draw': $ret_1 = RoomUsers::draw($data['room_id'], $data['uid'], $data['compete_uid']); break; case 'lose': $ret_1 = RoomUsers::lose($data['room_id'], $data['uid'], $data['compete_uid']); break; default: break; } ptrace($ret_1); RoomUserCards::where('room_id', $data['room_id']) ->where('uid', $data['uid']) ->where('compete_uid', $data['compete_uid']) ->where('used_order', $data['used_order']) ->update(['used' => 1, 'use_time' => datetime()]); RoomUserCards::where('room_id', $compaire_card['room_id']) ->where('uid', $compaire_card['uid']) ->where('compete_uid', $compaire_card['compete_uid']) ->where('used_order', $compaire_card['used_order']) ->update(['used' => 1, 'use_time' => datetime()]); $left_cards = [ '石頭' => RoomUserCards::left_card('石頭', $data['room_id'], $data['uid']), '剪刀' => RoomUserCards::left_card('剪刀', $data['room_id'], $data['uid']), '布' => RoomUserCards::left_card('布', $data['room_id'], $data['uid']) ]; $this->success('after_do_guess', ['result' => $result,'compete_type'=>$compaire_card['type'], 'left_cards'=>$left_cards]); $count_down_cards = RoomUserCards::count_down_cards($data['room_id']); $this->success('room_user_list', [ 'room_id' => $data['room_id'], 'list' => RoomUsers::all(['room_id' => $data['room_id']]), 'count_down_cards' => $count_down_cards, ], true); if ($ret_1 == 'win') { $this->notify_win($data['uid'], $data['room_id']); $this->notify_lose($data['compete_uid'], $data['room_id']); $this->success('notify', ['info'=>sprintf('%s round %d %s贏了%s', datetime(), $data['room_id'], User::where('id', $data['uid'])->value('nickname'), User::where('id', $data['compete_uid'])->value('nickname'))], true); } elseif ($ret_1 == 'lose') { $this->success('notify', ['info'=>sprintf('%s round %d %s贏了%s', datetime(), $data['room_id'], User::where('id', $data['compete_uid'])->value('nickname'), User::where('id', $data['uid'])->value('nickname'))], true); $this->notify_lose($data['uid'], $data['room_id']); $this->notify_win($data['compete_uid'], $data['room_id']); } else { if($count_down_cards == ['石頭'=>0,'剪刀'=>0, '布'=>0]){ $this->notify_draw($data['uid'], $data['room_id']); $this->notify_draw($data['compete_uid'], $data['room_id']); $this->success('notify', ['info'=>sprintf('%s round %d %s和%s打平', datetime(), $data['room_id'], User::where('id', $data['uid'])->value('nickname'), User::where('id', $data['compete_uid'])->value('nickname'))], true); } } } else { // ptrace('else compaire_card'); // ptrace($compaire_card); // ptrace(User::where('id', $data['compete_uid'])->value('type')); // ptrace(false !== $compaire_card && User::where('id', $data['compete_uid'])->value('type') == 'ai'); if (false !== $compaire_card && User::where('id', $data['compete_uid'])->value('type') == 'ai') { $card = RoomUserCards::get_random_card($data['room_id'], $data['compete_uid']); // ptrace('random_user_cards'); // ptrace($card); if ($card) { $ret = RoomUserCards::create([ 'room_id' => $data['room_id'], 'uid' => $data['compete_uid'], 'compete_uid' => $data['uid'], 'type' => $card['type'], 'used' => 0, 'used_order' => $card['used_order'], 'use_time' => null, ]); secho('task', 'ai出牌后再次判斷'); $this->judge($data); } else { $this->error('獲取ai的下張牌失敗'); } }else{ if($compaire_card === false){ exception('對手已結束游戲'); $this->error('對手已經結束游戲'); } } } } public function notify_win($uid, $room_id) { $fd = User::where('id', $uid)->value('fd'); if ($fd) { $egg_name = in_array(User::where('id', $uid)->value('nickname'), $this->egg_nickname)?$this->egg_name:''; $ret = ['code' => 1, 'msg' => 'win', 'data'=>['info' => sprintf('You won at room %d, %s', $room_id, datetime()), 'room_id'=>$room_id, 'egg_name'=>$egg_name]]; $this->server->push($fd, $this->pretty_json($ret)); } } public function notify_draw($uid, $room_id){ $fd = User::where('id', $uid)->value('fd'); if ($fd) { $egg_name = in_array(User::where('id', $uid)->value('nickname'), $this->egg_nickname)?$this->egg_name:''; $ret = ['code' => 1, 'msg' => 'draw', 'data'=>['info' => sprintf('You draw at room %d, %s', $room_id, datetime()), 'room_id'=>$room_id, 'egg_name'=>$egg_name]]; $this->server->push($fd, $this->pretty_json($ret)); } } public function notify_lose($uid, $room_id) { $fd = User::where('id', $uid)->value('fd'); if ($fd) { $egg_name = in_array(User::where('id', $uid)->value('nickname'), $this->egg_nickname)?$this->egg_name:''; $ret = ['code' => 1, 'msg' => 'lose', 'data'=>['info' => sprintf('You lose at room %d, %s', $room_id, datetime()), 'room_id'=>$room_id, 'egg_name'=>$egg_name]]; $this->server->push($fd, $this->pretty_json($ret)); } } } ~~~ #### 整體架構 Guess類,實現onMessage、onClose 方法,onMessage里通過固定參數op來定位到GuessMessage類里的一個方法。 GuessageMessage 類里,先定義了 $server、$from_fd 用于以后方便回送消息給客戶端。然后構造方法里傳入。 輔助方法 pretty_json ~~~ public function pretty_json($arr) { return json_encode($arr, JSON_UNESCAPED_UNICODE); } ~~~ msg_out(實現了廣播): ~~~ /** * 給客戶端發消息 * * @param string $msg * @param boolean $boardcast * @return boolean */ public function msg_out($msg, $boardcast = false) { if ($boardcast) { secho('msg_out', datetime() . ' boardcast'); foreach ($this->server->getClientList() as $fd) { $info = $this->server->getClientInfo($fd); secho('msg_out', sprintf('#%d 的待收消息為 %s', $fd, $msg)); if ($info !== false && isset($info['websocket_status'])) { $this->server->push($fd, $msg); } else { User::offline_fd($fd); secho('msg_out', sprintf('#%d客戶端離線', $fd)); } } return true; } else { $fd = $this->from_fd; secho('msg_out', datetime() . ' single'); secho('msg_out', sprintf('#%d 的待收消息為 %s', $fd, $msg)); $info = $this->server->getClientInfo($fd); if ($info !== false && isset($info['websocket_status'])) { return $this->server->push($fd, $msg); } else { secho('msg_out', sprintf('#%d客戶端離線', $fd)); User::offline_fd($fd); } return true; } } ~~~ 覆寫了success 和error方法: ~~~ public function success($msg, $data = [], $boardcast = false) { $msg = $this->pretty_json(['code' => 1, 'msg' => $msg, 'data' => $data]); return $this->msg_out($msg, $boardcast); } public function error($msg, $data = [], $boardcast = false) { $msg = $this->pretty_json(['code' => 0, 'msg' => $msg, 'data' => $data]); return $this->msg_out($msg, $boardcast); } ~~~ 然后就是處理各種入消息和出消息。 以注冊用戶(登錄)為例: ~~~ /** * 注冊用戶 * {"op":"reg_user", "nickname":"楊維杰"} * * @param object $server * @param object $frame * @param array $data * @return void */ public function reg_user($server, $frame, $data) { extract($data); if ($exist = User::where('nickname', $nickname)->find()) { if ($exist['online'] == 0) { $exist->online = 1; $exist->online_time = datetime(); $exist->fd = $frame->fd; $exist->save(); $this->success('after_reg_user', ['uid' => $exist['id'], 'fd'=>$frame->fd, 'nickname'=>$nickname]); } else { $this->error('nickname重復'); } } else { $ret = User::create([ 'nickname' => $nickname, 'fd' => $frame->fd, 'online_time' => datetime(), ]); $this->success('after_reg_user', ['uid' => $ret->id, 'fd'=>$frame->fd, 'nickname'=>$nickname]); } } ~~~ 傳入的消息格式為json` {"op":"reg_user", "nickname":"楊維杰"}`,通過extract 來轉換為對應變量,省著賦值各種變量。通過對入消息的處理,判斷是否報錯還是成功返回消息。用戶離線了登錄后更新為在線狀態,否則一個客戶端在線了,不允許同名用戶再登錄。一個user只對應一個fd。 ##### 關于在線、離線 fd在斷線重連后會自增,只有正確的fd才能正常發消息通知成功。為此websocket 必須自己實現一個登錄消息,來綁定user 和fd 的關系。然后onClose和發消息時檢測斷開鏈接里清空綁定。 User模型里清空某個fd的方法: ~~~ public static function offline_fd($fd) { return self::where('fd', $fd)->update(['fd' => 0, 'online' => 0, 'offline_time' => datetime()]); } ~~~ ###### onClose ~~~ public function onClose($server, $fd) { User::offline_fd($fd); echo "client {$fd} closed {$time}\n"; } ~~~ ###### 發消息檢測離線后清空fd ~~~ if ($info !== false && isset($info['websocket_status'])) { $this->server->push($fd, $msg); } else { User::offline_fd($fd); secho('msg_out', sprintf('#%d客戶端離線', $fd)); } ~~~ ###### ctrl+c 無法觸發close 的處理 ~~~ /** * 架構函數 * @access public */ public function init() { $pid = $this->getMasterPid(); if (!$this->isRunning($pid)) { User::offline(0); ~~~ init 沒啟動進程分支里,直接將所有fd置空,因為進入這里說明重啟服務了fd全失效了。 ##### 游戲流程 ![](https://box.kancloud.cn/1337d053f8130bfcaad14471ff28048e_864x784.png) ##### 核心算法 ###### 創建房間后的自動分配玩家 在 create_room 方法里: `RoomUsers::assign($room_id, $uid, $number);` 我們看RoomUsers 里的assign 方法: ~~~ public static function assign($room_id, $uid, $number) { if ($number < 2) { exception("房間里至少2個人"); } self::create([ 'uid' => $uid, 'type' => 'human', 'room_id' => $room_id, 'stars' => 3, ]); $loop_num = $number - 1; $faker = Factory::create('zh_CN'); for ($i = 0; $i < $loop_num; $i++) { $nickname = $faker->name; $uid = User::where('nickname', $nickname)->value('id'); if (!$uid) { $ret = User::create([ 'nickname' => $nickname, 'fd' => 0, 'type' => 'ai', 'online_time' => datetime(), ]); $uid = $ret->id; } self::create([ 'room_id' => $room_id, 'uid' => $uid, 'type' => 'ai', 'stars' => 3, ]); } return true; } ~~~ 其實就是先創建自己在這個房間里的用戶,然后隨機生成昵稱,看用戶表里是否有,沒有就創建用戶記錄(type=ai 表示機器人)后拿到新的uid后 插入RoomUsers表。 begin里找到可用戶的玩家后,將兩個玩家RoomUser里gaming 狀態標記為1。 后面預留了join_room 方法,同于替換ai為真人出牌。 ###### 出牌后的自動出牌算法 ~~~ /** * 猜 * {"op":"do_guess", "room_id":"1", "uid":1, "compete_uid": 3, "type": "石頭", "used": 1} * * @param object $server * @param object $frame * @param array $data * @return void */ public function do_guess($server, $frame, $data) { extract($data); $ret = RoomUserCards::create([ 'uid' => $uid, 'room_id' => $room_id, 'compete_uid' => $compete_uid, 'type' => $type, 'used' => 0, 'used_order' => RoomUserCards::where('room_id', $room_id)->where('uid', $uid)->where('used', 1)->count() + 1, 'use_time' => datetime(), ]); // 輪詢 // 數據上報 $data = $ret->toArray(); $this->judge($data); // 更新from 出牌記錄 // 更新對手出牌記錄 } public function judge($data) { echo print_r($data, true); $compaire_card = RoomUserCards::get_compare_cards($data); // ptrace('compaire_card'); // ptrace($compaire_card); if ($compaire_card) { $result = RoomUserCards::judge($data, $compaire_card); ptrace($result); switch ($result) { case 'win': $ret_1 = RoomUsers::win($data['room_id'], $data['uid'], $data['compete_uid']); break; case 'draw': $ret_1 = RoomUsers::draw($data['room_id'], $data['uid'], $data['compete_uid']); break; case 'lose': $ret_1 = RoomUsers::lose($data['room_id'], $data['uid'], $data['compete_uid']); break; default: break; } ptrace($ret_1); RoomUserCards::where('room_id', $data['room_id']) ->where('uid', $data['uid']) ->where('compete_uid', $data['compete_uid']) ->where('used_order', $data['used_order']) ->update(['used' => 1, 'use_time' => datetime()]); RoomUserCards::where('room_id', $compaire_card['room_id']) ->where('uid', $compaire_card['uid']) ->where('compete_uid', $compaire_card['compete_uid']) ->where('used_order', $compaire_card['used_order']) ->update(['used' => 1, 'use_time' => datetime()]); $left_cards = [ '石頭' => RoomUserCards::left_card('石頭', $data['room_id'], $data['uid']), '剪刀' => RoomUserCards::left_card('剪刀', $data['room_id'], $data['uid']), '布' => RoomUserCards::left_card('布', $data['room_id'], $data['uid']) ]; $this->success('after_do_guess', ['result' => $result,'compete_type'=>$compaire_card['type'], 'left_cards'=>$left_cards]); $count_down_cards = RoomUserCards::count_down_cards($data['room_id']); $this->success('room_user_list', [ 'room_id' => $data['room_id'], 'list' => RoomUsers::all(['room_id' => $data['room_id']]), 'count_down_cards' => $count_down_cards, ], true); if ($ret_1 == 'win') { $this->notify_win($data['uid'], $data['room_id']); $this->notify_lose($data['compete_uid'], $data['room_id']); $this->success('notify', ['info'=>sprintf('%s round %d %s贏了%s', datetime(), $data['room_id'], User::where('id', $data['uid'])->value('nickname'), User::where('id', $data['compete_uid'])->value('nickname'))], true); } elseif ($ret_1 == 'lose') { $this->success('notify', ['info'=>sprintf('%s round %d %s贏了%s', datetime(), $data['room_id'], User::where('id', $data['compete_uid'])->value('nickname'), User::where('id', $data['uid'])->value('nickname'))], true); $this->notify_lose($data['uid'], $data['room_id']); $this->notify_win($data['compete_uid'], $data['room_id']); } else { if($count_down_cards == ['石頭'=>0,'剪刀'=>0, '布'=>0]){ $this->notify_draw($data['uid'], $data['room_id']); $this->notify_draw($data['compete_uid'], $data['room_id']); $this->success('notify', ['info'=>sprintf('%s round %d %s和%s打平', datetime(), $data['room_id'], User::where('id', $data['uid'])->value('nickname'), User::where('id', $data['compete_uid'])->value('nickname'))], true); } } } else { // ptrace('else compaire_card'); // ptrace($compaire_card); // ptrace(User::where('id', $data['compete_uid'])->value('type')); // ptrace(false !== $compaire_card && User::where('id', $data['compete_uid'])->value('type') == 'ai'); if (false !== $compaire_card && User::where('id', $data['compete_uid'])->value('type') == 'ai') { $card = RoomUserCards::get_random_card($data['room_id'], $data['compete_uid']); // ptrace('random_user_cards'); // ptrace($card); if ($card) { $ret = RoomUserCards::create([ 'room_id' => $data['room_id'], 'uid' => $data['compete_uid'], 'compete_uid' => $data['uid'], 'type' => $card['type'], 'used' => 0, 'used_order' => $card['used_order'], 'use_time' => null, ]); secho('task', 'ai出牌后再次判斷'); $this->judge($data); } else { $this->error('獲取ai的下張牌失敗'); } }else{ if($compaire_card === false){ exception('對手已結束游戲'); $this->error('對手已經結束游戲'); } } } } ~~~ **do_guess** 里就是用戶選一張牌后,我先記錄下來,主要邏輯在**judge**里。 首先獲取對手的牌,就是找對手牌庫里維使用的牌。獲取不到的話,判斷對手是ai 的話,從所有12張牌里排除已出過的牌,隨機取一張。 1. 獲取對手的下張牌 RoomUserCards::get_compare_cards ~~~ public static function get_compare_cards($data) { $compete_user = RoomUsers::where('uid', $data['compete_uid'])->where('room_id', $data['room_id'])->find(); if ($compete_user['status'] != RoomUsers::$UNKNOWN) { return false; } $card = self::where('compete_uid', $data['uid']) ->where('room_id', $data['room_id']) ->where('uid', $data['compete_uid']) ->where('used', 0) ->order('used_order DESC') ->find() ?: []; return $card; } ~~~ 比賽結束就返回false,找不到返回[] 2. 獲取ai機器人的隨機未用牌 RoomUserCard::get_random_card ~~~ public static function get_random_card($room_id, $uid) { $all = ['石頭', '剪刀', '布', '石頭', '剪刀', '布', '石頭', '剪刀', '布', '石頭', '剪刀', '布']; $used_cards = self::where('room_id', $room_id)->where('uid', $uid)->where('used', 1)->column('type') ?: []; // ptrace('used_cards'); // ptrace($used_cards); $left_cards = $all; if ($used_cards) { foreach ($used_cards as $card) { $index = array_search($card, $left_cards); if ($index !== false) { unset($left_cards[$index]); } } } // ptrace('left_cards'); // ptrace($left_cards); if ($left_cards) { $next_order = self::where('room_id', $room_id)->where('uid', $uid)->where('used', 1)->count() + 1; shuffle($left_cards); // ptrace('after_shuffle'); // ptrace($left_cards); return array_merge([ 'type' => $left_cards[0], 'room_id' => $room_id, ], ['used_order' => $next_order]); } return []; } ~~~ 然后judge里 判斷兩張牌的輸贏,后將兩個牌標記為已使用,并根據情況轉移star `$result = RoomUserCards::judge($data, $compaire_card);` 每次判定結束后,再判斷同房間內用戶的輸贏,看某個人的stars是否為0 ,或者牌全出完了判斷是否平局。 發現有勝負后,通知原房間和比賽的人勝負消息。 也就 notify_draw、notify_win 和notify_lose 三個方法。 當然每次判定后,返回最新的房間內卡牌剩余計數。 具體見源碼。 至此后端猜拳消息全部結束。 ##### onMessage 里的事務 和不使用task的原因 開始測試的時候由于代碼錯誤多,總是出現臟數據,干脆寫了try catch db 事務,出問題全部回滾。 開始judge 部分是通過task 實現的,但是task通信是跨進程的,from_fd 獲取不到。只能同過產生task時傳到數據里。 且無法在task出問題的時候 回滾事務。所以舍棄了。順便說一下 task的返回在finish里可以接收到。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看