thinkphp5+GatewayWorker+Workerman聊天室,可以多人聊天,指定某个人进行聊天,还可以切换聊天房间

thinkphp5+GatewayWorker+Workerman聊天室,可以多人聊天,指定某个人进行聊天,还可以切换聊天房间
Windows版安装
a) 安装thinkphp5;

  1. composer create-project topthink/think tp5  --prefer-dist

复制代码

 

b) 进入tp5的目录,安装Windows版本的workerman;

  1. composer require workerman/workerman-for-win

复制代码

 

c} 安装Windows版本的gateway;

  1. composer require workerman/gateway-worker-for-win

复制代码

 

开始关键部分,服务端实现

控制器 控制器:app\index\controller\Sregister

  1. <?php
  2. namespace app\index\controller;
  3.  
  4. use Workerman\Worker;
  5. use GatewayWorker\Register;
  6.  
  7. class Sregister{
  8.  
  9.     public function __construct(){
  10.         // register 服务必须是text协议
  11.         $register = new Register('text://0.0.0.0:1236');
  12.         
  13.         // 如果不是在根目录启动,则运行runAll方法
  14.         if(!defined('GLOBAL_START'))
  15.         {
  16.             Worker::runAll();
  17.         }
  18.     }
  19. }

复制代码

 

控制器:app\index\controller\Sgateway

  1. <?php
  2. namespace app\index\controller;
  3.  
  4. use Workerman\Worker;
  5. use GatewayWorker\Gateway;
  6. use Workerman\Autoloader;
  7.  
  8. class Sgateway{
  9.     public function __construct(){
  10.         // gateway 进程
  11.         $gateway = new Gateway("Websocket://0.0.0.0:7272");
  12.         // 设置名称,方便status时查看
  13.         $gateway->name = 'ChatGateway';
  14.         // 设置进程数,gateway进程数建议与cpu核数相同
  15.         $gateway->count = 4;
  16.         // 分布式部署时请设置成内网ip(非127.0.0.1)
  17.         $gateway->lanIp = '127.0.0.1';
  18.         // 内部通讯起始端口,假如$gateway->count=4,起始端口为4000
  19.         // 则一般会使用4000 4001 4002 4003 4个端口作为内部通讯端口
  20.         $gateway->startPort = 2300;
  21.         // 心跳间隔
  22.         $gateway->pingInterval = 10;
  23.         // 心跳数据
  24.         $gateway->pingData = '{"type":"ping"}';
  25.         // 服务注册地址
  26.         $gateway->registerAddress = '127.0.0.1:1236';
  27.         
  28.         /*
  29.          // 当客户端连接上来时,设置连接的onWebSocketConnect,即在websocket握手时的回调
  30.          $gateway->onConnect = function($connection)
  31.          {
  32.          $connection->onWebSocketConnect = function($connection , $http_header)
  33.          {
  34.          // 可以在这里判断连接来源是否合法,不合法就关掉连接
  35.          // $_SERVER['HTTP_ORIGIN']标识来自哪个站点的页面发起的websocket链接
  36.          if($_SERVER['HTTP_ORIGIN'] != 'http://chat.workerman.net')
  37.          {
  38.          $connection->close();
  39.          }
  40.          // onWebSocketConnect 里面$_GET $_SERVER是可用的
  41.          // var_dump($_GET, $_SERVER);
  42.          };
  43.          };
  44.          */
  45.         
  46.         // 如果不是在根目录启动,则运行runAll方法
  47.         if(!defined('GLOBAL_START'))
  48.         {
  49.             Worker::runAll();
  50.         }
  51.         
  52.         
  53.     }
  54. }

复制代码

 

控制器:app\index\controller\Sbusinessworker

  1. <?php
  2. namespace app\index\controller;
  3.  
  4. use Workerman\Worker;
  5. use GatewayWorker\BusinessWorker;
  6. use Workerman\Autoloader;
  7.  
  8. class Sbusinessworker{
  9.     public function __construct(){
  10.         // bussinessWorker 进程
  11.         $worker = new BusinessWorker();
  12.         // worker名称
  13.         $worker->name = 'ChatBusinessWorker';
  14.         // bussinessWorker进程数量
  15.         $worker->count = 4;
  16.         // 服务注册地址
  17.         $worker->registerAddress = '127.0.0.1:1236';
  18.         //设置处理业务的类,此处制定Events的命名空间
  19.         $worker->eventHandler = 'app\index\controller\Events';
  20.         
  21.         // 如果不是在根目录启动,则运行runAll方法
  22.         if(!defined('GLOBAL_START'))
  23.         {
  24.             Worker::runAll();
  25.         }
  26.     }
  27. }

