首页 > 其他分享 >Springboot整合websocket(附详细案例代码)

Springboot整合websocket(附详细案例代码)

时间:2024-09-10 09:52:46浏览次数:10  
标签:WebSocket Springboot 案例 token 消息 import websocket 连接 客户端

文章目录


WebSocket简述

WebSocket是什么?

WebSocket 是一种网络通信协议,它提供了全双工(full-duplex)的通信渠道,允许客户端和服务端之间的双向数据交换。WebSocket 协议是在 HTTP 协议的基础上构建的,它通过一个初始的 HTTP 握手过程来建立连接,之后转换到二进制帧传输数据。
在这里插入图片描述

WebSocket 的特点

  • 全双工通信:WebSocket 支持客户端和服务端之间的双向通信,这意味着服务端可以主动向客户端发送信息,而不必等待客户端的请求。
  • 持久连接:WebSocket 连接一旦建立,就会保持打开状态,直到任何一方关闭连接。
  • 低延迟:相比轮询等其他实时通信方案,WebSocket 通信具有更低的延迟。
  • 安全性:WebSocket 可以通过 SSL/TLS 加密(wss://)来保证通信的安全性。
  • 实时性:WebSocket 支持实时数据推送,减少了轮询带来的延迟。
  • 性能:相比轮询,WebSocket 减少了请求次数,降低了带宽消耗。
  • 灵活性:WebSocket 可以用于多种类型的数据传输,包括文本、二进制数据等。

WebSocket 的工作流程

握手:客户端首先通过 HTTP 协议发起一个特殊的 GET 请求到服务端,这个请求包含了特定的升级头(Upgrade: websocket 和 Connection: Upgrade)以及其他必要的信息。
响应:服务端响应这个请求,并通过一个特定的算法生成一个接受密钥(accept key),然后发送回客户端。
建立连接:一旦握手成功,客户端和服务端之间的 WebSocket 连接就建立起来了。
数据交换:通过这个连接,客户端和服务端可以相互发送数据帧。

在这里插入图片描述

WebSocket的消息(帧)格式

WebSocket 的消息格式与 HTTP 请求和响应的消息格式有所不同。WebSocket 的消息格式可以是文本或二进制数据,并且 WebSocket 消息的传输是在一个已经建立的连接上进行的,因此不需要再进行 HTTP 请求和响应的握手操作。

WebSocket 消息格式由两个部分组成:消息头(帧头)和消息体(负载)。
消息头包含以下信息:

  • FIN: 表示这是一条完整的消息,一般情况下都是1。
  • RSV1、RSV2、RSV3: 暂时没有使用,一般都是0。
  • Opcode: 表示消息的类型,包括文本消息、二进制消息等。
  • Mask: 表示消息是否加密。
  • Payload length: 表示消息体的长度。
  • Masking key: 仅在消息需要加密时出现,用于对消息进行解密。

消息体就是实际传输的数据,可以是文本或二进制数据。

WebSocket 与 HTTP

HTTP 是一种基于请求/响应模式的应用层协议,用于在 Web 浏览器和 Web 服务器之间传输数据。它是一个无状态协议,意味着每次请求都是独立的,服务器不会记住之前的请求。
而WebSocket 是一种网络通信协议,它提供了全双工(full-duplex)的通信渠道,允许客户端和服务端之间的双向数据交换。WebSocket 协议是在 HTTP 协议的基础上构建的,它通过一个初始的 HTTP 握手过程来建立连接,之后转换到二进制帧传输数据。

springboot中整合WebSocket

pom依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

		<dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>4.6.3</version>
        </dependency>
        
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

实体类

用于存储客户端的相关信息

import lombok.Data;

import javax.websocket.Session;
import java.time.LocalDateTime;

/**
 * 客户端实体类
 *
 * @author qf
 * @since 2024/08/29 19:50
 */
@Data
public class ClientInfoEntity {

    /**
     * 客户端唯一标识
     */
    private String token;
    /**
     * 客户端连接的session
     */
    private Session session;
    /**
     * 连接存活时间
     */
    private LocalDateTime existTime;
}

配置类

握手配置类

在和客户端连接时需要用到代表每个客户端的唯一的连接标识,可以在握手配置类中为每个客户端生成一个唯一的连接标识,也可以让客户端传一个唯一的连接标识。本文使用让客户端传一个唯一的连接标识。

import org.springframework.context.annotation.Configuration;

import javax.servlet.http.HttpSession;
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;
import java.util.Map;
import java.util.UUID;

/**
 * 主要用于WebSocket的握手配置
 * @author qf
 * @since 2024/08/29 19:55
 */
@Configuration
public class GetHttpSessionConfig  extends ServerEndpointConfig.Configurator {
    /**
     * 注意:  每一个客户端发起握手,端点就有一个新的实列,那么引用的这个配置也是新的实列,这里sec的用户属性也不同就不会产生冲突。
     * 修改握手机制  就是第一次http发送过来的握手
     * @param sec   服务器websocket端点的配置
     * @param request
     * @param response
     */
    @Override
    public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
//        将从握手的请求中获取httpsession
        HttpSession httpSession =(HttpSession) request.getHttpSession();


        /**
         * 一般会在请求头中添加token 解析出来id作为键值对
         */
        Map<String, Object> properties = sec.getUserProperties();
        /**
         * 一个客户端和和服务器发起一次请求交互 就有一个唯一session
         * 设置唯一标识:为每个客户端生成一个唯一的UUID作为连接标识,并将其存储在UserProperties中,便于后续跟踪与管理
         */
//        properties.put(HttpSession.class.getName(),httpSession);
        String sessionKey = UUID.randomUUID().toString().replaceAll("-", "");
        properties.put("Connected",sessionKey);
    }
}

