Websocket的使用学习

什么是websocket?

WebSocket是一种在单个TCP连接上进行全双工通信的协议。

WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

https://baike.baidu.com/item/WebSocket/1953845?fr=aladdin

对websocket的理解:

https://www.cnblogs.com/guoqiang1/p/8296176.html

websocket的应用场景?

决定手头的工作是否需要使用WebSocket技术的方法很简单:

  • 你的应用提供多个用户相互交流吗?
  • 你的应用是展示服务器端经常变动的数据吗?

如果你的回答是肯定的,那么请考虑使用WebSocket。如果你仍然不确定,并想要更多的灵感,这有一些杀手锏的案例。 

1.社交订阅

对社交类的应用的一个裨益之处就是能够即时的知道你的朋友正在做什么。虽然听起来有点可怕,但是我们都喜欢这样做。你不会想要在数分钟之后才能知道一个家庭成员在馅饼制作大赛获胜或者一个朋友订婚的消息。你是在线的,所以你的订阅的更新应该是实时的。

2.多玩家游戏

网络正在迅速转变为游戏平台。在不使用插件(我指的是Flash)的情况下,网络开发者现在可以在浏览器中实现和体验高性能的游戏。无论你是在处理DOM元素、CSS动画,HTML5的canvas或者尝试使用WebGL,玩家之间的互动效率是至关重要的。我不想在我扣动扳机之后,我的对手却已经移动位置。 

3.协同编辑/编程

我们生活在分布式开发团队的时代。平时使用一个文档的副本就满足工作需求了,但是你最终需要有一个方式来合并所有的编辑副本。版本控制系统,比如Git能够帮助处理某些文件,但是当Git发现一个它不能解决的冲突时,你仍然需要去跟踪人们的修改历史。通过一个协同解决方案,比如WebSocket,我们能够工作在同一个文档,从而省去所有的合并版本。这样会很容易看出谁在编辑什么或者你在和谁同时在修改文档的同一部分。

4.点击流数据

分析用户与你网站的互动是提升你的网站的关键。HTTP的开销让我们只能优先考虑和收集最重要的数据部分。然后,经过六个月的线下分析,我们意识到我们应该收集一个不同的判断标准——一个看起来不是那么重要但是现在却影响了一个关键的决定。与HTTP请求的开销方式相比,使用Websocket,你可以由客户端发送不受限制的数据。想要在除页面加载之外跟踪鼠标的移动?只需要通过WebSocket连接发送这些数据到服务器,并存储在你喜欢的NoSQL数据库中就可以了(MongoDB是适合记录这样的事件的)。现在你可以通过回放用户在页面的动作来清楚的知道发生了什么。 

5.股票基金报价

金融界瞬息万变——几乎是每毫秒都在变化。我们人类的大脑不能持续以那样的速度处理那么多的数据,所以我们写了一些算法来帮我们处理这些事情。虽然你不一定是在处理高频的交易,但是,过时的信息也只能导致损失。当你有一个显示盘来跟踪你感兴趣的公司时,你肯定想要随时知道他们的价值,而不是10秒前的数据。使用WebSocket可以流式更新这些数据变化而不需要等待。

6.体育实况更新

现在我们开始讨论一个让人们激情澎湃的愚蠢的东西——体育。我不是运动爱好者,但是我知道运动迷们想要什么。当爱国者在打比赛的时候,我的妹夫将会沉浸于这场比赛中而不能自拔。那是一种疯狂痴迷的状态,完全发自内心的。我虽然不理解这个,但是我敬佩他们与运动之间的这种强烈的联系,所以,最后我能做的就是给他的体验中降低延迟。如果你在你的网站应用中包含了体育新闻,WebSocket能够助力你的用户获得实时的更新。 

7.多媒体聊天

视频会议并不能代替和真人相见,但当你不能在同一个屋子里见到你谈话的对象时,视频会议是个不错的选择。尽管视频会议私有化做的“不错”,但其使用还是很繁琐。我可是开放式网络的粉丝,所以用WebSockets getUserMedia API和HTML5音视频元素明显是个不错的选择。WebRTC的出现顺理成章的成为我刚才概括的组合体,它看起来很有希望,但其缺乏目前浏览器的支持,所以就取消了它成为候选人的资格。 

8.基于位置的应用

越来越多的开发者借用移动设备的GPS功能来实现他们基于位置的网络应用。如果你一直记录用户的位置(比如运行应用来记录运动轨迹),你可以收集到更加细致化的数据。如果你想实时的更新网络数据仪表盘(可以说是一个监视运动员的教练),HTTP协议显得有些笨拙。借用WebSocket TCP链接可以让数据飞起来。

