WebSocket实时通讯实践

1.WebSocket原理

  

WebSocket机制

以下简要介绍一下 WebSocket 的原理及运行机制。

WebSocket 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯,它建立在 TCP 之上,同 HTTP 一样通过 TCP 来传输数据,但是它和 HTTP 最大不同是:

  • WebSocket 是一种双向通信协议,在建立连接后,WebSocket 服务器和 Browser/Client Agent 都能主动的向对方发送或接收数据,就像 Socket 一样;
  • WebSocket 需要类似 TCP 的客户端和服务器端通过握手连接,连接成功后才能相互通信。

非 WebSocket 模式传统 HTTP 客户端与服务器的交互如下图所示:

图 1. 传统 HTTP 请求响应客户端服务器交互图
图 1. 传统 HTTP 请求响应客户端服务器交互图

使用 WebSocket 模式客户端与服务器的交互如下图:

图 2.WebSocket 请求响应客户端服务器交互图
图 2.WebSocket 请求响应客户端服务器交互图

上图对比可以看出,相对于传统 HTTP 每次请求-应答都需要客户端与服务端建立连接的模式,WebSocket 是类似 Socket 的 TCP 长连接的通讯模式,一旦 WebSocket 连接建立后,后续数据都以帧序列的形式传输。在客户端断开 WebSocket 连接或 Server 端断掉连接前,不需要客户端和服务端重新发起连接请求。在海量并发及客户端与服务器交互负载流量大的情况下,极大的节省了网络带宽资源的消耗,有明显的性能优势,且客户端发送和接受消息是在同一个持久连接上发起,实时性优势明显。

我们再通过客户端和服务端交互的报文看一下 WebSocket 通讯与传统 HTTP 的不同:

在客户端,new WebSocket 实例化一个新的 WebSocket 客户端对象,连接类似 ws://yourdomain:port/path 的服务端 WebSocket URL,WebSocket 客户端对象会自动解析并识别为 WebSocket 请求,从而连接服务端端口,执行双方握手过程,客户端发送数据格式类似:

清单 1.WebSocket 客户端连接报文
GET /webfin/websocket/ HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: xqBt3ImNzJbYqRINxEFlkg==
Origin: http://localhost:8080
Sec-WebSocket-Version: 13

可以看到,客户端发起的 WebSocket 连接报文类似传统 HTTP 报文,”Upgrade:websocket”参数值表明这是 WebSocket 类型请求,“Sec-WebSocket-Key”是 WebSocket 客户端发送的一个 base64 编码的密文,要求服务端必须返回一个对应加密的“Sec-WebSocket-Accept”应答,否则客户端会抛出“Error during WebSocket handshake”错误,并关闭连接。

服务端收到报文后返回的数据格式类似:

清单 2.WebSocket 服务端响应报文
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: K7DJLdLooIwIG/MOpvWFB3y3FE8=

“Sec-WebSocket-Accept”的值是服务端采用与客户端一致的密钥计算出来后返回客户端的,“HTTP/1.1 101 Switching Protocols”表示服务端接受 WebSocket 协议的客户端连接,经过这样的请求-响应处理后,客户端服务端的 WebSocket 连接握手成功, 后续就可以进行 TCP 通讯了。读者可以查阅WebSocket 协议栈了解 WebSocket 客户端和服务端更详细的交互数据格式。

在开发方面,WebSocket API 也十分简单,我们只需要实例化 WebSocket,创建连接,然后服务端和客户端就可以相互发送和响应消息,在下文 WebSocket 实现及案例分析部分,可以看到详细的 WebSocket API 及代码实现。


WebSocket客户端实现  HTML5支持

                   WebSocket 的实现分为客户端和服务端两部分,客户端(通常为浏览器)发出 WebSocket 连接请求,服务端响应,实现类似 TCP                  握手的动作,从而在浏览器客户端和 WebSocket 服务端之间形成一条 HTTP 长连接快速通道。两者之间后续进行直接的数据互相传                送,不再需要发起连接和响应。
              
