ubuntu部署Guacamole,Guacamole集成springboot,vue

前言

本文主要介绍的是如何docker安装Guacamole,进行浏览器远程桌面的访问。

Guacamole

Apache Guacamole 是一个无客户端远程桌面网关。它支持标准协议,如 VNC、RDP 和 SSH。我们称之为无客户端,因为不需要插件或客户端软件。
用户使用他们的网络浏览器连接到 Guacamole 服务器。用 JavaScript 编写的 Guacamole 客户端由 Guacamole 服务器内的网络服务器提供给用户。加载后,此客户端使用 Guacamole 协议通过 HTTP 连接回服务器。部署到 Guacamole 服务器的 Web 应用程序读取 Guacamole 协议并将其转发到 guacd,即原生的 Guacamole 代理。这个代理实际上解释了 Guacamole 协议的内容,代表用户连接到任意数量的远程桌面服务器。Guacamole 协议与 guacd 结合提供了协议不可知性:Guacamole 客户端和 Web 应用程序都不需要知道实际使用的是什么远程桌面协议

协议

Web 应用程序根本不了解任何远程桌面协议。它不包含对 VNC 或 RDP 或由 Guacamole堆栈支持的任何其他协议的支持。它其实只懂Guacamole协议,这是一个用于远程显示渲染和事件传输的协议。虽然具有这些属性的协议自然具有与远程桌面协议相同的能力,但远程桌面协议和 Guacamole 协议背后的设计原则是不同的:Guacamole 协议并非旨在实现特定桌面环境的功能。
作为远程显示和交互协议,Guacamole 实现了现有远程桌面协议的超集。因此,向 Guacamole 添加对特定远程桌面协议(如RDP)的支持涉及编写一个中间层,在远程桌面协议和 Guacamole 协议之间进行“转换”。实现这样的转换与实现任何本地客户端没有什么不同,只是这个特定的实现呈现到远程显示器而不是本地显示器。
处理这种翻译的中间层是 guacd

##guacd

guacd 是 Guacamole 的核心,它动态加载对远程桌面协议(称为“客户端插件”)的支持,并根据从 Web 应用程序收到的指令将它们连接到远程桌面。 guacd 是一个守护进程,它与 Guacamole 一起安装并在后台运行,监听来自 Web 应用程序的 TCP 连接。guacd 也不理解任何特定的远程桌面协议,而是实现了足够的 Guacamole 协议来确定需要加载哪些协议支持以及必须将哪些参数传递给它。加载客户端插件后,它会独立于 guacd 运行,并完全控制自身与 Web 应用程序之间的通信,直到客户端插件终止。 guacd 和所有客户端插件都依赖于一个公共库 libguac,这使得通过 Guacamole
协议进行的通信更容易也更抽象一些。

UltraVNC

UltraVNC是一款功能强大、易于使用且免费的远程 PC 访问软件,它可以在您自己的屏幕上显示另一台计算机的屏幕(通过互联网或网络)。该程序允许您使用鼠标和键盘远程控制另一台 PC。这意味着您可以在远程计算机上工作,就好像您坐在它前面一样,就在您当前的位置。

VNC,即远程帧缓冲协议 (RFB),允许通过 Internet 远程查看和控制桌面。VNC 服务器必须运行在共享桌面的计算机上,VNC 客户端必须运行在将访问共享桌面的计算机上

安装 Guacamole 所需的环境

本文编写环境: window11专业版 + VMware + Ubuntu官网最新版镜像虚拟机( 22.04 LTS)

docker安装教程: https://www.runoob.com/docker/ubuntu-docker-install.html

官网使用docker安装: https://guacamole.apache.org/doc/gug/guacamole-docker.html

docker推荐使用官方安装脚本自动安装

curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun

输入以上命令即可

接下来就可以输入docker的命令进行测试了

安装Guacamole

安装大体可以分为4部分:

  1. 安装Guacamole镜像 + 需要的数据库镜像(mysql/PostgreSQL)
  2. 由Guacamole镜像生成容器
  3. 初始化数据库 + 关联各个容器
  4. 启动容器
安装Guacamole镜像
docker pull guacamole/guacd
docker pull guacamole/guacamole
docker pull mysql

