笔记——WebSocket

前言

        记录学习历程,在学习笔记中有描述不正确的地方,欢迎小伙伴们评论指正

概念

        WebSocket协议是基于TCP的一种新的网络协议,它实现了客户端与服务器全双工(full-duplex)通信,允许服务器主动发送信息到客户端。

WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。

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

运用场景:弹幕、网页聊天系统、实时监控、股票行情推送等。

SocketJS

        1、是一个JavaScript库,提供一个类似WebSocket的对象
        2、提供一个连贯的跨浏览器的JavaScriptAPI,在浏览器和web服务器之间创建一个低延迟、全双工、跨域的通信通道。
        3、在底层SockJS首先尝试使用本地WebSocket,若失败,它可以使用各种浏览器特定的协议,并通过类似WebSocket的抽象方式呈现它们
        4、SockJS旨在适用于所有现代浏览器和不支持WebSocket协议的环境

StompJS

        它定义了可互操作的连接格式,以便任何可用的STOMP客户端都可以与任何STOMP消息代理进行通信,以在语言和平台之间提供简单而广泛的消息互操作性。简而言之、是一个简单的面向文本的消息传递协议。

WebSocket划分

        单播(Unicast):点对点、私信
        广播(Broadcast):所有人群进行的公告、发布订阅
        多播/组播(Multicast):特定人群进行的群聊、发布订阅

实例

        SpringBoot非注解方式(WebSocket、SockJS、Stomp)

创建Spring Boot项目,并在pom.xml注解中添加依赖


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>jquery</artifactId>
            <version>3.6.0</version>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>sockjs-client</artifactId>
            <version>1.0.2</version>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>stomp-websocket</artifactId>
            <version>2.3.3</version>
        </dependency>

创建WebSocket配置类并实现WebSocketMessageBrokerConfigurer,并使用注解@EnableWebSocketMessageBroker


@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    /**
     * 注册websocket端点(基站)
     * 发布或订阅消息时,需要先连接此端点
     * setAllowedOrigins("*") 可有可无;*表示允许其他域进行访问
     *      注:若使用setAllowedOrigins报错的情况下将setAllowedOrigins换成setAllowedOriginPatterns
     * withSockJS 表示开启sockjs支持
     * @param registry
     */
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        //registry.addEndpoint("oneToOne").setAllowedOrigins("*").withSockJS();//点对点端点
        registry.addEndpoint("oneToOne").setAllowedOriginPatterns("*").withSockJS();
        registry.addEndpoint("broadcast").withSockJS();//广播端点
    }

    /**
     * 配置消息代理【中介转发的意思】
     * enableSimpleBroker:服务端推送给客户端路径的前缀
     * setApplicationDestinationPrefixes:客户端发送数据到服务端路径的前缀
     * @param registry
     */
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/topic", "/user");
        registry.setApplicationDestinationPrefixes("/info");
    }

}

在Controller中使用@SendTo注解方式实现

@RestController
public class WebSocketController {
    @MessageMapping("/getInfo")
    @SendTo("/topic/get")
    public GetOutMessage getInfo(GetInMessage in){
        return new GetOutMessage(in.getContext());
    }
}

在static文件下创建html页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Parent</title>
  <script src="/webjars/jquery/3.6.0/jquery.min.js"></script>
  <script src="/webjars/sockjs-client/1.0.2/sockjs.min.js"></script>
  <script src="/webjars/stomp-websocket/2.3.3/stomp.min.js"></script>
  <script type="text/javascript">
    var stompClient = null;
    function setConnected(connected){
      document.getElementById("connect").disabled = connected;
      document.getElementById("disconnect").disabled = !connected;
      $("#response").html();
    }
    function connect() {
      var socket = new SockJS("/broadcast");
      stompClient = Stomp.over(socket);
      stompClient.connect({}, function(frame) {
        setConnected(true);
        console.log('Connected: ' + frame);
        stompClient.subscribe('/topic/get', function(response){
          var response1 = document.getElementById('response');
          var p = document.createElement('p');
          p.style.wordWrap = 'break-word';
          p.appendChild(document.createTextNode(response.body));
          response1.appendChild(p);
        });
      });
    }

    function disconnect() {
      if (stompClient != null) {
        stompClient.disconnect();
      }
      setConnected(false);
      console.log("Disconnected");
    }

    function sendTo() {
      var name = document.getElementById('name').value;
      var context = document.getElementById('context').value;
      console.info(name);
      stompClient.send("/info/getInfo", {}, JSON.stringify({ 'name': name, 'context': context }));
    }
  </script>