var ws = new WebSocket(“ws://echo.websocket.org”); 
 ws.onopen = function(){ws.send(“Test!”); }; 
 ws.onmessage = function(evt){console.log(evt.data);ws.close();}; 
 ws.onclose = function(evt){console.log(“WebSocketClosed!”);}; 
 ws.onerror = function(evt){console.log(“WebSocketError!”);};
代码说明:

第一行代码是在申请一个 WebSocket 对象,参数是需要连接的服务器端的地址,同 HTTP 协议开头一样,WebSocket 协议的 URL 使用 ws://开头,另外安全的 WebSocket 协议使用 wss://开头。

第二行到第五行为 WebSocket 对象注册消息的处理函数,WebSocket 对象一共支持四个消息 onopen, onmessage, onclose 和 onerror,有了这 4 个事件,我们就可以很容易很轻松的驾驭 WebSocket。

当 Browser 和 WebSocketServer 连接成功后,会触发 onopen 消息;如果连接失败,发送、接收数据失败或者处理数据出现错误,browser 会触发 onerror 消息;当 Browser 接收到 WebSocketServer 发送过来的数据时,就会触发 onmessage 消息,参数 evt 中包含 Server 传输过来的数据;当 Browser 接收到 WebSocketServer 端发送的关闭连接请求时,就会触发 onclose 消息。我们可以看出所有的操作都是采用异步回调的方式触发,这样不会阻塞 UI,可以获得更快的响应时间,更好的用户体验。

    
WebSocket的事件处理
      onopen      --  事件在连接成功时触发,达成第一个握手,准备传输数据
         eg:
           socket.onopen = function(event){
             console.log("Connection established");
             //在这类可以初始化资源,并展示一些用户友好提示信息
             var label = document.getElementById("status-label");
             label.innerHTML="Connection established";
           }


      onmessage   -- 数据传输事件,客户端随时监听Server,当服务器端发送给客户端数据时触发  --- 数据包括:文本、图片、二进制数据等
         eg:
           socket.onmessage = function(event){
             console.log("Data received");
             if(typeof event.data == "string"){
             var label = document.getElementById("status-label");
             label.innerHTML = event.data;
             }
           }


      onclose    --  关闭客户端与服务器的连接,不能在进行数据的传输。
                     关闭连接的原因有多种:服务器关闭,客户端调用close()方法关闭,或者TCP错误  --- 可通过 code,reason,wasClean参数检测连接关闭的原因
                     code    ---    用一个唯一的数字,表示连接中断的原因
                     reason  ---    连接中断的原因
                     wasClean  ---  用于判断连接关闭,是因为服务器的原因,还是未知的网络错误
         eg:
           socket.onclose = function(event){
           console.log("Connections closed");
           var code = event.code;
           var reason = event.reason;
           var wasClean = event.wasClean;

           var label = document.getElementById("status-label");

           if(wasClean){
               label.innerHTML = "Connection closed normally";
           }else{
               label.innerHTML ="Connection closed with message "+reason+"(Code: "+code+")";
           }
           }


      onerror    --  出错时触发的事件,该事件出发后,会接着触发close事件,终止连接  ---  注意出错时给出提示,并尝试重新建立连接
         eg:
           socket.onerror = function(event){
              console.log("Error occurred");
              //出错提示
              var label = document.getElementById("status-label");
              label.innerHTML = "Error: "+event;
           }


 WebSocket服务器端的实现(Java版)                  

@ServerEndpoint("/echo")
 public class EchoEndpoint {

 @OnOpen
 public void onOpen(Session session) throws IOException {
 //以下代码省略...
 }
 
 @OnMessage
 public String onMessage(String message) {
 //以下代码省略...
 }

 @Message(maxMessageSize=6)
 public void receiveMessage(String s) {
 //以下代码省略...
 } 

 @OnError
 public void onError(Throwable t) {
 //以下代码省略...
 }
 
 @OnClose
 public void onClose(Session session, CloseReason reason) {
 //以下代码省略...
 } 
 
 }


代码说明:
         

建立了一个 WebSocket 的服务端,@ServerEndpoint("/echo") 的 annotation 注释端点表示将 WebSocket 服务端运行在 ws://[Server 端 IP 或域名]:[Server 端口]/websockets/echo 的访问端点,客户端浏览器已经可以对 WebSocket 客户端 API 发起 HTTP 长连接了。

使用 ServerEndpoint 注释的类必须有一个公共的无参数构造函数,@onMessage 注解的 Java 方法用于接收传入的 WebSocket 信息,这个信息可以是文本格式,也可以是二进制格式。

OnOpen 在这个端点一个新的连接建立时被调用。参数提供了连接的另一端的更多细节。Session 表明两个 WebSocket 端点对话连接的另一端,可以理解为类似 HTTPSession 的概念。

OnClose 在连接被终止时调用。参数 closeReason 可封装更多细节,如为什么一个 WebSocket 连接关闭。

更高级的定制如 @Message 注释,MaxMessageSize 属性可以被用来定义消息字节最大限制,在示例程序中,如果超过 6 个字节的信息被接收,就报告错误和连接关闭。


2.WebSocket实践

利用websocket和java完成的消息推送功能,服务器用的是tomcat7.0,一些东西是自己琢磨的,也不知道恰不恰当,不恰当处,还请各位见谅,并指出。

程序简单来说,就是客户A可以发送消息给客户B,但有很多可以扩展的地方,

比如

1.如果加入数据库后,A发消息时客户B未上线,服务端将会把消息存在数据库中,等客户B上线后,在将消息取出发送给客户B

2.服务端也可发送消息到任意客户端上。

程序的运行效果截图如下(在chrome,搜狗,firefox下测试通过):代码将在最后给出

首先我们打开一个浏览器,显示输入您的名字,这里我输入soar

image

image

在打开第二个浏览器,这里我输入bill

image

image

这是如果我发送hello bill i am soar给bill,点击send

image

在另一个浏览器上就可以看到

image

Websocket

1.websocket是什么?

WebSocket是为解决客户端与服务端实时通信而产生的技术。其本质是先通过HTTP/HTTPS协议进行握手后创建一个用于交换数据的TCP连接,

此后服务端与客户端通过此TCP连接进行实时通信。

2.websocket的优点

以前我们实现推送技术,用的都是轮询,在特点的时间间隔有浏览器自动发出请求,将服务器的消息主动的拉回来,在这种情况下,我们需要不断的向服务器 发送请求,然而HTTP request 的header是非常长的,里面包含的数据可能只是一个很小的值,这样会占用很多的带宽和服务器资源。会占用大量的带宽和服务器资源。

WebSocket API最伟大之处在于服务器和客户端可以在给定的时间范围内的任意时刻,相互推送信息。在建立连接之后,服务器可以主动传送数据给客户端。

此外,服务器与客户端之间交换的标头信息很小。

WebSocket并不限于以Ajax(或XHR)方式通信,因为Ajax技术需要客户端发起请求,而WebSocket服务器和客户端可以彼此相互推送信息;

关于ajax,comet,websocket的详细介绍,和websocket报文的介绍,大家可以参看http://www.shaoqun.com/a/54588.aspx  网页设计]Ajax、Comet与Websocket,

我如果以后有时间,也会写出来的

3.如何使用websocket

客户端

在支持WebSocket的浏览器中,在创建socket之后。可以通过onopen,onmessage,onclose即onerror四个事件实现对socket进行响应

一个简单是示例

var ws = new WebSocket(“ws://localhost:8080”);
ws.onopen = function()
{
  console.log(“open”);
  ws.send(“hello”);
};
ws.onmessage = function(evt)
{
  console.log(evt.data)
};
ws.onclose = function(evt)
{
  console.log(“WebSocketClosed!”);
};
ws.onerror = function(evt)
{
  console.log(“WebSocketError!”);
};

1.var ws = new WebSocket(“ws://localhost:8080”);

申请一个WebSocket对象,参数是需要连接的服务器端的地址,同http协议使用http://开头一样,WebSocket协议的URL使用ws://开头,另外安全的WebSocket协议使用wss://开头。

ws.send(“hello”);

用于叫消息发送到服务端

 

2.ws.onopen = function() { console.log(“open”)};

当websocket创建成功时,即会触发onopen事件

 

3.ws.onmessage = function(evt) { console.log(evt.data) };

当客户端收到服务端发来的消息时,会触发onmessage事件,参数evt.data中包含server传输过来的数据

 

4.ws.onclose = function(evt) { console.log(“WebSocketClosed!”); };

当客户端收到服务端发送的关闭连接的请求时,触发onclose事件

 

5.ws.onerror = function(evt) { console.log(“WebSocketError!”); };

如果出现连接,处理,接收,发送数据失败的时候就会触发onerror事件

我们可以看出所有的操作都是采用事件的方式触发的,这样就不会阻塞UI,使得UI有更快的响应时间,得到更好的用户体验。

 

服务端

现在有很多的服务器软件支持websocket,比如node.js,jetty,tomcat等

这里我使用的是tomat 7.0和eclipse4.2

在tomcat下使用websocket首先需要导入相关的jar

tomcat7提供的与WebSocket相关的类均位于包org.apache.catalina.websocket之中(包org.apache.catalina.websocket的实现包含于文件catalina.jar之中

 

这里我们把tomcat的全部导入就行了

在build path->configure build path->librarise->add library->server runtime->apache tomcat v7.0

image

 

 

同时需要import以下包

import org.apache.catalina.websocket.MessageInbound;
import org.apache.catalina.websocket.StreamInbound;
import org.apache.catalina.websocket.WsOutbound;
import org.apache.catalina.websocket.WebSocketServlet;

 

我们需要两个类

第一个用于处理websocket请求

第二个用于处理每一次具体的WebSocket任务

 

第一个类


public class SocketServer extends WebSocketServlet {
    private static final long serialVersionUID = 1L;
    //……
    @Override
    protected StreamInbound createWebSocketInbound(String arg0,
            HttpServletRequest arg1) {
        // TODO Auto-generated method stub
        return new ChatWebSocket(users);
    }
}



这个Servlet继承自WebSocketServlet,实现createWebSocketInbound方法。该方法返回第二个类的实例。

 

第二个类


public class ChatWebSocket extends MessageInbound {

        @Override
        protected void onTextMessage(CharBuffer message) throws IOException {

        }

        @Override
        protected void onOpen(WsOutbound outbound) {
            
        }

        @Override
        protected void onClose(int status) {
            

        }

        @Override
        protected void onBinaryMessage(ByteBuffer arg0) throws IOException {

        }
//其余略

    }



 

protected void onTextMessage(CharBuffer message) throws IOException { }

文本消息响应

protected void onBinaryMessage(ByteBuffer arg0) throws IOException { }

二进制消息响应

protected void onOpen(WsOutbound outbound) { }

建立连接的触发的事件

protected void onClose(int status) { }

关闭连接时触发的事件

 

 

 

 

4.程序代码

html部分


<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<script type="text/javascript" src="js/jquery.js"></script>
<script type="text/javascript" src="js/socket.js"></script>
<title>无标题文档</title>
</head>
<script language="javascript">

</script>
<body>
<table>
  <tr>
    <td>Message</td>
    <td><input type="text" id="message"></td>
  </tr>
  <tr>
    <td>Name</td>
    <td><input type="text" id="othername"></td>
  </tr>
  <tr>
    <td><input id="sendbutton" type="button" value="send" onClick="click"  disabled="true">
      </input></td>
  </tr>
</table>
<script>

</script>
</body>
</html>



 

js部分(关于jquery部分不进行讲解)


var username = window.prompt("输入你的名字:");

document.write("Welcome<p id=\"username\">"+username+"</p>");

if (!window.WebSocket && window.MozWebSocket)
    window.WebSocket=window.MozWebSocket;
if (!window.WebSocket)
    alert("No Support ");
var ws;

$(document).ready(function(){
    
     $("#sendbutton").attr("disabled", false);
     $("#sendbutton").click(sendMessage);

    startWebSocket();
})

function sendMessage()
{
    var othername=$("#othername").val();
    var msg="MSG\t"+username+"_"+othername+"_"+$("#message").val();
    send(msg);
}
function send(data)
{
    console.log("Send:"+data);
    ws.send(data);
}
function startWebSocket()
{    
    ws = new WebSocket("ws://" + location.host + "/WebSocket/SocketServer");
    ws.onopen = function(){
        console.log("success open");
        $("#sendbutton").attr("disabled", false);
    };
     ws.onmessage = function(event)
     {
         console.log("RECEIVE:"+event.data);
         handleData(event.data); 
     };
      ws.onclose = function(event) { 
    console.log("Client notified socket has closed",event); 
  }; 
  
}

function handleData(data)
{
    var vals=data.split("\t");
    var msgType=vals[0];
    switch(msgType)
    {
    case "NAME":
        var msg=vals[1];
        var mes="NAME"+"\t"+msg+"_"+ username;
        send(mes);
        break;
    case "MSG":
        var val2s=vals[1].split("_");
        var from=val2s[0];
        var message=val2s[2];
        alert(from+":"+message);
        break;
    default:
        break;
            
    }
}



java部分


import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;

import javax.servlet.http.HttpServletRequest;
import java.util.Set;


import java.util.concurrent.CopyOnWriteArraySet;

import org.apache.catalina.websocket.MessageInbound;
import org.apache.catalina.websocket.StreamInbound;
import org.apache.catalina.websocket.WsOutbound;
import org.apache.catalina.websocket.WebSocketServlet;



public class SocketServer extends WebSocketServlet {
    private static final long serialVersionUID = 1L;
    public final Set<ChatWebSocket> users = new CopyOnWriteArraySet<ChatWebSocket>();

    public static int USERNUMBER = 1;
    @Override
    protected StreamInbound createWebSocketInbound(String arg0,
            HttpServletRequest arg1) {
        // TODO Auto-generated method stub
        return new ChatWebSocket(users);
    }
    public class ChatWebSocket extends MessageInbound {

        private String username;
        private Set<ChatWebSocket> users = new CopyOnWriteArraySet<ChatWebSocket>();;

        public ChatWebSocket() {

        }

        public ChatWebSocket(Set<ChatWebSocket> users) {
            this.users = users;
        }

        @Override
        protected void onTextMessage(CharBuffer message) throws IOException {
            // 这里处理的是文本数据
        }

        public void onMessage(String data) {
            String[] val1 = data.split("\\t");
            if(val1[0].equals("NAME"))
            {
                String[] val2=val1[1].split("_");
                for(ChatWebSocket user:users){
                    if (user.username.equals(val2[0])){
                        user.username=val2[1];
                    }
                }
            }
            else if(val1[0].equals("MSG"))
            {
                String[] val2=val1[1].split("_");
                for(ChatWebSocket user:users){
                    if (user.username.equals(val2[1])){
                        try {
                            CharBuffer temp=CharBuffer.wrap(data);
                            user.getWsOutbound().writeTextMessage(temp);
                        } catch (IOException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    }
                }        
            }
            else
            {
                System.out.println("ERROR");
            }

        }

        @Override
        protected void onOpen(WsOutbound outbound) {
            // this.connection=connection;
            this.username = "#" + String.valueOf(USERNUMBER);
            USERNUMBER++;
            try {
                String message = "NAME" + "\t" + this.username;
                CharBuffer buffer = CharBuffer.wrap(message);
                this.getWsOutbound().writeTextMessage(buffer);
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            users.add(this);
        }

        @Override
        protected void onClose(int status) {
            users.remove(this);

        }

        @Override
        protected void onBinaryMessage(ByteBuffer arg0) throws IOException {

        }

    }
    
}
 

解释

这里我的想法是

1 每个用户在访问的时候首先需要输入自己的名字,接着向服务端发送连接请求

2 服务端在接受到客户端的连接请求后,会new ChatWebSocket(users);用于处理这个请求,并把它加入在线的用户列表中,由于这个时候,服务端尚不知道客户的名字。它会给这个用户假定一个名字,#1,然后服务端会发送"NAME" + "\t" +“#1”给客户端,你叫什么?

3 客户端收到这个消息会知道,服务器在问自己叫什么名字,于是客户端会发送"NAME"+"\t"+“#1”+"_"+ 自己的名字到服务端,(我叫xxx)

4 服务端收到这个消息后根据#1在当前在线的用户列表中进行查找,将#1替换为客户的名字,这样服务端就知道了这个客户的名字了

5 当客户离开时,服务端会触发onClose事件,服务端会把当前用户从在线列表中移除

用图画出来类似这样(画的不好,—_—!!)

image

代码

js

ws = new WebSocket("ws://" + location.host + "/WebSocket/SocketServer");

 

连接服务端

 

java

protected StreamInbound createWebSocketInbound(String arg0,
            HttpServletRequest arg1) {
        // TODO Auto-generated method stub
        return new ChatWebSocket(users);
    }

创建一个chatwebsocket用于处理这个请求,触发该chatwebsocket对象的onOpen事件


@Override
    protected void onOpen(WsOutbound outbound) {
        // this.connection=connection;
        this.username = "#" + String.valueOf(USERNUMBER);
        USERNUMBER++;
        try {
            String message = "NAME" + "\t" + this.username;
            CharBuffer buffer = CharBuffer.wrap(message);
            this.getWsOutbound().writeTextMessage(buffer);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        users.add(this);
    }



为这个客户假定一个姓名,并发送NAME+“\t”+假定的姓名  给该客户端,同时将该客户端加入当前连接的客户列表中

 

js


function handleData(data)
{
    var vals=data.split("\t");
    var msgType=vals[0];
    switch(msgType)
    {
    case "NAME":
        var msg=vals[1];
        var mes="NAME"+"\t"+msg+"_"+ username;
        send(mes);
        break;
   //………            
    }
}



接受并处理服务端发来到的消息,发现是服务端问自己叫什么名字,于是发送”NAME"+"\t"+假定的名字+"_"+ 真正的名字 给服务端

 

 

java


public void onMessage(String data) {
        String[] val1 = data.split("\\t");
        if(val1[0].equals("NAME"))
        {
            String[] val2=val1[1].split("_");
            for(ChatWebSocket user:users){
                if (user.username.equals(val2[0])){
                    user.username=val2[1];
                }
            }
        }

//………
}



处理并接受客户端发来的消息,发现是客户端回复自己叫什么名字,于是在根据先前假定的名字在当前连接的客户列表中进行查找,将假名变成真名

 

js

function sendMessage()
{
    var othername=$("#othername").val();
    var msg="MSG\t"+username+"_"+othername+"_"+$("#message").val();
    send(msg);
}

客户对另一个人发起对话,消息格式为:“MSG”+自己的名字+_+对方的名字+_+消息

 

 

java


public void onMessage(String data) {
       ///…………
        else if(val1[0].equals("MSG"))
        {
            String[] val2=val1[1].split("_");
            for(ChatWebSocket user:users){
                if (user.username.equals(val2[1])){
                    try {
                        CharBuffer temp=CharBuffer.wrap(data);
                        user.getWsOutbound().writeTextMessage(temp);
                    } catch (IOException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            }        
        }
       ///…………

    }



发现是客户发送的消息,根据对方的姓名,在当前连接的客户列表中查找,并将消息发给他

 

 

js


function handleData(data)
{
    var vals=data.split("\t");
    var msgType=vals[0];
    switch(msgType)
    {
    ///…

    case "MSG":
        var val2s=vals[1].split("_");
        var from=val2s[0];
        var message=val2s[2];
        alert(from+":"+message);
        break;
    default:
        break;
            
    }
}



发现是另一个客户发来的消息,通过alert显示出来

 

java

@Override
        protected void onClose(int status) {
            users.remove(this);

        }

发现客户离开了,将客户从连接的客户列表中移除

可以改进的地方

1.若客户端A发送消息给B时,B不在线,可将消息存入数据库中,当发现B上线时,从数据库中取出,发送给B

2 服务端发送你叫什么时,可加入超时机制,若客户端一定时间内没有回复自己叫什么,则可将该客户从在线列表中删掉

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值