9.在线教育

上学花费越来越贵了,但互联网变得更快和更便宜。在线教育是学习的不错方式,尤其是你可以和老师以及其他同学一起交流。很自然,WebSockets是个不错的选择,可以多媒体聊天、文字聊天以及其它优势如与别人合作一起在公共数字黑板上画画... 

基于websocket的开发实例--聊天室

项目的基本框架是SSM,运行环境为JDK1.8和Tomcat8.5

1、添加websocket相关的依赖

    <!-- websocket start -->
    <!-- https://mvnrepository.com/artifact/javax.websocket/javax.websocket-api -->
	<dependency>
	    <groupId>javax.websocket</groupId>
	    <artifactId>javax.websocket-api</artifactId>
	    <version>1.1</version>
	</dependency>
	<dependency>
	    <groupId>org.springframework</groupId>
	    <artifactId>spring-websocket</artifactId>
	    <version>${spring.version}</version>
	</dependency>
	<!-- websocket end -->

 其中还会用到json相关的jar包,添加的依赖如下:

        <!-- json start -->
	    <dependency>
            <groupId>net.sf.json-lib</groupId>
            <artifactId>json-lib</artifactId>
            <version>2.4</version>
            <classifier>jdk15</classifier>
        </dependency>
	
		
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-lang3</artifactId>
			<version>3.1</version>
		</dependency>
		
		<dependency>
			<groupId>commons-beanutils</groupId>
			<artifactId>commons-beanutils</artifactId>
			<version>1.8.3</version>
		</dependency>
		
		<dependency>
			<groupId>commons-logging</groupId>
			<artifactId>commons-logging</artifactId>
			<version>1.1.1</version>
		</dependency>
		
		<dependency>
			<groupId>commons-collections</groupId>
			<artifactId>commons-collections</artifactId>
			<version>3.2.1</version>
		</dependency>
 
		<dependency>
			<groupId>net.sf.ezmorph</groupId>
			<artifactId>ezmorph</artifactId>
			<version>1.0.6</version>
		</dependency>
		<!-- json end -->

 2、编写websocket后端代码

核心代码MyWebSocket.java:

package com.bsk.controller;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CopyOnWriteArraySet;

import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.socket.server.standard.SpringConfigurator;

import com.bsk.entity.Content;
import com.bsk.service.ContentService;

/**
 * 该注解用来指定一个URI,客户端可以通过这个URI来连接到WebSocket
 * 类似Servlet的注解mapping。无需再web.xml中配置。
 * configurator = SpringConfigurator.class 是为了使该类可以通过Spring注入。
 * @author Lenovo
 *
 */
@ServerEndpoint(value = "/websocket", configurator = SpringConfigurator.class)
public class MyWebSocket {
	// 静态变量,用来记录当前在线连接数。把它设计成线程安全的
	private static int onlineCount = 0;
	@Autowired
	private ContentService contentService;
	/**
	 * concurrent 包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象
	 * 若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识
	 */
	private static CopyOnWriteArraySet<MyWebSocket> webSocketSet = new CopyOnWriteArraySet<MyWebSocket>();
	// 与客户端的连接会话,需要通过它来给客户端发送数据
	private Session session;
	
	public MyWebSocket() {}
	
	/**
	 * 连接建立成功调用方法
	 * @param session 可选参数,session为与某个客户端的连接会话,需要通过它来给客户端发送数据
	 */
	@OnOpen
	public void onOpen(Session session) {
		this.session = session;
		webSocketSet.add(this);     // 加入set中
		addOnlineCount();           // 在线数加一
		System.out.println("有新连接加入!当前在线人数为" + getOnlineCount());
	}
	
	
	/**
	 * 连接关闭调用的方法
	 */
	@OnClose
	public void onClose() {
		webSocketSet.remove(this);    // 从set中删除
		subOnlineCount();             // 在线数减一
		System.out.println("有一条连接关闭!当前在线人数为" + getOnlineCount());
	}
	
	/**
	 * 收到客户端消息后调用的方法
	 * @param message	客户端发送过来的消息
	 * @param session	可选的参数
	 */
	@OnMessage
	public void onMessage(String message, Session session) {
		System.out.println("来自客户端的消息:" + message);
		
		for (MyWebSocket myWebSocket : webSocketSet) {
			
			try {
				myWebSocket.sendMessage(message);
			} catch (IOException e) {
				e.printStackTrace();
				continue;
			}
			
		}
	}
	