复制代码

 

控制器:app\index\controller\Events

  1. <?php
  2. namespace app\index\controller;
  3.  
  4.  
  5. /**
  6.  * 用于检测业务代码死循环或者长时间阻塞等问题
  7.  * 如果发现业务卡死,可以将下面declare打开(去掉//注释),并执行php start.php reload
  8.  * 然后观察一段时间workerman.log看是否有process_timeout异常
  9.  */
  10. //declare(ticks=1);
  11.  
  12. /**
  13.  * 聊天主逻辑
  14.  * 主要是处理 onMessage onClose 
  15.  */
  16. use \GatewayWorker\Lib\Gateway;
  17.  
  18. class Events
  19. {
  20.    /**
  21.     * 有消息时
  22.     * @param int $client_id
  23.     * @param mixed $message
  24.     */
  25.    public static function onMessage($client_id, $message)
  26.    {
  27.         // debug
  28.         echo "client:{$_SERVER['REMOTE_ADDR']}:{$_SERVER['REMOTE_PORT']} gateway:{$_SERVER['GATEWAY_ADDR']}:{$_SERVER['GATEWAY_PORT']}  client_id:$client_id session:".json_encode($_SESSION)." onMessage:".$message."\n";
  29.         
  30.         // 客户端传递的是json数据
  31.         $message_data = json_decode($message, true);
  32.         if(!$message_data)
  33.         {
  34.             return ;
  35.         }
  36.         
  37.         // 根据类型执行不同的业务
  38.         switch($message_data['type'])
  39.         {
  40.             // 客户端回应服务端的心跳
  41.             case 'pong':
  42.                 return;
  43.             // 客户端登录 message格式: {type:login, name:xx, room_id:1} ,添加到客户端,广播给所有客户端xx进入聊天室
  44.             case 'login':
  45.                 // 判断是否有房间号
  46.                 if(!isset($message_data['room_id']))
  47.                 {
  48.                     throw new \Exception("\$message_data['room_id'] not set. client_ip:{$_SERVER['REMOTE_ADDR']} \$message:$message");
  49.                 }
  50.                 
  51.                 // 把房间号昵称放到session中
  52.                 $room_id = $message_data['room_id'];
  53.                 $client_name = htmlspecialchars($message_data['client_name']);
  54.                 $_SESSION['room_id'] = $room_id;
  55.                 $_SESSION['client_name'] = $client_name;
  56.               
  57.                 // 获取房间内所有用户列表 
  58.                 $clients_list = Gateway::getClientSessionsByGroup($room_id);
  59.                 foreach($clients_list as $tmp_client_id=>$item)
  60.                 {
  61.                     $clients_list[$tmp_client_id] = $item['client_name'];
  62.                 }
  63.                 $clients_list[$client_id] = $client_name;
  64.                 
  65.                 // 转播给当前房间的所有客户端,xx进入聊天室 message {type:login, client_id:xx, name:xx} 
  66.                 $new_message = array('type'=>$message_data['type'], 'client_id'=>$client_id, 'client_name'=>htmlspecialchars($client_name), 'time'=>date('Y-m-d H:i:s'));
  67.                 Gateway::sendToGroup($room_id, json_encode($new_message));
  68.                 Gateway::joinGroup($client_id, $room_id);
  69.                
  70.                 // 给当前用户发送用户列表 
  71.                 $new_message['client_list'] = $clients_list;
  72.                 Gateway::sendToCurrentClient(json_encode($new_message));
  73.                 return;
  74.                 
  75.             // 客户端发言 message: {type:say, to_client_id:xx, content:xx}
  76.             case 'say':
  77.                 // 非法请求
  78.                 if(!isset($_SESSION['room_id']))
  79.                 {
  80.                     throw new \Exception("\$_SESSION['room_id'] not set. client_ip:{$_SERVER['REMOTE_ADDR']}");
  81.                 }
  82.                 $room_id = $_SESSION['room_id'];
  83.                 $client_name = $_SESSION['client_name'];
  84.                 
  85.                 // 私聊
  86.                 if($message_data['to_client_id'] != 'all')
  87.                 {
  88.                     $new_message = array(
  89.                         'type'=>'say',
  90.                         'from_client_id'=>$client_id, 
  91.                         'from_client_name' =>$client_name,
  92.                         'to_client_id'=>$message_data['to_client_id'],
  93.                         'content'=>"<b>对你说: </b>".nl2br(htmlspecialchars($message_data['content'])),
  94.                         'time'=>date('Y-m-d H:i:s'),
  95.                     );
  96.                     Gateway::sendToClient($message_data['to_client_id'], json_encode($new_message));
  97.                     $new_message['content'] = "<b>你对".htmlspecialchars($message_data['to_client_name'])."说: </b>".nl2br(htmlspecialchars($message_data['content']));
  98.                     return Gateway::sendToCurrentClient(json_encode($new_message));
  99.                 }
  100.                 
  101.                 $new_message = array(
  102.                     'type'=>'say', 
  103.                     'from_client_id'=>$client_id,
  104.                     'from_client_name' =>$client_name,
  105.                     'to_client_id'=>'all',
  106.                     'content'=>nl2br(htmlspecialchars($message_data['content'])),
  107.                     'time'=>date('Y-m-d H:i:s'),
  108.                 );
  109.                 return Gateway::sendToGroup($room_id ,json_encode($new_message));
  110.         }
  111.    }
  112.    
  113.    /**
  114.     * 当客户端断开连接时
  115.     * @param integer $client_id 客户端id
  116.     */
  117.    public static function onClose($client_id)
  118.    {
  119.        // debug
  120.        echo "client:{$_SERVER['REMOTE_ADDR']}:{$_SERVER['REMOTE_PORT']} gateway:{$_SERVER['GATEWAY_ADDR']}:{$_SERVER['GATEWAY_PORT']}  client_id:$client_id onClose:''\n";
  121.        
  122.        // 从房间的客户端列表中删除
  123.        if(isset($_SESSION['room_id']))
  124.        {
  125.            $room_id = $_SESSION['room_id'];
  126.            $new_message = array('type'=>'logout', 'from_client_id'=>$client_id, 'from_client_name'=>$_SESSION['client_name'], 'time'=>date('Y-m-d H:i:s'));
  127.            Gateway::sendToGroup($room_id, json_encode($new_message));
  128.        }
  129.    }
  130.   
  131. }