WebSocket配置类

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

/**
 * WebSocketConfig类的主要功能是配置和管理WebSocket端点,
 *      确保它们在应用程序启动时被正确初始化和注册,以便能够处理WebSocket连接和通信。
 *
 * @author qf
 * @since 2024/08/29 20:02
 */
@Configuration
public class WebSocketConfig {

    /**
     * 该方法用来创建并返回一个ServerEndpointExporter实例。
     * 这个实例的作用是扫描并自动配置所有使用@ServerEndpoint注解标记的WebSocket端点
     *
     * @return ServerEndpointExporter:这是一个用于自动检测和管理WebSocket端点的类。
     *          通过将其实例化并配置为Spring管理的Bean,可以确保所有WebSocket端点在应用程序启动时被自动初始化和注册。
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }
}

自定义异常类

public class ServiceException extends RuntimeException {

    private Integer code;

    private String msg;

    public ServiceException(Integer code, String msg) {
        super();
        this.msg = msg;
        this.code = code;
    }

    public ServiceException(String msg) {
        this(1, msg);
    }

    @Override
    public String getMessage() {
        return msg;
    }

    /**
     * @return code
     */
    public Integer getCode() {
        return code;
    }
}

webSocket服务类


import cn.hutool.core.util.ObjectUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.CrossOrigin;

import javax.annotation.PostConstruct;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 该类负责监听客户端的连接、断开连接、接收消息、发送消息等操作。
 *
 * @author qf
 * @since 2024/08/29 19:50
 */
@Slf4j
@Component
@CrossOrigin(origins = "*")
@ServerEndpoint(value = "/webSocket/{token}", configurator = GetHttpSessionConfig.class)
public class ChatEndpoint2 {
    //key:客户端连接唯一标识(token)
    //value:ClientInfoEntity
    private static final Map<String, ClientInfoEntity> uavWebSocketInfoMap = new ConcurrentHashMap<String, ClientInfoEntity>();

    private static final int EXIST_TIME_HOUR = 6;

    /**
     * 连接建立成功调用的方法
     *
     * @param session 第一个参数必须是session
     * @param sec
     * @param token   代表客户端的唯一标识
     */
    @OnOpen
    public void onOpen(Session session, EndpointConfig sec, @PathParam("token") String token) {
        if (uavWebSocketInfoMap.containsKey(token)) {
            throw new ServiceException("token已建立连接");
        }
        //开始进入页面默认监听所有无人机
        //把成功建立连接的会话在实体类中保存
        ClientInfoEntity entity = new ClientInfoEntity();
        entity.setToken(token);
        entity.setSession(session);
        //默认连接6个小时
        entity.setExistTime(LocalDateTime.now().plusHours(EXIST_TIME_HOUR));
        uavWebSocketInfoMap.put(token, entity);
        //之所以获取http session 是为了获取获取httpsession中的数据 (用户名 /账号/信息)
        log.info("WebSocket 连接建立成功: " + token);
    }