	/**
	 * 发生错误时调用的方法
	 * @param session
	 * @param error
	 */
	@OnError
	public void onError(Session session, Throwable error) {
		System.out.println("发生错误");
		error.printStackTrace();
	}
	
	/**
	 * 根据需要,保存数据的内部方法
	 * @param message
	 * @throws IOException
	 */
	private void sendMessage(String message) throws IOException{
		// 保存数据到数据库
		Content content = new Content();
		content.setContent(message);
		SimpleDateFormat sm = new SimpleDateFormat("yyyy-MM-dd HH:mm:dd");
		content.setCreatedate(sm.format(new Date()));
		contentService.insertSelective(content);
		this.session.getBasicRemote().sendText(message);
		
	}

	private static synchronized void subOnlineCount() {
		MyWebSocket.onlineCount--;
	}

	private static synchronized int getOnlineCount() {
		return onlineCount;
	}

	private static synchronized void addOnlineCount() {
		MyWebSocket.onlineCount++;
	}
	
	
	
	
}

看起来也比较简单主要就是使用那几个注解。每当有一个客户端连入、关闭、发送消息都会调用各自注解的方法。 

加入@ServerEndpoint(value = "/websocket",configurator = SpringConfigurator.class)这个注解即可利用Spring注入,

使用这个注解就需要添加第二个 maven依赖spring-websocket,然后就可以做消息的保存操作了。

对应数据库中content表的实体类:

package com.bsk.entity;

public class Content {
    private Integer contentid;

    private String contentname;

    private String content;

    private String createdate;

    public Integer getContentid() {
        return contentid;
    }

    public void setContentid(Integer contentid) {
        this.contentid = contentid;
    }

    public String getContentname() {
        return contentname;
    }

    public void setContentname(String contentname) {
        this.contentname = contentname;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public String getCreatedate() {
        return createdate;
    }

    public void setCreatedate(String createdate) {
        this.createdate = createdate;
    }

    @Override
    public String toString() {
        return "Content{" +
                "contentid=" + contentid +
                ", contentname='" + contentname + '\'' +
                ", content='" + content + '\'' +
                ", createdate='" + createdate + '\'' +
                '}';
    }
}

相对应的ContentMapper.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.bsk.mapper.ContentMapper" >
    <resultMap id="BaseResultMap" type="com.bsk.entity.Content" >
        <id column="contentId" property="contentid" jdbcType="INTEGER"/>
        <result property="contentname" column="contentname"/>
        <result property="content" column="content"/>
        <result property="createdate" column="createdate"/>
    </resultMap>
    <sql id="Base_Column_List" >
        contentId, contentname, content,createdate
    </sql>

    <select id="findContentList" parameterType="String" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List"/>
        from content
    </select>

    <insert id="insertSelective" parameterType="com.bsk.entity.Content" >
        insert into content
        <trim prefix="(" suffix=")" suffixOverrides="," >
            <if test="contentid != null">
                contentId,
            </if>
            <if test="contentname != null">
                contentname,
            </if>
            <if test="content != null" >
                content,
            </if>
            <if test="createdate != null">
                createdate,
            </if>
        </trim>
        <trim prefix="values (" suffix=")" suffixOverrides="," >
            <if test="contentid != null">
                #{contentId,jdbcType=INTEGER},
            </if>
            <if test="contentname != null">
                #{contentname,jdbcType=VARCHAR},
            </if>
            <if test="content != null" >
                #{content,jdbcType=VARCHAR},
            </if>
            <if test="createdate != null">
                #{createdate,jdbcType=VARCHAR},
            </if>
        </trim>
    </insert>


</mapper>

接口ContentMapper.java:

package com.bsk.mapper;

import java.util.List;

import com.bsk.entity.Content;

public interface ContentMapper {
	/**
     * 返回所有内容
     * @return
     */
    List<Content> findContentList();

    /**
     * 新增
     * @param content
     * @return
     */
    int insertSelective(Content content) ;
}

又通过Dao层,到Service层,暴露对content表操作的接口方法

在MyWebSocket.java中自动注入ContentService.java接口

controller代码:

package com.bsk.controller;

import java.util.List;

import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import com.bsk.entity.Content;
import com.bsk.service.ContentService;
import com.bsk.util.CommonUtil;

import net.sf.json.JSONObject;

@Controller
public class MainController {
	private Logger logger = LoggerFactory.getLogger(MainController.class);
	