复制代码

 

代码目录截图


然后在项目根目录 新增入口文件 start_register.php 、start_gateway.php 、start_businessworker.php三个入口文件

文件:start_register.php

  1. <?php 
  2. define('APP_PATH', __DIR__ . '/application/');
  3. define('BIND_MODULE','index/Sregister');
  4. // 加载框架引导文件
  5. require __DIR__ . '/thinkphp/start.php';

复制代码

 

文件:start_gateway.php

  1. <?php
  2. define('APP_PATH', __DIR__ . '/application/');
  3. define('BIND_MODULE','index/Sgateway');
  4. // 加载框架引导文件
  5. require __DIR__ . '/thinkphp/start.php';

复制代码

 

文件:start_businessworker.php

  1. <?php
  2. define('APP_PATH', __DIR__ . '/application/');
  3. define('BIND_MODULE','index/Sbusinessworker');
  4. // 加载框架引导文件
  5. require __DIR__ . '/thinkphp/start.php';

复制代码

 

[b]由于PHP-CLI在windows系统无法实现多进程以及守护进程,所以只能把三个文件放到bat文件,然后双击启动

bat文件:start_for_win.bat

  1. php start_register.php start_gateway.php start_businessworker.phppause

复制代码

 

代码目录截图


启动程序

在项目的根目录下双击启动 start_for_win.bat