</head>
<body onload="disconnect()">
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being enabled. Please enable
  Javascript and reload this page!</h2></noscript>
<div>
  <div>
    <button id="connect" onclick="connect();">Connect</button>
    <button id="disconnect" disabled="disabled" onclick="disconnect();">Disconnect</button>
  </div>
  <div id="conversationDiv">
    <labal>消息来源</labal><input type="text" id="name" />
    <labal>内容</labal><input type="text" id="context" />
    <button id="send" onclick="sendTo();">Send</button>
    <p id="response"></p>
  </div>
</div>

</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Parent Child</title>
  <script src="/webjars/jquery/3.6.0/jquery.min.js"></script>
  <script src="/webjars/sockjs-client/1.0.2/sockjs.min.js"></script>
  <script src="/webjars/stomp-websocket/2.3.3/stomp.min.js"></script>
  <script type="text/javascript">
    var stompClient = null;
    function setConnected(connected){
      document.getElementById("connect").disabled = connected;
      document.getElementById("disconnect").disabled = !connected;
      $("#response").html();
    }
    function connect() {
      var socket = new SockJS("/broadcast");
      stompClient = Stomp.over(socket);
      stompClient.connect({}, function(frame) {
        setConnected(true);
        console.log('Connected: ' + frame);
        stompClient.subscribe('/topic/get', function(response){
          var response1 = document.getElementById('response');
          var p = document.createElement('p');
          p.style.wordWrap = 'break-word';
          p.appendChild(document.createTextNode(response.body));
          response1.appendChild(p);
        });
      });
    }

    function disconnect() {
      if (stompClient != null) {
        stompClient.disconnect();
      }
      setConnected(false);
      console.log("Disconnected");
    }

  </script>
</head>
<body onload="disconnect()">
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being enabled. Please enable
  Javascript and reload this page!</h2></noscript>
<div>
  <div>
    <button id="connect" onclick="connect();">Connect</button>
    <button id="disconnect" disabled="disabled" onclick="disconnect();">Disconnect</button>
  </div>
  <div id="conversationDiv">
    <p id="response"></p>
  </div>
</div>

</body>
</html>

在Controller中使用SimpMessagingTemplate方式实现。

注:在单播订阅时,convertAndSendToUser和convertAndSend在前端订阅的时候地址拼接的方式不同。代码中有注释说明

@RestController
public class WebSocketController {

    @Autowired
    private SimpMessagingTemplate template;

    @MessageMapping("/oneToBroadcast")
    public void oneToBroadcast(GetInMessage in) throws Exception{
        in.setFrom(in.getName());
        in.setTo("全体成员");
        template.convertAndSend("/topic/getResponse", in.toString());
    }

    @MessageMapping("/toOne")
    public void toOne(GetInMessage in) throws Exception{
        //template.convertAndSendToUser(in.getTo(),"/message",in.toString());
        //或者使用convertAndSend类实现点对点的私信传输
        template.convertAndSend("/user/message/"+in.getTo(),new GetInMessage(in.getFrom()+"发送的信息"+in.getContext()));
    }

    @MessageMapping("/getInfo")
    @SendTo("/topic/get")
    public GetOutMessage getInfo(GetInMessage in){
        return new GetOutMessage(in.getContext());
    }
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Broadcast</title>
    <script src="/webjars/jquery/3.6.0/jquery.min.js"></script>
    <script src="/webjars/sockjs-client/1.0.2/sockjs.min.js"></script>
    <script src="/webjars/stomp-websocket/2.3.3/stomp.min.js"></script>
    <script type="text/javascript">
        var stompClient = null;
        function setConnected(connected){
            document.getElementById("connect").disabled = connected;
            document.getElementById("disconnect").disabled = !connected;
            $("#response").html();
        }
        function connect() {
            var socket = new SockJS("/broadcast");
            stompClient = Stomp.over(socket);
            stompClient.connect({}, function(frame) {
                setConnected(true);
                console.log('Connected: ' + frame);
                stompClient.subscribe('/topic/getResponse', function(response){
                    var response1 = document.getElementById('response');
                    var p = document.createElement('p');
                    p.style.wordWrap = 'break-word';
                    p.appendChild(document.createTextNode(response.body));
                    response1.appendChild(p);
                });
            });
        }