通过docker images命令可以看到目前已经有了3个镜像

guacd容器初始化
docker run --name guacd -d guacamole/guacd

guacd 将在其默认端口 4822 上进行侦听

mysql容器初始化
docker run -itd --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql
  • -p 3306:3306 :映射容器服务的 3306 端口到宿主机的 3306 端口,外部主机可以直接通过 宿主机ip:3306 访问到 MySQL 的服务。
  • MYSQL_ROOT_PASSWORD=123456:设置 MySQL 服务 root 用户的密码。

通过 docker ps 命令查看mysqlguacd是否安装成功

由于Guacamole需要使用数据库来进行身份验证,所以我们还需要对mysql数据写入一个供guacamole访问的用户和数据库

guacamole提供一个生成自己所需的sql文件供我们使用,使用以下的命令:

docker run --rm guacamole/guacamole /opt/guacamole/bin/initdb.sh --mysql > initdb.sql

此命令会在你当前的文件夹下生成一个initdb.sql文件,比如你在桌面目录下执行的,那么你桌面上就会生成这样的一个文件

生成此脚本后,我们需要完成:

  1. 为 Guacamole 创建一个数据库,例如guacamole_db.
  2. 为 Guacamole 创建一个可以访问此数据库的用户,例如guacamole_user.
  3. 在新创建的数据库上运行脚本

现在我们进入数据库:

//进入mysql容器
docker exec -it mysql /bin/bash
//进入数据库
mysql -uroot -p123456
//创建数据库
CREATE DATABASE guacamole_db;
//创建供Guacamole访问的用户,并赋予权限
//这里因为我们的mysql是用的容器,如果用@'localhost'到后面我们用宿主机进入后台的时候,是无法访问的,我们应该用'%'这个权限
//如果你的mysql是用的宿主机的mysql,那么可以试试localhost
CREATE USER 'guacamole_user'@'%' IDENTIFIED BY 'some_password';
GRANT SELECT,INSERT,UPDATE,DELETE ON guacamole_db.* TO 'guacamole_user'@'%';
//刷新用户
FLUSH PRIVILEGES;
//退出数据库
exit
//退出mysql容器内部
exit
//执行把宿主机的sql文件copy到mysql容器内部
//docker cp 要拷贝的文件路径 容器名:要拷贝到容器里面对应的路径
//我这里是因为我的文件就在桌面,如果你在其他目录下,填写路径就可以比如: /home/initdb.sql
docker cp initdb.sql mysql:/
//这个时候进入mysql容器查看文件
docker exec -it mysql bash
//此时应该就有这个sql文件了
ls
//执行mysql执行sql脚本
//语法: mysql –u用户名 –p密码 –D数据库 <  [sql脚本文件路径全名]
mysql -uroot -p123456 -Dguacamole_db<initdb.sql
//查看脚本是否执行成功
//进入数据库
mysql -uroot -p123456
//查看数据库
show databases;
//进入guacamole_db数据库
use guacamole_db;
//查看guacamole_db表
show tables;
//如果看到十几个表就证明执行成功了
//退出mysql和容器
exit
exit
guacamole容器初始化
//请先不要执行,先看参数解析
docker run --name guacamole --restart=always  --link guacd:guacd --link mysql:mysql -e MYSQL_DATABASE=guacamole_db -e MYSQL_USER=guacamole_user -e MYSQL_PASSWORD=some_password -d -p 9090:8080 guacamole/guacamole
  1. --link guacd:guacd 使Guacamole容器和guacd容器关联,这个地方冒号前面的guacd是你创建这个容器时指定的name参数
  2. --link mysql:mysql 使Guacamole容器和mysql容器管理,这个地方冒号前面的guacd是你创建这个容器时指定的name参数
  3. -e MYSQL_DATABASE=guacamole_db 上面初始化数据库时,创建的guacamole_db数据库
  4. -e MYSQL_USER=guacamole_user 上面初始化用户时,创建的guacamole_user用户
  5. -e MYSQL_PASSWORD=song_password 上面初始化用户密码时,创建的some_password密码
  6. -p 9090:8080 绑定宿主机的9090端口