服务端实现
view : tp5\application\index\view\index.html

  1. <html><head>
  2.   <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  3.   <title>workerman-chat PHP聊天室 Websocket(HTLM5/Flash)+PHP多进程socket实时推送技术</title>
  4.   <script type="text/javascript">
  5.   //WebSocket = null;
  6.   </script>
  7.   <link href="__PUBLIC__/chat/css/bootstrap.min.css" rel="stylesheet">
  8.   <link href="__PUBLIC__/chat/css/style.css" rel="stylesheet">
  9.   <!-- Include these three JS files: -->
  10.   <script type="text/javascript" src="__PUBLIC__/chat/js/swfobject.js"></script>
  11.   <script type="text/javascript" src="__PUBLIC__/chat/js/web_socket.js"></script>
  12.   <script type="text/javascript" src="__PUBLIC__/chat/js/jquery.min.js"></script>
  13.  
  14.   <script type="text/javascript">
  15.     if (typeof console == "undefined") {    this.console = { log: function (msg) {  } };}
  16.     // 如果浏览器不支持websocket,会使用这个flash自动模拟websocket协议,此过程对开发者透明
  17.     WEB_SOCKET_SWF_LOCATION = "__PUBLIC__/chat/swf/WebSocketMain.swf";
  18.     // 开启flash的websocket debug
  19.     WEB_SOCKET_DEBUG = true;
  20.       
  21.     var ws, name, client_list={};
  22.  
  23.     // 连接服务端
  24.     function connect() {
  25.        // 创建websocket
  26.        ws = new WebSocket("ws://"+document.domain+":7272");
  27.        // 当socket连接打开时,输入用户名
  28.        ws.onopen = onopen;
  29.        // 当有消息时根据消息类型显示不同信息
  30.        ws.onmessage = onmessage; 
  31.        ws.onclose = function() {
  32.           console.log("连接关闭,定时重连");
  33.           connect();
  34.        };
  35.        ws.onerror = function() {
  36.            console.log("出现错误");
  37.        };
  38.     }
  39.  
  40.     // 连接建立时发送登录信息
  41.     function onopen()
  42.     {
  43.         if(!name)
  44.         {
  45.             show_prompt();
  46.         }
  47.         // 登录
  48.         var login_data = '{"type":"login","client_name":"'+name.replace(/"/g, '\\"')+'","room_id":"<?php echo isset($_GET['room_id']) ? $_GET['room_id'] : 1?>"}';
  49.         console.log("websocket握手成功,发送登录数据:"+login_data);
  50.         ws.send(login_data);
  51.     }
  52.  
  53.     // 服务端发来消息时
  54.     function onmessage(e)
  55.     {
  56.         console.log(e.data);
  57.         var data = eval("("+e.data+")");
  58.         switch(data['type']){
  59.             // 服务端ping客户端
  60.             case 'ping':
  61.                 ws.send('{"type":"pong"}');
  62.                 break;;
  63.             // 登录 更新用户列表
  64.             case 'login':
  65.                 //{"type":"login","client_id":xxx,"client_name":"xxx","client_list":"[...]","time":"xxx"}
  66.                 say(data['client_id'], data['client_name'],  data['client_name']+' 加入了聊天室', data['time']);
  67.                 if(data['client_list'])
  68.                 {
  69.                     client_list = data['client_list'];
  70.                 }
  71.                 else
  72.                 {
  73.                     client_list[data['client_id']] = data['client_name']; 
  74.                 }
  75.                 flush_client_list();
  76.                 console.log(data['client_name']+"登录成功");
  77.                 break;
  78.             // 发言
  79.             case 'say':
  80.                 //{"type":"say","from_client_id":xxx,"to_client_id":"all/client_id","content":"xxx","time":"xxx"}
  81.                 say(data['from_client_id'], data['from_client_name'], data['content'], data['time']);
  82.                 break;
  83.             // 用户退出 更新用户列表
  84.             case 'logout':
  85.                 //{"type":"logout","client_id":xxx,"time":"xxx"}
  86.                 say(data['from_client_id'], data['from_client_name'], data['from_client_name']+' 退出了', data['time']);
  87.                 delete client_list[data['from_client_id']];
  88.                 flush_client_list();
  89.         }
  90.     }
  91.  
  92.     // 输入姓名
  93.     function show_prompt(){  
  94.         name = prompt('输入你的名字:', '');
  95.         if(!name || name=='null'){  
  96.             name = '游客';
  97.         }
  98.     }
  99.  
  100.     // 提交对话
  101.     function onSubmit() {
  102.       var input = document.getElementById("textarea");
  103.       var to_client_id = $("#client_list option:selected").attr("value");
  104.       var to_client_name = $("#client_list option:selected").text();
  105.       ws.send('{"type":"say","to_client_id":"'+to_client_id+'","to_client_name":"'+to_client_name+'","content":"'+input.value.replace(/"/g, '\\"').replace(/\n/g,'\\n').replace(/\r/g, '\\r')+'"}');
  106.       input.value = "";
  107.       input.focus();
  108.     }
  109.  
  110.     // 刷新用户列表框
  111.     function flush_client_list(){
  112.         var userlist_window = $("#userlist");
  113.         var client_list_slelect = $("#client_list");
  114.         userlist_window.empty();
  115.         client_list_slelect.empty();
  116.         userlist_window.append('<h4>在线用户</h4><ul>');
  117.         client_list_slelect.append('<option value="all" id="cli_all">所有人</option>');
  118.         for(var p in client_list){
  119.             userlist_window.append('<li id="'+p+'">'+client_list[p]+'</li>');
  120.             client_list_slelect.append('<option value="'+p+'">'+client_list[p]+'</option>');
  121.         }
  122.         $("#client_list").val(select_client_id);
  123.         userlist_window.append('</ul>');
  124.     }
  125.  
  126.     // 发言
  127.     function say(from_client_id, from_client_name, content, time){
  128.         $("#dialog").append('<div class="speech_item"><img src="http://lorempixel.com/38/38/?'+from_client_id+'" class="user_icon" /> '+from_client_name+' <br> '+time+'<div style="clear:both;"></div><p class="triangle-isosceles top">'+content+'</p> </div>');
  129.     }
  130.  
  131.     $(function(){
  132.         select_client_id = 'all';
  133.         $("#client_list").change(function(){
  134.              select_client_id = $("#client_list option:selected").attr("value");
  135.         });
  136.     });
  137.   </script>
  138. </head>
  139. <body onload="connect();">
  140.     <div class="container">
  141.         <div class="row clearfix">
  142.             <div class="col-md-1 column">
  143.             </div>
  144.             <div class="col-md-6 column">
  145.                <div class="thumbnail">
  146.                    <div class="caption" id="dialog"></div>
  147.                </div>
  148.                <form onsubmit="onSubmit(); return false;">
  149.                     <select style="margin-bottom:8px" id="client_list">
  150.                         <option value="all">所有人</option>
  151.                     </select>
  152.                     <textarea class="textarea thumbnail" id="textarea"></textarea>
  153.                     <div class="say-btn"><input type="submit" class="btn btn-default" value="发表" /></div>
  154.                </form>
  155.                <div>
  156.                    <b>房间列表:</b>(当前在 房间<?php echo isset($_GET['room_id'])&&intval($_GET['room_id'])>0 ? intval($_GET['room_id']):1; ?>)<br>
  157.                    <a href="/?room_id=1">房间1</a>    <a href="/?room_id=2">房间2</a>    <a href="/?room_id=3">房间3</a>    <a href="/?room_id=4">房间4</a>
  158.                <br><br>
  159.                </div>
  160.                <p class="cp">PHP多进程+Websocket(HTML5/Flash)+PHP Socket实时推送技术    Powered by <a href="http://www.workerman.net/workerman-chat" target="_blank">workerman-chat</a></p>
  161.             </div>
  162.             <div class="col-md-3 column">
  163.                <div class="thumbnail">
  164.                    <div class="caption" id="userlist"></div>
  165.                </div>
  166.               
  167.             </div>
  168.         </div>
  169.     </div>
  170.     <script type="text/javascript">var _bdhmProtocol = (("https:" == document.location.protocol) ? " https://" : " http://");document.write(unescape("%3Cscript src='" + _bdhmProtocol + "hm.baidu.com/h.js%3F7b1919221e89d2aa5711e4deb935debd' type='text/javascript'%3E%3C/script%3E"));</script>
  171. </body>
  172. </html>

复制代码

 

运行结果截图


linux版安装
a) 安装thinkphp5;

  1. composer create-project topthink/think tp5  --prefer-dist

复制代码

 

b) 进入tp5的目录,安装linux版本的workerman;

  1. composer require topthink/think-worker

复制代码

 

c} 安装linux版本的gateway;

  1. composer require workerman/gateway-worker-for-win