        function disconnect() {
            if (stompClient != null) {
                stompClient.disconnect();
            }
            setConnected(false);
            console.log("Disconnected");
        }

        function sendTo() {
            var name = document.getElementById('name').value;
            var context = document.getElementById('context').value;
            console.info(name);
            stompClient.send("/info/oneToBroadcast", {}, JSON.stringify({ 'name': name, 'context': context }));
        }
    </script>
</head>
<body onload="disconnect()">
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being enabled. Please enable
    Javascript and reload this page!</h2></noscript>
<div>
    <div>
        <button id="connect" onclick="connect();">Connect</button>
        <button id="disconnect" disabled="disabled" onclick="disconnect();">Disconnect</button>
    </div>
    <div id="conversationDiv">
        <labal>消息来源</labal><input type="text" id="name" />
        <labal>内容</labal><input type="text" id="context" />
        <button id="send" onclick="sendTo();">Send</button>
        <p id="response"></p>
    </div>
</div>

</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>ToOne</title>
  <script src="/webjars/jquery/3.6.0/jquery.min.js"></script>
  <script src="/webjars/sockjs-client/1.0.2/sockjs.min.js"></script>
  <script src="/webjars/stomp-websocket/2.3.3/stomp.min.js"></script>
  <script type="text/javascript">
    var stompClient = null;
    function setConnected(connected){
      document.getElementById("connect").disabled = connected;
      document.getElementById("disconnect").disabled = !connected;
      $("#response").html();
    }
    function connect() {
      var socket = new SockJS("/oneToOne");
      stompClient = Stomp.over(socket);
      stompClient.connect({}, function(frame) {
        setConnected(true);
        console.log('Connected: ' + frame);
        //convertAndSendToUser的路径拼接方式
        stompClient.subscribe('/user/'+document.getElementById('from').value+'/message', function(response){
        //convertAndSend的路劲拼接方式
        //stompClient.subscribe('/user/message/'+document.getElementById('from').value, function(response){
          var response1 = document.getElementById('response');
          var p = document.createElement('p');
          p.style.wordWrap = 'break-word';
          p.appendChild(document.createTextNode(response.body));
          response1.appendChild(p);
        });
      });
    }

    function disconnect() {
      if (stompClient != null) {
        stompClient.disconnect();
      }
      setConnected(false);
      console.log("断开连接");
    }

    function sendTo() {
      console.log("发送信息");
      var from = document.getElementById('from').value;
      var to = document.getElementById('to').value;
      var name = document.getElementById('name').value;
      var context = document.getElementById('context').value;
      console.info(name+"发送信息"+context);
      stompClient.send("/info/toOne", {}, JSON.stringify({ 'from': from, 'to': to, 'name': name, 'context': context}));
    }
  </script>
</head>
<body onload="disconnect()">
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being enabled. Please enable
  Javascript and reload this page!</h2></noscript>
<div>
  <div>
    <labal>用户</labal><input type="text" id="from" />
    <labal>发送对象</labal><input type="text" id="to" />
    <button id="connect" onclick="connect();">Connect</button>
    <button id="disconnect" disabled="disabled" onclick="disconnect();">Disconnect</button>
  </div>
  <div id="conversationDiv">
    <labal>标题</labal><input type="text" id="name" />
    <labal>发送内容</labal><input type="text" id="context" />
    <button id="send" onclick="sendTo();">Send</button>
    <p id="response"></p>
  </div>
</div>

</body>
</html>

当前实例可将SimpMessagingTemplate做一步提取。

@Service
public class WebSocketService {

    @Autowired
    private SimpMessagingTemplate template;

    public void broadcast(GetInMessage inMessage){
        template.convertAndSend("/topic/getResponse", inMessage.toString());
    }

    public void unicast(GetInMessage inMessage){
        template.convertAndSendToUser(inMessage.getTo(),"/message",inMessage.toString());
        //或者使用convertAndSend类实现点对点的私信传输
        //template.convertAndSend("/user/message/"+in.getTo(),new GetInMessage(in.getFrom()+"发送的信息"+in.getContext()));
    }
}
@RestController
public class WebSocketController {

    @Autowired
    private WebSocketService webSocketService;

    @MessageMapping("/oneToBroadcast")
    public void oneToBroadcast(GetInMessage in) throws Exception{
        in.setFrom(in.getName());
        in.setTo("全体成员");
        webSocketService.broadcast(in);
    }