如果上面的操作中,创建的用户名、数据库名、密码不同的话请自行更改

其他额外的一些参数,如果严格按照上文的步骤是不需要的:

MYSQL_HOSTNAME 用于 Guacamole 身份验证的数据库的主机名。如果您不使用 Docker 来提供 MySQL 数据库,则这是必需的

MYSQL_PORT Guacamole 在连接到 MySQL 时应该使用的端口。此环境变量是可选的。如果未提供,将使用标准 MySQL 端口 3306

此时可以执行docker ps 查看三个容器是否都正常执行

如果都启动成功了,那么就可以在宿主机上打开浏览器输入http://localhost:9090/guacamole,来进入后台管理页面,默认账号密码都是guacadmin

常见打不开后台报错

  • 如果3个容器全部都正常启动,那么还是访问不了后台,比如我们访问localhost:9090也没有显示tomcat的404的标志,那可能是宿主机的端口映射没有被容器的端口映射上,如果你也是虚拟机环境,试试重启一下

重启之后我们需要重启启动容器

docker ps -a
docker start mysql
docker start guacd
docker start guacamole
docker ps -a
  • 如果不能访问后台,我们需要先检查数据库的初始化表是否都正常初始化,用我们创建的用户,登录一遍mysql看看是否正确

如果mysql没有问题,那么我们就需要看看guacamole容器的报错日志

docker logs 容器id

看看日志里面有没有什么报错,比如无法连接数据库之类的问题

  • Cause: java.sql.SQLException: Access denied for user ‘guacamole_user’@‘172.17.0.4’ (using password: YES)

解决:如果在mysql创建用户的时候,权限使用的localhost的话,使用docker-mysql容器连接的话,是代表不了localhost的,需要使用%

  • 如果我们挂起了虚拟机,然后再次使用的时候,会发现打不开后台了,这个时候也是需要重新启动下虚拟机,然后重启下容器即可

##使用DRP方式测试远程连接

云服务器配置:win10服务器版本 非window版本可以试试SSH连接

点击设置

创建一个RDP连接

编辑连接:

  • 名称:随便起
  • 位置:用默认的root就行
  • 协议:RDP

参数:

​ 网络:

  • 主机名: 要被连接的主机ip
  • 端口:支持RDP连接的window端口都默认是3389

认证:

  • 用户名:远程服务器的用户名,云服务器默认用户名为: Administrator
  • 密码:服务器密码
  • 忽略服务器证书:这个要勾上

直接到最下面保存即可

来到首页,找到我们刚刚新建的连接,双击即可,等待连接成功

使用VNC方式测试远程连接

云服务器环境:window10服务器版本 非window版本可以试试SSH连接

创建一个VNC连接

编辑连接:

  • 名称:随便起
  • 位置:用默认的root就行
  • 协议:VNC

参数:

​ 网络:

  • 主机名: 要被连接的主机ip
  • 端口:安装的VNCServe 默认端口是5900

认证:

  • 用户名:远程服务器的用户名,云服务器默认用户名为: Administrator
  • 密码:安装的VNCServe里面的VNC密码

直接到最下面保存即可

这个时候我们只是创建了一个连接,我们还需要在被远程的机器上安装UltraVNC工具,下文有一章会介绍如何安装使用

使用DRP方式连接内网window机器

测试window版本:win10 + 专业版本

创建一个DRP连接

编辑连接:

  • 名称:随便起
  • 位置:用默认的root就行
  • 协议:DRP

参数:

​ 网络:

  • 主机名: 要被连接的主机ip
  • 端口:默认3389
  • 忽略服务器证书:这个要勾上

认证:

  • 用户名:电脑用户名,
  • 密码:电脑密码

直接到最下面保存即可

这个地方如果你要访问的机器没有用户名和密码全部空着即可

如果访问不通,请把被访问机器里面的防火墙关掉或者把3389这个端口开通

打开被访问机器里面关于运行被远程控制等选项

win10家庭版不支持远程,所以无法使用DRP和VNC方式

如果有绑定微软账号,那么用户名其实是你微软账号的用户名,密码同理,如果你电脑开通了PIN登录,但是你需要输入的密码还是微软账号的密码