复制代码

 

  1. 关键部分,服务端实现

复制代码

 

控制器 app\index\controller\Gate

  1. <?php 
  2. /**
  3.  * linux workerman例子测试
  4.  * 需要在Linux系统控制台进行启动,启动文件位于根目录的start.php文件中
  5.  * Windows无法进行同时启动多个协议
  6.  * 由于PHP-CLI在windows系统无法实现多进程以及守护进程,所以windows版本Workerman建议仅作开发调试使用。
  7.  */
  8. namespace app\index\controller;
  9.  
  10. use Workerman\Worker;
  11. use GatewayWorker\Gateway;
  12. use GatewayWorker\Register;
  13. use GatewayWorker\BusinessWorker;
  14.  
  15.  
  16. class Gate
  17. {
  18.     /**
  19.      * 构造函数
  20.      * @access public
  21.      */
  22.     public function __construct(){
  23.         
  24.         //初始化各个GatewayWorker
  25.         //初始化register register 服务必须是text协议
  26.         $register = new Register('text://0.0.0.0:1236');
  27.     
  28.         //初始化 bussinessWorker 进程
  29.         $worker = new BusinessWorker();
  30.         // worker名称
  31.         $worker->name = 'ChatBusinessWorker';
  32.         // bussinessWorker进程数量
  33.         $worker->count = 4;
  34.         // 服务注册地址
  35.         $worker->registerAddress = '127.0.0.1:1236';
  36.         //设置处理业务的类,此处制定Events的命名空间
  37.         $worker->eventHandler = 'app\index\controller\Events';
  38.         // 初始化 gateway 进程
  39.         $gateway = new Gateway("websocket://0.0.0.0:7272");
  40.         // 设置名称,方便status时查看
  41.         $gateway->name = 'ChatGateway';
  42.         $gateway->count = 4;
  43.         // 分布式部署时请设置成内网ip(非127.0.0.1)
  44.         $gateway->lanIp = '127.0.0.1';
  45.         // 内部通讯起始端口,假如$gateway->count=4,起始端口为4000
  46.         // 则一般会使用4000 4001 4002 4003 4个端口作为内部通讯端口
  47.         $gateway->startPort = 2300;
  48.         // 心跳间隔
  49.         $gateway->pingInterval = 10;
  50.         // 心跳数据
  51.         $gateway->pingData = '{"type":"ping"}';
  52.         // 服务注册地址
  53.         $gateway->registerAddress = '127.0.0.1:1236';
  54.     
  55.         //运行所有Worker;
  56.         Worker::runAll();
  57.     }
  58. }