	@Autowired
    private ContentService contentService;
	
	
	@RequestMapping("/turnToWebSocketIndex")
    public ModelAndView turnToWebSocketIndex() {
		ModelAndView mv = new ModelAndView();
		mv.setViewName("websocket/websocket");
		return mv;
    }
	
	/**
     * 加载聊天记录
     *
     * @param response
     */
    @RequestMapping("/content_load")
    public void content_load(HttpServletResponse response) {
        JSONObject jsonObject = new JSONObject();
        try {
            JSONObject jo = new JSONObject();
            List<Content> list = contentService.findContentList();
            jo.put("contents", list);
            jsonObject = CommonUtil.parseJson("1", "操作成功", jo);
        } catch (Exception e) {
            logger.error("操作异常", e);
            CommonUtil.parseJson("2", "操作异常", "");
        }
        CommonUtil.responseBuildJson(response, jsonObject);
    }
}

3、编写前端代码

websocket.jsp

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8" %>
<%
    String path = request.getContextPath();
    String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";
%>

<!DOCTYPE HTML>
<html>
<head>
    <base href="<%=basePath%>">
    <!-- Bootstrap -->
    <link rel="stylesheet"
          href="http://cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css">
    <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
    <!--[if lt IE 9]>
    <script src="//cdn.bootcss.com/html5shiv/3.7.2/html5shiv.min.js"></script>
    <script src="//cdn.bootcss.com/respond.js/1.4.2/respond.min.js"></script>
    <![endif]-->
    <script type="text/javascript" charset="utf-8" src="<%=path%>/ueditor/ueditor.config.js"></script>
    <script type="text/javascript" charset="utf-8" src="<%=path%>/ueditor/ueditor.all.min.js"> </script>
    <!--建议手动加在语言,避免在ie下有时因为加载语言失败导致编辑器加载失败-->
    <!--这里加载的语言文件会覆盖你在配置项目里添加的语言类型,比如你在配置项目里配置的是英文,这里加载的中文,那最后就是中文-->
    <script type="text/javascript" charset="utf-8" src="<%=path%>/ueditor/lang/zh-cn/zh-cn.js"></script>

    <title>聊天室</title>
</head>

<body data="/spring-mybatis-redis-websocket">
<input id="text" type="text"/>
<button onclick="send()">发送</button>
<button onclick="closeWebSocket()">关闭连接</button>
<div id="message">
</div>


<div class="container-fluid">
    <div class="row">
        <div class="col-md-12">
            <div class="panel panel-primary">
                <div class="panel-heading">聊天室</div>
                <div id="msg" class="panel-body">

                </div>
                <div class="panel-footer">
                    在线人数<span id="onlineCount">1</span>人
                </div>
            </div>
        </div>
    </div>
</div>

<div class="container-fluid">
    <div class="row">
        <div class="col-md-12">
            <script id="editor" type="text/plain" style="width:1024px;height:200px;"></script>
        </div>
    </div>

</div>

<div class="container-fluid">
    <div class="row">
        <div class="col-md-12">
            <p class="text-right">
            <button onclick="sendMsg();" class="btn btn-success">发送</button>
            </p>
        </div>
    </div>

</div>

</body>

<script type="text/javascript">
    var ue = UE.getEditor('editor');
    var websocket = null;

    //判断当前浏览器是否支持WebSocket
    if ('WebSocket' in window) {
        websocket = new WebSocket("ws://127.0.0.1:8080/spring-mybatis-redis-websocket/websocket");
    }
    else {
        alert("对不起!你的浏览器不支持webSocket")
    }

    //连接发生错误的回调方法
    websocket.onerror = function () {
        setMessageInnerHTML("error");
    };

    //连接成功建立的回调方法
    websocket.onopen = function (event) {
        setMessageInnerHTML("加入连接");
    };

    //接收到消息的回调方法
    websocket.onmessage = function (event) {
        setMessageInnerHTML(event.data);
    };

    //连接关闭的回调方法
    websocket.onclose = function () {
        setMessageInnerHTML("断开连接");
    };

    //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,
    // 防止连接还没断开就关闭窗口,server端会抛异常。
    window.onbeforeunload = function () {
        var is = confirm("确定关闭窗口?");
        if (is){
            websocket.close();
        }
    };

    //将消息显示在网页上
    function setMessageInnerHTML(innerHTML) {
        $("#msg").append(innerHTML+"<br/>")
    };

    //关闭连接
    function closeWebSocket() {
        websocket.close();
    }

    //发送消息
    function send() {
        var message = $("#text").val() ;
        websocket.send(message);
        $("#text").val("") ;
    }