如果我们附加没有第二台机器供我们测试,我们可以测试用虚拟机里面的机器远程连接我们本机,PRD的方式会在连接成功后的几秒显示有冲突而退出PRD,VNC方式可以套娃一直连接

使用VNC方式连接内网window机器

测试window版本:win10 + 专业版本

创建一个VNC连接

编辑连接:

  • 名称:随便起
  • 位置:用默认的root就行
  • 协议:VNC

参数:

​ 网络:

  • 主机名: 要被连接的主机ip
  • 端口:安装的VNCServe 默认端口是5900

认证:

  • 用户名:电脑用户名,
  • 密码:安装的VNCServe里面的VNC密码

直接到最下面保存即可

这个地方如果你要访问的机器没有用户名和密码全部空着即可

如果访问不通,请把被访问机器里面的防火墙关掉或者把3389这个端口开通

打开被访问机器里面关于运行被远程控制等选项

win10家庭版不支持远程,所以无法使用DRP和VNC方式

如果有绑定微软账号,那么用户名其实是你微软账号的用户名,密码同理,如果你电脑开通了PIN登录,但是你需要输入的密码还是微软账号的密码

如果我们附近没有第二台机器供我们测试,我们可以测试用虚拟机里面的机器远程连接我们本机,PRD的方式会在连接成功后的几秒显示有冲突而退出PRD,VNC方式可以套娃一直连接

window安装UltraVNC

官网下载合适的版本即可: https://uvnc.com/

一直next即可

安装之后,他会在本地启一个uvnc_service 服务

我们打开安装 UltraVNC Server 软件,我们底部任务栏会出现他的图标,在他的图标上右边打开Admin properties

  1. VNC Password:VNC连接时的密码
  2. View-Only Password:连接后只允许看的密码
  3. 默认VNC的连接端口

更改完之后直接apply-ok即可

集成Guacamole API

需要的环境

https://guacamole.apache.org/api-documentation/

我们集成的话需要上面链接里面的 Java (guacamole-common) JavaScript (guacamole-common-js)两个包

  1. Java (guacamole-common)

    这个使用maven下载即可

    <dependency>
        <groupId>org.apache.guacamole</groupId>
        <artifactId>guacamole-common</artifactId>
        <version>1.4.0</version>
    </dependency>
    
  2. JavaScript (guacamole-common-js)

    这个包也是有maven,但是我本机下不下来,我看其他人也有这种情况,所以此包在下文中采用下载包的形式

    https://search.maven.org/artifact/org.apache.guacamole/guacamole-common-js/1.4.0/pom

    选择下载zip包解压,即可

###Servlet项目集成

  1. 先新建一个Servlet项目

  2. 放入maven Guacamole依赖

<!-- Main Guacamole library -->
<dependency>
    <groupId>org.apache.guacamole</groupId>
    <artifactId>guacamole-common</artifactId>
    <version>1.4.0</version>
</dependency>
  1. 创建相应文件

  • 创建TutorialGuacamoleTunnelServlet
  • 在webapp目录下,放入上文下载的js压缩包里面的内容(里面的js其实真正用到的只有all.jsall.min.js这两个选择一个使用即可)
  • 创建index.html文件
  1. 填充文件内容
  • TutorialGuacamoleTunnelServlet文件内容:
public class TutorialGuacamoleTunnelServlet extends GuacamoleHTTPTunnelServlet {
    @Override
    protected GuacamoleTunnel doConnect(HttpServletRequest request) throws GuacamoleException {
		//此方法为http的连接方式,如果是websocker方式,请继承相关的类`GuacamoleWebSocketTunnelEndpoint`
        // Create our configuration
        GuacamoleConfiguration config = new GuacamoleConfiguration();
        config.setProtocol("rdp");
        config.setParameter("hostname", "自己测试的远程ip");
        config.setParameter("port", "测试连接端口");
        config.setParameter("username", "用户名");
        config.setParameter("password", "密码");
        config.setParameter("ignore-cert", "true");

        // Connect to guacd - everything is hard-coded here.
        GuacamoleSocket socket = new ConfiguredGuacamoleSocket(
                new InetGuacamoleSocket("运行guacd的机器ip", 4822),
                config
        );

        // Return a new tunnel which uses the connected socket
        return new SimpleGuacamoleTunnel(socket);
    }
}
  • web.xml文件内容