    @MessageMapping("/toOne")
    public void toOne(GetInMessage in) throws Exception{
        webSocketService.unicast(in);
    }

    @MessageMapping("/getInfo")
    @SendTo("/topic/get")
    public GetOutMessage getInfo(GetInMessage in){
        return new GetOutMessage(in.getContext());
    }
}

小结

@SendTo和SimpMessagingTemplate的区别?

        SendTo不通用,固定发送给指定的订阅者。
        SimpMessagingTemplate灵活,支持多种发送方式。

setAllowedOrigins("*")造成的异常

java.lang.IllegalArgumentException: When allowCredentials is true, allowedOrigins cannot contain the special value "*" since that cannot be set on the "Access-Control-Allow-Origin" response header. To allow credentials to a set of origins, list them explicitly or consider using "allowedOriginPatterns" instead.

解决方案在代码中有注释:就是将setAllowedOrigins换成setAllowedOriginPatterns完美解决。

事件监听器

监听器事件类型

        SessionSubscribeEvent:订阅事件
        SessionUnsubscribeEvent:取消订阅事件
        SessionConnectEvent:建立连接事件
        SessionDisconnectEvent:断开连接事件

注:

        1、监听器类需要实现接口ApplicationListener<T>,T表示监听事件类型
        2、在监听器类上加注解@Component

@Component
public class SubscribeEvenListener implements ApplicationListener<SessionSubscribeEvent> {
    @Override
    public void onApplicationEvent(SessionSubscribeEvent event) {
        StompHeaderAccessor accessor = StompHeaderAccessor.wrap(event.getMessage());
        System.out.println("SubscribeEvenListener 订阅监听事件 类型:"+accessor.getCommand());
    }
}
@Component
public class ConnectEventListener implements ApplicationListener<SessionConnectEvent> {
    @Override
    public void onApplicationEvent(SessionConnectEvent event) {
        StompHeaderAccessor accessor = StompHeaderAccessor.wrap(event.getMessage());
        System.out.println("ConnectEventListener 连接监听器 类型:"+accessor.getCommand());
    }
}

StompHeaderAccessor简单消息传递协议中处理消息头的基类,通过该类可以获取到消息类型、会话ID等信息。

拦截器

        WebSocket结合Spring Boot的拦截器:HandshakeInterceptor握手拦截器。

http握手拦截器,可以通过这个类的方法获取到request、reponse。

拦截器需要在websocket配置文件中启用:

.addInterceptors(new HttpSessionHandshakeInterceptor())
public class WebSocketInterceptor implements HandshakeInterceptor { //extends HttpSessionHandshakeInterceptor {
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
        System.out.println("WebSocketInterceptor ------ beforeHandshake");
        return true;
    }

    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
        System.out.println("WebSocketInterceptor ------ afterHandshake");
    }
}
@Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("oneToOne").addInterceptors(new HttpSessionHandshakeInterceptor()).setAllowedOriginPatterns("*").withSockJS();
        registry.addEndpoint("broadcast").addInterceptors(new HttpSessionHandshakeInterceptor()).withSockJS();//广播端点
    }

到这里...不得不说!!!这个拦截器

不能用!不能用!!不能用!!!

执行不进去!没找出来啥原因。来个小伙伴指点下哈

Channel拦截器 

        ChannelInterceptorAdapter频道拦截器适配器结合HandshakeInterceptor实现上线下线功能。

        Channel拦截器和HTTP握手拦截器有所不同;握手拦截器对应HTTP请求,可以拿到request参数,包括sessionId之类的。而channel拦截器只是连接一个Channel(频道),类似管道,消息通过这些管道做一些操作。

实现

        只看执行顺序。创建频道拦截器并实现ChannelInterceptor,并重写需要的方法。一般常用一下三个。

package learn.websocket.learnwebsocket.interceptor;

import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.ChannelInterceptor;

/**
 * @title: WebSocketChannelInterceptor
 * @Description 频道拦截器
 * @Date: 2021/12/8 16:13
 * @Version 1.0
 */
public class WebSocketChannelInterceptor implements ChannelInterceptor {