    /**
     * 当断开连接时调用该方法
     *
     * @param session
     */
    @OnClose
    public void onClose(Session session, @PathParam("token") String token) {
        // 找到关闭会话对应的用户 ID 并从 uavWebSocketInfoMap 中移除
        if (ObjectUtil.isNotEmpty(token) && uavWebSocketInfoMap.containsKey(token)) {
            uavWebSocketInfoMap.remove(token);
            log.info("WebSocket 连接关闭成功: " + token);
        }
    }

    /**
     * 接受消息
     * 这是接收和处理来自用户的消息的地方。我们需要在这里处理消息逻辑,可能包括广播消息给所有连接的用户。
     *
     */
    @OnMessage
    public void onMessage(Session session, @PathParam("token") String token, String message) throws IOException {
        log.info("接收到消息:" + message);

        ClientInfoEntity entity = uavWebSocketInfoMap.get(token);
        //如果是心跳包
        if("heartbeat".equals(message)){
            //只要接受到客户端的消息就进行续命(时间)
            entity.setExistTime(LocalDateTime.now().plusHours(EXIST_TIME_HOUR));
            if (entity.getSession().isOpen()) {
                entity.getSession().getBasicRemote().sendText("{\"msg\": \"success\", \"code\": 0}");
            }
            return;
        }
        //业务逻辑

        //只要接受到客户端的消息就进行续命(时间)
        entity.setExistTime(LocalDateTime.now().plusHours(EXIST_TIME_HOUR));
        if (entity.getSession().isOpen()) {
            entity.getSession().getBasicRemote().sendText("{\"msg\": \"success\", \"code\": 0}");
        }
    }

    /**
     * 处理WebSocket中发生的任何异常。可以记录这些错误或尝试恢复。
     */
    @OnError
    public void one rror(Throwable error) {
        log.error("报错信息:" + error.getMessage());
        error.printStackTrace();

    }

    private static final SimpleDateFormat FORMAT = new SimpleDateFormat("yyyy:MM:dd hh:mm:ss");

    /**
     * 发生消息定时器
     */
    @PostConstruct
    @Scheduled(cron = "0/1 * *  * * ? ")
    public void refreshDate() {
        //开启定时任务,1秒一次向前台发送当前时间
        //当没有客户端连接时阻塞等待
        if (!uavWebSocketInfoMap.isEmpty()) {
            //超过存活时间进行删除
            Iterator<Map.Entry<String, ClientInfoEntity>> iterator = uavWebSocketInfoMap.entrySet().iterator();
            while (iterator.hasNext()) {
                Map.Entry<String, ClientInfoEntity> entry = iterator.next();
                if (entry.getValue().getExistTime().compareTo(LocalDateTime.now()) <= 0) {
                    log.info("WebSocket " + entry.getKey() + " 已到存活时间,自动断开连接");
                    try {
                        entry.getValue().getSession().close();
                    } catch (IOException e) {
                        log.error("WebSocket 连接关闭失败: " + entry.getKey() + " - " + e.getMessage());
                    }
                    //过期则进行移除
                    iterator.remove();
                }
            }
            sendMessage(FORMAT.format(new Date()));
        }
    }

    /**
     * 群发信息的方法
     *
     * @param message 消息
     */
    public void sendMessage(String message) {
        System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())
                + "发送全体消息:" + message);
        //循环客户端map发送消息
        uavWebSocketInfoMap.values().forEach(item -> {
            //向每个用户发送文本信息。这里getAsyncRemote()解释一下,向用户发送文本信息有两种方式,
            // 一种是getBasicRemote,一种是getAsyncRemote
            //区别:getAsyncRemote是异步的,不会阻塞,而getBasicRemote是同步的,会阻塞,由于同步特性,第二行的消息必须等待第一行的发送完成才能进行。
            // 而第一行的剩余部分消息要等第二行发送完才能继续发送,所以在第二行会抛出IllegalStateException异常。所以如果要使用getBasicRemote()同步发送消息
            // 则避免尽量一次发送全部消息,使用部分消息来发送,可以看到下面sendMessageToTarget方法内就用的getBasicRemote,因为这个方法是根据用户id来私发的,所以不是全部一起发送。
            item.getSession().getAsyncRemote().sendText(message);
        });
    }
}

websocket中Session的 getBasicRemote() 和 getAsyncRemote() 方法的区别