<!-- Basic config -->
<welcome-file-list>
    <welcome-file>index.html</welcome-file>
</welcome-file-list>
<!-- Guacamole Tunnel Servlet -->
<servlet>
    <description>Tunnel servlet.</description>
    <servlet-name>Tunnel</servlet-name>
    <servlet-class>
        <!-- TutorialGuacamoleTunnelServlet类的具体路径,具体到类名 -->
        com.example.e.类名
    </servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>Tunnel</servlet-name>
    <url-pattern>/tunnel</url-pattern>
</servlet-mapping>
  • index.html文件内容
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<script type="text/javascript"
        src="guacamole-common-js/all.min.js"></script>

<!-- Display -->
<div id="display"></div>

<script type="text/javascript"> /* <![CDATA[ */

// Get display div from document
var display = document.getElementById("display");

// Instantiate client, using an HTTP tunnel for communications.
var guac = new Guacamole.Client(
    new Guacamole.HTTPTunnel("tunnel")
);

// Add client to display div
display.appendChild(guac.getDisplay().getElement());

// Error handler
guac.onerror = function (error) {
    alert(error);
};

// Connect
guac.connect();

// Disconnect on close
window.onunload = function () {
    guac.disconnect();
}

// Mouse
var mouse = new Guacamole.Mouse(guac.getDisplay().getElement());

mouse.onmousedown =
    mouse.onmouseup =
        mouse.onmousemove = function (mouseState) {
            guac.sendMouseState(mouseState);
        };

// Keyboard
var keyboard = new Guacamole.Keyboard(document);

keyboard.onkeydown = function (keysym) {
    guac.sendKeyEvent(1, keysym);
};

keyboard.onkeyup = function (keysym) {
    guac.sendKeyEvent(0, keysym);
};

/* ]]> */</script>
</body>
</html>
  1. 启动创建项目时配置的tomcat服务,打开tomcat服务配置里面的url,如果连接参数配置正确,此时就可以看到了

springboot + vue 项目集成

springboot + vue项目请自行创建

  1. 配置maven
<!-- 远程控制-->
<dependency>
    <groupId>org.apache.guacamole</groupId>
    <artifactId>guacamole-common</artifactId>
    <version>1.4.0</version>
</dependency>

<!-- 添加servlet支持-->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>4.0.1</version>
    <scope>provided</scope>
</dependency>

因为api依赖servlet的支持,所以需要额外添加servlet的包,Servlet集成的时候,新建项目自带这个包

  1. 创建TutorialGuacamoleTunnelServlet类,放入合适的包中即可
//配置请求路径 /tunnel
@WebServlet(name = "Tunnel", urlPatterns = "/tunnel")
public class TutorialGuacamoleTunnelServlet extends GuacamoleHTTPTunnelServlet {
    @Override
    protected GuacamoleTunnel doConnect(HttpServletRequest request) throws GuacamoleException {

        GuacamoleConfiguration config = new GuacamoleConfiguration();
        //config.setProtocol("vnc");
		//根据header参数来进行参数化
        config.setProtocol(request.getHeader("loginType"));
        config.setParameter("hostname", request.getHeader("hostName"));
        config.setParameter("port", request.getHeader("port"));
        config.setParameter("username", request.getHeader("userName"));
        config.setParameter("password", request.getHeader("password"));
        config.setParameter("ignore-cert", request.getHeader("ignore"));

        GuacamoleSocket socket = new ConfiguredGuacamoleSocket(
                new InetGuacamoleSocket("192.168.150.128", 4822),
                config
        );

        return new SimpleGuacamoleTunnel(socket);
    }


}

  1. 在springboot的启动类里面加上注解
//扫描这个目录下的servlet,这个不需要具体到类名,到包名即可
@ServletComponentScan("com.framework.servlet")
  1. 解除此url的访问后端接口权限(非必要步骤,可省略)因为集成在真实的项目里,而我们项目用的是spring security所以需要对这个url进行解除访问限制
