From f5dbbf936b06e024abc3e1c84dcb829fe721006a Mon Sep 17 00:00:00 2001 From: u2nyakim Date: Fri, 29 Aug 2025 17:49:56 +0800 Subject: [PATCH] =?UTF-8?q?up.=20=E6=B7=BB=E5=8A=A0gateway=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/{Crontab.php => SysCrontab.php} | 2 +- app/command/admin/SysGateway.php | 94 ++ config/console.php | 3 +- extend/.gitignore | 2 - extend/GatewayClient/.gitignore | 3 + extend/GatewayClient/Context.php | 115 ++ extend/GatewayClient/Gateway.php | 1400 ++++++++++++++++ extend/GatewayClient/GatewayProtocol.php | 187 +++ extend/GatewayClient/MIT-LICENSE.txt | 21 + extend/GatewayClient/README.md | 91 + extend/GatewayClient/composer.json | 11 + extend/GatewayWorker/BusinessWorker.php | 522 ++++++ extend/GatewayWorker/Gateway.php | 1216 ++++++++++++++ extend/GatewayWorker/Lib/Context.php | 136 ++ extend/GatewayWorker/Lib/Gateway.php | 1459 +++++++++++++++++ .../Protocols/GatewayProtocol.php | 228 +++ extend/GatewayWorker/Register.php | 194 +++ 17 files changed, 5680 insertions(+), 4 deletions(-) rename app/command/admin/{Crontab.php => SysCrontab.php} (98%) create mode 100644 app/command/admin/SysGateway.php delete mode 100644 extend/.gitignore create mode 100644 extend/GatewayClient/.gitignore create mode 100644 extend/GatewayClient/Context.php create mode 100644 extend/GatewayClient/Gateway.php create mode 100644 extend/GatewayClient/GatewayProtocol.php create mode 100644 extend/GatewayClient/MIT-LICENSE.txt create mode 100644 extend/GatewayClient/README.md create mode 100644 extend/GatewayClient/composer.json create mode 100644 extend/GatewayWorker/BusinessWorker.php create mode 100644 extend/GatewayWorker/Gateway.php create mode 100644 extend/GatewayWorker/Lib/Context.php create mode 100644 extend/GatewayWorker/Lib/Gateway.php create mode 100644 extend/GatewayWorker/Protocols/GatewayProtocol.php create mode 100644 extend/GatewayWorker/Register.php diff --git a/app/command/admin/Crontab.php b/app/command/admin/SysCrontab.php similarity index 98% rename from app/command/admin/Crontab.php rename to app/command/admin/SysCrontab.php index 3568486..9e1f37e 100644 --- a/app/command/admin/Crontab.php +++ b/app/command/admin/SysCrontab.php @@ -13,7 +13,7 @@ use think\facade\Console; /** * 定时任务 */ -class Crontab extends Command +class SysCrontab extends Command { protected function configure() { diff --git a/app/command/admin/SysGateway.php b/app/command/admin/SysGateway.php new file mode 100644 index 0000000..b0ce29c --- /dev/null +++ b/app/command/admin/SysGateway.php @@ -0,0 +1,94 @@ +setName('admin:gateway') + ->addArgument('action', Argument::OPTIONAL, "start|stop|restart|reload|status|connections", 'start') + ->addOption('mode', 'm', Option::VALUE_OPTIONAL, 'Run the workerman server in daemon mode.') + ->setDescription('后台系统网关服务'); + } + + protected function execute(Input $input, Output $output) + { + $action = $input->getArgument('action'); + $mode = $input->getOption('mode'); + global $argv; + $argv = []; + array_unshift($argv, 'think', $action); + if ($mode == 'd') { + $argv[] = '-d'; + } else if ($mode == 'g') { + $argv[] = '-g'; + } + + + $register = $this->startRegister(); + $gateway = $this->startGateway(); + + $worker = new BusinessWorker(); + $worker->name = 'ChatBusinessWorker'; + $worker->count = 4; + $worker->registerAddress = '127.0.0.1:1236'; + + // 运行所有服务 + Worker::runAll(); + } + + + private function startRegister(): Register + { + return new Register('text://127.0.0.1:1236'); + } + private function startGateway(): \GatewayWorker\Gateway + { +// gateway 进程 + $gateway = new \GatewayWorker\Gateway("Websocket://0.0.0.0:7272"); +// 设置名称,方便status时查看 + $gateway->name = 'ChatGateway'; +// 设置进程数,一般两个进程就足够 + $gateway->count = 2; +// 分布式部署时请设置成内网ip(非127.0.0.1) + $gateway->lanIp = '127.0.0.1'; +// 内部通讯起始端口。假如$gateway->count=2,起始端口为2300 +// 则一般会使用2300 2301 2个端口作为内部通讯端口 + $gateway->startPort = 2300; +// 心跳间隔 + $gateway->pingInterval = 10; +// 心跳数据 + $gateway->pingData = '{"type":"ping"}'; +// 服务注册地址 + $gateway->registerAddress = '127.0.0.1:1236'; + + /* + // 当客户端连接上来时,设置连接的onWebSocketConnect,即在websocket握手时的回调 + $gateway->onConnect = function($connection) + { + $connection->onWebSocketConnect = function($connection , $http_header) + { + // 可以在这里判断连接来源是否合法,不合法就关掉连接 + // $_SERVER['HTTP_ORIGIN']标识来自哪个站点的页面发起的websocket链接 + if($_SERVER['HTTP_ORIGIN'] != 'http://chat.workerman.net') + { + $connection->close(); + } + // onWebSocketConnect 里面$_GET $_SERVER是可用的 + // var_dump($_GET, $_SERVER); + }; + }; + */ + + return $gateway; + } +} \ No newline at end of file diff --git a/config/console.php b/config/console.php index ca87d9c..17b8326 100644 --- a/config/console.php +++ b/config/console.php @@ -5,7 +5,8 @@ return [ // 指令定义 'commands' => [ - \app\command\admin\Crontab::class, + \app\command\admin\SysCrontab::class, + \app\command\admin\SysGateway::class, \app\command\admin\Worker::class, ], ]; diff --git a/extend/.gitignore b/extend/.gitignore deleted file mode 100644 index c96a04f..0000000 --- a/extend/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/extend/GatewayClient/.gitignore b/extend/GatewayClient/.gitignore new file mode 100644 index 0000000..ec383f1 --- /dev/null +++ b/extend/GatewayClient/.gitignore @@ -0,0 +1,3 @@ +/vendor +/.vscode +/.idea \ No newline at end of file diff --git a/extend/GatewayClient/Context.php b/extend/GatewayClient/Context.php new file mode 100644 index 0000000..e281422 --- /dev/null +++ b/extend/GatewayClient/Context.php @@ -0,0 +1,115 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +/** + * 上下文 包含当前用户uid, 内部通信local_ip local_port socket_id ,以及客户端client_ip client_port + */ +class Context +{ + /** + * 内部通讯id + * @var string + */ + public static $local_ip; + /** + * 内部通讯端口 + * @var int + */ + public static $local_port; + /** + * 客户端ip + * @var string + */ + public static $client_ip; + /** + * 客户端端口 + * @var int + */ + public static $client_port; + /** + * client_id + * @var string + */ + public static $client_id; + /** + * 连接connection->id + * @var int + */ + public static $connection_id; + + /** + * 旧的session + * + * @var string + */ + public static $old_session; + + /** + * 编码session + * @param mixed $session_data + * @return string + */ + public static function sessionEncode($session_data = '') + { + if ($session_data !== '') { + return serialize($session_data); + } + + return ''; + } + + /** + * 解码session + * @param string $session_buffer + * @return mixed + */ + public static function sessionDecode($session_buffer) + { + return unserialize($session_buffer); + } + + /** + * 清除上下文 + * @return void + */ + public static function clear() + { + static::$local_ip = static::$local_port = static::$client_ip = static::$client_port = + static::$client_id = static::$connection_id = static::$old_session = null; + } + + /** + * 通讯地址到client_id的转换 + * @return string + */ + public static function addressToClientId($local_ip, $local_port, $connection_id) + { + return bin2hex(pack('NnN', $local_ip, $local_port, $connection_id)); + } + + /** + * client_id到通讯地址的转换 + * @return array + */ + public static function clientIdToAddress($client_id) + { + if (strlen($client_id) !== 20) { + throw new \Exception("client_id $client_id is invalid"); + } + return unpack('Nlocal_ip/nlocal_port/Nconnection_id' ,pack('H*', $client_id)); + } + +} \ No newline at end of file diff --git a/extend/GatewayClient/Gateway.php b/extend/GatewayClient/Gateway.php new file mode 100644 index 0000000..99cf8af --- /dev/null +++ b/extend/GatewayClient/Gateway.php @@ -0,0 +1,1400 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +if (! class_exists(\Composer\Autoload\ClassLoader::class)) { + require_once __DIR__ . '/Context.php'; + require_once __DIR__ . '/GatewayProtocol.php'; +} + +/** + * 数据发送相关 + */ +class Gateway +{ + /** + * gateway 实例 + * + * @var object + */ + protected static $businessWorker = null; + + /** + * 注册中心地址 + * + * @var string|array + */ + public static $registerAddress = '127.0.0.1:1236'; + + /** + * 秘钥 + * @var string + */ + public static $secretKey = ''; + + /** + * 链接超时时间 + * @var int + */ + public static $connectTimeout = 3; + + /** + * 与Gateway是否是长链接 + * @var bool + */ + public static $persistentConnection = false; + + /** + * 是否清除注册地址缓存 + * @var bool + */ + public static $addressesCacheDisable = false; + + /** + * 向所有客户端连接(或者 client_id_array 指定的客户端连接)广播消息 + * + * @param string $message 向客户端发送的消息 + * @param array $client_id_array 客户端 id 数组 + * @param array $exclude_client_id 不给这些client_id发 + * @param bool $raw 是否发送原始数据(即不调用gateway的协议的encode方法) + * @return void + * @throws Exception + */ + public static function sendToAll($message, $client_id_array = null, $exclude_client_id = null, $raw = false) + { + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_SEND_TO_ALL; + $gateway_data['body'] = $message; + if ($raw) { + $gateway_data['flag'] |= GatewayProtocol::FLAG_NOT_CALL_ENCODE; + } + + if ($exclude_client_id) { + if (!is_array($exclude_client_id)) { + $exclude_client_id = array($exclude_client_id); + } + if ($client_id_array) { + $exclude_client_id = array_flip($exclude_client_id); + } + } + + if ($client_id_array) { + if (!is_array($client_id_array)) { + echo new \Exception('bad $client_id_array:'.var_export($client_id_array, true)); + return; + } + $data_array = array(); + foreach ($client_id_array as $client_id) { + if (isset($exclude_client_id[$client_id])) { + continue; + } + $address = Context::clientIdToAddress($client_id); + if ($address) { + $key = long2ip($address['local_ip']) . ":{$address['local_port']}"; + $data_array[$key][$address['connection_id']] = $address['connection_id']; + } + } + foreach ($data_array as $addr => $connection_id_list) { + $the_gateway_data = $gateway_data; + $the_gateway_data['ext_data'] = json_encode(array('connections' => $connection_id_list)); + static::sendToGateway($addr, $the_gateway_data); + } + return; + } elseif (empty($client_id_array) && is_array($client_id_array)) { + return; + } + + if (!$exclude_client_id) { + return static::sendToAllGateway($gateway_data); + } + + $address_connection_array = static::clientIdArrayToAddressArray($exclude_client_id); + + // 如果有businessWorker实例,说明运行在workerman环境中,通过businessWorker中的长连接发送数据 + if (static::$businessWorker) { + foreach (static::$businessWorker->gatewayConnections as $address => $gateway_connection) { + $gateway_data['ext_data'] = isset($address_connection_array[$address]) ? + json_encode(array('exclude'=> $address_connection_array[$address])) : ''; + /** @var TcpConnection $gateway_connection */ + $gateway_connection->send($gateway_data); + } + } // 运行在其它环境中,通过注册中心得到gateway地址 + else { + $all_addresses = static::getAllGatewayAddressesFromRegister(); + foreach ($all_addresses as $address) { + $gateway_data['ext_data'] = isset($address_connection_array[$address]) ? + json_encode(array('exclude'=> $address_connection_array[$address])) : ''; + static::sendToGateway($address, $gateway_data); + } + } + + } + + /** + * 向某个client_id对应的连接发消息 + * + * @param string $client_id + * @param string $message + * @return void + */ + public static function sendToClient($client_id, $message) + { + return static::sendCmdAndMessageToClient($client_id, GatewayProtocol::CMD_SEND_TO_ONE, $message); + } + + /** + * 判断某个uid是否在线 + * + * @param string $uid + * @return int 0|1 + */ + public static function isUidOnline($uid) + { + return (int)static::getClientIdByUid($uid); + } + + public static function isUidsOnline($uids) + { + return array_map(function ($item) { + return (int) $item; + }, static::batchGetClientIdByUid($uids)); + } + + /** + * 判断client_id对应的连接是否在线 + * + * @param string $client_id + * @return int 0|1 + */ + public static function isOnline($client_id) + { + $address_data = Context::clientIdToAddress($client_id); + if (!$address_data) { + return 0; + } + $address = long2ip($address_data['local_ip']) . ":{$address_data['local_port']}"; + if (isset(static::$businessWorker)) { + if (!isset(static::$businessWorker->gatewayConnections[$address])) { + return 0; + } + } + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_IS_ONLINE; + $gateway_data['connection_id'] = $address_data['connection_id']; + return (int)static::sendAndRecv($address, $gateway_data); + } + + /** + * 获取所有在线用户的session,client_id为 key(弃用,请用getAllClientSessions代替) + * + * @param string $group + * @return array + */ + public static function getAllClientInfo($group = '') + { + echo "Warning: Gateway::getAllClientInfo is deprecated and will be removed in a future, please use Gateway::getAllClientSessions instead."; + return static::getAllClientSessions($group); + } + + /** + * 获取所有在线client_id的session,client_id为 key + * + * @param string $group + * @return array + */ + public static function getAllClientSessions($group = '') + { + $gateway_data = GatewayProtocol::$empty; + if (!$group) { + $gateway_data['cmd'] = GatewayProtocol::CMD_GET_ALL_CLIENT_SESSIONS; + } else { + $gateway_data['cmd'] = GatewayProtocol::CMD_GET_CLIENT_SESSIONS_BY_GROUP; + $gateway_data['ext_data'] = $group; + } + $status_data = array(); + $all_buffer_array = static::getBufferFromAllGateway($gateway_data); + foreach ($all_buffer_array as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $data) { + if ($data) { + foreach ($data as $connection_id => $session_buffer) { + $client_id = Context::addressToClientId($local_ip, $local_port, $connection_id); + if ($client_id === Context::$client_id) { + $status_data[$client_id] = (array)$_SESSION; + } else { + $status_data[$client_id] = $session_buffer ? Context::sessionDecode($session_buffer) : array(); + } + } + } + } + } + return $status_data; + } + + /** + * 获取某个组的连接信息(弃用,请用getClientSessionsByGroup代替) + * + * @param string $group + * @return array + */ + public static function getClientInfoByGroup($group) + { + echo "Warning: Gateway::getClientInfoByGroup is deprecated and will be removed in a future, please use Gateway::getClientSessionsByGroup instead."; + return static::getAllClientSessions($group); + } + + /** + * 获取某个组的所有client_id的session信息 + * + * @param string $group + * + * @return array + */ + public static function getClientSessionsByGroup($group) + { + if (static::isValidGroupId($group)) { + return static::getAllClientSessions($group); + } + return array(); + } + + /** + * 获取所有在线client_id数 + * + * @return int + */ + public static function getAllClientIdCount() + { + return static::getClientCountByGroup(); + } + + /** + * 获取所有在线client_id数(getAllClientIdCount的别名) + * + * @return int + */ + public static function getAllClientCount() + { + return static::getAllClientIdCount(); + } + + /** + * 获取某个组的在线client_id数 + * + * @param string $group + * @return int + */ + public static function getClientIdCountByGroup($group = '') + { + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_GET_CLIENT_COUNT_BY_GROUP; + $gateway_data['ext_data'] = $group; + $total_count = 0; + $all_buffer_array = static::getBufferFromAllGateway($gateway_data); + foreach ($all_buffer_array as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $count) { + if ($count) { + $total_count += $count; + } + } + } + return $total_count; + } + + /** + * getClientIdCountByGroup 函数的别名 + * + * @param string $group + * @return int + */ + public static function getClientCountByGroup($group = '') + { + return static::getClientIdCountByGroup($group); + } + + /** + * 批量获取群组ID内客户端个数. + */ + public static function batchGetClientIdCountByGroup(array $groups): array + { + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_BATCH_GET_CLIENT_COUNT_BY_GROUP; + $gateway_data['ext_data'] = json_encode($groups); + $all_buffer_array = static::getBufferFromAllGateway($gateway_data); + + $return = []; + foreach ($all_buffer_array as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $data) { + foreach ($data as $group => $count) { + if (! isset($return[$group])) { + $return[$group] = 0; + } + $return[$group] += $count; + } + } + } + return $return; + } + + /** + * 获取某个群组在线client_id列表 + * + * @param string $group + * @return array + */ + public static function getClientIdListByGroup($group) + { + if (!static::isValidGroupId($group)) { + return array(); + } + + $data = static::select(array('uid'), array('groups' => is_array($group) ? $group : array($group))); + $client_id_map = array(); + foreach ($data as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $items) { + //$items = ['connection_id'=>['uid'=>x, 'group'=>[x,x..], 'session'=>[..]], 'client_id'=>[..], ..]; + foreach ($items as $connection_id => $info) { + $client_id = Context::addressToClientId($local_ip, $local_port, $connection_id); + $client_id_map[$client_id] = $client_id; + } + } + } + return $client_id_map; + } + + /** + * 获取集群所有在线client_id列表 + * + * @return array + */ + public static function getAllClientIdList() + { + return static::formatClientIdFromGatewayBuffer(static::select(array('uid'))); + } + + /** + * 格式化client_id + * + * @param $data + * @return array + */ + protected static function formatClientIdFromGatewayBuffer($data) + { + $client_id_list = array(); + foreach ($data as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $items) { + //$items = ['connection_id'=>['uid'=>x, 'group'=>[x,x..], 'session'=>[..]], 'client_id'=>[..], ..]; + foreach ($items as $connection_id => $info) { + $client_id = Context::addressToClientId($local_ip, $local_port, $connection_id); + $client_id_list[$client_id] = $client_id; + } + } + } + return $client_id_list; + } + + + /** + * 获取与 uid 绑定的 client_id 列表 + * + * @param string $uid + * @return array + */ + public static function getClientIdByUid($uid) + { + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_GET_CLIENT_ID_BY_UID; + $gateway_data['ext_data'] = $uid; + $client_list = array(); + $all_buffer_array = static::getBufferFromAllGateway($gateway_data); + foreach ($all_buffer_array as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $connection_id_array) { + if ($connection_id_array) { + foreach ($connection_id_array as $connection_id) { + $client_list[] = Context::addressToClientId($local_ip, $local_port, $connection_id); + } + } + } + } + return $client_list; + } + + /** + * 批量获取与 uid 绑定的 client_id 列表 + * + * @param array $uids + * @return array + */ + public static function batchGetClientIdByUid($uids) + { + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_BATCH_GET_CLIENT_ID_BY_UID; + $gateway_data['ext_data'] = json_encode($uids); + $client_list = []; + $all_buffer_array = static::getBufferFromAllGateway($gateway_data); + foreach ($all_buffer_array as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $uid_connection_id_array) { + if ($uid_connection_id_array) { + foreach ($uid_connection_id_array as $uid => $connection_ids) { + if (! isset($client_list[$uid])) { + $client_list[$uid] = []; + } + foreach ($connection_ids as $connection_id) { + $client_list[$uid][] = Context::addressToClientId($local_ip, $local_port, $connection_id); + } + } + } + } + } + return $client_list; + } + + /** + * 获取某个群组在线uid列表 + * + * @param string $group + * @return array + */ + public static function getUidListByGroup($group) + { + if (!static::isValidGroupId($group)) { + return array(); + } + + $group = is_array($group) ? $group : array($group); + $data = static::select(array('uid'), array('groups' => $group)); + $uid_map = array(); + foreach ($data as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $items) { + //$items = ['connection_id'=>['uid'=>x, 'group'=>[x,x..], 'session'=>[..]], 'client_id'=>[..], ..]; + foreach ($items as $connection_id => $info) { + if (!empty($info['uid'])) { + $uid_map[$info['uid']] = $info['uid']; + } + } + } + } + return $uid_map; + } + + /** + * 获取某个群组在线uid数 + * + * @param string $group + * @return int + */ + public static function getUidCountByGroup($group) + { + if (static::isValidGroupId($group)) { + return count(static::getUidListByGroup($group)); + } + return 0; + } + + /** + * 获取全局在线uid列表 + * + * @return array + */ + public static function getAllUidList() + { + $data = static::select(array('uid')); + $uid_map = array(); + foreach ($data as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $items) { + //$items = ['connection_id'=>['uid'=>x, 'group'=>[x,x..], 'session'=>[..]], 'client_id'=>[..], ..]; + foreach ($items as $connection_id => $info) { + if (!empty($info['uid'])) { + $uid_map[$info['uid']] = $info['uid']; + } + } + } + } + return $uid_map; + } + + /** + * 获取全局在线uid数 + * @return int + */ + public static function getAllUidCount() + { + return count(static::getAllUidList()); + } + + /** + * 通过client_id获取uid + * + * @param $client_id + * @return mixed + */ + public static function getUidByClientId($client_id) + { + $data = static::select(array('uid'), array('client_id'=>array($client_id))); + foreach ($data as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $items) { + //$items = ['connection_id'=>['uid'=>x, 'group'=>[x,x..], 'session'=>[..]], 'client_id'=>[..], ..]; + foreach ($items as $info) { + return $info['uid']; + } + } + } + } + + /** + * 获取所有在线的群组id + * + * @return array + */ + public static function getAllGroupIdList() + { + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_GET_GROUP_ID_LIST; + $group_id_list = array(); + $all_buffer_array = static::getBufferFromAllGateway($gateway_data); + foreach ($all_buffer_array as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $group_id_array) { + if (is_array($group_id_array)) { + foreach ($group_id_array as $group_id) { + if (!isset($group_id_list[$group_id])) { + $group_id_list[$group_id] = $group_id; + } + } + } + } + } + return $group_id_list; + } + + + /** + * 获取所有在线分组的uid数量,也就是每个分组的在线用户数 + * + * @return array + */ + public static function getAllGroupUidCount() + { + $group_uid_map = static::getAllGroupUidList(); + $group_uid_count_map = array(); + foreach ($group_uid_map as $group_id => $uid_list) { + $group_uid_count_map[$group_id] = count($uid_list); + } + return $group_uid_count_map; + } + + + + /** + * 获取所有分组uid在线列表 + * + * @return array + */ + public static function getAllGroupUidList() + { + $data = static::select(array('uid','groups')); + $group_uid_map = array(); + foreach ($data as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $items) { + //$items = ['connection_id'=>['uid'=>x, 'group'=>[x,x..], 'session'=>[..]], 'client_id'=>[..], ..]; + foreach ($items as $connection_id => $info) { + if (empty($info['uid']) || empty($info['groups'])) { + break; + } + $uid = $info['uid']; + foreach ($info['groups'] as $group_id) { + if(!isset($group_uid_map[$group_id])) { + $group_uid_map[$group_id] = array(); + } + $group_uid_map[$group_id][$uid] = $uid; + } + } + } + } + return $group_uid_map; + } + + /** + * 获取所有群组在线client_id列表 + * + * @return array + */ + public static function getAllGroupClientIdList() + { + $data = static::select(array('groups')); + $group_client_id_map = array(); + foreach ($data as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $items) { + //$items = ['connection_id'=>['uid'=>x, 'group'=>[x,x..], 'session'=>[..]], 'client_id'=>[..], ..]; + foreach ($items as $connection_id => $info) { + if (empty($info['groups'])) { + break; + } + $client_id = Context::addressToClientId($local_ip, $local_port, $connection_id); + foreach ($info['groups'] as $group_id) { + if(!isset($group_client_id_map[$group_id])) { + $group_client_id_map[$group_id] = array(); + } + $group_client_id_map[$group_id][$client_id] = $client_id; + } + } + } + } + return $group_client_id_map; + } + + /** + * 获取所有群组在线client_id数量,也就是获取每个群组在线连接数 + * + * @return array + */ + public static function getAllGroupClientIdCount() + { + $group_client_map = static::getAllGroupClientIdList(); + $group_client_count_map = array(); + foreach ($group_client_map as $group_id => $client_id_list) { + $group_client_count_map[$group_id] = count($client_id_list); + } + return $group_client_count_map; + } + + + /** + * 根据条件到gateway搜索数据 + * + * @param array $fields + * @param array $where + * @return array + */ + protected static function select($fields = array('session','uid','groups'), $where = array()) + { + $t = microtime(true); + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_SELECT; + $gateway_data['ext_data'] = array('fields' => $fields, 'where' => $where); + $gateway_data_list = array(); + // 有client_id,能计算出需要和哪些gateway通讯,只和必要的gateway通讯能降低系统负载 + if (isset($where['client_id'])) { + $client_id_list = $where['client_id']; + unset($gateway_data['ext_data']['where']['client_id']); + $gateway_data['ext_data']['where']['connection_id'] = array(); + foreach ($client_id_list as $client_id) { + $address_data = Context::clientIdToAddress($client_id); + if (!$address_data) { + continue; + } + $address = long2ip($address_data['local_ip']) . ":{$address_data['local_port']}"; + if (!isset($gateway_data_list[$address])) { + $gateway_data_list[$address] = $gateway_data; + } + $gateway_data_list[$address]['ext_data']['where']['connection_id'][$address_data['connection_id']] = $address_data['connection_id']; + } + foreach ($gateway_data_list as $address => $item) { + $gateway_data_list[$address]['ext_data'] = json_encode($item['ext_data']); + } + // 有其它条件,则还是需要向所有gateway发送 + if (count($where) !== 1) { + $gateway_data['ext_data'] = json_encode($gateway_data['ext_data']); + foreach (static::getAllGatewayAddress() as $address) { + if (!isset($gateway_data_list[$address])) { + $gateway_data_list[$address] = $gateway_data; + } + } + } + $data = static::getBufferFromSomeGateway($gateway_data_list); + } else { + $gateway_data['ext_data'] = json_encode($gateway_data['ext_data']); + $data = static::getBufferFromAllGateway($gateway_data); + } + + return $data; + } + + /** + * 生成验证包,用于验证此客户端的合法性 + * + * @return string + */ + protected static function generateAuthBuffer() + { + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_GATEWAY_CLIENT_CONNECT; + $gateway_data['body'] = json_encode(array( + 'secret_key' => static::$secretKey, + )); + return GatewayProtocol::encode($gateway_data); + } + + /** + * 批量向某些gateway发包,并得到返回数组 + * + * @param array $gateway_data_array + * @return array + * @throws Exception + */ + protected static function getBufferFromSomeGateway($gateway_data_array) + { + $gateway_buffer_array = array(); + $auth_buffer = static::$secretKey ? static::generateAuthBuffer() : ''; + foreach ($gateway_data_array as $address => $gateway_data) { + if ($auth_buffer) { + $gateway_buffer_array[$address] = $auth_buffer.GatewayProtocol::encode($gateway_data); + } else { + $gateway_buffer_array[$address] = GatewayProtocol::encode($gateway_data); + } + } + return static::getBufferFromGateway($gateway_buffer_array); + } + + /** + * 批量向所有 gateway 发包,并得到返回数组 + * + * @param string|array $gateway_data + * @return array + * @throws Exception + */ + protected static function getBufferFromAllGateway($gateway_data) + { + $addresses = static::getAllGatewayAddress(); + $gateway_buffer_array = array(); + $gateway_buffer = GatewayProtocol::encode($gateway_data); + $gateway_buffer = static::$secretKey ? static::generateAuthBuffer() . $gateway_buffer : $gateway_buffer; + foreach ($addresses as $address) { + $gateway_buffer_array[$address] = $gateway_buffer; + } + + return static::getBufferFromGateway($gateway_buffer_array); + } + + /** + * 获取所有gateway内部通讯地址 + * + * @return array + * @throws Exception + */ + protected static function getAllGatewayAddress() + { + if (isset(static::$businessWorker)) { + $addresses = static::$businessWorker->getAllGatewayAddresses(); + if (empty($addresses)) { + throw new Exception('businessWorker::getAllGatewayAddresses return empty'); + } + } else { + $addresses = static::getAllGatewayAddressesFromRegister(); + if (empty($addresses)) { + return array(); + } + } + return $addresses; + } + + /** + * 批量向gateway发送并获取数据 + * @param $gateway_buffer_array + * @return array + */ + protected static function getBufferFromGateway($gateway_buffer_array) + { + $client_array = $status_data = $client_address_map = $receive_buffer_array = $recv_length_array = array(); + // 批量向所有gateway进程发送请求数据 + foreach ($gateway_buffer_array as $address => $gateway_buffer) { + $client = stream_socket_client("tcp://$address", $errno, $errmsg, static::$connectTimeout); + if ($client && strlen($gateway_buffer) === stream_socket_sendto($client, $gateway_buffer)) { + $socket_id = (int)$client; + $client_array[$socket_id] = $client; + $client_address_map[$socket_id] = explode(':', $address); + $receive_buffer_array[$socket_id] = ''; + } + } + // 超时5秒 + $timeout = 5; + $time_start = microtime(true); + // 批量接收请求 + while (count($client_array) > 0) { + $write = $except = array(); + $read = $client_array; + if (@stream_select($read, $write, $except, $timeout)) { + foreach ($read as $client) { + $socket_id = (int)$client; + $buffer = stream_socket_recvfrom($client, 65535); + if ($buffer !== '' && $buffer !== false) { + $receive_buffer_array[$socket_id] .= $buffer; + $receive_length = strlen($receive_buffer_array[$socket_id]); + if (empty($recv_length_array[$socket_id]) && $receive_length >= 4) { + $recv_length_array[$socket_id] = current(unpack('N', $receive_buffer_array[$socket_id])); + } + if (!empty($recv_length_array[$socket_id]) && $receive_length >= $recv_length_array[$socket_id] + 4) { + unset($client_array[$socket_id]); + } + } elseif (feof($client)) { + unset($client_array[$socket_id]); + } + } + } + if (microtime(true) - $time_start > $timeout) { + break; + } + } + $format_buffer_array = array(); + foreach ($receive_buffer_array as $socket_id => $buffer) { + $local_ip = ip2long($client_address_map[$socket_id][0]); + $local_port = $client_address_map[$socket_id][1]; + $format_buffer_array[$local_ip][$local_port] = unserialize(substr($buffer, 4)); + } + return $format_buffer_array; + } + + /** + * 踢掉某个客户端,并以$message通知被踢掉客户端 + * + * @param string $client_id + * @param string $message + * @return void + */ + public static function closeClient($client_id, $message = null) + { + $address_data = Context::clientIdToAddress($client_id); + if (!$address_data) { + return false; + } + $address = long2ip($address_data['local_ip']) . ":{$address_data['local_port']}"; + return static::kickAddress($address, $address_data['connection_id'], $message); + } + + /** + * 踢掉某个客户端并直接立即销毁相关连接 + * + * @param string $client_id + * @return bool + */ + public static function destoryClient($client_id) + { + if ($client_id === Context::$client_id) { + return static::destoryCurrentClient(); + } // 不是发给当前用户则使用存储中的地址 + else { + $address_data = Context::clientIdToAddress($client_id); + if (!$address_data) { + return false; + } + $address = long2ip($address_data['local_ip']) . ":{$address_data['local_port']}"; + return static::destroyAddress($address, $address_data['connection_id']); + } + } + + /** + * 踢掉当前客户端并直接立即销毁相关连接 + * + * @return bool + * @throws Exception + */ + public static function destoryCurrentClient() + { + if (!Context::$connection_id) { + throw new Exception('destoryCurrentClient can not be called in async context'); + } + $address = long2ip(Context::$local_ip) . ':' . Context::$local_port; + return static::destroyAddress($address, Context::$connection_id); + } + + /** + * 将 client_id 与 uid 绑定 + * + * @param string $client_id + * @param int|string $uid + * @return void + */ + public static function bindUid($client_id, $uid) + { + static::sendCmdAndMessageToClient($client_id, GatewayProtocol::CMD_BIND_UID, '', $uid); + } + + /** + * 将 client_id 与 uid 解除绑定 + * + * @param string $client_id + * @param int|string $uid + * @return void + */ + public static function unbindUid($client_id, $uid) + { + static::sendCmdAndMessageToClient($client_id, GatewayProtocol::CMD_UNBIND_UID, '', $uid); + } + + /** + * 将 client_id 加入组 + * + * @param string $client_id + * @param int|string $group + * @return void + */ + public static function joinGroup($client_id, $group) + { + + static::sendCmdAndMessageToClient($client_id, GatewayProtocol::CMD_JOIN_GROUP, '', $group); + } + + /** + * 将 client_id 离开组 + * + * @param string $client_id + * @param int|string $group + * + * @return void + */ + public static function leaveGroup($client_id, $group) + { + static::sendCmdAndMessageToClient($client_id, GatewayProtocol::CMD_LEAVE_GROUP, '', $group); + } + + /** + * 取消分组 + * + * @param int|string $group + * + * @return void + */ + public static function ungroup($group) + { + if (!static::isValidGroupId($group)) { + return false; + } + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_UNGROUP; + $gateway_data['ext_data'] = $group; + return static::sendToAllGateway($gateway_data); + + } + + /** + * 向所有 uid 发送 + * + * @param int|string|array $uid + * @param string $message + * + * @return void + */ + public static function sendToUid($uid, $message) + { + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_SEND_TO_UID; + $gateway_data['body'] = $message; + + if (!is_array($uid)) { + $uid = array($uid); + } + + $gateway_data['ext_data'] = json_encode($uid); + + static::sendToAllGateway($gateway_data); + } + + /** + * 向 group 发送 + * + * @param int|string|array $group 组(不允许是 0 '0' false null array()等为空的值) + * @param string $message 消息 + * @param array $exclude_client_id 不给这些client_id发 + * @param bool $raw 发送原始数据(即不调用gateway的协议的encode方法) + * + * @return void + */ + public static function sendToGroup($group, $message, $exclude_client_id = null, $raw = false) + { + if (!static::isValidGroupId($group)) { + return false; + } + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_SEND_TO_GROUP; + $gateway_data['body'] = $message; + if ($raw) { + $gateway_data['flag'] |= GatewayProtocol::FLAG_NOT_CALL_ENCODE; + } + + if (!is_array($group)) { + $group = array($group); + } + + // 分组发送,没有排除的client_id,直接发送 + $default_ext_data_buffer = json_encode(array('group'=> $group, 'exclude'=> null)); + if (empty($exclude_client_id)) { + $gateway_data['ext_data'] = $default_ext_data_buffer; + return static::sendToAllGateway($gateway_data); + } + + // 分组发送,有排除的client_id,需要将client_id转换成对应gateway进程内的connectionId + if (!is_array($exclude_client_id)) { + $exclude_client_id = array($exclude_client_id); + } + + $address_connection_array = static::clientIdArrayToAddressArray($exclude_client_id); + // 如果有businessWorker实例,说明运行在workerman环境中,通过businessWorker中的长连接发送数据 + if (static::$businessWorker) { + foreach (static::$businessWorker->gatewayConnections as $address => $gateway_connection) { + $gateway_data['ext_data'] = isset($address_connection_array[$address]) ? + json_encode(array('group'=> $group, 'exclude'=> $address_connection_array[$address])) : + $default_ext_data_buffer; + /** @var TcpConnection $gateway_connection */ + $gateway_connection->send($gateway_data); + } + } // 运行在其它环境中,通过注册中心得到gateway地址 + else { + $addresses = static::getAllGatewayAddressesFromRegister(); + foreach ($addresses as $address) { + $gateway_data['ext_data'] = isset($address_connection_array[$address]) ? + json_encode(array('group'=> $group, 'exclude'=> $address_connection_array[$address])) : + $default_ext_data_buffer; + static::sendToGateway($address, $gateway_data); + } + } + } + + /** + * 更新 session,框架自动调用,开发者不要调用 + * + * @param string $client_id + * @param string $session_str + * @return bool + */ + public static function setSocketSession($client_id, $session_str) + { + return static::sendCmdAndMessageToClient($client_id, GatewayProtocol::CMD_SET_SESSION, '', $session_str); + } + + /** + * 设置 session,原session值会被覆盖 + * + * @param string $client_id + * @param array $session + * + * @return void + */ + public static function setSession($client_id, array $session) + { + if (Context::$client_id === $client_id) { + $_SESSION = $session; + Context::$old_session = $_SESSION; + } + static::setSocketSession($client_id, Context::sessionEncode($session)); + } + + /** + * 更新 session,实际上是与老的session合并 + * + * @param string $client_id + * @param array $session + * + * @return void + */ + public static function updateSession($client_id, array $session) + { + if (Context::$client_id === $client_id) { + $_SESSION = array_replace_recursive((array)$_SESSION, $session); + Context::$old_session = $_SESSION; + } + static::sendCmdAndMessageToClient($client_id, GatewayProtocol::CMD_UPDATE_SESSION, '', Context::sessionEncode($session)); + } + + /** + * 获取某个client_id的session + * + * @param string $client_id + * @return mixed false表示出错、null表示用户不存在、array表示具体的session信息 + */ + public static function getSession($client_id) + { + $address_data = Context::clientIdToAddress($client_id); + if (!$address_data) { + return false; + } + $address = long2ip($address_data['local_ip']) . ":{$address_data['local_port']}"; + if (isset(static::$businessWorker)) { + if (!isset(static::$businessWorker->gatewayConnections[$address])) { + return null; + } + } + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_GET_SESSION_BY_CLIENT_ID; + $gateway_data['connection_id'] = $address_data['connection_id']; + return static::sendAndRecv($address, $gateway_data); + } + + /** + * 向某个用户网关发送命令和消息 + * + * @param string $client_id + * @param int $cmd + * @param string $message + * @param string $ext_data + * @return boolean + */ + protected static function sendCmdAndMessageToClient($client_id, $cmd, $message, $ext_data = '') + { + // 如果是发给当前用户则直接获取上下文中的地址 + if ($client_id === Context::$client_id || $client_id === null) { + $address = long2ip(Context::$local_ip) . ':' . Context::$local_port; + $connection_id = Context::$connection_id; + } else { + $address_data = Context::clientIdToAddress($client_id); + if (!$address_data) { + return false; + } + $address = long2ip($address_data['local_ip']) . ":{$address_data['local_port']}"; + $connection_id = $address_data['connection_id']; + } + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = $cmd; + $gateway_data['connection_id'] = $connection_id; + $gateway_data['body'] = $message; + if (!empty($ext_data)) { + $gateway_data['ext_data'] = $ext_data; + } + + return static::sendToGateway($address, $gateway_data); + } + + /** + * 发送数据并返回 + * + * @param int $address + * @param mixed $data + * @return bool + * @throws Exception + */ + protected static function sendAndRecv($address, $data) + { + $buffer = GatewayProtocol::encode($data); + $buffer = static::$secretKey ? static::generateAuthBuffer() . $buffer : $buffer; + $client = stream_socket_client("tcp://$address", $errno, $errmsg, static::$connectTimeout); + if (!$client) { + throw new Exception("can not connect to tcp://$address $errmsg"); + } + if (strlen($buffer) === stream_socket_sendto($client, $buffer)) { + $timeout = 5; + // 阻塞读 + stream_set_blocking($client, 1); + // 1秒超时 + stream_set_timeout($client, 1); + $all_buffer = ''; + $time_start = microtime(true); + $pack_len = 0; + while (1) { + $buf = stream_socket_recvfrom($client, 655350); + if ($buf !== '' && $buf !== false) { + $all_buffer .= $buf; + } else { + if (feof($client)) { + throw new Exception("connection close tcp://$address"); + } elseif (microtime(true) - $time_start > $timeout) { + break; + } + continue; + } + $recv_len = strlen($all_buffer); + if (!$pack_len && $recv_len >= 4) { + $pack_len= current(unpack('N', $all_buffer)); + } + // 回复的数据都是以\n结尾 + if (($pack_len && $recv_len >= $pack_len + 4) || microtime(true) - $time_start > $timeout) { + break; + } + } + // 返回结果 + return unserialize(substr($all_buffer, 4)); + } else { + throw new Exception("sendAndRecv($address, \$bufer) fail ! Can not send data!", 502); + } + } + + /** + * 发送数据到网关 + * + * @param string $address + * @param array $gateway_data + * @return bool + */ + protected static function sendToGateway($address, $gateway_data) + { + return static::sendBufferToGateway($address, GatewayProtocol::encode($gateway_data)); + } + + /** + * 发送buffer数据到网关 + * @param string $address + * @param string $gateway_buffer + * @return bool + */ + protected static function sendBufferToGateway($address, $gateway_buffer) + { + // 有$businessWorker说明是workerman环境,使用$businessWorker发送数据 + if (static::$businessWorker) { + if (!isset(static::$businessWorker->gatewayConnections[$address])) { + return false; + } + return static::$businessWorker->gatewayConnections[$address]->send($gateway_buffer, true); + } + // 非workerman环境 + $gateway_buffer = static::$secretKey ? static::generateAuthBuffer() . $gateway_buffer : $gateway_buffer; + $flag = static::$persistentConnection ? STREAM_CLIENT_PERSISTENT | STREAM_CLIENT_CONNECT : STREAM_CLIENT_CONNECT; + $client = stream_socket_client("tcp://$address", $errno, $errmsg, static::$connectTimeout, $flag); + return strlen($gateway_buffer) == stream_socket_sendto($client, $gateway_buffer); + } + + /** + * 向所有 gateway 发送数据 + * + * @param string $gateway_data + * @throws Exception + * + * @return void + */ + protected static function sendToAllGateway($gateway_data) + { + $buffer = GatewayProtocol::encode($gateway_data); + // 如果有businessWorker实例,说明运行在workerman环境中,通过businessWorker中的长连接发送数据 + if (static::$businessWorker) { + foreach (static::$businessWorker->gatewayConnections as $gateway_connection) { + /** @var TcpConnection $gateway_connection */ + $gateway_connection->send($buffer, true); + } + } // 运行在其它环境中,通过注册中心得到gateway地址 + else { + $all_addresses = static::getAllGatewayAddressesFromRegister(); + foreach ($all_addresses as $address) { + static::sendBufferToGateway($address, $buffer); + } + } + } + + /** + * 踢掉某个网关的 socket + * + * @param string $address + * @param int $connection_id + * @return bool + */ + protected static function kickAddress($address, $connection_id, $message) + { + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_KICK; + $gateway_data['connection_id'] = $connection_id; + $gateway_data['body'] = $message; + return static::sendToGateway($address, $gateway_data); + } + + /** + * 销毁某个网关的 socket + * + * @param string $address + * @param int $connection_id + * @return bool + */ + protected static function destroyAddress($address, $connection_id) + { + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_DESTROY; + $gateway_data['connection_id'] = $connection_id; + return static::sendToGateway($address, $gateway_data); + } + + /** + * 将clientid数组转换成address数组 + * + * @param array $client_id_array + * @return array + */ + protected static function clientIdArrayToAddressArray(array $client_id_array) + { + $address_connection_array = array(); + foreach ($client_id_array as $client_id) { + $address_data = Context::clientIdToAddress($client_id); + if ($address_data) { + $address = long2ip($address_data['local_ip']) . + ":{$address_data['local_port']}"; + $address_connection_array[$address][$address_data['connection_id']] = $address_data['connection_id']; + } + } + return $address_connection_array; + } + + /** + * 设置 gateway 实例 + * + * @param \GatewayWorker\BusinessWorker $business_worker_instance + */ + public static function setBusinessWorker($business_worker_instance) + { + static::$businessWorker = $business_worker_instance; + } + + /** + * 获取通过注册中心获取所有 gateway 通讯地址 + * + * @return array + * @throws Exception + */ + protected static function getAllGatewayAddressesFromRegister() + { + static $addresses_cache, $last_update; + if (static::$addressesCacheDisable) { + $addresses_cache = null; + } + $time_now = time(); + $expiration_time = 1; + $register_addresses = (array)static::$registerAddress; + $client = null; + if(empty($addresses_cache) || $time_now - $last_update > $expiration_time) { + foreach ($register_addresses as $register_address) { + set_error_handler(function(){}); + $client = stream_socket_client('tcp://' . $register_address, $errno, $errmsg, static::$connectTimeout); + restore_error_handler(); + if ($client) { + break; + } + } + if (!$client) { + throw new Exception('Can not connect to tcp://' . $register_address . ' ' . $errmsg); + } + + fwrite($client, '{"event":"worker_connect","secret_key":"' . static::$secretKey . '"}' . "\n"); + stream_set_timeout($client, 5); + $ret = fgets($client, 655350); + if (!$ret || !$data = json_decode(trim($ret), true)) { + throw new Exception('getAllGatewayAddressesFromRegister fail. tcp://' . + $register_address . ' return ' . var_export($ret, true)); + } + $last_update = $time_now; + $addresses_cache = $data['addresses']; + } + if (!$addresses_cache) { + throw new Exception('Gateway::getAllGatewayAddressesFromRegister() with registerAddress:' . + json_encode(static::$registerAddress) . ' return ' . var_export($addresses_cache, true)); + } + return $addresses_cache; + } + + /** + * 检查群组id是否合法 + * + * @param $group + * @return bool + */ + protected static function isValidGroupId($group) + { + if (empty($group)) { + echo new \Exception('group('.var_export($group, true).') empty'); + return false; + } + return true; + } +} diff --git a/extend/GatewayClient/GatewayProtocol.php b/extend/GatewayClient/GatewayProtocol.php new file mode 100644 index 0000000..fb6c893 --- /dev/null +++ b/extend/GatewayClient/GatewayProtocol.php @@ -0,0 +1,187 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +/** + * Gateway 与 Worker 间通讯的二进制协议 + * + * struct GatewayProtocol + * { + * unsigned int pack_len, + * unsigned char cmd,//命令字 + * unsigned int local_ip, + * unsigned short local_port, + * unsigned int client_ip, + * unsigned short client_port, + * unsigned int connection_id, + * unsigned char flag, + * unsigned short gateway_port, + * unsigned int ext_len, + * char[ext_len] ext_data, + * char[pack_length-HEAD_LEN] body//包体 + * } + * NCNnNnNCnN + */ +class GatewayProtocol +{ + // 发给worker,gateway有一个新的连接 + const CMD_ON_CONNECT = 1; + // 发给worker的,客户端有消息 + const CMD_ON_MESSAGE = 3; + // 发给worker上的关闭链接事件 + const CMD_ON_CLOSE = 4; + // 发给gateway的向单个用户发送数据 + const CMD_SEND_TO_ONE = 5; + // 发给gateway的向所有用户发送数据 + const CMD_SEND_TO_ALL = 6; + // 发给gateway的踢出用户 + // 1、如果有待发消息,将在发送完后立即销毁用户连接 + // 2、如果无待发消息,将立即销毁用户连接 + const CMD_KICK = 7; + // 发给gateway的立即销毁用户连接 + const CMD_DESTROY = 8; + // 发给gateway,通知用户session更新 + const CMD_UPDATE_SESSION = 9; + // 获取在线状态 + const CMD_GET_ALL_CLIENT_SESSIONS = 10; + // 判断是否在线 + const CMD_IS_ONLINE = 11; + // client_id绑定到uid + const CMD_BIND_UID = 12; + // 解绑 + const CMD_UNBIND_UID = 13; + // 向uid发送数据 + const CMD_SEND_TO_UID = 14; + // 根据uid获取绑定的clientid + const CMD_GET_CLIENT_ID_BY_UID = 15; + // 批量获取uid列表批量获取绑定的clientid + const CMD_BATCH_GET_CLIENT_ID_BY_UID = 16; + // 加入组 + const CMD_JOIN_GROUP = 20; + // 离开组 + const CMD_LEAVE_GROUP = 21; + // 向组成员发消息 + const CMD_SEND_TO_GROUP = 22; + // 获取组成员 + const CMD_GET_CLIENT_SESSIONS_BY_GROUP = 23; + // 获取组在线连接数 + const CMD_GET_CLIENT_COUNT_BY_GROUP = 24; + // 按照条件查找 + const CMD_SELECT = 25; + // 获取在线的群组ID + const CMD_GET_GROUP_ID_LIST = 26; + // 取消分组 + const CMD_UNGROUP = 27; + // 批量获取群组ID内客户端个数 + const CMD_BATCH_GET_CLIENT_COUNT_BY_GROUP = 28; + // worker连接gateway事件 + const CMD_WORKER_CONNECT = 200; + // 心跳 + const CMD_PING = 201; + // GatewayClient连接gateway事件 + const CMD_GATEWAY_CLIENT_CONNECT = 202; + // 根据client_id获取session + const CMD_GET_SESSION_BY_CLIENT_ID = 203; + // 发给gateway,覆盖session + const CMD_SET_SESSION = 204; + // 当websocket握手时触发,只有websocket协议支持此命令字 + const CMD_ON_WEBSOCKET_CONNECT = 205; + // 包体是标量 + const FLAG_BODY_IS_SCALAR = 0x01; + // 通知gateway在send时不调用协议encode方法,在广播组播时提升性能 + const FLAG_NOT_CALL_ENCODE = 0x02; + /** + * 包头长度 + * + * @var int + */ + const HEAD_LEN = 28; + public static $empty = array( + 'cmd' => 0, + 'local_ip' => 0, + 'local_port' => 0, + 'client_ip' => 0, + 'client_port' => 0, + 'connection_id' => 0, + 'flag' => 0, + 'gateway_port' => 0, + 'ext_data' => '', + 'body' => '', + ); + /** + * 返回包长度 + * + * @param string $buffer + * @return int return current package length + */ + public static function input($buffer) + { + if (strlen($buffer) < self::HEAD_LEN) { + return 0; + } + $data = unpack("Npack_len", $buffer); + return $data['pack_len']; + } + /** + * 获取整个包的 buffer + * + * @param mixed $data + * @return string + */ + public static function encode($data) + { + $flag = (int)is_scalar($data['body']); + if (!$flag) { + $data['body'] = serialize($data['body']); + } + $data['flag'] |= $flag; + $ext_len = strlen($data['ext_data']); + $package_len = self::HEAD_LEN + $ext_len + strlen($data['body']); + return pack("NCNnNnNCnN", $package_len, + $data['cmd'], $data['local_ip'], + $data['local_port'], $data['client_ip'], + $data['client_port'], $data['connection_id'], + $data['flag'], $data['gateway_port'], + $ext_len) . $data['ext_data'] . $data['body']; + } + /** + * 从二进制数据转换为数组 + * + * @param string $buffer + * @return array + */ + public static function decode($buffer) + { + $data = unpack("Npack_len/Ccmd/Nlocal_ip/nlocal_port/Nclient_ip/nclient_port/Nconnection_id/Cflag/ngateway_port/Next_len", + $buffer); + if ($data['ext_len'] > 0) { + $data['ext_data'] = substr($buffer, self::HEAD_LEN, $data['ext_len']); + if ($data['flag'] & self::FLAG_BODY_IS_SCALAR) { + $data['body'] = substr($buffer, self::HEAD_LEN + $data['ext_len']); + } else { + $data['body'] = unserialize(substr($buffer, self::HEAD_LEN + $data['ext_len'])); + } + } else { + $data['ext_data'] = ''; + if ($data['flag'] & self::FLAG_BODY_IS_SCALAR) { + $data['body'] = substr($buffer, self::HEAD_LEN); + } else { + $data['body'] = unserialize(substr($buffer, self::HEAD_LEN)); + } + } + return $data; + } +} diff --git a/extend/GatewayClient/MIT-LICENSE.txt b/extend/GatewayClient/MIT-LICENSE.txt new file mode 100644 index 0000000..fd6b1c8 --- /dev/null +++ b/extend/GatewayClient/MIT-LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2009-2015 walkor and contributors (see https://github.com/walkor/workerman/contributors) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/extend/GatewayClient/README.md b/extend/GatewayClient/README.md new file mode 100644 index 0000000..e67f7cb --- /dev/null +++ b/extend/GatewayClient/README.md @@ -0,0 +1,91 @@ +# GatewayClient + +GatewayWorker1.0请使用[1.0版本的GatewayClient](https://github.com/walkor/GatewayClient/releases/tag/v1.0) + +GatewayWorker2.0.1-2.0.4请使用[2.0.4版本的GatewayClient](https://github.com/walkor/GatewayClient/releases/tag/2.0.4) + +GatewayWorker2.0.5-2.0.6版本请使用[2.0.6版本的GatewayClient](https://github.com/walkor/GatewayClient/releases/tag/2.0.6) + +GatewayWorker2.0.7版本请使用 [2.0.7版本的GatewayClient](https://github.com/walkor/GatewayClient/releases/tag/v2.0.7) + +GatewayWorker3.0.0-3.0.7版本请使用 [3.0.0版本的GatewayClient](https://github.com/walkor/GatewayClient/releases/tag/v3.0.0)
+ +GatewayWorker3.0.8及以上版本请使用 [3.0.13版本的GatewayClient](https://github.com/walkor/GatewayClient/releases/tag/v3.0.13)
+ +注意:GatewayClient3.0.0以后支持composer并加了命名空间```GatewayClient```
+ +[如何查看GatewayWorker版本请点击这里](http://doc2.workerman.net/get-gateway-version.html) + +## 安装 +**方法一** +``` +composer require workerman/gatewayclient +``` +使用时引入`vendor/autoload.php` 类似如下: +```php +use GatewayClient\Gateway; +require_once '真实路径/vendor/autoload.php'; +``` + +**方法二** +下载源文件到任意目录,手动引入 `GatewayClient/Gateway.php`, 类似如下: +```php +use GatewayClient\Gateway; +require_once '真实路径/GatewayClient/Gateway.php'; +``` + +## 使用 +```php +// GatewayClient 3.0.0版本以后加了命名空间 +use GatewayClient\Gateway; + +// composer安装 +require_once '真实路径/vendor/autoload.php'; + +// 源文件引用 +//require_once '真实路径/GatewayClient/Gateway.php'; + +/** + * === 指定registerAddress表明与哪个GatewayWorker(集群)通讯。=== + * GatewayWorker里用Register服务来区分集群,即一个GatewayWorker(集群)只有一个Register服务, + * GatewayClient要与之通讯必须知道这个Register服务地址才能通讯,这个地址格式为 ip:端口 , + * 其中ip为Register服务运行的ip(如果GatewayWorker是单机部署则ip就是运行GatewayWorker的服务器ip), + * 端口是对应ip的服务器上start_register.php文件中监听的端口,也就是GatewayWorker启动时看到的Register的端口。 + * GatewayClient要想推送数据给客户端,必须知道客户端位于哪个GatewayWorker(集群), + * 然后去连这个GatewayWorker(集群)Register服务的 ip:端口,才能与对应GatewayWorker(集群)通讯。 + * 这个 ip:端口 在GatewayClient一侧使用 Gateway::$registerAddress 来指定。 + * + * === 如果GatewayClient和GatewayWorker不在同一台服务器需要以下步骤 === + * 1、需要设置start_gateway.php中的lanIp为实际的本机内网ip(如不在一个局域网也可以设置成外网ip),设置完后要重启GatewayWorker + * 2、GatewayClient这里的Gateway::$registerAddress的地址填写实际运行Register的服务器ip和端口 + * 3、需要开启GatewayWorker所在服务器的防火墙,让以下端口可以被GatewayClient所在服务器访问, + * 端口包括Rgister服务的端口以及start_gateway.php中lanIp与startPort指定的几个端口 + * + * === 如果GatewayClient和GatewayWorker在同一台服务器 === + * GatewayClient和Register服务都在一台服务器上,ip填写127.0.0.1及即可,无需其它设置。 + **/ +Gateway::$registerAddress = '127.0.0.1:1236'; + +// GatewayClient支持GatewayWorker中的所有接口(Gateway::closeCurrentClient Gateway::sendToCurrentClient除外) +Gateway::sendToAll($data); +Gateway::sendToClient($client_id, $data); +Gateway::closeClient($client_id); +Gateway::isOnline($client_id); +Gateway::bindUid($client_id, $uid); +Gateway::isUidOnline($uid); +Gateway::isUidsOnline($uids); +Gateway::getClientIdByUid($uid); +Gateway::unbindUid($client_id, $uid); +Gateway::sendToUid($uid, $dat); +Gateway::joinGroup($client_id, $group); +Gateway::sendToGroup($group, $data); +Gateway::leaveGroup($client_id, $group); +Gateway::getClientCountByGroup($group); +Gateway::getClientSessionsByGroup($group); +Gateway::getAllClientCount(); +Gateway::getAllClientSessions(); +Gateway::setSession($client_id, $session); +Gateway::updateSession($client_id, $session); +Gateway::getSession($client_id); +``` + diff --git a/extend/GatewayClient/composer.json b/extend/GatewayClient/composer.json new file mode 100644 index 0000000..d49859f --- /dev/null +++ b/extend/GatewayClient/composer.json @@ -0,0 +1,11 @@ +{ + "name" : "workerman/gatewayclient", + "type" : "library", + "homepage": "http://www.workerman.net", + "license" : "MIT", + "autoload": { + "psr-4": { + "GatewayClient\\": "./" + } + } +} diff --git a/extend/GatewayWorker/BusinessWorker.php b/extend/GatewayWorker/BusinessWorker.php new file mode 100644 index 0000000..3965d29 --- /dev/null +++ b/extend/GatewayWorker/BusinessWorker.php @@ -0,0 +1,522 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace GatewayWorker; + +use RuntimeException; +use Workerman\Connection\TcpConnection; + +use Workerman\Events\Swoole; +use Workerman\Events\Swow; +use Workerman\Worker; +use Workerman\Timer; +use Workerman\Connection\AsyncTcpConnection; +use GatewayWorker\Protocols\GatewayProtocol; +use GatewayWorker\Lib\Context; + +/** + * + * BusinessWorker 用于处理Gateway转发来的数据 + * + * @author walkor + * + */ +class BusinessWorker extends Worker +{ + /** + * 保存与 gateway 的连接 connection 对象 + * + * @var array + */ + public $gatewayConnections = array(); + + /** + * 注册中心地址 + * + * @var string|array + */ + public $registerAddress = '127.0.0.1:1236'; + + /** + * 事件处理类,默认是 Event 类 + * + * @var string + */ + public $eventHandler = 'Events'; + + /** + * 秘钥 + * + * @var string + */ + public $secretKey = ''; + + /** + * businessWorker进程将消息转发给gateway进程的发送缓冲区大小 + * + * @var int + */ + public $sendToGatewayBufferSize = 10240000; + + /** + * 保存用户设置的 worker 启动回调 + * + * @var callable|null + */ + protected $_onWorkerStart = null; + + /** + * 保存用户设置的 workerReload 回调 + * + * @var callable|null + */ + protected $_onWorkerReload = null; + + /** + * 保存用户设置的 workerStop 回调 + * + * @var callable|null + */ + protected $_onWorkerStop= null; + + /** + * 到注册中心的连接 + * + * @var AsyncTcpConnection + */ + protected $_registerConnection = null; + + /** + * 处于连接状态的 gateway 通讯地址 + * + * @var array + */ + protected $_connectingGatewayAddresses = array(); + + /** + * 所有 geteway 内部通讯地址 + * + * @var array + */ + protected $_gatewayAddresses = array(); + + /** + * 等待连接个 gateway 地址 + * + * @var array + */ + protected $_waitingConnectGatewayAddresses = array(); + + /** + * Event::onConnect 回调 + * + * @var callable|null + */ + protected $_eventOnConnect = null; + + /** + * Event::onMessage 回调 + * + * @var callable|null + */ + protected $_eventOnMessage = null; + + /** + * Event::onClose 回调 + * + * @var callable|null + */ + protected $_eventOnClose = null; + + /** + * websocket回调 + * + * @var null + */ + protected $_eventOnWebSocketConnect = null; + + /** + * SESSION 版本缓存 + * + * @var array + */ + protected $_sessionVersion = array(); + + /** + * 用于保持长连接的心跳时间间隔 + * + * @var int + */ + const PERSISTENCE_CONNECTION_PING_INTERVAL = 25; + + /** + * 构造函数 + * + * @param string $socket_name + * @param array $context_option + */ + public function __construct($socket_name = '', $context_option = array()) + { + parent::__construct($socket_name, $context_option); + $backrace = debug_backtrace(); + $this->_autoloadRootPath = dirname($backrace[0]['file']); + } + + /** + * {@inheritdoc} + */ + public function run(): void + { + $this->_onWorkerStart = $this->onWorkerStart; + $this->_onWorkerReload = $this->onWorkerReload; + $this->_onWorkerStop = $this->onWorkerStop; + $this->onWorkerStop = array($this, 'onWorkerStop'); + $this->onWorkerStart = array($this, 'onWorkerStart'); + $this->onWorkerReload = array($this, 'onWorkerReload'); + parent::run(); + } + + /** + * 当进程启动时一些初始化工作 + * + * @return void + */ + protected function onWorkerStart() + { + if (function_exists('opcache_reset')) { + opcache_reset(); + } + + if (!class_exists('\Protocols\GatewayProtocol')) { + class_alias('GatewayWorker\Protocols\GatewayProtocol', 'Protocols\GatewayProtocol'); + } + + if (!is_array($this->registerAddress)) { + $this->registerAddress = array($this->registerAddress); + } + $this->connectToRegister(); + + \GatewayWorker\Lib\Gateway::setBusinessWorker($this); + \GatewayWorker\Lib\Gateway::$secretKey = $this->secretKey; + if ($this->_onWorkerStart) { + call_user_func($this->_onWorkerStart, $this); + } + + if (is_callable($this->eventHandler . '::onWorkerStart')) { + call_user_func($this->eventHandler . '::onWorkerStart', $this); + } + + // 设置回调 + if (is_callable($this->eventHandler . '::onConnect')) { + $this->_eventOnConnect = $this->eventHandler . '::onConnect'; + } + + if (is_callable($this->eventHandler . '::onMessage')) { + $this->_eventOnMessage = $this->eventHandler . '::onMessage'; + } else { + echo "Waring: {$this->eventHandler}::onMessage is not callable\n"; + } + + if (is_callable($this->eventHandler . '::onClose')) { + $this->_eventOnClose = $this->eventHandler . '::onClose'; + } + + if (is_callable($this->eventHandler . '::onWebSocketConnect')) { + $this->_eventOnWebSocketConnect = $this->eventHandler . '::onWebSocketConnect'; + } + + } + + /** + * onWorkerReload 回调 + * + * @param Worker $worker + */ + protected function onWorkerReload($worker) + { + // 防止进程立刻退出 + $worker->reloadable = false; + // 延迟 0.05 秒退出,避免 BusinessWorker 瞬间全部退出导致没有可用的 BusinessWorker 进程 + Timer::add(0.05, array('Workerman\Worker', 'stopAll')); + // 执行用户定义的 onWorkerReload 回调 + if ($this->_onWorkerReload) { + call_user_func($this->_onWorkerReload, $this); + } + } + + /** + * 当进程关闭时一些清理工作 + * + * @return void + */ + protected function onWorkerStop() + { + if ($this->_onWorkerStop) { + call_user_func($this->_onWorkerStop, $this); + } + if (is_callable($this->eventHandler . '::onWorkerStop')) { + call_user_func($this->eventHandler . '::onWorkerStop', $this); + } + } + + /** + * 连接服务注册中心 + * + * @return void + */ + public function connectToRegister() + { + foreach ($this->registerAddress as $register_address) { + $register_connection = new AsyncTcpConnection("text://{$register_address}"); + $secret_key = $this->secretKey; + $register_connection->onConnect = function () use ($register_connection, $secret_key, $register_address) { + $register_connection->send('{"event":"worker_connect","secret_key":"' . $secret_key . '"}'); + // 如果Register服务器不在本地服务器,则需要保持心跳 + if (strpos($register_address, '127.0.0.1') !== 0) { + $register_connection->ping_timer = Timer::add(self::PERSISTENCE_CONNECTION_PING_INTERVAL, function () use ($register_connection) { + $register_connection->send('{"event":"ping"}'); + }); + } + }; + $register_connection->onClose = function ($register_connection) { + if(!empty($register_connection->ping_timer)) { + Timer::del($register_connection->ping_timer); + } + $register_connection->reconnect(1); + }; + $register_connection->onMessage = array($this, 'onRegisterConnectionMessage'); + $register_connection->connect(); + } + } + + + /** + * 当注册中心发来消息时 + * + * @return void + */ + public function onRegisterConnectionMessage($register_connection, $data) + { + $data = json_decode($data, true); + if (!isset($data['event'])) { + echo "Received bad data from Register\n"; + return; + } + $event = $data['event']; + switch ($event) { + case 'broadcast_addresses': + if (!is_array($data['addresses'])) { + echo "Received bad data from Register. Addresses empty\n"; + return; + } + $addresses = $data['addresses']; + $this->_gatewayAddresses = array(); + foreach ($addresses as $addr) { + $this->_gatewayAddresses[$addr] = $addr; + } + $this->checkGatewayConnections($addresses); + break; + default: + echo "Receive bad event:$event from Register.\n"; + } + } + + /** + * 当 gateway 转发来数据时 + * + * @param TcpConnection $connection + * @param mixed $data + */ + public function onGatewayMessage($connection, $data) + { + $cmd = $data['cmd']; + if ($cmd === GatewayProtocol::CMD_PING) { + return; + } + // 上下文数据 + Context::$client_ip = $data['client_ip']; + Context::$client_port = $data['client_port']; + Context::$local_ip = $data['local_ip']; + Context::$local_port = $data['local_port']; + Context::$connection_id = $data['connection_id']; + Context::$client_id = Context::addressToClientId($data['local_ip'], $data['local_port'], + $data['connection_id']); + // $_SERVER 变量 + $_SERVER = array( + 'REMOTE_ADDR' => long2ip($data['client_ip']), + 'REMOTE_PORT' => $data['client_port'], + 'GATEWAY_ADDR' => long2ip($data['local_ip']), + 'GATEWAY_PORT' => $data['gateway_port'], + 'GATEWAY_CLIENT_ID' => Context::$client_id, + ); + // 检查session版本,如果是过期的session数据则拉取最新的数据 + if ($cmd !== GatewayProtocol::CMD_ON_CLOSE && isset($this->_sessionVersion[Context::$client_id]) && $this->_sessionVersion[Context::$client_id] !== crc32($data['ext_data'])) { + $_SESSION = Context::$old_session = \GatewayWorker\Lib\Gateway::getSession(Context::$client_id); + $this->_sessionVersion[Context::$client_id] = crc32($data['ext_data']); + } else { + if (!isset($this->_sessionVersion[Context::$client_id])) { + $this->_sessionVersion[Context::$client_id] = crc32($data['ext_data']); + } + // 尝试解析 session + if ($data['ext_data'] != '') { + Context::$old_session = $_SESSION = Context::sessionDecode($data['ext_data']); + } else { + Context::$old_session = $_SESSION = null; + } + } + + // 尝试执行 Event::onConnection、Event::onMessage、Event::onClose + switch ($cmd) { + case GatewayProtocol::CMD_ON_CONNECT: + if ($this->_eventOnConnect) { + call_user_func($this->_eventOnConnect, Context::$client_id); + } + break; + case GatewayProtocol::CMD_ON_MESSAGE: + if ($this->_eventOnMessage) { + call_user_func($this->_eventOnMessage, Context::$client_id, $data['body']); + } + break; + case GatewayProtocol::CMD_ON_CLOSE: + unset($this->_sessionVersion[Context::$client_id]); + if ($this->_eventOnClose) { + call_user_func($this->_eventOnClose, Context::$client_id); + } + break; + case GatewayProtocol::CMD_ON_WEBSOCKET_CONNECT: + if ($this->_eventOnWebSocketConnect) { + call_user_func($this->_eventOnWebSocketConnect, Context::$client_id, $data['body']); + } + break; + } + + // session 必须是数组 + if ($_SESSION !== null && !is_array($_SESSION)) { + throw new RuntimeException('$_SESSION must be an array. But $_SESSION=' . var_export($_SESSION, true) . ' is not array.'); + } + + // 判断 session 是否被更改 + if ($_SESSION !== Context::$old_session && $cmd !== GatewayProtocol::CMD_ON_CLOSE) { + // 如果是swoole或者swow环境,不允许使用 $_SESSION 全局变量,协程会导致 $_SESSION 污染 + if (Worker::$eventLoopClass === Swoole::class || Worker::$eventLoopClass === Swow::class) { + echo new RuntimeException('Can not use $_SESSION in swoole or swow environment. Please use \GatewayWorker\Lib\Gateway::setSession to set session data.'); + } + $session_str_now = $_SESSION !== null ? Context::sessionEncode($_SESSION) : ''; + \GatewayWorker\Lib\Gateway::setSocketSession(Context::$client_id, $session_str_now); + $this->_sessionVersion[Context::$client_id] = crc32($session_str_now); + } + + Context::clear(); + } + + /** + * 当与 Gateway 的连接断开时触发 + * + * @param TcpConnection $connection + * @return void + */ + public function onGatewayClose($connection) + { + $addr = $connection->remoteAddr; + unset($this->gatewayConnections[$addr], $this->_connectingGatewayAddresses[$addr]); + if (isset($this->_gatewayAddresses[$addr]) && !isset($this->_waitingConnectGatewayAddresses[$addr])) { + Timer::add(1, array($this, 'tryToConnectGateway'), array($addr), false); + $this->_waitingConnectGatewayAddresses[$addr] = $addr; + } + } + + /** + * 尝试连接 Gateway 内部通讯地址 + * + * @param string $addr + */ + public function tryToConnectGateway($addr) + { + if (!isset($this->gatewayConnections[$addr]) && !isset($this->_connectingGatewayAddresses[$addr]) && isset($this->_gatewayAddresses[$addr])) { + $gateway_connection = new AsyncTcpConnection("GatewayProtocol://$addr"); + $gateway_connection->remoteAddr = $addr; + $gateway_connection->onConnect = array($this, 'onConnectGateway'); + $gateway_connection->onMessage = array($this, 'onGatewayMessage'); + $gateway_connection->onClose = array($this, 'onGatewayClose'); + $gateway_connection->onError = array($this, 'onGatewayError'); + $gateway_connection->maxSendBufferSize = $this->sendToGatewayBufferSize; + if (TcpConnection::$defaultMaxSendBufferSize == $gateway_connection->maxSendBufferSize) { + $gateway_connection->maxSendBufferSize = 50 * 1024 * 1024; + } + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_WORKER_CONNECT; + $gateway_data['body'] = json_encode(array( + 'worker_key' =>"{$this->name}:{$this->id}", + 'secret_key' => $this->secretKey, + )); + $gateway_connection->send($gateway_data); + $gateway_connection->connect(); + $this->_connectingGatewayAddresses[$addr] = $addr; + } + unset($this->_waitingConnectGatewayAddresses[$addr]); + } + + /** + * 检查 gateway 的通信端口是否都已经连 + * 如果有未连接的端口,则尝试连接 + * + * @param array $addresses_list + */ + public function checkGatewayConnections($addresses_list) + { + if (empty($addresses_list)) { + return; + } + foreach ($addresses_list as $addr) { + if (!isset($this->_waitingConnectGatewayAddresses[$addr])) { + $this->tryToConnectGateway($addr); + } + } + } + + /** + * 当连接上 gateway 的通讯端口时触发 + * 将连接 connection 对象保存起来 + * + * @param TcpConnection $connection + * @return void + */ + public function onConnectGateway($connection) + { + $this->gatewayConnections[$connection->remoteAddr] = $connection; + unset($this->_connectingGatewayAddresses[$connection->remoteAddr], $this->_waitingConnectGatewayAddresses[$connection->remoteAddr]); + } + + /** + * 当与 gateway 的连接出现错误时触发 + * + * @param TcpConnection $connection + * @param int $error_no + * @param string $error_msg + */ + public function onGatewayError($connection, $error_no, $error_msg) + { + echo "GatewayConnection Error : $error_no ,$error_msg\n"; + } + + /** + * 获取所有 Gateway 内部通讯地址 + * + * @return array + */ + public function getAllGatewayAddresses() + { + return $this->_gatewayAddresses; + } +} diff --git a/extend/GatewayWorker/Gateway.php b/extend/GatewayWorker/Gateway.php new file mode 100644 index 0000000..cf41491 --- /dev/null +++ b/extend/GatewayWorker/Gateway.php @@ -0,0 +1,1216 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace GatewayWorker; + +use GatewayWorker\Lib\Context; + +use Workerman\Connection\TcpConnection; + +use Workerman\Worker; +use Workerman\Timer; +use Workerman\Autoloader; +use Workerman\Connection\AsyncTcpConnection; +use GatewayWorker\Protocols\GatewayProtocol; + +/** + * + * Gateway,基于Worker 开发 + * 用于转发客户端的数据给Worker处理,以及转发Worker的数据给客户端 + * + * @author walkor + * + */ +class Gateway extends Worker +{ + + /** + * 随机负载均衡 + * + *@var string + */ + const ROUTER_RANDOM = 'router_random'; + + /** + * 最小连接数负载均衡模式 + * + * @var string + */ + const ROUTER_LEAST_CONNECTIONS = 'router_least_connections'; + + /** + * Gateway 默认负载均衡模式 + * + * @var string $selectLoadBalancingMode + */ + public static $selectLoadBalancingMode = self::ROUTER_LEAST_CONNECTIONS; + + + /** + * 本机 IP + * 单机部署默认 127.0.0.1,如果是分布式部署,需要设置成本机 IP + * + * @var string + */ + public $lanIp = '127.0.0.1'; + + /** + * 如果宿主机为192.168.1.2 , gatewayworker in docker container (172.25.0.2) + * 此时 lanIp=192.68.1.2 GatewayClientSDK 能连上,但是$this->_innerTcpWorker stream_socket_server(): Unable to connect to tcp://192.168.1.2:2901 (Address not available) in + * 此时 lanIp=172.25.0.2 GatewayClientSDK stream_socket_server(): Unable to connect to tcp://172.25.0.2:2901 (Address not available) , $this->_innerTcpWorker 正常监听 + * + * solution: + * $gateway->lanIp=192.168.1.2 ; + * $gateway->innerTcpWorkerListen=172.25.0.2; // || 0.0.0.0 + * + * GatewayClientSDK connect 192.168.1.2:lanPort + * $this->_innerTcpWorker listen $gateway->innerTcpWorkerListen:lanPort + * + */ + public $innerTcpWorkerListen=''; + + /** + * 本机端口 + * + * @var string + */ + public $lanPort = 0; + + /** + * gateway 内部通讯起始端口,每个 gateway 实例应该都不同,步长1000 + * + * @var int + */ + public $startPort = 2000; + + /** + * 注册服务地址,用于注册 Gateway BusinessWorker,使之能够通讯 + * + * @var string|array + */ + public $registerAddress = '127.0.0.1:1236'; + + /** + * 心跳时间间隔 + * + * @var int + */ + public $pingInterval = 0; + + /** + * $pingNotResponseLimit * $pingInterval 时间内,客户端未发送任何数据,断开客户端连接 + * + * @var int + */ + public $pingNotResponseLimit = 0; + + /** + * 服务端向客户端发送的心跳数据 + * + * @var string + */ + public $pingData = ''; + + /** + * 秘钥 + * + * @var string + */ + public $secretKey = ''; + + /** + * 路由函数 + * + * @var callable|null + */ + public $router = null; + + + /** + * gateway进程转发给businessWorker进程的发送缓冲区大小 + * + * @var int + */ + public $sendToWorkerBufferSize = 10240000; + + /** + * gateway进程将数据发给客户端时每个客户端发送缓冲区大小 + * + * @var int + */ + public $sendToClientBufferSize = 1024000; + + /** + * 协议加速 + * + * @var bool + */ + public $protocolAccelerate = false; + + /** + * BusinessWorker 连接成功之后触发 + * + * @var callable|null + */ + public $onBusinessWorkerConnected = null; + + /** + * BusinessWorker 关闭时触发 + * + * @var callable|null + */ + public $onBusinessWorkerClose = null; + + /** + * 最小连接数负载均衡记录表,用于新上线业务服务器负载足够均衡 + * [ ip+businessworker key => 连接记录, ip+businessworker key => 连接记录, .... ] + * + * @var array + */ + protected static $leastConnectionsRecord = array(); + + /** + * 保存客户端的所有 connection 对象 + * + * @var array + */ + protected $_clientConnections = array(); + + /** + * uid 到 connection 的映射,一对多关系 + */ + protected $_uidConnections = array(); + + /** + * group 到 connection 的映射,一对多关系 + * + * @var array + */ + protected $_groupConnections = array(); + + /** + * 保存所有 worker 的内部连接的 connection 对象 + * + * @var array + */ + protected $_workerConnections = array(); + + /** + * gateway 内部监听 worker 内部连接的 worker + * + * @var Worker + */ + protected $_innerTcpWorker = null; + + /** + * 当 worker 启动时 + * + * @var callable|null + */ + protected $_onWorkerStart = null; + + /** + * 当有客户端连接时 + * + * @var callable|null + */ + protected $_onConnect = null; + + /** + * 当客户端发来消息时 + * + * @var callable|null + */ + protected $_onMessage = null; + + /** + * 当客户端连接关闭时 + * + * @var callable|null + */ + protected $_onClose = null; + + /** + * 当 worker 停止时 + * + * @var callable|null + */ + protected $_onWorkerStop = null; + + /** + * 进程启动时间 + * + * @var int + */ + protected $_startTime = 0; + + /** + * gateway 监听的端口 + * + * @var int + */ + protected $_gatewayPort = 0; + + /** + * connectionId 记录器 + * @var int + */ + protected static $_connectionIdRecorder = 0; + + /** + * 用于保持长连接的心跳时间间隔 + * + * @var int + */ + const PERSISTENCE_CONNECTION_PING_INTERVAL = 25; + + /** + * 构造函数 + * + * @param string $socket_name + * @param array $context_option + */ + public function __construct($socket_name, $context_option = array()) + { + parent::__construct($socket_name, $context_option); + $this->reloadable = false; + $this->_gatewayPort = substr(strrchr($socket_name,':'),1); + $this->router = array("\\GatewayWorker\\Gateway", 'routerBind'); + + $backtrace = debug_backtrace(); + $this->_autoloadRootPath = dirname($backtrace[0]['file']); + } + + /** + * {@inheritdoc} + */ + public function run(): void + { + // 保存用户的回调,当对应的事件发生时触发 + $this->_onWorkerStart = $this->onWorkerStart; + $this->onWorkerStart = array($this, 'onWorkerStart'); + // 保存用户的回调,当对应的事件发生时触发 + $this->_onConnect = $this->onConnect; + $this->onConnect = array($this, 'onClientConnect'); + + // onMessage禁止用户设置回调 + $this->onMessage = array($this, 'onClientMessage'); + + // 保存用户的回调,当对应的事件发生时触发 + $this->_onClose = $this->onClose; + $this->onClose = array($this, 'onClientClose'); + // 保存用户的回调,当对应的事件发生时触发 + $this->_onWorkerStop = $this->onWorkerStop; + $this->onWorkerStop = array($this, 'onWorkerStop'); + + if (!is_array($this->registerAddress)) { + $this->registerAddress = array($this->registerAddress); + } + + // 记录进程启动的时间 + $this->_startTime = time(); + // 运行父方法 + parent::run(); + } + + /** + * 当客户端发来数据时,转发给worker处理 + * + * @param TcpConnection $connection + * @param mixed $data + */ + public function onClientMessage($connection, $data) + { + $connection->pingNotResponseCount = -1; + $this->sendToWorker(GatewayProtocol::CMD_ON_MESSAGE, $connection, $data); + } + + /** + * 当客户端连接上来时,初始化一些客户端的数据 + * 包括全局唯一的client_id、初始化session等 + * + * @param TcpConnection $connection + */ + public function onClientConnect($connection) + { + $connection->id = self::generateConnectionId(); + // 保存该连接的内部通讯的数据包报头,避免每次重新初始化 + $connection->gatewayHeader = array( + 'local_ip' => ip2long(gethostbyname($this->lanIp)), + 'local_port' => $this->lanPort, + 'client_ip' => ip2long($connection->getRemoteIp()), + 'client_port' => $connection->getRemotePort(), + 'gateway_port' => $this->_gatewayPort, + 'connection_id' => $connection->id, + 'flag' => 0, + ); + // 连接的 session + $connection->session = ''; + // 该连接的心跳参数 + $connection->pingNotResponseCount = -1; + // 该链接发送缓冲区大小 + $connection->maxSendBufferSize = $this->sendToClientBufferSize; + // 保存客户端连接 connection 对象 + $this->_clientConnections[$connection->id] = $connection; + + // 如果用户有自定义 onConnect 回调,则执行 + if ($this->_onConnect) { + call_user_func($this->_onConnect, $connection); + if (isset($connection->onWebSocketConnect)) { + $connection->_onWebSocketConnect = $connection->onWebSocketConnect; + } + } + if ($connection->protocol === '\Workerman\Protocols\Websocket' || $connection->protocol === 'Workerman\Protocols\Websocket') { + $connection->onWebSocketConnect = array($this, 'onWebsocketConnect'); + } + + $this->sendToWorker(GatewayProtocol::CMD_ON_CONNECT, $connection); + } + + /** + * websocket握手时触发 + * + * @param $connection + * @param $request + */ + public function onWebsocketConnect($connection, $request) + { + if (isset($connection->_onWebSocketConnect)) { + call_user_func($connection->_onWebSocketConnect, $connection, $request); + unset($connection->_onWebSocketConnect); + } + if (is_object($request)) { + $server = [ + 'QUERY_STRING' => $request->queryString(), + 'REQUEST_METHOD' => $request->method(), + 'REQUEST_URI' => $request->uri(), + 'SERVER_PROTOCOL' => "HTTP/" . $request->protocolVersion(), + 'SERVER_NAME' => $request->host(false), + 'CONTENT_TYPE' => $request->header('content-type'), + 'REMOTE_ADDR' => $connection->getRemoteIp(), + 'REMOTE_PORT' => $connection->getRemotePort(), + 'SERVER_PORT' => $connection->getLocalPort(), + ]; + foreach ($request->header() as $key => $header) { + $key = str_replace('-', '_', strtoupper($key)); + $server["HTTP_$key"] = $header; + } + $data = array('get' => $request->get(), 'server' => $server, 'cookie' => $request->cookie()); + } else { + $data = array('get' => $_GET, 'server' => $_SERVER, 'cookie' => $_COOKIE); + } + $this->sendToWorker(GatewayProtocol::CMD_ON_WEBSOCKET_CONNECT, $connection, $data); + } + + /** + * 生成connection id + * @return int + */ + protected function generateConnectionId() + { + $max_unsigned_int = 4294967295; + if (self::$_connectionIdRecorder >= $max_unsigned_int) { + self::$_connectionIdRecorder = 0; + } + while(++self::$_connectionIdRecorder <= $max_unsigned_int) { + if(!isset($this->_clientConnections[self::$_connectionIdRecorder])) { + break; + } + } + return self::$_connectionIdRecorder; + } + + /** + * 发送数据给 worker 进程 + * + * @param int $cmd + * @param TcpConnection $connection + * @param mixed $body + * @return bool + */ + protected function sendToWorker($cmd, $connection, $body = '') + { + $gateway_data = $connection->gatewayHeader; + $gateway_data['cmd'] = $cmd; + $gateway_data['body'] = $body; + $gateway_data['ext_data'] = $connection->session; + if ($this->_workerConnections) { + // 调用路由函数,选择一个worker把请求转发给它 + /** @var TcpConnection $worker_connection */ + $worker_connection = call_user_func($this->router, $this->_workerConnections, $connection, $cmd, $body); + if (false === $worker_connection->send($gateway_data)) { + $msg = "SendBufferToWorker fail. May be the send buffer are overflow. See http://doc2.workerman.net/send-buffer-overflow.html"; + static::error($msg); + return false; + } + } // 没有可用的 worker + else { + // gateway 启动后 1-2 秒内 SendBufferToWorker fail 是正常现象,因为与 worker 的连接还没建立起来, + // 所以不记录日志,只是关闭连接 + $time_diff = 2; + if (time() - $this->_startTime >= $time_diff) { + $msg = 'SendBufferToWorker fail. The connections between Gateway and BusinessWorker are not ready. See http://doc2.workerman.net/send-buffer-to-worker-fail.html'; + static::error($msg); + } + $connection->destroy(); + return false; + } + return true; + } + + /** + * 随机路由,返回 worker connection 标识 + * + * @param array $worker_connections + * @return string + */ + public static function routerRand(array $worker_connections) : string + { + return array_rand($worker_connections); + } + + /** + * 返回最少客户端连接数量的业务服务器标识 + * 新上线服务器由于客户端连接数过低,会先分配给新服务器 + * + * @throws \Exception + * @param array $leastConnections + * @return string + */ + protected static function routerLeastConnectionsRecord(array $leastConnections) : string + { + if (empty($leastConnections)) + { + throw new \Exception("The routing record is empty."); + } + + // 返回最少客户端连接数量的业务服务器地址 + return array_search(min($leastConnections), $leastConnections, true); + } + + /** + * @param array $worker_connections + * @param string $selectLoadBalancingMode + * @return string + * @throws \Exception + */ + protected static function businessWorkerAddress(array $worker_connections, string $selectLoadBalancingMode) + { + switch ($selectLoadBalancingMode) + { + case static::ROUTER_LEAST_CONNECTIONS: + // 选择连接最少的businessWorker 服务器 + $businessWorkerAddress = static::routerLeastConnectionsRecord(static::$leastConnectionsRecord); + // 更新轮询表连接数量 + static::$leastConnectionsRecord[$businessWorkerAddress]++; + return $businessWorkerAddress; + case static::ROUTER_RANDOM: + // 随机轮询 + return static::routerRand($worker_connections); + default: + throw new \Exception("The load balancing mode is not supported."); + } + } + + /** + * client_id 与 worker 绑定 + * + * @param array $worker_connections + * @param TcpConnection $client_connection + * @param int $cmd + * @param mixed $buffer + * @return TcpConnection + * @throws \Exception + */ + public static function routerBind($worker_connections, $client_connection, $cmd, $buffer) + { + if (!isset($client_connection->businessworker_address) || !isset($worker_connections[$client_connection->businessworker_address])) { + $client_connection->businessworker_address = static::businessWorkerAddress($worker_connections, static::$selectLoadBalancingMode); + } + return $worker_connections[$client_connection->businessworker_address]; + } + + /** + * 当客户端关闭时 + * + * @param TcpConnection $connection + */ + public function onClientClose($connection) + { + // 尝试通知 worker,触发 Event::onClose + $this->sendToWorker(GatewayProtocol::CMD_ON_CLOSE, $connection); + + // 客户端下线,更新路由表数据 + if(static::$selectLoadBalancingMode === static::ROUTER_LEAST_CONNECTIONS && + isset($connection->businessworker_address)) + { + // 客户端连接数 >0,减少连接数 + if((static::$leastConnectionsRecord[$connection->businessworker_address])??0 > 0) + { + static::$leastConnectionsRecord[$connection->businessworker_address]--; + } + } + + unset($this->_clientConnections[$connection->id]); + // 清理 uid 数据 + if (!empty($connection->uid)) { + $uid = $connection->uid; + unset($this->_uidConnections[$uid][$connection->id]); + if (empty($this->_uidConnections[$uid])) { + unset($this->_uidConnections[$uid]); + } + } + // 清理 group 数据 + if (!empty($connection->groups)) { + foreach ($connection->groups as $group) { + unset($this->_groupConnections[$group][$connection->id]); + if (empty($this->_groupConnections[$group])) { + unset($this->_groupConnections[$group]); + } + } + } + // 触发 onClose + if ($this->_onClose) { + call_user_func($this->_onClose, $connection); + } + } + + /** + * 当 Gateway 启动的时候触发的回调函数 + * + * @return void + */ + public function onWorkerStart() + { + // 分配一个内部通讯端口 + $this->lanPort = $this->startPort + $this->id; + + // 如果有设置心跳,则定时执行 + if ($this->pingInterval > 0) { + $timer_interval = $this->pingNotResponseLimit > 0 ? $this->pingInterval / 2 : $this->pingInterval; + Timer::add($timer_interval, array($this, 'ping')); + } + + // 如果BusinessWorker ip不是127.0.0.1,则需要加gateway到BusinessWorker的心跳 + if ($this->lanIp !== '127.0.0.1') { + Timer::add(self::PERSISTENCE_CONNECTION_PING_INTERVAL, array($this, 'pingBusinessWorker')); + } + + if (!class_exists('\Protocols\GatewayProtocol')) { + class_alias('GatewayWorker\Protocols\GatewayProtocol', 'Protocols\GatewayProtocol'); + } + + //如为公网IP监听,直接换成0.0.0.0 ,否则用内网IP + $listen_ip=filter_var($this->lanIp,FILTER_VALIDATE_IP,FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)?'0.0.0.0':$this->lanIp; + + //Use scenario to see line 64 + if($this->innerTcpWorkerListen != '') { + $listen_ip = $this->innerTcpWorkerListen; + } + + // 初始化 gateway 内部的监听,用于监听 worker 的连接已经连接上发来的数据 + $this->_innerTcpWorker = new Worker("GatewayProtocol://{$listen_ip}:{$this->lanPort}"); + $this->_innerTcpWorker->reusePort = false; + $this->_innerTcpWorker->listen(); + $this->_innerTcpWorker->name = 'GatewayInnerWorker'; + + if ($this->_autoloadRootPath && class_exists(Autoloader::class)) { + Autoloader::setRootPath($this->_autoloadRootPath); + } + + // 设置内部监听的相关回调 + $this->_innerTcpWorker->onMessage = array($this, 'onWorkerMessage'); + + $this->_innerTcpWorker->onConnect = array($this, 'onWorkerConnect'); + $this->_innerTcpWorker->onClose = array($this, 'onWorkerClose'); + + // 注册 gateway 的内部通讯地址,worker 去连这个地址,以便 gateway 与 worker 之间建立起 TCP 长连接 + $this->registerAddress(); + + if ($this->_onWorkerStart) { + call_user_func($this->_onWorkerStart, $this); + } + } + + + /** + * 当 worker 通过内部通讯端口连接到 gateway 时 + * + * @param TcpConnection $connection + */ + public function onWorkerConnect($connection) + { + $connection->maxSendBufferSize = $this->sendToWorkerBufferSize; + $connection->authorized = !$this->secretKey; + } + + /** + * 当 worker 发来数据时 + * + * @param TcpConnection $connection + * @param mixed $data + * @throws \Exception + * + * @return void + */ + public function onWorkerMessage($connection, $data) + { + $cmd = $data['cmd']; + if (empty($connection->authorized) && $cmd !== GatewayProtocol::CMD_WORKER_CONNECT && $cmd !== GatewayProtocol::CMD_GATEWAY_CLIENT_CONNECT) { + self::error("Unauthorized request from " . $connection->getRemoteIp() . ":" . $connection->getRemotePort()); + $connection->close(); + return; + } + switch ($cmd) { + // BusinessWorker连接Gateway + case GatewayProtocol::CMD_WORKER_CONNECT: + $worker_info = json_decode($data['body'], true); + if ($worker_info['secret_key'] !== $this->secretKey) { + self::error("Gateway: Worker key does not match ".var_export($this->secretKey, true)." !== ". var_export($this->secretKey)); + $connection->close(); + return; + } + $key = $connection->getRemoteIp() . ':' . $worker_info['worker_key']; + // 在一台服务器上businessWorker->name不能相同 + if (isset($this->_workerConnections[$key])) { + self::error("Gateway: Worker->name conflict. Key:{$key}"); + // 关闭老的,使用新的 + $this->_workerConnections[$key]->close(); + } + $connection->key = $key; + $this->_workerConnections[$key] = $connection; + $connection->authorized = true; + // 新上线业务服务器,初始路由表为0 + if(static::$selectLoadBalancingMode === static::ROUTER_LEAST_CONNECTIONS) { + static::$leastConnectionsRecord[$key] = 0; + } + if ($this->onBusinessWorkerConnected) { + call_user_func($this->onBusinessWorkerConnected, $connection); + } + return; + // GatewayClient连接Gateway + case GatewayProtocol::CMD_GATEWAY_CLIENT_CONNECT: + $worker_info = json_decode($data['body'], true); + if ($worker_info['secret_key'] !== $this->secretKey) { + self::error("Gateway: GatewayClient key does not match ".var_export($this->secretKey, true)." !== ".var_export($this->secretKey, true)); + $connection->close(); + return; + } + $connection->authorized = true; + return; + // 向某客户端发送数据,Gateway::sendToClient($client_id, $message); + case GatewayProtocol::CMD_SEND_TO_ONE: + if (isset($this->_clientConnections[$data['connection_id']])) { + $raw = (bool)($data['flag'] & GatewayProtocol::FLAG_NOT_CALL_ENCODE); + $body = $data['body']; + if (!$raw && $this->protocolAccelerate && $this->protocol) { + $body = $this->preEncodeForClient($body); + $raw = true; + } + $this->_clientConnections[$data['connection_id']]->send($body, $raw); + } + return; + // 踢出用户,Gateway::closeClient($client_id, $message); + case GatewayProtocol::CMD_KICK: + if (isset($this->_clientConnections[$data['connection_id']])) { + $this->_clientConnections[$data['connection_id']]->close($data['body']); + } + return; + // 立即销毁用户连接, Gateway::destroyClient($client_id); + case GatewayProtocol::CMD_DESTROY: + if (isset($this->_clientConnections[$data['connection_id']])) { + $this->_clientConnections[$data['connection_id']]->destroy(); + } + return; + // 广播, Gateway::sendToAll($message, $client_id_array) + case GatewayProtocol::CMD_SEND_TO_ALL: + $raw = (bool)($data['flag'] & GatewayProtocol::FLAG_NOT_CALL_ENCODE); + $body = $data['body']; + if (!$raw && $this->protocolAccelerate && $this->protocol) { + $body = $this->preEncodeForClient($body); + $raw = true; + } + $ext_data = $data['ext_data'] ? json_decode($data['ext_data'], true) : ''; + // $client_id_array 不为空时,只广播给 $client_id_array 指定的客户端 + if (isset($ext_data['connections'])) { + foreach ($ext_data['connections'] as $connection_id) { + if (isset($this->_clientConnections[$connection_id])) { + $this->_clientConnections[$connection_id]->send($body, $raw); + } + } + } // $client_id_array 为空时,广播给所有在线客户端 + else { + $exclude_connection_id = !empty($ext_data['exclude']) ? $ext_data['exclude'] : null; + foreach ($this->_clientConnections as $client_connection) { + if (!isset($exclude_connection_id[$client_connection->id])) { + $client_connection->send($body, $raw); + } + } + } + return; + case GatewayProtocol::CMD_SELECT: + $client_info_array = array(); + $ext_data = json_decode($data['ext_data'], true); + if (!$ext_data) { + echo 'CMD_SELECT ext_data=' . var_export($data['ext_data'], true) . '\r\n'; + $buffer = serialize($client_info_array); + $connection->send(pack('N', strlen($buffer)) . $buffer, true); + return; + } + $fields = $ext_data['fields']; + $where = $ext_data['where']; + if ($where) { + $connection_box_map = array( + 'groups' => $this->_groupConnections, + 'uid' => $this->_uidConnections + ); + // $where = ['groups'=>[x,x..], 'uid'=>[x,x..], 'connection_id'=>[x,x..]] + foreach ($where as $key => $items) { + if ($key !== 'connection_id') { + $connections_box = $connection_box_map[$key]; + foreach ($items as $item) { + if (isset($connections_box[$item])) { + foreach ($connections_box[$item] as $connection_id => $client_connection) { + if (!isset($client_info_array[$connection_id])) { + $client_info_array[$connection_id] = array(); + // $fields = ['groups', 'uid', 'session'] + foreach ($fields as $field) { + $client_info_array[$connection_id][$field] = isset($client_connection->$field) ? $client_connection->$field : null; + } + } + } + + } + } + } else { + foreach ($items as $connection_id) { + if (isset($this->_clientConnections[$connection_id])) { + $client_connection = $this->_clientConnections[$connection_id]; + $client_info_array[$connection_id] = array(); + // $fields = ['groups', 'uid', 'session'] + foreach ($fields as $field) { + $client_info_array[$connection_id][$field] = isset($client_connection->$field) ? $client_connection->$field : null; + } + } + } + } + } + } else { + foreach ($this->_clientConnections as $connection_id => $client_connection) { + foreach ($fields as $field) { + $client_info_array[$connection_id][$field] = isset($client_connection->$field) ? $client_connection->$field : null; + } + } + } + $buffer = serialize($client_info_array); + $connection->send(pack('N', strlen($buffer)) . $buffer, true); + return; + // 获取在线群组列表 + case GatewayProtocol::CMD_GET_GROUP_ID_LIST: + $buffer = serialize(array_keys($this->_groupConnections)); + $connection->send(pack('N', strlen($buffer)) . $buffer, true); + return; + // 重新赋值 session + case GatewayProtocol::CMD_SET_SESSION: + if (isset($this->_clientConnections[$data['connection_id']])) { + $this->_clientConnections[$data['connection_id']]->session = $data['ext_data']; + } + return; + // session合并 + case GatewayProtocol::CMD_UPDATE_SESSION: + if (!isset($this->_clientConnections[$data['connection_id']])) { + return; + } else { + if (!$this->_clientConnections[$data['connection_id']]->session) { + $this->_clientConnections[$data['connection_id']]->session = $data['ext_data']; + return; + } + $session = Context::sessionDecode($this->_clientConnections[$data['connection_id']]->session); + $session_for_merge = Context::sessionDecode($data['ext_data']); + $session = array_replace_recursive($session, $session_for_merge); + $this->_clientConnections[$data['connection_id']]->session = Context::sessionEncode($session); + } + return; + case GatewayProtocol::CMD_GET_SESSION_BY_CLIENT_ID: + if (!isset($this->_clientConnections[$data['connection_id']])) { + $session = serialize(null); + } else { + if (!$this->_clientConnections[$data['connection_id']]->session) { + $session = serialize(array()); + } else { + $session = $this->_clientConnections[$data['connection_id']]->session; + } + } + $connection->send(pack('N', strlen($session)) . $session, true); + return; + // 获得客户端sessions + case GatewayProtocol::CMD_GET_ALL_CLIENT_SESSIONS: + $client_info_array = array(); + foreach ($this->_clientConnections as $connection_id => $client_connection) { + $client_info_array[$connection_id] = $client_connection->session; + } + $buffer = serialize($client_info_array); + $connection->send(pack('N', strlen($buffer)) . $buffer, true); + return; + // 判断某个 client_id 是否在线 Gateway::isOnline($client_id) + case GatewayProtocol::CMD_IS_ONLINE: + $buffer = serialize((int)isset($this->_clientConnections[$data['connection_id']])); + $connection->send(pack('N', strlen($buffer)) . $buffer, true); + return; + // 将 client_id 与 uid 绑定 + case GatewayProtocol::CMD_BIND_UID: + $uid = $data['ext_data']; + if (empty($uid)) { + echo "bindUid(client_id, uid) uid empty, uid=" . var_export($uid, true); + return; + } + $connection_id = $data['connection_id']; + if (!isset($this->_clientConnections[$connection_id])) { + return; + } + $client_connection = $this->_clientConnections[$connection_id]; + if (isset($client_connection->uid)) { + $current_uid = $client_connection->uid; + unset($this->_uidConnections[$current_uid][$connection_id]); + if (empty($this->_uidConnections[$current_uid])) { + unset($this->_uidConnections[$current_uid]); + } + } + $client_connection->uid = $uid; + $this->_uidConnections[$uid][$connection_id] = $client_connection; + return; + // client_id 与 uid 解绑 Gateway::unbindUid($client_id, $uid); + case GatewayProtocol::CMD_UNBIND_UID: + $connection_id = $data['connection_id']; + if (!isset($this->_clientConnections[$connection_id])) { + return; + } + $client_connection = $this->_clientConnections[$connection_id]; + if (isset($client_connection->uid)) { + $current_uid = $client_connection->uid; + unset($this->_uidConnections[$current_uid][$connection_id]); + if (empty($this->_uidConnections[$current_uid])) { + unset($this->_uidConnections[$current_uid]); + } + $client_connection->uid_info = ''; + $client_connection->uid = null; + } + return; + // 发送数据给 uid Gateway::sendToUid($uid, $msg); + case GatewayProtocol::CMD_SEND_TO_UID: + $raw = (bool)($data['flag'] & GatewayProtocol::FLAG_NOT_CALL_ENCODE); + $body = $data['body']; + if (!$raw && $this->protocolAccelerate && $this->protocol) { + $body = $this->preEncodeForClient($body); + $raw = true; + } + $uid_array = json_decode($data['ext_data'], true); + foreach ($uid_array as $uid) { + if (!empty($this->_uidConnections[$uid])) { + foreach ($this->_uidConnections[$uid] as $connection) { + /** @var TcpConnection $connection */ + $connection->send($body, $raw); + } + } + } + return; + // 将 $client_id 加入用户组 Gateway::joinGroup($client_id, $group); + case GatewayProtocol::CMD_JOIN_GROUP: + $group = $data['ext_data']; + if (empty($group)) { + echo "join(group) group empty, group=" . var_export($group, true); + return; + } + $connection_id = $data['connection_id']; + if (!isset($this->_clientConnections[$connection_id])) { + return; + } + $client_connection = $this->_clientConnections[$connection_id]; + if (!isset($client_connection->groups)) { + $client_connection->groups = array(); + } + $client_connection->groups[$group] = $group; + $this->_groupConnections[$group][$connection_id] = $client_connection; + return; + // 将 $client_id 从某个用户组中移除 Gateway::leaveGroup($client_id, $group); + case GatewayProtocol::CMD_LEAVE_GROUP: + $group = $data['ext_data']; + if (empty($group)) { + echo "leave(group) group empty, group=" . var_export($group, true); + return; + } + $connection_id = $data['connection_id']; + if (!isset($this->_clientConnections[$connection_id])) { + return; + } + $client_connection = $this->_clientConnections[$connection_id]; + if (!isset($client_connection->groups[$group])) { + return; + } + unset($client_connection->groups[$group], $this->_groupConnections[$group][$connection_id]); + if (empty($this->_groupConnections[$group])) { + unset($this->_groupConnections[$group]); + } + return; + // 解散分组 + case GatewayProtocol::CMD_UNGROUP: + $group = $data['ext_data']; + if (empty($group)) { + echo "leave(group) group empty, group=" . var_export($group, true); + return; + } + if (empty($this->_groupConnections[$group])) { + return; + } + foreach ($this->_groupConnections[$group] as $client_connection) { + unset($client_connection->groups[$group]); + } + unset($this->_groupConnections[$group]); + return; + // 向某个用户组发送消息 Gateway::sendToGroup($group, $msg); + case GatewayProtocol::CMD_SEND_TO_GROUP: + $raw = (bool)($data['flag'] & GatewayProtocol::FLAG_NOT_CALL_ENCODE); + $body = $data['body']; + if (!$raw && $this->protocolAccelerate && $this->protocol) { + $body = $this->preEncodeForClient($body); + $raw = true; + } + $ext_data = json_decode($data['ext_data'], true); + $group_array = $ext_data['group']; + $exclude_connection_id = $ext_data['exclude']; + + foreach ($group_array as $group) { + if (!empty($this->_groupConnections[$group])) { + foreach ($this->_groupConnections[$group] as $connection) { + if(!isset($exclude_connection_id[$connection->id])) + { + /** @var TcpConnection $connection */ + $connection->send($body, $raw); + } + } + } + } + return; + // 获取某用户组成员信息 Gateway::getClientSessionsByGroup($group); + case GatewayProtocol::CMD_GET_CLIENT_SESSIONS_BY_GROUP: + $group = $data['ext_data']; + if (!isset($this->_groupConnections[$group])) { + $buffer = serialize(array()); + $connection->send(pack('N', strlen($buffer)) . $buffer, true); + return; + } + $client_info_array = array(); + foreach ($this->_groupConnections[$group] as $connection_id => $client_connection) { + $client_info_array[$connection_id] = $client_connection->session; + } + $buffer = serialize($client_info_array); + $connection->send(pack('N', strlen($buffer)) . $buffer, true); + return; + // 获取用户组成员数 Gateway::getClientCountByGroup($group); + case GatewayProtocol::CMD_GET_CLIENT_COUNT_BY_GROUP: + $group = $data['ext_data']; + $count = 0; + if ($group !== '') { + if (isset($this->_groupConnections[$group])) { + $count = count($this->_groupConnections[$group]); + } + } else { + $count = count($this->_clientConnections); + } + $buffer = serialize($count); + $connection->send(pack('N', strlen($buffer)) . $buffer, true); + return; + // 获取与某个 uid 绑定的所有 client_id Gateway::getClientIdByUid($uid); + case GatewayProtocol::CMD_GET_CLIENT_ID_BY_UID: + $uid = $data['ext_data']; + if (empty($this->_uidConnections[$uid])) { + $buffer = serialize(array()); + } else { + $buffer = serialize(array_keys($this->_uidConnections[$uid])); + } + $connection->send(pack('N', strlen($buffer)) . $buffer, true); + return; + // 批量获取与 uid 绑定的所有 client_id Gateway::batchGetClientIdByUid($uid); + case GatewayProtocol::CMD_BATCH_GET_CLIENT_ID_BY_UID: + $uids = json_decode($data['ext_data']); + $return = []; + foreach ($uids as $uid) { + if (empty($this->_uidConnections[$uid])) { + $return[$uid] = []; + } else { + $return[$uid] = array_keys($this->_uidConnections[$uid]); + } + } + $buffer = serialize($return); + + $connection->send(pack('N', strlen($buffer)) . $buffer, true); + return; + // 批量获取群组ID内客户端个数 + case GatewayProtocol::CMD_BATCH_GET_CLIENT_COUNT_BY_GROUP: + $groups = json_decode($data['ext_data'], true); + $return = []; + foreach ($groups as $group) { + if (isset($this->_groupConnections[$group])) { + $return[$group] = count($this->_groupConnections[$group]); + } else { + $return[$group] = 0; + } + } + + $buffer = serialize($return); + $connection->send(pack('N', strlen($buffer)) . $buffer, true); + return; + default : + $err_msg = "gateway inner pack err cmd=$cmd"; + echo $err_msg; + } + } + + + /** + * 当worker连接关闭时 + * + * @param TcpConnection $connection + */ + public function onWorkerClose($connection) + { + if (isset($connection->key)) { + // 业务服务器下线, 清理路由表数据 + if (static::$selectLoadBalancingMode === static::ROUTER_LEAST_CONNECTIONS) + { + unset(static::$leastConnectionsRecord[$connection->key]); + } + unset($this->_workerConnections[$connection->key]); + if ($this->onBusinessWorkerClose) { + call_user_func($this->onBusinessWorkerClose, $connection); + } + } + } + + /** + * 存储当前 Gateway 的内部通信地址 + * + * @return bool + */ + public function registerAddress() + { + $address = $this->lanIp . ':' . $this->lanPort; + foreach ($this->registerAddress as $register_address) { + $register_connection = new AsyncTcpConnection("text://{$register_address}"); + $secret_key = $this->secretKey; + $register_connection->onConnect = function($register_connection) use ($address, $secret_key, $register_address){ + $register_connection->send('{"event":"gateway_connect", "address":"' . $address . '", "secret_key":"' . $secret_key . '"}'); + // 如果Register服务器不在本地服务器,则需要保持心跳 + if (strpos($register_address, '127.0.0.1') !== 0) { + $register_connection->ping_timer = Timer::add(self::PERSISTENCE_CONNECTION_PING_INTERVAL, function () use ($register_connection) { + $register_connection->send('{"event":"ping"}'); + }); + } + }; + $register_connection->onClose = function ($register_connection) { + if(!empty($register_connection->ping_timer)) { + Timer::del($register_connection->ping_timer); + } + $register_connection->reconnect(1); + }; + $register_connection->connect(); + } + } + + + /** + * 心跳逻辑 + * + * @return void + */ + public function ping() + { + $ping_data = $this->pingData ? (string)$this->pingData : null; + $raw = false; + if ($this->protocolAccelerate && $ping_data && $this->protocol) { + $ping_data = $this->preEncodeForClient($ping_data); + $raw = true; + } + // 遍历所有客户端连接 + foreach ($this->_clientConnections as $connection) { + // 上次发送的心跳还没有回复次数大于限定值就断开 + if ($this->pingNotResponseLimit > 0 && + $connection->pingNotResponseCount >= $this->pingNotResponseLimit * 2 + ) { + $connection->destroy(); + continue; + } + // $connection->pingNotResponseCount 为 -1 说明最近客户端有发来消息,则不给客户端发送心跳 + $connection->pingNotResponseCount++; + if ($ping_data) { + if ($connection->pingNotResponseCount === 0 || + ($this->pingNotResponseLimit > 0 && $connection->pingNotResponseCount % 2 === 1) + ) { + continue; + } + $connection->send($ping_data, $raw); + } + } + } + + /** + * 向 BusinessWorker 发送心跳数据,用于保持长连接 + * + * @return void + */ + public function pingBusinessWorker() + { + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_PING; + foreach ($this->_workerConnections as $connection) { + $connection->send($gateway_data); + } + } + + /** + * @param mixed $data + * + * @return string + */ + protected function preEncodeForClient($data) + { + foreach ($this->_clientConnections as $client_connection) { + return call_user_func(array($client_connection->protocol, 'encode'), $data, $client_connection); + } + } + + /** + * 当 gateway 关闭时触发,清理数据 + * + * @return void + */ + public function onWorkerStop() + { + // 尝试触发用户设置的回调 + if ($this->_onWorkerStop) { + call_user_func($this->_onWorkerStop, $this); + } + } + + /** + * error. + * @param string $msg + */ + public static function error($msg) + { + Timer::add(1, function() use ($msg) { + Worker::log($msg); + }, null, false); + } +} diff --git a/extend/GatewayWorker/Lib/Context.php b/extend/GatewayWorker/Lib/Context.php new file mode 100644 index 0000000..22ebccb --- /dev/null +++ b/extend/GatewayWorker/Lib/Context.php @@ -0,0 +1,136 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace GatewayWorker\Lib; + +use Exception; + +/** + * 上下文 包含当前用户 uid, 内部通信 local_ip local_port socket_id,以及客户端 client_ip client_port + */ +class Context +{ + /** + * 内部通讯 id + * + * @var string + */ + public static $local_ip; + + /** + * 内部通讯端口 + * + * @var int + */ + public static $local_port; + + /** + * 客户端 ip + * + * @var string + */ + public static $client_ip; + + /** + * 客户端端口 + * + * @var int + */ + public static $client_port; + + /** + * client_id + * + * @var string + */ + public static $client_id; + + /** + * 连接 connection->id + * + * @var int + */ + public static $connection_id; + + /** + * 旧的session + * + * @var string + */ + public static $old_session; + + /** + * 编码 session + * + * @param mixed $session_data + * @return string + */ + public static function sessionEncode($session_data = '') + { + if ($session_data !== '') { + return serialize($session_data); + } + return ''; + } + + /** + * 解码 session + * + * @param string $session_buffer + * @return mixed + */ + public static function sessionDecode($session_buffer) + { + return unserialize($session_buffer); + } + + /** + * 清除上下文 + * + * @return void + */ + public static function clear() + { + self::$local_ip = self::$local_port = self::$client_ip = self::$client_port = + self::$client_id = self::$connection_id = self::$old_session = null; + } + + /** + * 通讯地址到 client_id 的转换 + * + * @param int $local_ip + * @param int $local_port + * @param int $connection_id + * @return string + */ + public static function addressToClientId($local_ip, $local_port, $connection_id) + { + return bin2hex(pack('NnN', $local_ip, $local_port, $connection_id)); + } + + /** + * client_id 到通讯地址的转换 + * + * @param string $client_id + * @return array + * @throws Exception + */ + public static function clientIdToAddress($client_id) + { + if (strlen($client_id) !== 20) { + echo new Exception("client_id $client_id is invalid"); + return false; + } + return unpack('Nlocal_ip/nlocal_port/Nconnection_id', pack('H*', $client_id)); + } +} diff --git a/extend/GatewayWorker/Lib/Gateway.php b/extend/GatewayWorker/Lib/Gateway.php new file mode 100644 index 0000000..89d11b4 --- /dev/null +++ b/extend/GatewayWorker/Lib/Gateway.php @@ -0,0 +1,1459 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace GatewayWorker\Lib; + +use Exception; +use GatewayWorker\Protocols\GatewayProtocol; +use Workerman\Connection\TcpConnection; + +/** + * 数据发送相关 + */ +class Gateway +{ + /** + * gateway 实例 + * + * @var object + */ + protected static $businessWorker = null; + + /** + * 注册中心地址 + * + * @var string|array + */ + public static $registerAddress = '127.0.0.1:1236'; + + /** + * 秘钥 + * @var string + */ + public static $secretKey = ''; + + /** + * 链接超时时间 + * @var int + */ + public static $connectTimeout = 3; + + /** + * 与Gateway是否是长链接 + * @var bool + */ + public static $persistentConnection = true; + + /** + * 是否清除注册地址缓存 + * @var bool + */ + public static $addressesCacheDisable = false; + + /** + * 与gateway建立的连接 + * @var array + */ + protected static $gatewayConnections = []; + + /** + * 向所有客户端连接(或者 client_id_array 指定的客户端连接)广播消息 + * + * @param string $message 向客户端发送的消息 + * @param array $client_id_array 客户端 id 数组 + * @param array $exclude_client_id 不给这些client_id发 + * @param bool $raw 是否发送原始数据(即不调用gateway的协议的encode方法) + * @return void + * @throws Exception + */ + public static function sendToAll($message, $client_id_array = null, $exclude_client_id = null, $raw = false) + { + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_SEND_TO_ALL; + $gateway_data['body'] = $message; + if ($raw) { + $gateway_data['flag'] |= GatewayProtocol::FLAG_NOT_CALL_ENCODE; + } + + if ($exclude_client_id) { + if (!is_array($exclude_client_id)) { + $exclude_client_id = array($exclude_client_id); + } + if ($client_id_array) { + $exclude_client_id = array_flip($exclude_client_id); + } + } + + if ($client_id_array) { + if (!is_array($client_id_array)) { + echo new \Exception('bad $client_id_array:'.var_export($client_id_array, true)); + return; + } + $data_array = array(); + foreach ($client_id_array as $client_id) { + if (isset($exclude_client_id[$client_id])) { + continue; + } + $address = Context::clientIdToAddress($client_id); + if ($address) { + $key = long2ip($address['local_ip']) . ":{$address['local_port']}"; + $data_array[$key][$address['connection_id']] = $address['connection_id']; + } + } + foreach ($data_array as $addr => $connection_id_list) { + $the_gateway_data = $gateway_data; + $the_gateway_data['ext_data'] = json_encode(array('connections' => $connection_id_list)); + static::sendToGateway($addr, $the_gateway_data); + } + return; + } elseif (empty($client_id_array) && is_array($client_id_array)) { + return; + } + + if (!$exclude_client_id) { + return static::sendToAllGateway($gateway_data); + } + + $address_connection_array = static::clientIdArrayToAddressArray($exclude_client_id); + + // 如果有businessWorker实例,说明运行在workerman环境中,通过businessWorker中的长连接发送数据 + if (static::$businessWorker) { + foreach (static::$businessWorker->gatewayConnections as $address => $gateway_connection) { + $gateway_data['ext_data'] = isset($address_connection_array[$address]) ? + json_encode(array('exclude'=> $address_connection_array[$address])) : ''; + /** @var TcpConnection $gateway_connection */ + $gateway_connection->send($gateway_data); + } + } // 运行在其它环境中,通过注册中心得到gateway地址 + else { + $all_addresses = static::getAllGatewayAddressesFromRegister(); + foreach ($all_addresses as $address) { + $gateway_data['ext_data'] = isset($address_connection_array[$address]) ? + json_encode(array('exclude'=> $address_connection_array[$address])) : ''; + static::sendToGateway($address, $gateway_data); + } + } + + } + + /** + * 向某个client_id对应的连接发消息 + * + * @param string $client_id + * @param string $message + * @param bool $raw + * @return bool + */ + public static function sendToClient($client_id, $message, $raw = false) + { + return static::sendCmdAndMessageToClient($client_id, GatewayProtocol::CMD_SEND_TO_ONE, $message, '', $raw); + } + + /** + * 向当前客户端连接发送消息 + * + * @param string $message + * @param bool $raw + * @return bool + */ + public static function sendToCurrentClient($message, $raw = false) + { + return static::sendCmdAndMessageToClient(null, GatewayProtocol::CMD_SEND_TO_ONE, $message, '', $raw); + } + + /** + * 判断某个uid是否在线 + * + * @param string $uid + * @return int 0|1 + */ + public static function isUidOnline($uid) + { + return (int)static::getClientIdByUid($uid); + } + + /** + * 判断client_id对应的连接是否在线 + * + * @param string $client_id + * @return int 0|1 + */ + public static function isOnline($client_id) + { + $address_data = Context::clientIdToAddress($client_id); + if (!$address_data) { + return 0; + } + $address = long2ip($address_data['local_ip']) . ":{$address_data['local_port']}"; + if (isset(static::$businessWorker)) { + if (!isset(static::$businessWorker->gatewayConnections[$address])) { + return 0; + } + } + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_IS_ONLINE; + $gateway_data['connection_id'] = $address_data['connection_id']; + return (int)static::sendAndRecv($address, $gateway_data); + } + + /** + * 获取所有在线用户的session,client_id为 key(弃用,请用getAllClientSessions代替) + * + * @param string $group + * @return array + */ + public static function getAllClientInfo($group = '') + { + echo "Warning: Gateway::getAllClientInfo is deprecated and will be removed in a future, please use Gateway::getAllClientSessions instead."; + return static::getAllClientSessions($group); + } + + /** + * 获取所有在线client_id的session,client_id为 key + * + * @param string $group + * @return array + */ + public static function getAllClientSessions($group = '') + { + $gateway_data = GatewayProtocol::$empty; + if (!$group) { + $gateway_data['cmd'] = GatewayProtocol::CMD_GET_ALL_CLIENT_SESSIONS; + } else { + $gateway_data['cmd'] = GatewayProtocol::CMD_GET_CLIENT_SESSIONS_BY_GROUP; + $gateway_data['ext_data'] = $group; + } + $status_data = array(); + $all_buffer_array = static::getBufferFromAllGateway($gateway_data); + foreach ($all_buffer_array as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $data) { + if ($data) { + foreach ($data as $connection_id => $session_buffer) { + $client_id = Context::addressToClientId($local_ip, $local_port, $connection_id); + if ($client_id === Context::$client_id) { + $status_data[$client_id] = (array)$_SESSION; + } else { + $status_data[$client_id] = $session_buffer ? Context::sessionDecode($session_buffer) : array(); + } + } + } + } + } + return $status_data; + } + + /** + * 获取某个组的连接信息(弃用,请用getClientSessionsByGroup代替) + * + * @param string $group + * @return array + */ + public static function getClientInfoByGroup($group) + { + echo "Warning: Gateway::getClientInfoByGroup is deprecated and will be removed in a future, please use Gateway::getClientSessionsByGroup instead."; + return static::getAllClientSessions($group); + } + + /** + * 获取某个组的所有client_id的session信息 + * + * @param string $group + * + * @return array + */ + public static function getClientSessionsByGroup($group) + { + if (static::isValidGroupId($group)) { + return static::getAllClientSessions($group); + } + return array(); + } + + /** + * 获取所有在线client_id数 + * + * @return int + */ + public static function getAllClientIdCount() + { + return static::getClientCountByGroup(); + } + + /** + * 获取所有在线client_id数(getAllClientIdCount的别名) + * + * @return int + */ + public static function getAllClientCount() + { + return static::getAllClientIdCount(); + } + + /** + * 获取某个组的在线client_id数 + * + * @param string $group + * @return int + */ + public static function getClientIdCountByGroup($group = '') + { + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_GET_CLIENT_COUNT_BY_GROUP; + $gateway_data['ext_data'] = $group; + $total_count = 0; + $all_buffer_array = static::getBufferFromAllGateway($gateway_data); + foreach ($all_buffer_array as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $count) { + if ($count) { + $total_count += $count; + } + } + } + return $total_count; + } + + /** + * getClientIdCountByGroup 函数的别名 + * + * @param string $group + * @return int + */ + public static function getClientCountByGroup($group = '') + { + return static::getClientIdCountByGroup($group); + } + + /** + * 获取某个群组在线client_id列表 + * + * @param string $group + * @return array + */ + public static function getClientIdListByGroup($group) + { + if (!static::isValidGroupId($group)) { + return array(); + } + + $data = static::select(array('uid'), array('groups' => is_array($group) ? $group : array($group))); + $client_id_map = array(); + foreach ($data as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $items) { + //$items = ['connection_id'=>['uid'=>x, 'group'=>[x,x..], 'session'=>[..]], 'client_id'=>[..], ..]; + foreach ($items as $connection_id => $info) { + $client_id = Context::addressToClientId($local_ip, $local_port, $connection_id); + $client_id_map[$client_id] = $client_id; + } + } + } + return $client_id_map; + } + + /** + * 获取集群所有在线client_id列表 + * + * @return array + */ + public static function getAllClientIdList() + { + return static::formatClientIdFromGatewayBuffer(static::select(array('uid'))); + } + + /** + * 格式化client_id + * + * @param $data + * @return array + */ + protected static function formatClientIdFromGatewayBuffer($data) + { + $client_id_list = array(); + foreach ($data as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $items) { + //$items = ['connection_id'=>['uid'=>x, 'group'=>[x,x..], 'session'=>[..]], 'client_id'=>[..], ..]; + foreach ($items as $connection_id => $info) { + $client_id = Context::addressToClientId($local_ip, $local_port, $connection_id); + $client_id_list[$client_id] = $client_id; + } + } + } + return $client_id_list; + } + + + /** + * 获取与 uid 绑定的 client_id 列表 + * + * @param string $uid + * @return array + */ + public static function getClientIdByUid($uid) + { + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_GET_CLIENT_ID_BY_UID; + $gateway_data['ext_data'] = $uid; + $client_list = array(); + $all_buffer_array = static::getBufferFromAllGateway($gateway_data); + foreach ($all_buffer_array as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $connection_id_array) { + if ($connection_id_array) { + foreach ($connection_id_array as $connection_id) { + $client_list[] = Context::addressToClientId($local_ip, $local_port, $connection_id); + } + } + } + } + return $client_list; + } + /** + * 获取与 uid 绑定的 client_id 列表 + * + * @param string $uid + * @return array + */ + public static function getClientIdByUids( $uids ) + { + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_BATCH_GET_CLIENT_ID_BY_UID; + $gateway_data['ext_data'] = json_encode( $uids ); + $client_list = array(); + $all_buffer_array = static::getBufferFromAllGateway($gateway_data); + foreach ($all_buffer_array as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $connection_id_array) { + if ($connection_id_array) { + foreach ($connection_id_array as $uid =>$connection_id) { + if( $connection_id ){ + if( !isset( $client_list[$uid] ) ){ + $client_list[$uid] = []; + } + $client_list[$uid][] = Context::addressToClientId($local_ip, $local_port, $connection_id ); + + } + + } + } + } + } + return $client_list; + } + + /** + * 获取某个群组在线uid列表 + * + * @param string $group + * @return array + */ + public static function getUidListByGroup($group) + { + if (!static::isValidGroupId($group)) { + return array(); + } + + $group = is_array($group) ? $group : array($group); + $data = static::select(array('uid'), array('groups' => $group)); + $uid_map = array(); + foreach ($data as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $items) { + //$items = ['connection_id'=>['uid'=>x, 'group'=>[x,x..], 'session'=>[..]], 'client_id'=>[..], ..]; + foreach ($items as $connection_id => $info) { + if (!empty($info['uid'])) { + $uid_map[$info['uid']] = $info['uid']; + } + } + } + } + return $uid_map; + } + + /** + * 获取某个群组在线uid数 + * + * @param string $group + * @return int + */ + public static function getUidCountByGroup($group) + { + if (static::isValidGroupId($group)) { + return count(static::getUidListByGroup($group)); + } + return 0; + } + + /** + * 获取全局在线uid列表 + * + * @return array + */ + public static function getAllUidList() + { + $data = static::select(array('uid')); + $uid_map = array(); + foreach ($data as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $items) { + //$items = ['connection_id'=>['uid'=>x, 'group'=>[x,x..], 'session'=>[..]], 'client_id'=>[..], ..]; + foreach ($items as $connection_id => $info) { + if (!empty($info['uid'])) { + $uid_map[$info['uid']] = $info['uid']; + } + } + } + } + return $uid_map; + } + + /** + * 获取全局在线uid数 + * @return int + */ + public static function getAllUidCount() + { + return count(static::getAllUidList()); + } + + /** + * 通过client_id获取uid + * + * @param $client_id + * @return mixed + */ + public static function getUidByClientId($client_id) + { + $data = static::select(array('uid'), array('client_id'=>array($client_id))); + foreach ($data as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $items) { + //$items = ['connection_id'=>['uid'=>x, 'group'=>[x,x..], 'session'=>[..]], 'client_id'=>[..], ..]; + foreach ($items as $info) { + return $info['uid']; + } + } + } + } + + /** + * 获取所有在线的群组id + * + * @return array + */ + public static function getAllGroupIdList() + { + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_GET_GROUP_ID_LIST; + $group_id_list = array(); + $all_buffer_array = static::getBufferFromAllGateway($gateway_data); + foreach ($all_buffer_array as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $group_id_array) { + if (is_array($group_id_array)) { + foreach ($group_id_array as $group_id) { + if (!isset($group_id_list[$group_id])) { + $group_id_list[$group_id] = $group_id; + } + } + } + } + } + return $group_id_list; + } + + + /** + * 获取所有在线分组的uid数量,也就是每个分组的在线用户数 + * + * @return array + */ + public static function getAllGroupUidCount() + { + $group_uid_map = static::getAllGroupUidList(); + $group_uid_count_map = array(); + foreach ($group_uid_map as $group_id => $uid_list) { + $group_uid_count_map[$group_id] = count($uid_list); + } + return $group_uid_count_map; + } + + + + /** + * 获取所有分组uid在线列表 + * + * @return array + */ + public static function getAllGroupUidList() + { + $data = static::select(array('uid','groups')); + $group_uid_map = array(); + foreach ($data as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $items) { + //$items = ['connection_id'=>['uid'=>x, 'group'=>[x,x..], 'session'=>[..]], 'client_id'=>[..], ..]; + foreach ($items as $connection_id => $info) { + if (empty($info['uid']) || empty($info['groups'])) { + break; + } + $uid = $info['uid']; + foreach ($info['groups'] as $group_id) { + if(!isset($group_uid_map[$group_id])) { + $group_uid_map[$group_id] = array(); + } + $group_uid_map[$group_id][$uid] = $uid; + } + } + } + } + return $group_uid_map; + } + + /** + * 获取所有群组在线client_id列表 + * + * @return array + */ + public static function getAllGroupClientIdList() + { + $data = static::select(array('groups')); + $group_client_id_map = array(); + foreach ($data as $local_ip => $buffer_array) { + foreach ($buffer_array as $local_port => $items) { + //$items = ['connection_id'=>['uid'=>x, 'group'=>[x,x..], 'session'=>[..]], 'client_id'=>[..], ..]; + foreach ($items as $connection_id => $info) { + if (empty($info['groups'])) { + break; + } + $client_id = Context::addressToClientId($local_ip, $local_port, $connection_id); + foreach ($info['groups'] as $group_id) { + if(!isset($group_client_id_map[$group_id])) { + $group_client_id_map[$group_id] = array(); + } + $group_client_id_map[$group_id][$client_id] = $client_id; + } + } + } + } + return $group_client_id_map; + } + + /** + * 获取所有群组在线client_id数量,也就是获取每个群组在线连接数 + * + * @return array + */ + public static function getAllGroupClientIdCount() + { + $group_client_map = static::getAllGroupClientIdList(); + $group_client_count_map = array(); + foreach ($group_client_map as $group_id => $client_id_list) { + $group_client_count_map[$group_id] = count($client_id_list); + } + return $group_client_count_map; + } + + + /** + * 根据条件到gateway搜索数据 + * + * @param array $fields + * @param array $where + * @return array + */ + protected static function select($fields = array('session','uid','groups'), $where = array()) + { + $t = microtime(true); + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_SELECT; + $gateway_data['ext_data'] = array('fields' => $fields, 'where' => $where); + $gateway_data_list = array(); + // 有client_id,能计算出需要和哪些gateway通讯,只和必要的gateway通讯能降低系统负载 + if (isset($where['client_id'])) { + $client_id_list = $where['client_id']; + unset($gateway_data['ext_data']['where']['client_id']); + $gateway_data['ext_data']['where']['connection_id'] = array(); + foreach ($client_id_list as $client_id) { + $address_data = Context::clientIdToAddress($client_id); + if (!$address_data) { + continue; + } + $address = long2ip($address_data['local_ip']) . ":{$address_data['local_port']}"; + if (!isset($gateway_data_list[$address])) { + $gateway_data_list[$address] = $gateway_data; + } + $gateway_data_list[$address]['ext_data']['where']['connection_id'][$address_data['connection_id']] = $address_data['connection_id']; + } + foreach ($gateway_data_list as $address => $item) { + $gateway_data_list[$address]['ext_data'] = json_encode($item['ext_data']); + } + // 有其它条件,则还是需要向所有gateway发送 + if (count($where) !== 1) { + $gateway_data['ext_data'] = json_encode($gateway_data['ext_data']); + foreach (static::getAllGatewayAddress() as $address) { + if (!isset($gateway_data_list[$address])) { + $gateway_data_list[$address] = $gateway_data; + } + } + } + $data = static::getBufferFromSomeGateway($gateway_data_list); + } else { + $gateway_data['ext_data'] = json_encode($gateway_data['ext_data']); + $data = static::getBufferFromAllGateway($gateway_data); + } + + return $data; + } + + /** + * 生成验证包,用于验证此客户端的合法性 + * + * @return string + */ + protected static function generateAuthBuffer() + { + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_GATEWAY_CLIENT_CONNECT; + $gateway_data['body'] = json_encode(array( + 'secret_key' => static::$secretKey, + )); + return GatewayProtocol::encode($gateway_data); + } + + /** + * 批量向某些gateway发包,并得到返回数组 + * + * @param array $gateway_data_array + * @return array + * @throws Exception + */ + protected static function getBufferFromSomeGateway($gateway_data_array) + { + $gateway_buffer_array = array(); + $auth_buffer = static::$secretKey ? static::generateAuthBuffer() : ''; + foreach ($gateway_data_array as $address => $gateway_data) { + if ($auth_buffer) { + $gateway_buffer_array[$address] = $auth_buffer.GatewayProtocol::encode($gateway_data); + } else { + $gateway_buffer_array[$address] = GatewayProtocol::encode($gateway_data); + } + } + return static::getBufferFromGateway($gateway_buffer_array); + } + + /** + * 批量向所有 gateway 发包,并得到返回数组 + * + * @param string $gateway_data + * @return array + * @throws Exception + */ + protected static function getBufferFromAllGateway($gateway_data) + { + $addresses = static::getAllGatewayAddress(); + $gateway_buffer_array = array(); + $gateway_buffer = GatewayProtocol::encode($gateway_data); + $gateway_buffer = static::$secretKey ? static::generateAuthBuffer() . $gateway_buffer : $gateway_buffer; + foreach ($addresses as $address) { + $gateway_buffer_array[$address] = $gateway_buffer; + } + + return static::getBufferFromGateway($gateway_buffer_array); + } + + /** + * 获取所有gateway内部通讯地址 + * + * @return array + * @throws Exception + */ + protected static function getAllGatewayAddress() + { + if (isset(static::$businessWorker)) { + $addresses = static::$businessWorker->getAllGatewayAddresses(); + if (empty($addresses)) { + throw new Exception('businessWorker::getAllGatewayAddresses return empty'); + } + } else { + $addresses = static::getAllGatewayAddressesFromRegister(); + if (empty($addresses)) { + return array(); + } + } + return $addresses; + } + + /** + * 批量向gateway发送并获取数据 + * @param $gateway_buffer_array + * @return array + */ + protected static function getBufferFromGateway($gateway_buffer_array) + { + $client_array = $status_data = $client_address_map = $receive_buffer_array = $recv_length_array = array(); + // 批量向所有gateway进程发送请求数据 + foreach ($gateway_buffer_array as $address => $gateway_buffer) { + $client = static::getGatewayConnection("tcp://$address"); + if (strlen($gateway_buffer) === stream_socket_sendto($client, $gateway_buffer)) { + $socket_id = (int)$client; + $client_array[$socket_id] = $client; + $client_address_map[$socket_id] = explode(':', $address); + $receive_buffer_array[$socket_id] = ''; + } + } + // 超时5秒 + $timeout = 5; + $time_start = microtime(true); + // 批量接收请求 + while (count($client_array) > 0) { + $write = $except = array(); + $read = $client_array; + if (@stream_select($read, $write, $except, $timeout)) { + foreach ($read as $client) { + $socket_id = (int)$client; + $buffer = stream_socket_recvfrom($client, 65535); + if ($buffer !== '' && $buffer !== false) { + $receive_buffer_array[$socket_id] .= $buffer; + $receive_length = strlen($receive_buffer_array[$socket_id]); + if (empty($recv_length_array[$socket_id]) && $receive_length >= 4) { + $recv_length_array[$socket_id] = current(unpack('N', $receive_buffer_array[$socket_id])); + } + if (!empty($recv_length_array[$socket_id]) && $receive_length >= $recv_length_array[$socket_id] + 4) { + unset($client_array[$socket_id]); + } + } elseif (feof($client)) { + unset($client_array[$socket_id]); + } + } + } + if (microtime(true) - $time_start > $timeout) { + static::$gatewayConnections = []; + break; + } + } + $format_buffer_array = array(); + foreach ($receive_buffer_array as $socket_id => $buffer) { + $local_ip = ip2long($client_address_map[$socket_id][0]); + $local_port = $client_address_map[$socket_id][1]; + $format_buffer_array[$local_ip][$local_port] = unserialize(substr($buffer, 4)); + } + return $format_buffer_array; + } + + /** + * 踢掉某个客户端,并以$message通知被踢掉客户端 + * + * @param string $client_id + * @param string $message + * @return void + */ + public static function closeClient($client_id, $message = null) + { + if ($client_id === Context::$client_id) { + return static::closeCurrentClient($message); + } // 不是发给当前用户则使用存储中的地址 + else { + $address_data = Context::clientIdToAddress($client_id); + if (!$address_data) { + return false; + } + $address = long2ip($address_data['local_ip']) . ":{$address_data['local_port']}"; + return static::kickAddress($address, $address_data['connection_id'], $message); + } + } + + /** + * 踢掉当前客户端,并以$message通知被踢掉客户端 + * + * @param string $message + * @return bool + * @throws Exception + */ + public static function closeCurrentClient($message = null) + { + if (!Context::$connection_id) { + throw new Exception('closeCurrentClient can not be called in async context'); + } + $address = long2ip(Context::$local_ip) . ':' . Context::$local_port; + return static::kickAddress($address, Context::$connection_id, $message); + } + + /** + * 踢掉某个客户端并直接立即销毁相关连接 + * + * @param string $client_id + * @return bool + */ + public static function destoryClient($client_id) + { + if ($client_id === Context::$client_id) { + return static::destoryCurrentClient(); + } // 不是发给当前用户则使用存储中的地址 + else { + $address_data = Context::clientIdToAddress($client_id); + if (!$address_data) { + return false; + } + $address = long2ip($address_data['local_ip']) . ":{$address_data['local_port']}"; + return static::destroyAddress($address, $address_data['connection_id']); + } + } + + /** + * 踢掉当前客户端并直接立即销毁相关连接 + * + * @return bool + * @throws Exception + */ + public static function destoryCurrentClient() + { + if (!Context::$connection_id) { + throw new Exception('destoryCurrentClient can not be called in async context'); + } + $address = long2ip(Context::$local_ip) . ':' . Context::$local_port; + return static::destroyAddress($address, Context::$connection_id); + } + + /** + * 将 client_id 与 uid 绑定 + * + * @param string $client_id + * @param int|string $uid + * @return void + */ + public static function bindUid($client_id, $uid) + { + static::sendCmdAndMessageToClient($client_id, GatewayProtocol::CMD_BIND_UID, '', $uid); + } + + /** + * 将 client_id 与 uid 解除绑定 + * + * @param string $client_id + * @param int|string $uid + * @return void + */ + public static function unbindUid($client_id, $uid) + { + static::sendCmdAndMessageToClient($client_id, GatewayProtocol::CMD_UNBIND_UID, '', $uid); + } + + /** + * 将 client_id 加入组 + * + * @param string $client_id + * @param int|string $group + * @return void + */ + public static function joinGroup($client_id, $group) + { + + static::sendCmdAndMessageToClient($client_id, GatewayProtocol::CMD_JOIN_GROUP, '', $group); + } + + /** + * 将 client_id 离开组 + * + * @param string $client_id + * @param int|string $group + * + * @return void + */ + public static function leaveGroup($client_id, $group) + { + static::sendCmdAndMessageToClient($client_id, GatewayProtocol::CMD_LEAVE_GROUP, '', $group); + } + + /** + * 取消分组 + * + * @param int|string $group + * + * @return void + */ + public static function ungroup($group) + { + if (!static::isValidGroupId($group)) { + return false; + } + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_UNGROUP; + $gateway_data['ext_data'] = $group; + return static::sendToAllGateway($gateway_data); + + } + + /** + * 向所有 uid 发送 + * + * @param int|string|array $uid + * @param string $message + * @param bool $raw + * + * @return void + */ + public static function sendToUid($uid, $message, $raw = false) + { + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_SEND_TO_UID; + $gateway_data['body'] = $message; + if ($raw) { + $gateway_data['flag'] |= GatewayProtocol::FLAG_NOT_CALL_ENCODE; + } + + if (!is_array($uid)) { + $uid = array($uid); + } + + $gateway_data['ext_data'] = json_encode($uid); + + static::sendToAllGateway($gateway_data); + } + + /** + * 向 group 发送 + * + * @param int|string|array $group 组(不允许是 0 '0' false null array()等为空的值) + * @param string $message 消息 + * @param array $exclude_client_id 不给这些client_id发 + * @param bool $raw 发送原始数据(即不调用gateway的协议的encode方法) + * + * @return void + */ + public static function sendToGroup($group, $message, $exclude_client_id = null, $raw = false) + { + if (!static::isValidGroupId($group)) { + return false; + } + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_SEND_TO_GROUP; + $gateway_data['body'] = $message; + if ($raw) { + $gateway_data['flag'] |= GatewayProtocol::FLAG_NOT_CALL_ENCODE; + } + + if (!is_array($group)) { + $group = array($group); + } + + // 分组发送,没有排除的client_id,直接发送 + $default_ext_data_buffer = json_encode(array('group'=> $group, 'exclude'=> null)); + if (empty($exclude_client_id)) { + $gateway_data['ext_data'] = $default_ext_data_buffer; + return static::sendToAllGateway($gateway_data); + } + + // 分组发送,有排除的client_id,需要将client_id转换成对应gateway进程内的connectionId + if (!is_array($exclude_client_id)) { + $exclude_client_id = array($exclude_client_id); + } + + $address_connection_array = static::clientIdArrayToAddressArray($exclude_client_id); + // 如果有businessWorker实例,说明运行在workerman环境中,通过businessWorker中的长连接发送数据 + if (static::$businessWorker) { + foreach (static::$businessWorker->gatewayConnections as $address => $gateway_connection) { + $gateway_data['ext_data'] = isset($address_connection_array[$address]) ? + json_encode(array('group'=> $group, 'exclude'=> $address_connection_array[$address])) : + $default_ext_data_buffer; + /** @var TcpConnection $gateway_connection */ + $gateway_connection->send($gateway_data); + } + } // 运行在其它环境中,通过注册中心得到gateway地址 + else { + $addresses = static::getAllGatewayAddressesFromRegister(); + foreach ($addresses as $address) { + $gateway_data['ext_data'] = isset($address_connection_array[$address]) ? + json_encode(array('group'=> $group, 'exclude'=> $address_connection_array[$address])) : + $default_ext_data_buffer; + static::sendToGateway($address, $gateway_data); + } + } + } + + /** + * 更新 session,框架自动调用,开发者不要调用 + * + * @param string $client_id + * @param string $session_str + * @return bool + */ + public static function setSocketSession($client_id, $session_str) + { + return static::sendCmdAndMessageToClient($client_id, GatewayProtocol::CMD_SET_SESSION, '', $session_str); + } + + /** + * 设置 session,原session值会被覆盖 + * + * @param string $client_id + * @param array $session + * + * @return void + */ + public static function setSession($client_id, array $session) + { + if (Context::$client_id === $client_id) { + $_SESSION = $session; + Context::$old_session = $_SESSION; + } + static::setSocketSession($client_id, Context::sessionEncode($session)); + } + + /** + * 更新 session,实际上是与老的session合并 + * + * @param string $client_id + * @param array $session + * + * @return void + */ + public static function updateSession($client_id, array $session) + { + if (Context::$client_id === $client_id) { + $_SESSION = array_replace_recursive((array)$_SESSION, $session); + Context::$old_session = $_SESSION; + } + static::sendCmdAndMessageToClient($client_id, GatewayProtocol::CMD_UPDATE_SESSION, '', Context::sessionEncode($session)); + } + + /** + * 获取某个client_id的session + * + * @param string $client_id + * @return mixed false表示出错、null表示用户不存在、array表示具体的session信息 + */ + public static function getSession($client_id) + { + $address_data = Context::clientIdToAddress($client_id); + if (!$address_data) { + return false; + } + $address = long2ip($address_data['local_ip']) . ":{$address_data['local_port']}"; + if (isset(static::$businessWorker)) { + if (!isset(static::$businessWorker->gatewayConnections[$address])) { + return null; + } + } + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_GET_SESSION_BY_CLIENT_ID; + $gateway_data['connection_id'] = $address_data['connection_id']; + return static::sendAndRecv($address, $gateway_data); + } + + /** + * 向某个用户网关发送命令和消息 + * + * @param string $client_id + * @param int $cmd + * @param string $message + * @param string $ext_data + * @param bool $raw + * @return boolean + */ + protected static function sendCmdAndMessageToClient($client_id, $cmd, $message, $ext_data = '', $raw = false) + { + // 如果是发给当前用户则直接获取上下文中的地址 + if ($client_id === Context::$client_id || $client_id === null) { + $address = long2ip(Context::$local_ip) . ':' . Context::$local_port; + $connection_id = Context::$connection_id; + } else { + $address_data = Context::clientIdToAddress($client_id); + if (!$address_data) { + return false; + } + $address = long2ip($address_data['local_ip']) . ":{$address_data['local_port']}"; + $connection_id = $address_data['connection_id']; + } + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = $cmd; + $gateway_data['connection_id'] = $connection_id; + $gateway_data['body'] = $message; + if (!empty($ext_data)) { + $gateway_data['ext_data'] = $ext_data; + } + if ($raw) { + $gateway_data['flag'] |= GatewayProtocol::FLAG_NOT_CALL_ENCODE; + } + + return static::sendToGateway($address, $gateway_data); + } + + /** + * 发送数据并返回 + * + * @param int $address + * @param mixed $data + * @return bool + * @throws Exception + */ + protected static function sendAndRecv($address, $data) + { + $buffer = GatewayProtocol::encode($data); + $buffer = static::$secretKey ? static::generateAuthBuffer() . $buffer : $buffer; + $address = "tcp://$address"; + $client = static::getGatewayConnection($address); + if (strlen($buffer) === stream_socket_sendto($client, $buffer)) { + $timeout = 5; + // 阻塞读 + stream_set_blocking($client, 1); + // 1秒超时 + stream_set_timeout($client, 1); + $all_buffer = ''; + $time_start = microtime(true); + $pack_len = 0; + while (1) { + $buf = stream_socket_recvfrom($client, 655350); + if ($buf !== '' && $buf !== false) { + $all_buffer .= $buf; + } else { + if (feof($client)) { + unset(static::$gatewayConnections[$address]); + throw new Exception("connection close $address"); + } elseif (microtime(true) - $time_start > $timeout) { + unset(static::$gatewayConnections[$address]); + break; + } + continue; + } + $recv_len = strlen($all_buffer); + if (!$pack_len && $recv_len >= 4) { + $pack_len= current(unpack('N', $all_buffer)); + } + if (microtime(true) - $time_start > $timeout) { + unset(static::$gatewayConnections[$address]); + break; + } + // 回复的数据都是以\n结尾 + if (($pack_len && $recv_len >= $pack_len + 4)) { + break; + } + } + // 返回结果 + return unserialize(substr($all_buffer, 4)); + } else { + throw new Exception("sendAndRecv($address, \$bufer) fail ! Can not send data!", 502); + } + } + + /** + * 发送数据到网关 + * + * @param string $address + * @param array $gateway_data + * @return bool + */ + protected static function sendToGateway($address, $gateway_data) + { + return static::sendBufferToGateway($address, GatewayProtocol::encode($gateway_data)); + } + + /** + * 发送buffer数据到网关 + * @param string $address + * @param string $gateway_buffer + * @return bool + */ + protected static function sendBufferToGateway($address, $gateway_buffer) + { + // 有$businessWorker说明是workerman环境,使用$businessWorker发送数据 + if (static::$businessWorker) { + if (!isset(static::$businessWorker->gatewayConnections[$address])) { + return false; + } + return static::$businessWorker->gatewayConnections[$address]->send($gateway_buffer, true); + } + // 非workerman环境 + $gateway_buffer = static::$secretKey ? static::generateAuthBuffer() . $gateway_buffer : $gateway_buffer; + $client = static::getGatewayConnection("tcp://$address"); + return strlen($gateway_buffer) == stream_socket_sendto($client, $gateway_buffer); + } + + /** + * 向所有 gateway 发送数据 + * + * @param string $gateway_data + * @throws Exception + * + * @return void + */ + protected static function sendToAllGateway($gateway_data) + { + $buffer = GatewayProtocol::encode($gateway_data); + // 如果有businessWorker实例,说明运行在workerman环境中,通过businessWorker中的长连接发送数据 + if (static::$businessWorker) { + foreach (static::$businessWorker->gatewayConnections as $gateway_connection) { + /** @var TcpConnection $gateway_connection */ + $gateway_connection->send($buffer, true); + } + } // 运行在其它环境中,通过注册中心得到gateway地址 + else { + $all_addresses = static::getAllGatewayAddressesFromRegister(); + foreach ($all_addresses as $address) { + static::sendBufferToGateway($address, $buffer); + } + } + } + + /** + * 踢掉某个网关的 socket + * + * @param string $address + * @param int $connection_id + * @return bool + */ + protected static function kickAddress($address, $connection_id, $message) + { + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_KICK; + $gateway_data['connection_id'] = $connection_id; + $gateway_data['body'] = $message; + return static::sendToGateway($address, $gateway_data); + } + + /** + * 销毁某个网关的 socket + * + * @param string $address + * @param int $connection_id + * @return bool + */ + protected static function destroyAddress($address, $connection_id) + { + $gateway_data = GatewayProtocol::$empty; + $gateway_data['cmd'] = GatewayProtocol::CMD_DESTROY; + $gateway_data['connection_id'] = $connection_id; + return static::sendToGateway($address, $gateway_data); + } + + /** + * 将clientid数组转换成address数组 + * + * @param array $client_id_array + * @return array + */ + protected static function clientIdArrayToAddressArray(array $client_id_array) + { + $address_connection_array = array(); + foreach ($client_id_array as $client_id) { + $address_data = Context::clientIdToAddress($client_id); + if ($address_data) { + $address = long2ip($address_data['local_ip']) . + ":{$address_data['local_port']}"; + $address_connection_array[$address][$address_data['connection_id']] = $address_data['connection_id']; + } + } + return $address_connection_array; + } + + /** + * 设置 gateway 实例 + * + * @param \GatewayWorker\BusinessWorker $business_worker_instance + */ + public static function setBusinessWorker($business_worker_instance) + { + static::$businessWorker = $business_worker_instance; + } + + /** + * 获取通过注册中心获取所有 gateway 通讯地址 + * + * @return array + * @throws Exception + */ + protected static function getAllGatewayAddressesFromRegister() + { + static $addresses_cache, $last_update; + if (static::$addressesCacheDisable) { + $addresses_cache = null; + } + $time_now = time(); + $expiration_time = 1; + $register_addresses = (array)static::$registerAddress; + if(empty($addresses_cache[static::$registerAddress]) || $time_now - $last_update[static::$registerAddress] > $expiration_time) { + foreach ($register_addresses as $register_address) { + $client = stream_socket_client('tcp://' . $register_address, $errno, $errmsg, static::$connectTimeout); + if ($client) { + break; + } + } + if (!$client) { + throw new Exception('Can not connect to tcp://' . $register_address . ' ' . $errmsg); + } + + fwrite($client, '{"event":"worker_connect","secret_key":"' . static::$secretKey . '"}' . "\n"); + stream_set_timeout($client, 5); + $ret = fgets($client, 655350); + if (!$ret || !$data = json_decode(trim($ret), true)) { + throw new Exception('getAllGatewayAddressesFromRegister fail. tcp://' . + $register_address . ' return ' . var_export($ret, true)); + } + $last_update[static::$registerAddress] = $time_now; + $addresses_cache[static::$registerAddress] = $data['addresses']; + } + if (!$addresses_cache[static::$registerAddress]) { + throw new Exception('Gateway::getAllGatewayAddressesFromRegister() with registerAddress:' . + json_encode(static::$registerAddress) . ' return ' . var_export($addresses_cache[static::$registerAddress], true)); + } + return $addresses_cache[static::$registerAddress]; + } + + /** + * 检查群组id是否合法 + * + * @param $group + * @return bool + */ + protected static function isValidGroupId($group) + { + if (empty($group)) { + echo new \Exception('group('.var_export($group, true).') empty'); + return false; + } + return true; + } + + /** + * 获取与gateway的连接,用于数据返回 + * + * @param $address + * @return mixed + * @throws Exception + */ + protected static function getGatewayConnection($address) + { + $ttl = 50; + $time = time(); + if (isset(static::$gatewayConnections[$address])) { + $created_time = static::$gatewayConnections[$address]['created_time']; + $connection = static::$gatewayConnections[$address]['connection']; + if ($time - $created_time > $ttl || !is_resource($connection) || feof($connection)) { + \set_error_handler(function () {}); + fclose($connection); + \restore_error_handler(); + unset(static::$gatewayConnections[$address]); + } + } + if (!isset(static::$gatewayConnections[$address])) { + $client = stream_socket_client($address, $errno, $errmsg, static::$connectTimeout); + if (!$client) { + throw new Exception("can not connect to $address $errmsg"); + } + static::$gatewayConnections[$address] = [ + 'created_time' => $time, + 'connection' => $client + ]; + } + $client = static::$gatewayConnections[$address]['connection']; + if (!static::$persistentConnection) { + static::$gatewayConnections = []; + } + return $client; + } +} + +if (!class_exists('\Protocols\GatewayProtocol')) { + class_alias('GatewayWorker\Protocols\GatewayProtocol', 'Protocols\GatewayProtocol'); +} diff --git a/extend/GatewayWorker/Protocols/GatewayProtocol.php b/extend/GatewayWorker/Protocols/GatewayProtocol.php new file mode 100644 index 0000000..6960ab5 --- /dev/null +++ b/extend/GatewayWorker/Protocols/GatewayProtocol.php @@ -0,0 +1,228 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace GatewayWorker\Protocols; + +/** + * Gateway 与 Worker 间通讯的二进制协议 + * + * struct GatewayProtocol + * { + * unsigned int pack_len, + * unsigned char cmd,//命令字 + * unsigned int local_ip, + * unsigned short local_port, + * unsigned int client_ip, + * unsigned short client_port, + * unsigned int connection_id, + * unsigned char flag, + * unsigned short gateway_port, + * unsigned int ext_len, + * char[ext_len] ext_data, + * char[pack_length-HEAD_LEN] body//包体 + * } + * NCNnNnNCnN + */ +class GatewayProtocol +{ + // 发给worker,gateway有一个新的连接 + const CMD_ON_CONNECT = 1; + + // 发给worker的,客户端有消息 + const CMD_ON_MESSAGE = 3; + + // 发给worker上的关闭链接事件 + const CMD_ON_CLOSE = 4; + + // 发给gateway的向单个用户发送数据 + const CMD_SEND_TO_ONE = 5; + + // 发给gateway的向所有用户发送数据 + const CMD_SEND_TO_ALL = 6; + + // 发给gateway的踢出用户 + // 1、如果有待发消息,将在发送完后立即销毁用户连接 + // 2、如果无待发消息,将立即销毁用户连接 + const CMD_KICK = 7; + + // 发给gateway的立即销毁用户连接 + const CMD_DESTROY = 8; + + // 发给gateway,通知用户session更新 + const CMD_UPDATE_SESSION = 9; + + // 获取在线状态 + const CMD_GET_ALL_CLIENT_SESSIONS = 10; + + // 判断是否在线 + const CMD_IS_ONLINE = 11; + + // client_id绑定到uid + const CMD_BIND_UID = 12; + + // 解绑 + const CMD_UNBIND_UID = 13; + + // 向uid发送数据 + const CMD_SEND_TO_UID = 14; + + // 根据uid获取绑定的clientid + const CMD_GET_CLIENT_ID_BY_UID = 15; + + // 批量获取uid列表批量获取绑定的clientid + const CMD_BATCH_GET_CLIENT_ID_BY_UID = 16; + + // 加入组 + const CMD_JOIN_GROUP = 20; + + // 离开组 + const CMD_LEAVE_GROUP = 21; + + // 向组成员发消息 + const CMD_SEND_TO_GROUP = 22; + + // 获取组成员 + const CMD_GET_CLIENT_SESSIONS_BY_GROUP = 23; + + // 获取组在线连接数 + const CMD_GET_CLIENT_COUNT_BY_GROUP = 24; + + // 按照条件查找 + const CMD_SELECT = 25; + + // 获取在线的群组ID + const CMD_GET_GROUP_ID_LIST = 26; + + // 取消分组 + const CMD_UNGROUP = 27; + + // 批量获取群组ID内客户端个数 + const CMD_BATCH_GET_CLIENT_COUNT_BY_GROUP = 28; + + // worker连接gateway事件 + const CMD_WORKER_CONNECT = 200; + + // 心跳 + const CMD_PING = 201; + + // GatewayClient连接gateway事件 + const CMD_GATEWAY_CLIENT_CONNECT = 202; + + // 根据client_id获取session + const CMD_GET_SESSION_BY_CLIENT_ID = 203; + + // 发给gateway,覆盖session + const CMD_SET_SESSION = 204; + + // 当websocket握手时触发,只有websocket协议支持此命令字 + const CMD_ON_WEBSOCKET_CONNECT = 205; + + // 包体是标量 + const FLAG_BODY_IS_SCALAR = 0x01; + + // 通知gateway在send时不调用协议encode方法,在广播组播时提升性能 + const FLAG_NOT_CALL_ENCODE = 0x02; + + /** + * 包头长度 + * + * @var int + */ + const HEAD_LEN = 28; + + public static $empty = array( + 'cmd' => 0, + 'local_ip' => 0, + 'local_port' => 0, + 'client_ip' => 0, + 'client_port' => 0, + 'connection_id' => 0, + 'flag' => 0, + 'gateway_port' => 0, + 'ext_data' => '', + 'body' => '', + ); + + /** + * 返回包长度 + * + * @param string $buffer + * @return int return current package length + */ + public static function input($buffer) + { + if (strlen($buffer) < self::HEAD_LEN) { + return 0; + } + + $data = unpack("Npack_len", $buffer); + return $data['pack_len']; + } + + /** + * 获取整个包的 buffer + * + * @param mixed $data + * @return string + */ + public static function encode($data) + { + $flag = (int)is_scalar($data['body']); + if (!$flag) { + $data['body'] = serialize($data['body']); + } + $data['flag'] |= $flag; + $ext_len = strlen($data['ext_data']??''); + $package_len = self::HEAD_LEN + $ext_len + strlen($data['body']); + return pack("NCNnNnNCnN", $package_len, + $data['cmd'], $data['local_ip'], + $data['local_port'], $data['client_ip'], + $data['client_port'], $data['connection_id'], + $data['flag'], $data['gateway_port'], + $ext_len) . $data['ext_data'] . $data['body']; + } + + /** + * 从二进制数据转换为数组 + * + * @param string $buffer + * @return array + */ + public static function decode($buffer) + { + $data = unpack("Npack_len/Ccmd/Nlocal_ip/nlocal_port/Nclient_ip/nclient_port/Nconnection_id/Cflag/ngateway_port/Next_len", + $buffer); + if ($data['ext_len'] > 0) { + $data['ext_data'] = substr($buffer, self::HEAD_LEN, $data['ext_len']); + if ($data['flag'] & self::FLAG_BODY_IS_SCALAR) { + $data['body'] = substr($buffer, self::HEAD_LEN + $data['ext_len']); + } else { + // 防止反序列化成类实例 + try { + $data['body'] = unserialize(substr($buffer, self::HEAD_LEN + $data['ext_len']), ['allowed_classes' => false]); + } catch (\Throwable $e) {} + } + } else { + $data['ext_data'] = ''; + if ($data['flag'] & self::FLAG_BODY_IS_SCALAR) { + $data['body'] = substr($buffer, self::HEAD_LEN); + } else { + // 防止反序列化成类实例 + try { + $data['body'] = unserialize(substr($buffer, self::HEAD_LEN), ['allowed_classes' => false]); + } catch (\Throwable $e) {} + } + } + return $data; + } +} diff --git a/extend/GatewayWorker/Register.php b/extend/GatewayWorker/Register.php new file mode 100644 index 0000000..da9fbeb --- /dev/null +++ b/extend/GatewayWorker/Register.php @@ -0,0 +1,194 @@ + + * @copyright walkor + * @link http://www.workerman.net/ + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ +namespace GatewayWorker; + +use Workerman\Worker; +use Workerman\Timer; + +/** + * + * 注册中心,用于注册 Gateway 和 BusinessWorker + * + * @author walkor + * + */ +class Register extends Worker +{ + + /** + * 秘钥 + * @var string + */ + public $secretKey = ''; + + /** + * 所有 gateway 的连接 + * + * @var array + */ + protected $_gatewayConnections = array(); + + /** + * 所有 worker 的连接 + * + * @var array + */ + protected $_workerConnections = array(); + + /** + * 进程启动时间 + * + * @var int + */ + protected $_startTime = 0; + + + public function __construct(string $socketName = '', array $contextOption = []) + { + $this->name = 'Register'; + $this->reloadable = false; + parent::__construct($socketName, $contextOption); + } + + /** + * {@inheritdoc} + */ + public function run(): void + { + // 设置 onMessage 连接回调 + $this->onConnect = array($this, 'onConnect'); + + // 设置 onMessage 回调 + $this->onMessage = array($this, 'onMessage'); + + // 设置 onClose 回调 + $this->onClose = array($this, 'onClose'); + + // 记录进程启动的时间 + $this->_startTime = time(); + + // 强制使用text协议 + $this->protocol = '\Workerman\Protocols\Text'; + + // reusePort + $this->reusePort = false; + + // 运行父方法 + parent::run(); + } + + /** + * 设置个定时器,将未及时发送验证的连接关闭 + * + * @param \Workerman\Connection\ConnectionInterface $connection + * @return void + */ + public function onConnect($connection) + { + $connection->timeout_timerid = Timer::add(10, function () use ($connection) { + Worker::log("Register auth timeout (".$connection->getRemoteIp()."). See http://doc2.workerman.net/register-auth-timeout.html"); + $connection->close(); + }, null, false); + } + + /** + * 设置消息回调 + * + * @param \Workerman\Connection\ConnectionInterface $connection + * @param string $buffer + * @return void + */ + public function onMessage($connection, $buffer) + { + // 删除定时器 + Timer::del($connection->timeout_timerid); + $data = @json_decode($buffer, true); + if (empty($data['event'])) { + $error = "Bad request for Register service. Request info(IP:".$connection->getRemoteIp().", Request Buffer:$buffer). See http://doc2.workerman.net/register-auth-timeout.html"; + Worker::log($error); + return $connection->close($error); + } + $event = $data['event']; + $secret_key = isset($data['secret_key']) ? $data['secret_key'] : ''; + // 开始验证 + switch ($event) { + // 是 gateway 连接 + case 'gateway_connect': + if (empty($data['address'])) { + echo "address not found\n"; + return $connection->close(); + } + if ($secret_key !== $this->secretKey) { + Worker::log("Register: Key does not match ".var_export($secret_key, true)." !== ".var_export($this->secretKey, true)); + return $connection->close(); + } + $this->_gatewayConnections[$connection->id] = $data['address']; + $this->broadcastAddresses(); + break; + // 是 worker 连接 + case 'worker_connect': + if ($secret_key !== $this->secretKey) { + Worker::log("Register: Key does not match ".var_export($secret_key, true)." !== ".var_export($this->secretKey, true)); + return $connection->close(); + } + $this->_workerConnections[$connection->id] = $connection; + $this->broadcastAddresses($connection); + break; + case 'ping': + break; + default: + Worker::log("Register unknown event:$event IP: ".$connection->getRemoteIp()." Buffer:$buffer. See http://doc2.workerman.net/register-auth-timeout.html"); + $connection->close(); + } + } + + /** + * 连接关闭时 + * + * @param \Workerman\Connection\ConnectionInterface $connection + */ + public function onClose($connection) + { + // 删除定时器 + Timer::del($connection->timeout_timerid); + if (isset($this->_gatewayConnections[$connection->id])) { + unset($this->_gatewayConnections[$connection->id]); + $this->broadcastAddresses(); + } + if (isset($this->_workerConnections[$connection->id])) { + unset($this->_workerConnections[$connection->id]); + } + } + + /** + * 向 BusinessWorker 广播 gateway 内部通讯地址 + * + * @param \Workerman\Connection\ConnectionInterface $connection + */ + public function broadcastAddresses($connection = null) + { + $data = array( + 'event' => 'broadcast_addresses', + 'addresses' => array_unique(array_values($this->_gatewayConnections)), + ); + $buffer = json_encode($data); + if ($connection) { + $connection->send($buffer); + return; + } + foreach ($this->_workerConnections as $con) { + $con->send($buffer); + } + } +}