websocket中Session的 getBasicRemote() 和 getAsyncRemote() 方法有以下主要区别

  1. 同步 vs 异步
    getBasicRemote():同步发送消息的方式。发送消息时会阻塞当前线程,直到消息发送完成。
    getAsyncRemote():异步发送消息的方式。发送消息后立即返回,不阻塞当前线程。
  2. 性能和并发
    getBasicRemote():由于同步特性,可能会导致性能瓶颈,尤其是在高并发场景下。
    getAsyncRemote():异步特性使得它更适合高并发场景,提高系统的响应速度和吞吐量。
  3. 使用场景
    getBasicRemote():适用于消息量较小且对实时性要求较高的场景。
    getAsyncRemote():适用于大量消息发送或者对实时性要求相对较低的场景。
  4. 总之,getBasicRemote():同步发送,适合小量消息和高实时性需求。
    getAsyncRemote():异步发送,适合大量消息和高并发场景。

前端代码

代码为.uve后缀文件,直接将代码复制到uve项目中即可。

<template>
<!-- websocketceshi -->
  <div class="layout">
    <div class="msgBody">{{ msg }}</div>
    <input v-model="sendMsg" style="width:200px;height:30px;margin-top:20px"/>
    <button @click="sendMessage" style="width:100px;height:30px;">发送</button>
    <button @click="close" style="width:100px;height:30px;">断开链接</button>
    <button @click="init" style="width:100px;height:30px;">建立链接</button>
  </div>
</template>

<script>
export default {
  name: "LayOut",
  data() {
    return {
      msg: "",
      sendMsg: "",
      //后台的地址,只需要动localhost:8089部分,改成你后端的地址。
      //后面webSocket是后台设定的接口地址,uuid_test001是你前台的客户端唯一id(可以使用uuid生成)。
      //用于区分不同的客户端,比如你多个客户端连接后台,后台推送数据的时候需要根据这个id不同,给对应的人推送,不然就推送到所有建立链接的网页上了
      path: "ws://localhost:8089/qf/webSocket/uuid_test001",
      //存websocket实例化的
      socket: "",
    };
  },
  methods: {
    //用于前台发送数据到后台,调用websocket中的send方法把数据发过去。
    sendMessage() {
      this.socket.send(this.sendMsg);
    },
    //初始化建立前后台链接
    init() {
      if (typeof WebSocket === "undefined") {
        alert("您的浏览器不支持socket");
      } else {
        // 实例化socket
        this.socket = new WebSocket(this.path);
        // 监听socket连接
        this.socket.onopen = this.open;
        // 监听socket错误信息
        this.socket.onerror = this.error;
        // 监听socket消息
        this.socket.onmessage = this.getMessage;
        this.socket.onclose = this.close;
      }
    },
    //链接成功时的回调函数
    open() {
      console.log("socket连接成功");
    },
    //链接错误时的回调
    error(err) {
      console.log("连接错误" + err);
    },
    //后台消息推送过来,接收的函数,参数为后台推过来的数据。
    getMessage(msg) {
      this.msg = msg.data;
    },
    //链接关闭的回调
    close(event) {
      //socket是链接的实例,close就是关闭链接
      this.socket.close()
      console.log("断开链接成功");
    },
  },
  created() {
    //开局初始化建立链接
    this.init();
  },
};
</script>
<style scoped>
.layout {
  position: relative;
  width: 100%;
  height: 100%;
}
.msgBody {
  width: 500px;
  height: 300px;
  border: 1px solid rgb(95, 79, 79);
}
</style>

心跳机制

在WebSocket中使用心跳机制是为了保持 WebSocket 连接的活跃状态而设计的一种机制。在长时间没有数据传输的情况下,WebSocket 连接可能会被中间的网络设备(如路由器或防火墙)认为已经断开,从而导致连接失效。心跳机制通过定期发送小的数据包来保持连接的活动状态,从而避免这种情况的发生。

心跳机制的作用:

  1. 保持连接活跃:在网络设备中,长时间没有数据传输的连接可能会被视为断开,心跳机制可以防止这种情况。
  2. 检测连接状态:心跳机制可以帮助检测连接是否仍然有效,及时发现并处理连接中断的情况。
  3. 优化资源使用:心跳机制可以确保无效的连接被及时关闭,释放资源。

实现心跳机制的方式
心跳机制可以通过多种方式实现,常见的方法包括定时发送心跳包、响应心跳请求以及超时重试等。

添加心跳机制的前端代码