//只允许登录用户访问,这个地方需要这个接口认证token,然后认证token,后面会讲
.antMatchers("/tunnel/**").authenticated()
//允许任何人访问
.antMatchers("/tunnel/**").permitAll()   
这两个选一个即可
  1. 前端新建路由配置
  {
    path: "/remote-page",
    //比如我准备展示远程桌面的页面放入了这个路径下面
    component: () => import("@/views/business/tunnel/index"),
  },
  1. 请自行在相应的目录下新建页面
  2. 我这里的逻辑是一个页面有一个登录按钮点击打开新页面,这个新页面就是展示远程桌面的页面,为此打开新页面的页面逻辑如下
//配置内容请自行填写
const loginForm = {
    hostName: "0.0.0.0"
    ignore: "true"
    loginType: "rdp"
    password: "123"
    port: "3389"
    userName: "pass"
}
let openUrl = window.location.origin + "/remote-page?";
for (const [key, value] of Object.entries(loginForm)) {
    openUrl += "&" + key + "=" + value;
}
window.open(openUrl, "_blank");
  1. 展示远程桌面的文件内容
<template>
  <div>
    <!-- Display -->
    <div id="display"></div>
  </div>
</template>

<script>
//如果你后端配置的是需要token认证的,请使用你们项目里面自己带的获取token的方法
import { getToken } from "@/utils/auth";

export default {
  data() {
    return {
      params: {},
    };
  },
  mounted() {
    this.initUrlParams();
    this.initGuacamole();
  },

  methods: {
    // 初始化url
    initUrlParams() {
      this.params = this.getQueryObject(window.location.href);
      //添加token认证
      this.params["Authorization"] = "Bearer " + getToken();
    },

    // 初始化视频
    initGuacamole() {
      var display = document.getElementById("display");

      var guac = new Guacamole.Client(
        new Guacamole.HTTPTunnel("tunnel", true, this.params)
      );

      display.appendChild(guac.getDisplay().getElement());

      guac.onerror = (error) => {
        if (error.code === 519) {
          this.$message.error(
            "连接失败,请检查连接参数,10秒后自动关闭本页面!"
          );
          setTimeout(() => {
            window.close();
          }, 10000);
        } else {
          this.$message.error(error);
        }
      };

      guac.connect();

      window.onunload = function () {
        guac.disconnect();
      };

      var mouse = new Guacamole.Mouse(guac.getDisplay().getElement());

      mouse.onmousedown =
        mouse.onmouseup =
        mouse.onmousemove =
          function (mouseState) {
            guac.sendMouseState(mouseState);
          };

      var keyboard = new Guacamole.Keyboard(document);

      keyboard.onkeydown = function (keysym) {
        guac.sendKeyEvent(1, keysym);
      };

      keyboard.onkeyup = function (keysym) {
        guac.sendKeyEvent(0, keysym);
      };
    },
    
    //解析url参数
     getQueryObject(url){
      url = url == null ? window.location.href : url
      const search = url.substring(url.lastIndexOf('?') + 1)
      const obj = {}
      const reg = /([^?&=]+)=([^?&=]*)/g
      search.replace(reg, (rs, $1, $2) => {
        const name = decodeURIComponent($1)
        let val = decodeURIComponent($2)
        val = String(val)
        obj[name] = val
        return rs
      })
      return obj
     }  
  },
};
</script>

<style lang="scss" scoped></style>

  1. 引入guacamole-common-js

原包使用import导入的话会提示找不到Guacamole对象,所以我更改了下源码,建议大家先不改,先试试好不好报错,再来决定需要不需要改

这里我使用的是all.js把里面的第一行代码

var Guacamole = Guacamole || {};
=>
window.Guacamole = Guacamole || {};

main.js里面引入

//请自行更改路径
import "@/utils/guacamole-common-js/all.js";

如果你不想更改源码并且也报错的话,那么可以直接放在public目录下,然后在index.html里面引入

<script src="<%= BASE_URL %>all.js"></script>
  1. 现在已经可以启动查看了,这中间的步骤如果大家有错误可能确实是我没想起来,没有写上,但是这些步骤已经足够精简了
  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 19
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值