    /**
     * 发送消息之前调用此方法
     * @param message
     * @param channel
     * @return
     */
    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        System.out.println("WebSocketChannelInterceptor ------ preSend");
        return ChannelInterceptor.super.preSend(message, channel);
    }

    /**
     * 调用发送消息时立即执行此方法
     * @param message
     * @param channel
     * @param sent
     */
    @Override
    public void postSend(Message<?> message, MessageChannel channel, boolean sent) {
        System.out.println("WebSocketChannelInterceptor ------ postSend");
        //业务Code...
    }

    /**
     * 无论是发送成功或者是异常之后都会调用此方法,一般用于资源释放
     * @param message
     * @param channel
     * @param sent
     * @param ex
     */
    @Override
    public void afterSendCompletion(Message<?> message, MessageChannel channel, boolean sent, Exception ex) {
        System.out.println("WebSocketChannelInterceptor ------ afterSendCompletion");
    }

}

在WebSocketConfig配置文件中添加 configureClientInboundChannel 和 configureClientOutboundChannel 方法。

    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(new WebSocketChannelInterceptor());
    }

    @Override
    public void configureClientOutboundChannel(ChannelRegistration registration) {
        registration.interceptors(new WebSocketChannelInterceptor());
    }

Over,查看执行顺序...

注解方式 

        Spring Boot + WebSocket 自己给自己发送信息。直接上代码

package learn.websocket.annotation.learnwebsocketannotation.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * @title: WebSocketConfig
 * @Description 开启WebSocket支持
 * @Date: 2021/12/8 9:16
 * @Version 1.0
 */
@Configuration
public class WebSocketConfig {

    /**
     * 注入ServerEndpointExporter,@Bean会自动注册使用@ServerEndpoint注解声明的websocket端点endpoint
     * @return
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }
}
package learn.websocket.annotation.learnwebsocketannotation.utils;

import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @title: WebSocketUtils
 * @Description 自己对自己发送消息
 * @Date: 2021/12/8 9:27
 * @Version 1.0
 */
@Component
@ServerEndpoint("/ws/test")
public class WebSocketUtils {

    private static AtomicInteger atomicInteger = new AtomicInteger(0);

    /**
     * 连接建立成功调用的方法
     * @param session
     */
    @OnOpen
    public void onOpen(Session session){
        atomicInteger.incrementAndGet();
        System.out.println("建立连接成功 onOpen 线程数量:"+atomicInteger.get()+" sessionID:"+session.getId());
    }

    /**
     * 连接关闭调用的方法
     * @param session
     */
    @OnClose
    public void onClose(Session session){
        atomicInteger.incrementAndGet();
        System.out.println("连接关闭成功 onOpen 线程数量:"+atomicInteger.get()+" sessionID:"+session.getId());
    }

    /**
     * 接收到客户端消息后调用的方法
     * @param msg 客户端发送的消息
     * @param session
     */
    @OnMessage
    public void onMessage(String msg, Session session) throws IOException {
        atomicInteger.incrementAndGet();
        System.out.println("接收到客户端发送的消息 onMessage:"+ msg+" 线程数量:"+atomicInteger.get()+" sessionID:"+session.getId());
        session.getBasicRemote().sendText(msg);
    }

    /**
     * 出现错误时调用的方法
     * @param session
     * @param throwable
     */
    @OnError
    public void onError(Session session, Throwable throwable){
        atomicInteger.incrementAndGet();
        System.out.println("错误 onError 线程数量:"+atomicInteger.get()+" sessionID:"+session.getId());
        throwable.printStackTrace();
    }
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Annotation Broadcast Demo</title>
    <script type="text/javascript">
        var websocket = null;
        if('WebSocket' in window){
            websocket=new WebSocket("ws://localhost:6005/ws/test");
        }else{
            console.log("Not Found WebSocket");
        }

        //建立连接回调函数
        websocket.onopen=function (event){
            setMessage("建立连接");
        }
        //关闭连接回调函数
        websocket.onclose=function (event){
            setMessage("关闭连接");
        }
        //发送消息回调函数
        websocket.onmessage=function (event){
            setMessage("发送消息:"+ event.data);
        }
        //出现错误回调函数
        websocket.onerror=function (event){
            setMessage("错误");
        }
        function setMessage(innerHTML) {
            document.getElementById('div_msg').innerHTML += innerHTML + '<br/>';
        }

        function sendto(){
            var _txtMsg=document.getElementById("txtMsg").value;
            console.log(_txtMsg);
            websocket.send(_txtMsg);
        }
        function closeto(){
            websocket.close();
        }
    </script>
</head>
<body>
<input type="text" id="txtMsg" />
<button onclick="sendto()">发送消息</button>
<button onclick="closeto()">断开连接</button>
<div id="div_msg"></div>
</body>
</html>

Over...

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值