复制代码

 

入口文件
文件: start.php

  1. <?php
  2. /**
  3.  * workerman + GatewayWorker
  4.  * 此文件只能在Linux运行
  5.  * run with command
  6.  * php start.php start
  7.  */
  8. ini_set('display_errors', 'on');
  9. if(strpos(strtolower(PHP_OS), 'win') === 0)
  10. {
  11.     exit("start.php not support windows.\n");
  12. }
  13. //检查扩展
  14. if(!extension_loaded('pcntl'))
  15. {
  16.     exit("Please install pcntl extension. See http://doc3.workerman.net/appendices/install-extension.html\n");
  17. }
  18. if(!extension_loaded('posix'))
  19. {
  20.     exit("Please install posix extension. See http://doc3.workerman.net/appendices/install-extension.html\n");
  21. }
  22.  
  23. define('APP_PATH', __DIR__ . '/application/');
  24. define('BIND_MODULE','chat/Gate');
  25. // 加载框架引导文件
  26. require __DIR__ . '/thinkphp/start.php';

复制代码

 

启动程序

  1. php start.php start

复制代码

 

客户端跟Windows一样就可以了

workerman官网:http://www.workerman.net/
workerman文档:http://doc3.workerman.net/
GatewayWorker文档:http://doc3.workerman.net/

附件 tp5.zip ( 837.52 KB 下载:905 次 )

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值