    function sendMsg(){
        var msg = ue.getContent();
        websocket.send(msg);
        ue.setContent('');
    }
</script>

<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="http://cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script>
<!-- Include all compiled plugins (below), or include individual files as needed -->
<script src="http://cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
<script type="text/javascript" src="<%=path%>/static/js/Globals.js"></script>
<script type="text/javascript" src="<%=path%>/static/js/websocket.js"></script>
</html>

重要的就是那几个JS方法,都写有注释。需要注意的是这里

//判断当前浏览器是否支持WebSocket
    if ('WebSocket' in window) {
        websocket = new WebSocket("ws://127.0.0.1:8080/spring-mybatis-redis-websocket/websocket");
    }
    else {
        alert("对不起!你的浏览器不支持webSocket")
    }

4、百度的Ueditor富文本编辑器

相关配置:

首先需要将jsp-->lib下的jar包加入到项目中,

出现错误

An error occurred at line: 6 in the generated java file
Only a type can be imported. com.baidu.ueditor.ActionEnter resolves to a package

An error occurred at line: 11 in the jsp file: /jsp/controller.jsp
ActionEnter cannot be resolved to a type
8: 
9:  String rootPath = application.getRealPath( "/" );
10: 
11:  out.write( new ActionEnter( request, rootPath ).exec() );
12: 
13: %>

     解决方法:

     请参考Only a type can be imported. com.baidu.ueditor.ActionEnter resolves to a package

之后修改该目录下的config.json文件,如下图所示:

主要修改以下内容即可:

"imageActionName": "uploadimage", /* 执行上传图片的action名称 */
    "imageFieldName": "upfile", /* 提交的图片表单名称 */
    "imageMaxSize": 2048000, /* 上传大小限制,单位B */
    "imageAllowFiles": [".png", ".jpg", ".jpeg", ".gif", ".bmp"], /* 上传图片格式显示 */
    "imageCompressEnable": true, /* 是否压缩图片,默认是true */
    "imageCompressBorder": 1600, /* 图片压缩最长边限制 */
    "imageInsertAlign": "none", /* 插入的图片浮动方式 */
    "imageUrlPrefix": "http://127.0.0.1:8080/spring-mybatis-redis-websocket", /* 图片访问路径前缀 */
    "imagePathFormat": "/ueditor/jsp/upload/image/{yyyy}{mm}{dd}/{time}{rand:6}", /* 上传保存路径,可以自定义保存路径和文件名格式 */

 主要是要修改imageUrlPrefix为你自己的项目地址就可以了。ueditor一个我认为很不错的就是他支持图片、多图、截图上传,而且都不需要手动编写后端接口,所有上传的文件、图片都会保存到项目发布出去的jsp-->upload文件夹下一看就明白了。

其中值得注意一点的是,由于项目采用了Spring MVC并拦截了所有的请求,导致静态资源不能访问,如果是需要用到上传txt文件之类的需求可以参照web.xml中修改,如下:

    <!-- 静态资源文件配置 start -->
	<servlet-mapping>
		<servlet-name>default</servlet-name>
		<url-pattern>*.css</url-pattern>
	</servlet-mapping>
	<servlet-mapping>
		<servlet-name>default</servlet-name>
		<url-pattern>*.js</url-pattern>
	</servlet-mapping>
	<servlet-mapping>
		<servlet-name>default</servlet-name>
		<url-pattern>*.png</url-pattern>
	</servlet-mapping>
	<servlet-mapping>
		<servlet-name>default</servlet-name>
		<url-pattern>*.jpg</url-pattern>
	</servlet-mapping>
	<servlet-mapping>
		<servlet-name>default</servlet-name>
		<url-pattern>*.gif</url-pattern>
	</servlet-mapping>
	<servlet-mapping>
        <servlet-name>default</servlet-name>
        <url-pattern>*.swf</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>default</servlet-name>
        <url-pattern>*.html</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>default</servlet-name>
        <url-pattern>*.txt</url-pattern>
    </servlet-mapping>
<!--     <servlet-mapping> -->
<!--         <servlet-name>default</servlet-name> -->
<!--         <url-pattern>*.ico</url-pattern> -->
<!--     </servlet-mapping> -->
	<!-- 静态资源文件配置 end -->

 

这样就可以访问txt文件了,如果还需要上传PPT之类的就以此类推。

整个代码的实现参考文章:https://crossoverjie.top/2016/09/04/SSM5/

 

 

 

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

笑看风云路

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值