<template>
    <!-- websocketceshi -->
    <div class="layout">
      <div class="msgBody">{{ msg }}</div>
      <input
        v-model="sendMsg"
        style="width: 200px; height: 30px; margin-top: 20px"
      />
      <button @click="websocketsend(sendMsg)" style="width: 100px; height: 30px">
        发送
      </button>
      <button @click="websocketclose" style="width: 100px; height: 30px">
        断开链接
      </button>
      <button @click="initWebSocket" style="width: 100px; height: 30px">
        建立链接
      </button>
    </div>
  </template>
  
  <script>
  export default {
    name: "LayOut",
    data() {
      return {
        websock: null, //建立的连接
        lockReconnect: false, //是否真正建立连接
        timeout: 20 * 1000, //20秒一次心跳
        timeoutObj: null, //心跳心跳倒计时
        serverTimeoutObj: null, //心跳倒计时
        timeoutnum: null, //断开 重连倒计时
        msg: "", //显示的值
        sendMsg: "", //输入框的值
      };
    },
    created() {
      // //页面刚进入时开启长连接
      this.initWebSocket();
    },
    destroyed() {
      //页面销毁时关闭长连接
      this.websocketclose();
    },
    methods: {
            //建立连接,初始化weosocket
      initWebSocket() {
        //后台地址,前面的ws不动,后面是后台地址,我是本地运行的所以填的本地,自行更改。再后面webSocket是后端的接口地址,uuid_test002是参数
        const wsuri = "ws://localhost:8089/qf/webSocket/uuid_test000";
        //建立连接
        this.websock = new WebSocket(wsuri);
        //连接成功
        this.websock.onopen = this.websocketonopen;
        //连接错误
        this.websock.onerror = this.websocketonerror;
        //接收信息
        this.websock.onmessage = this.websocketonmessage;
        //连接关闭
        this.websock.onclose = this.websocketclose;
      },
      reconnect() {
        //重新连接
        var that = this;
        //判断链接状态,true就是链接,false是断开,这里如果是链接状态就不继续执行了,跳出来。
        if (that.lockReconnect) {
          return;
        }
        //把链接状态改为true
        that.lockReconnect = true;
        //没连接上会一直重连,设置延迟避免请求过多
        that.timeoutnum && clearTimeout(that.timeoutnum);
        that.timeoutnum = setTimeout(function () {
          //初始化新连接
          that.initWebSocket();
          //把链接状态改为false
          that.lockReconnect = false;
        }, 5000);
      },
      reset() {
        //重置心跳
        var that = this;
        //清除时间
        clearTimeout(that.timeoutObj);
        clearTimeout(that.serverTimeoutObj);
        //重启心跳
        that.start();
      },
      start() {
        //开启心跳
        var self = this;
        //有延迟时间的就清除掉
        self.timeoutObj && clearTimeout(self.timeoutObj);
        self.serverTimeoutObj && clearTimeout(self.serverTimeoutObj);
        //从新创建计时器
        self.timeoutObj = setTimeout(function () {
          //这里发送一个心跳,后端收到后,返回一个心跳消息
          if (self.websock.readyState == 1) {
            //如果连接正常发送信息到后台
            self.websock.send("ping");
          } else {
            //否则重连
            self.reconnect();
          }
          self.serverTimeoutObj = setTimeout(function () {
            //超时关闭
            self.websock.close();
          }, self.timeout);
        }, self.timeout);
      },
      //链接成功时执行的方法
      websocketonopen() {
        //连接成功事件
        this.websocketsend("heartbeat");
        //提示成功
        console.log("连接成功", 3);
        //开启心跳
        this.start();
      },
      //连接失败事件
      websocketonerror(e) {
        //错误
        console.log("WebSocket连接发生错误");
        //重连
        this.reconnect();
      },
      //连接关闭事件
      websocketclose(e) {
        this.websock.close();
        //提示关闭
        console.log("连接已关闭");
        //重连
        this.reconnect();
      },
      //接收服务器推送的信息
      websocketonmessage(event) {
        //打印收到服务器的内容
        console.log("收到服务器信息", event.data);
        this.msg = event.data;
        //收到服务器信息,心跳重置
        this.reset();
      },
      websocketsend(msg) {
        //向服务器发送信息
        this.websock.send(msg);
      },
    },
  };
  </script>
  <style scoped>
  .layout {
    position: relative;
    width: 100%;
    height: 100%;
  }
  .msgBody {
    width: 500px;
    height: 300px;
    border: 1px solid rgb(95, 79, 79);
  }
  </style>
  
  

参考文章:
websocket前后端交互

标签:WebSocket,Springboot,案例,token,消息,import,websocket,连接,客户端
From: https://blog.csdn.net/weixin_46425661/article/details/141897511

相关文章

  • 最新扣子(Coze)实战案例:Coze触发器的用法,做一个运动提醒Bot,完全免费教程
    大家好,我是斜杠君。我的教程《AI应用开发系列教程之扣子(Coze)实战教程》正在免费开放中,如果想学习AI应用搭建,请关注公众号,获取免费教程。​最近有同学问,扣子(Coze)中的触发器如何使用。今天就和大家说说扣子平台触发器的使用方法。什么是触发器Coze平台的触发器功能允许你的B......
  • 216基于Springboot + vue实现的校园管理系统(含论文+答辩PPT)
    作者主页:夜未央5788 简介:Java领域优质创作者、Java项目、学习资料、技术互助文末获取源码项目介绍基于Springboot+vue实现的校园管理系统(含论文+答辩PPT)本系统包含管理员、用户、院校管理员三个角色。管理员角色:用户管理、院校管理、单位类别管理、院校管理员管......
  • 前端登录注册页面springboot+vue2全开发!
    需求目标:有“登录界面”和“注册界面”以及“功能操作界面”:我们打开程序会自动进入“登录界面”,如果密码输入正确则直接进入“功能操作界面”,在“登录界面”我们可以点击注册进入“注册页面”,注册好了可以再跳回到“登录界面”进行登录。代码实现:(1)登录操作后端开发见我博......
  • springboot+vue+mybatis计算机毕业设计老年人健康管理系统+PPT+论文+讲解+售后
    近些年来,随着科技的飞速发展,互联网的普及逐渐延伸到各行各业中,给人们生活带来了十分的便利,老年人健康管理系统利用计算机网络实现信息化管理,使整个老年人健康管理的发展和服务水平有显著提升。本文拟采用Eclipse开发工具,JSP技术,SSM框架进行开发,后台使用MySQL数据库进行信息......
  • 案例分析:如何用设计模式优化性能1
    设计模式就是对常用开发技巧进行的总结,它使得程序员之间交流问题,有了更专业、便捷的方式。比如,我们在《02|理论分析:性能优化有章可循,谈谈常用的切入点》中提到,I/O模块使用的是装饰器模式,你就能很容易想到I/O模块的代码组织方式。事实上,大多数设计模式并不能增加程序的性......
  • 案例分析:如何用设计模式优化性能12
    设计模式就是对常用开发技巧进行的总结,它使得程序员之间交流问题,有了更专业、便捷的方式。比如,我们在《02|理论分析:性能优化有章可循,谈谈常用的切入点》中提到,I/O模块使用的是装饰器模式,你就能很容易想到I/O模块的代码组织方式。事实上,大多数设计模式并不能增加程序的性......
  • 案例分析:如何用设计模式优化性能2
    设计模式就是对常用开发技巧进行的总结,它使得程序员之间交流问题,有了更专业、便捷的方式。比如,我们在《02|理论分析:性能优化有章可循,谈谈常用的切入点》中提到,I/O模块使用的是装饰器模式,你就能很容易想到I/O模块的代码组织方式。事实上,大多数设计模式并不能增加程序的性......
  • springboot+vue+mybatis计算机毕业设计网上购物系统+PPT+论文+讲解+售后
    本文首先实现了网上购物系统设计与实现管理技术的发展随后依照传统的软件开发流程,最先为系统挑选适用的言语和软件开发平台,依据需求分析开展控制模块制做和数据库查询构造设计,随后依据系统整体功能模块的设计,制作系统的功能模块图、E-R图。随后,设计框架,依据设计的框架撰写编码......
  • tsx 基本使用方式案例、tsx 使用class 方式
    tsx子组件、tsx使用class方式tsx组件代码import{defineComponent,PropType,h,computed,ref,watch}from'vue';importtype{Reactive,Ref}from'vue';importstylesfrom'../scss/child.module.scss';//函数局部组件实现constmyDiv=......
  • 爬虫案例2-爬取视频的三种方式之一:requests篇(1)
    @目录前言爬虫步骤确定网址,发送请求获取响应数据对响应数据进行解析保存数据完整源码共勉博客前言本文写了一个爬取视频的案例,使用requests库爬取了好看视频的视频,并进行保存到本地。后续也会更新selenium篇和DrissionPage篇。当然,爬取图片肯定不止这三种方法,还有基于python的sc......