55 WebSocket

news2024/11/26 12:21:32

55 WebSocket

参考资料

  1. WebSocket

  2. SpringBoot使用WebSocket

  3. SpringBoot 集成WebSocket详解

前言

  • WebSocket是HTML5下一种新的协议(websocket协议本质上是一个基于tcp的协议)
  • 它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯的目的
  • WebSocket是一个持久化的协议
  • WebSocket约定了一个通信的规范,通过一个握手的机制,客户端和服务器之间能建立一个类似tcp的连接,从而方便它们之间的通信
  • 在WebSocket出现之前,web交互一般是基于http协议的短连接或者长连接
  • WebSocket是一种全新的协议,不属于HTTP无状态协议,协议名为"ws"

WebSocket和HTTP在这里插入图片描述

相同点

  • 都是基于TCP协议即都是可靠性传输协议;
  • 都是应用层协议;

不同点

  1. WebSocket是双向通信协议,模拟Socket协议,可以双向发送或接受信息
  2. HTTP是单向的
  3. WebSocket是需要浏览器和服务器握手进行建立连接的
  4. 而HTTP是浏览器发起向服务器的连接,服务器预先并不知道这个连接

WebSocket在建立握手时,数据是通过HTTP传输的。但是建立之后,在真正传输时候是不需要HTTP协议的

总结(总体过程):

  • 首先,客户端发起http请求,经过3次握手后,建立起TCP连接;http请求里存放WebSocket支持的版本号等信息,如:Upgrade、Connection、WebSocket-Version等;
  • 然后,服务器收到客户端的握手请求后,同样采用HTTP协议回馈数据;
  • 最后,客户端收到连接成功的消息后,开始借助于TCP传输信道进行全双工通信。

WebSocket解决的问题

HTTP存在的问题

如果是基于HTTP协议,在我们开发的时候,如果后端服务器想要给客户端发送信息是无法实现的,我们只能等客户端发送请求后才能把该信息发送给客户端。

这是因为HTTP协议是无状态的,每当一次会话完成后,服务端都不知道下一次的客户端是谁,需要每次知道对方是谁,才进行相应的响应,因此本身对于实时通讯就是一种极大的障碍。

http协议采用一次请求,一次响应,每次请求和响应就携带有大量的header头,对于实时通讯来说,解析请求头也是需要一定的时间,因此,效率也更低下。

最重要的是,需要客户端主动发,服务端被动发,也就是一次请求,一次响应,不能实现主动发送。

long poll(长轮询)

HTTP为了解决这个问题提出来一种技术,那就是long poll(长轮询),基于HTTP的特性,简单点说,就是客户端发起长轮询,如果服务端的数据没有发生变更,会 hold 住请求,直到服务端的数据发生变化,或者等待一定时间超时才会返回。返回后,客户端又会立即再次发起下一次长轮询。

很显然这样会占用很多的资源,如果用户量很大,很多请求都在等待,这需要很大的并发量。

Ajax轮询

同样也是为了解决这个问题,提出了Ajax轮询,即让前端页面每隔一段时间就向后端发送一个请求。

  • 总的来看,Ajax轮询存在的问题:
  1. 推送延迟。
  2. 服务端压力。配置一般不会发生变化,频繁的轮询会给服务端造成很大的压力。
  3. 推送延迟和服务端压力无法中和。降低轮询的间隔,延迟降低,压力增加;增加轮询的间隔,压力降低,延迟增高
websocket的改进

一旦WebSocket连接建立后,后续数据都以帧序列的形式传输。在客户端断开WebSocket连接或Server端中断连接前,不需要客户端和服务端重新发起连接请求。在海量并发及客户端与服务器交互负载流量大的情况下,极大的节省了网络带宽资源的消耗,有明显的性能优势,且客户端发送和接受消息是在同一个持久连接上发起,实现了“真·长链接”,实时性优势明显。

WebSocket建立连接是通过TCP的三次握手,其接下来的通信也是通过升级HTTP来达到效果的。

在这里插入图片描述

连接建立过程

WebSocket协议的建立连接过程如下:

  • 客户端发送一个HTTP请求到服务器,请求中包括希望升级为WebSocket协议的信息,即在请求头中包含Upgrade字段,值为"websocket";
  • 服务器收到请求后,会返回一个HTTP 101状态码,表示同意升级为WebSocket协议,同时在响应头中添加Upgrade字段和Connection字段,告知客户端已升级为WebSocket协议;
  • 客户端收到服务器的响应后,会通过TCP通道进行传输通信。

在SpringBoot中实现WebSocket实时通信

为了加深对WebSocket的了解以及它的使用,为此用一个简单的小例子来使用WebSocket,前端基于vue2,后端基于SpringBoot、WebSocket来简单的做一个好友聊天的功能页面。

具体页面情况如下:

在这里插入图片描述

WebSocket

下面就一步一步来介绍其实现。

WebSocket依赖注入问题

在 Spring Boot WebSocket 中,依赖注入可能会出现问题,尤其是在 WebSocket 端点类中使用 @Autowired 注解时。原因主要与 WebSocket 的生命周期管理和 Spring 的管理机制之间的差异有关。

WebSocket 实例由 Web 容器管理而非 Spring 容器。

解决方案:

package com.fang.screw.chat.config;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

import javax.websocket.server.ServerEndpointConfig;

/**
 * @FileName CustomSpringConfigurator
 * @Description 这段代码的主要作用是将Spring框架与WebSocket集成,使得WebSocket端点能够通过Spring的依赖注入机制获取Spring管理的Bean。
 * 具体来说,它通过实现ServerEndpointConfig.Configurator接口和ApplicationContextAware接口,将Spring的ApplicationContext
 * 注入到WebSocket配置中,从而允许WebSocket端点使用Spring的依赖注入功能。
 * @Author yaoHui
 * @date 2024-10-08
 **/
public class CustomSpringConfigurator extends ServerEndpointConfig.Configurator implements ApplicationContextAware {

    private static volatile BeanFactory context;

    @Override
    public  <T> T getEndpointInstance(Class<T> clazz) {
        return context.getBean(clazz);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        CustomSpringConfigurator.context = applicationContext;
    }

}

@Component
@Slf4j
@ServerEndpoint("/websocket/{myUserId}")
public class WebSocket {

    private static MessageMapper messageMapper;

    @Autowired
    public void setMessageMapper(MessageMapper messageMapper){
        WebSocket.messageMapper = messageMapper;
    }
 }

前端

更详细的代码请点击查看
点击查看

<template>

  <div id="app">

    <div class="main1">
      .
    </div>

    <div class="main">

      <div class="contact">
        <div class="top">
          <div class="left">
            <img class="avatar" src="" alt="" />
          </div>

          <div class="right">
            {{ user.username }}
          </div>

          <!-- 添加好友按钮 -->
          <button @click="showModal = true" class="add-friend-button">Add Friend</button>

          <!-- 弹出的小页面(模态框) -->
          <div v-if="showModal" class="modal">
            <div class="modal-content">
              <div class="modal-header">
                <h2>Add a Friend</h2>
                <span @click="showModal = false" class="close">&times;</span>
              </div>

              <!-- 搜索框 -->
              <div class="search-section">
                <input v-model="searchQuery" type="text" placeholder="Search by username or phone number"
                  class="search-input" @keyup.enter="searchFriends" />
              </div>

              <!-- 搜索结果展示 -->
              <div class="friend-list">
                <div v-for="friend in filteredFriends" :key="friend.id" class="friend-item">
                  <div class="friend-avatar">
                    <img :src="friend.avatar" :alt="friend.username" />
                  </div>
                  <div class="friend-info">
                    <div class="friend-name">{{ friend.username }}</div>
                    <button @click="addFriend(friend)" class="add-button">Add</button>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>

        <div v-if="friends.length" class="bottom">
          <div v-for="(friend, i) in friends" class="friend" :class="{activeColor: isActive(i)}"
            @click="selectFriend(friend)">
            <div class="left">
              <img class="avatar" src="" alt="" />
            </div>
            <div class="right">
              {{ friend.userBName }}
            </div>
          </div>
        </div>

        <div v-else class="info">
          <div class="msg">
            还没有好友~~~
          </div>
        </div>
      </div>

      <div v-if="selectedFriend" class="dialog">
        <div class="top1">
          <div class="name">
            {{ selectedFriend.userBName }}
          </div>
        </div>
        <div class="middle" @mouseover="over" @mouseout="out">
          <div v-if="msgList.length">
            <div v-for="msg in msgList">
              <div class="msg"
                :style="msg.sendUser === selectedFriend.userB ? 'flex-direction: row;' : 'flex-direction: row-reverse;'">
                <div class="avatar">
                  <img alt="" src="" />
                </div>
                <div v-if="msg.sendUser === selectedFriend.userB" style="flex: 13;">
                  <div class="bubble-msg-left" style="margin-right: 75px;">
                    {{ msg.message }}
                  </div>
                </div>
                <div v-else style="flex: 13;">
                  <div class="bubble-msg-right" style="margin-left: 75px;">
                    {{ msg.message }}
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
        <div class="line"></div>
        <div class="bottom">
          <label>
            <textarea class="messageText" maxlength="256" v-model="msg" :placeholder="hint"
              @keydown.enter="sendMsg($event)"></textarea>
          </label>
          <button class="send" :class="{emptyText: isEmptyText}" title="按下 ENTER 发送" @click="sendMsg()">发送</button>
        </div>
      </div>
      <div v-else class="info">
        <div class="msg">
          找个好友聊天吧~~~
        </div>
      </div>

    </div>
  </div>

</template>

<script>
  export default {
    data () {
      return {
        showModal: false,   // 控制模态框的显示与隐藏
        searchQuery: '',    // 搜索框的输入内容
        msg: '',
        friends: [],
        selectedFriend: null,
        chatHistory: [],
        newMessage: '',
        websocket: null,
        hint: '',
        bubbleMsg: '',
        interval: null,
        isEmptyText: true,
        msgList: [],
        filteredFriends: [] // 搜索到的好友
      };
    },
    computed: {
      user () {
        return JSON.parse(localStorage.getItem('currentUser'))
      }
    },
    watch: {
      msgList () {
        const mid = document.querySelector('.middle')
        this.$nextTick(() => {
          mid && (mid.scrollTop = mid.scrollHeight)
          document.querySelector('.messageText').focus()
        })
      },
      msg () {
        this.isEmptyText = !this.msg
      }
    },
    methods: {
      searchFriends () {
        // 模拟通过用户名或电话号码搜索好友
        this.$http.get(this.$constant.baseURL + "/upm/user/getUserByUserNameOrEmail/" + this.searchQuery)
          .then((res) => {
            if (!this.$common.isEmpty(res.data)) {
              this.filteredFriends = res.data;
            }
          })
          .catch((error) => {
            this.$message({
              message: error.message,
              type: "error"
            });
          });
      },
      addFriend (friend) {
        alert(`Friend request sent to ${friend.username}`);

        this.$http.post(this.$constant.baseURL + "/chat/addFriend", {
          "id": friend.id
        })
          .then((res) => {
            alert("添加成功");
          })
          .catch((error) => {
            this.$message({
              message: error.message,
              type: "error"
            });
          });

        // 执行添加好友的逻辑
      },
      over () {
        this.setColor('#c9c7c7')
      },
      out () {
        this.setColor('#0000')
      },
      setColor (color) {
        document.documentElement.style.setProperty('--scroll-color', `${color}`)
      },

      getFriends () {
        this.$http.get(this.$constant.baseURL + "/chat/getFriendsListById")
          .then((res) => {
            if (!this.$common.isEmpty(res.data)) {
              this.friends = res.data
            }
          })
          .catch((error) => {
            this.$message({
              message: error.message,
              type: "error"
            });
          });
      },
      selectFriend (friend) {
        this.selectedFriend = friend;
        this.loadChatHistory(friend);
      },
      loadChatHistory (friend) {
        this.$http.post(this.$constant.baseURL + "/chat/message/getMessage", friend)
          .then((res) => {
            if (!this.$common.isEmpty(res.data)) {
              this.msgList = res.data;
            }
          })
          .catch((error) => {
            this.$message({
              message: error.message,
              type: "error"
            });
          });
      },
      sendMsg (e) {
        if (e) {
          e.preventDefault()
        }
        if (!this.msg) {
          this.hint = '信息不可为空!'
          return
        }

        let entity = {
          sendUser: JSON.parse(localStorage.getItem('currentUser')).id,
          receiveUser: this.selectedFriend.userB,
          message: this.msg.trim(),
          // time: new Date()
        }
        this.websocket.send(JSON.stringify(entity))
        var ha = JSON.stringify(entity);
        this.msgList.push(entity);
        this.msg = ''
        this.hint = ''
      }
      ,
      sendMessage () {
        if (this.newMessage.trim() === '') return;

        const message = { content: this.newMessage, isMine: true };
        this.chatHistory.push(message);

        let entity = {
          sendUser: JSON.parse(localStorage.getItem('currentUser')).id,
          receiveUser: this.selectedFriend.userB,
          message: this.newMessage.trim(),
          // time: new Date()
        }

        this.websocket.send(JSON.stringify(entity)); // Send message through WebSocket
        this.newMessage = '';
      },
      setupWebSocket () {
        this.websocket = new WebSocket(`ws://localhost:56281/websocket/${JSON.parse(localStorage.getItem('currentUser')).id}`);
        this.websocket.onmessage = (event) => {
          const receivedMessage = JSON.parse(event.data);
          this.msgList.push(receivedMessage);
        };
      },
      setContact (index) {
        this.active = index
        delete this.friendList[index].password
        this.$emit('set-contact', this.friendList[index])
      },
      isActive (index) {
        return this.active === index
      }
    },
    mounted () {
      this.setupWebSocket();
      this.getFriends();
    },
  };
</script>

<style scoped>
  .contact {
    width: 360px;
    height: 100%;
    float: left;
    border-right: #d0d0d0 1px solid;
  }

  .top {
    width: 100%;
    height: 80px;
    display: flex;
    align-items: center;
    border-bottom: #e0dfdf 1px solid;
  }

  .activeColor {
    background-color: #c9cbcb;
  }

  .top .left {
    flex: 1;
    text-align: center;
  }

  .avatar {
    width: 48px;
    height: 48px;
    border-radius: 4px;
  }

  .top .right {
    flex: 3;
    color: black;
    /* 设置字体颜色为黑色 */
  }

  .friend {
    width: 360px;
    height: 60px;
    line-height: 60px;
    display: flex;
    align-items: center;
    border-bottom: #faf7f7 1px solid;
  }

  .friend .left {
    flex: 1;
    margin-top: 24px;
    text-align: center;
  }

  .friend .right {
    flex: 3;
    color: #575454;
    font-size: 14px;
    color: black;
    /* 设置字体颜色为黑色 */
  }

  .friend .avatar {
    width: 36px;
    height: 36px;
  }

  .info {
    margin-top: 230px;
  }

  .info .msg {
    text-align: center;
  }

  #app {
    width: 100%;
    height: 100%;
    background-size: cover;
    background-image: url("../assets/img/chat-bg.jpg");
  }

  .main {
    width: 1080px;
    height: 648px;
    margin-top: 72px;
    margin-left: auto;
    margin-right: auto;
    border-radius: 5px;
    background-color: #efeded;
    border: #d0d0d0 1px solid;
    box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04);
    /* 新增的 Flexbox 样式 */
    display: flex;
    /* 启用 Flexbox 布局 */
    flex-direction: row;
    /* 水平方向排列子元素 */
    justify-content: space-between;
    /* 子元素间的距离自动分配,或根据需要调整 */
  }

  :root {
    --scroll-color: #0000;
  }

  .dialog {
    width: 719px;
    height: 100%;
    float: right;
  }

  .name {
    position: relative;
    top: 22px;
    left: 25px;
  }

  .info {
    width: 719px;
    height: 100%;
    display: flex;
    align-items: center;
  }

  .info .msg {
    flex: 1;
    text-align: center;
  }

  .top1 {
    width: 100%;
    height: 60px;
    border-bottom: #d0d0d0 1px solid;
  }

  .top1::after {
    content: " ";
    float: right;
    position: relative;
    top: 40px;
    border: 4px solid #0000;
    border-top-color: #8e9292;
  }

  .middle {
    height: 432px;
    overflow: auto;
    padding: 10px;
    margin: 6px 0 11px 0;
  }

  .middle::-webkit-scrollbar {
    width: 8px;
    height: 1px;
  }

  .middle::-webkit-scrollbar-thumb {
    border-radius: 8px;
    background-color: var(--scroll-color);
  }

  .middle::-webkit-scrollbar-track {
    background: #efeded;
    border-radius: 4px;
  }

  .middle .msg {
    display: flex;
  }

  .avatar {
    margin: 8px;
    flex: 1;
  }

  .avatar img {
    width: 36px;
    height: 36px;
    border-radius: 4px;
  }

  .bubble-msg-left,
  .bubble-msg-right {
    padding: 10px;
    font-size: 14px;
    margin-top: 10px;
    line-height: 24px;
    border-radius: 5px;
    width: fit-content;
    line-break: anywhere;
  }

  .bubble-msg-left {
    float: left;
    color: black;
    margin-left: -12px;
    text-indent: -0.5em;
    background-color: white;
  }

  .bubble-msg-right {
    float: right;
    color: white;
    background-color: #1e6ee1;
  }

  .bubble-msg-right::before {
    content: " ";
    float: right;
    position: relative;
    left: 18px;
    border: 4px solid #0000;
    border-left-color: #1e6ee1;
  }

  .bubble-msg-left::before {
    content: " ";
    float: left;
    position: relative;
    left: -18px;
    border: 4px solid #0000;
    border-right-color: white;
  }

  .line {
    width: 100%;
    height: 0;
    position: relative;
    top: -6px;
    border-top: #d0d0d0 1px solid;
  }

  .dialog .bottom {
    padding-left: 10px;
    padding-right: 25px;
  }

  .messageText {
    position: relative;
    margin-right: 2px;
    font: 14px/1.5 Helvetica, Arial, Tahoma, 微软雅黑;
    width: 100%;
    height: 106px;
    outline: none;
    background: #efeded;
    border: 0 none;
    overflow-y: auto;
    -webkit-box-sizing: border-box;
    box-sizing: border-box;
    resize: none;
    vertical-align: middle;
    display: inline-block;
  }

  .dialog .bottom::after {
    content: " ";
    float: right;
    position: relative;
    top: -121px;
    left: 75px;
    border: 4px solid #0000;
    border-bottom-color: #8e9292;
  }

  .send {
    float: right;
    position: relative;
    top: -20px;
    left: 10px;
    background-color: #51a5e6;
    border: #87ceeb;
    color: #fff;
    font-size: 12px;
    width: 50px;
    height: 22px;
    border-radius: 3px;
  }

  .send:focus {
    outline: none;
  }

  .emptyText {
    background-color: #d0d0d0;
  }

  .name {
    color: black;
    /* 设置字体颜色为黑色 */
  }




  /* 主体按钮样式 */
  .add-friend-button {
    padding: 10px 20px;
    background-color: #007bff;
    color: white;
    font-size: 18px;
    border: none;
    border-radius: 20px;
    cursor: pointer;
    transition: background-color 0.3s;
  }

  .add-friend-button:hover {
    background-color: #0056b3;
  }

  /* 模态框背景 */
  .modal {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.5);
    display: flex;
    justify-content: center;
    align-items: center;
  }

  /* 模态框内容 */
  .modal-content {
    background-color: #fff;
    padding: 20px;
    border-radius: 15px;
    width: 400px;
    max-width: 100%;
    box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.2);
  }

  /* 模态框头部 */
  .modal-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 20px;
  }

  .modal-header h2 {
    margin: 0;
    font-size: 24px;
    font-family: 'KaiTi', serif;
  }

  .close {
    cursor: pointer;
    font-size: 24px;
  }

  /* 搜索框样式 */
  .search-input {
    width: 100%;
    padding: 10px;
    border: 2px solid #ccc;
    border-radius: 10px;
    font-size: 18px;
    margin-bottom: 20px;
  }

  /* 好友列表样式 */
  .friend-list {
    display: flex;
    flex-direction: column;
  }

  /* 单个好友项 */
  .friend-item {
    display: flex;
    align-items: center;
    margin-bottom: 15px;
    padding: 10px;
    border: 1px solid #ccc;
    border-radius: 10px;
  }

  .friend-avatar img {
    width: 50px;
    height: 50px;
    border-radius: 50%;
    margin-right: 15px;
  }

  .friend-info {
    display: flex;
    justify-content: space-between;
    align-items: center;
    width: 100%;
  }

  .friend-name {
    font-size: 18px;
    font-family: 'KaiTi', serif;
  }

  /* 添加好友按钮 */
  .add-button {
    padding: 8px 15px;
    background-color: #28a745;
    color: white;
    border: none;
    border-radius: 10px;
    cursor: pointer;
    font-size: 16px;
    transition: background-color 0.3s;
  }

  .add-button:hover {
    background-color: #218838;
  }
</style>

后端

更详细的代码请点击查看
点击

@Component
@Slf4j
@ServerEndpoint("/websocket/{myUserId}")
public class WebSocket {

    /**
     * 与客户端的连接会话,需要通过他来给客户端发消息
     */
    private Session session;

    /**
     * 当前用户ID
     */
    private Integer userId;

    /**
     *  concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
     *  虽然@Component默认是单例模式的,但springboot还是会为每个websocket连接初始化一个bean,所以可以用一个静态set保存起来。
     */
    private static final CopyOnWriteArraySet<WebSocket> webSockets =new CopyOnWriteArraySet<>();

    /**
     *用来存在线连接用户信息
     */
    private static final ConcurrentHashMap<Integer,Session> sessionPool = new ConcurrentHashMap<Integer,Session>();

    private static MessageMapper messageMapper;

    @Autowired
    public void setMessageMapper(MessageMapper messageMapper){
        WebSocket.messageMapper = messageMapper;
    }

    /**
     * 连接成功方法
     * @param session 连接会话
     * @param userId 用户编号
     */
    @OnOpen
    public void onOpen(Session session , @PathParam("myUserId") Integer userId){
        try {
            this.session = session;
            this.userId = userId;
            webSockets.add(this);
            sessionPool.put(userId, session);
            log.info("【websocket消息】 用户:" + userId + " 加入连接...");
        } catch (Exception e) {
            log.error("---------------WebSocket连接异常---------------");
        }
    }

    /**
     * 关闭连接
     */
    @OnClose
    public void onClose(){
        try {
            webSockets.remove(this);
            sessionPool.remove(this.userId);
            log.info("【websocket消息】 用户:"+ this.userId + " 断开连接...");
        } catch (Exception e) {
            log.error("---------------WebSocket断开异常---------------");
        }
    }

    @OnMessage
    public void onMessage(String body){
        try {
            //将Body解析
            MessageEntityDecode messageEntityDecode = new MessageEntityDecode();
            MessageVO messageVO = messageEntityDecode.decode(body);
            log.info(messageVO.toString());
            sendOneMessage(messageVO.getReceiveUser(),messageVO.getMessage());

            MessagePO messagePO = messageVO.transformPO();
            messageMapper.insert(messagePO);
            
//            }
        } catch (Exception e) {
            log.error("---------------WebSocket消息异常---------------");
            e.printStackTrace();
        }
    }
        /**
     * 单点消息
     * @param userId
     * @param message
     */
    public void sendOneMessage(Integer userId, String message) {
        Session session = sessionPool.get(userId);
        if (session != null&&session.isOpen()) {
            try {
                log.info("【websocket消息】 单点消息:"+message);
                MessageVO messageVO = new MessageVO();
                messageVO.setMessage(message);
                messageVO.setSendUser(this.userId);
                messageVO.setReceiveUser(userId);

                MessageEntityEncode messageEntityEncode = new MessageEntityEncode();
                session.getAsyncRemote().sendText(messageEntityEncode.encode(messageVO));
            } catch (Exception e) {
                log.error("---------------WebSocket单点消息发送异常---------------");
            }
        }
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2201949.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

AI大模型微调产品经理面试必备全攻略,非常详细收藏我这一篇就够了

前言 这两天跟很多做程序员的朋友聊天&#xff0c;怎么看全网火爆的大模型。让我挺意外的是&#xff0c;大家的反馈普遍都很焦虑 。 在AI大模型微调领域的产品经理面试中&#xff0c;总会遇到一系列与技术细节、项目经验、市场趋势以及职业规划相关的问题。以下是一些建议的面…

RabbitMQ中如何解决消息堆积问题,如何保证消息有序性

RabbitMQ中如何解决消息堆积问题 如何保证消息有序性 只需要让一个消息队列只对应一个消费者即可

cdr激活码序列号coredraw2024安装包破解版coreldraw2024永久序列号最新

&#x1f31f;设计界的新宠儿来啦&#xff01;CorelDRAW 2024震撼登场&#x1f389; ### &#x1f308; 开篇狂想曲&#xff1a;设计师们的“瑞士军刀”升级记&#xff01;&#x1f3a8; 嘿&#xff0c;亲爱的创意达人们&#x1f44b;&#xff01;今天&#xff0c;就让我们共同…

6.将扩散模型与其他生成模型的关联(1)

在本章中&#xff0c;我们首先介绍其他5种重要的生成模型&#xff0c;包括变分自编码器抗网络、归一化流、自回归模型和基于能量的模型&#xff0c;分析它们的优点和局限性&#xff0c;并说明这些生成模型是如何通过纳入扩散模型而得到促进的。1 .变分自编码器与扩散模型 …

Java | Leetcode Java题解之第455题分发饼干

题目&#xff1a; 题解&#xff1a; class Solution {public int findContentChildren(int[] g, int[] s) {Arrays.sort(g);Arrays.sort(s);int m g.length, n s.length;int count 0;for (int i 0, j 0; i < m && j < n; i, j) {while (j < n &&…

鸿蒙开发之ArkUI 界面篇 二十五 点赞综合案例

要实现如下图效果&#xff0c;红框处是点赞数&#xff0c;点击会变色和数字增加1&#xff1a; 我们首先分析布局结构&#xff0c;整体式垂直方向的布局&#xff0c;外层容器自然是Colum&#xff0c;上层是图片组件Image&#xff0c;接下来是Text组件&#xff0c;接下来是Row组件…

Unity实战案例全解析 类宝可梦回合制的初级案例 源码分析(加了注释和流程图)

这是一个老教程了&#xff0c;但是对于没有写过回合制的初级程序同学来讲是比较适合的&#xff0c;也可以直接看源码&#xff0c;半小时内可以解决战斗 当然&#xff0c;我也没写过回合制系统所以就到处找&#xff0c;思路明白了就能自己修改了 视频教程 - 油管链接 Turn-Bas…

ComfyUI | 5分钟部署最新Flux大模型

Midjourney 和 Stable Diffusion 都是目前流行的 AI 图像生成工具&#xff0c;它们能够根据文本描述生成高质量的图像。都是基于深度学习技术的文本到图像生成模型&#xff0c;但它们各自基于不同的大模型。 但最近推出了一款比前两者更强大&#xff0c;生成图像更加逼真&…

UE4 材质学习笔记04(着色器性能优化)

一.着色器性能优化 1.衡量着色器的性能 衡量着色器性能的主要方法有三个 第一个&#xff1a;可以使用场景的视图模式的优化视图模式的着色器复杂度 下面的滑条代表了着色器指令的复杂度 如果场景大部分是绿色的&#xff0c;说明着色器耗能低&#xff0c;反之白色则是很糟糕…

VS Code安装以及配置

安装 1. 下载安装 VScode官网 注意&#xff0c;这一步最好全部打勾 2. 设置默认terminal为cmd 3. 修改Run Code的配置 参考&#xff1a; https://blog.csdn.net/weixin_46474921/article/details/132841711

2024年诺贝尔物理学奖 机器学习与神经网络领域前景面面观 如何抉择

近日&#xff0c;2024年诺贝尔物理学奖颁发给了机器学习与神经网络领域的研究者&#xff0c;这是历史上首次出现这样的情况。这项奖项原本只授予对自然现象和物质的物理学研究作出重大贡献的科学家&#xff0c;如今却将全球范围内对机器学习和神经网络的研究和开发作为了一种能…

【element-tiptap】如何增加一个扩展项,为文字渲染颜色?

源码地址&#xff1a; https://github.com/Leecason/element-tiptap 可以看到&#xff0c;当前这个页面的文字&#xff0c;都是黑色的&#xff08;除了链接&#xff09; 酱紫有些单调&#xff0c;我喜欢五颜六色的。那么这篇文章就来看下菜单项里面如何增加一个颜色的扩展&…

Anaconda保姆安装教程

步骤1&#xff1a;下载Anaconda安装包 访问官网&#xff1a; 进入Anaconda官网下载页面&#xff0c;官网会根据电脑的操作系统自动选择适合的操作系统安装程序。 尝试进入清华大学开源软件镜像站&#xff0c;选择想要的版本进行下载&#xff0c;通常下载速度较快。 本文以从…

OpenAI重磅发布交互界面canvas,让ChatGPT成为写作和编程利器

OpenAI 宣布推出类似 Anthropic 的 Artifacts 的应用 canvas&#xff0c;并称「这是一种使用 ChatGPT 写作和编程的新方式」。 在 Claude 中试过 Artifacts 的朋友都知道&#xff0c;这能极大提升 LLM 输出结果的表现力&#xff0c;其支持输出文本文件、代码、网页、SVG 等等。…

Windows11 24H2 64位专业精简版:告别卡顿,流畅运行!

今日&#xff0c;系统之家小编给您分享2024年最新发布的Windows11 24H2精简版系统下载&#xff0c;该版本系统采用微软官方Windows11 24H2 26100.2033 专业版离线制作&#xff0c;安全无毒&#xff0c;不符合硬件要求的电脑也能升级。本次更新修复了系统蓝屏、绿屏的安全问题&a…

【赵渝强老师】K8s中的有状态控制器StatefulSet

在K8s中&#xff0c;StatefulSets将Pod部署成有状态的应用程序。通过使用StatefulSets控制器&#xff0c;可以为Pod提供持久存储和持久的唯一性标识符。StatefulSets控制器与Deployment控制器不同的是&#xff0c;StatefulSets控制器为管理的Pod维护了一个有粘性的标识符。无论…

数据科学初学者都应该知道的 15 个基本统计概念

一、介绍 数据科学的核心是统计学&#xff0c;它已经存在了几个世纪&#xff0c;但在当今的数字时代仍然至关重要。为什么&#xff1f;因为基本的统计概念是数据分析的支柱&#xff0c;使我们能够理解每天生成的大量数据。这就像与数据对话&#xff0c;统计学可以帮助我们提出正…

【读书笔记·VLSI电路设计方法解密】问题7:什么是基于标准单元的专用集成电路 (ASIC) 设计方法论

标准单元方法论是一种基于预组装库单元的芯片设计方法。该库中包含的标准单元和宏单元(例如存储器、I/O、特殊功能单元、锁相环(PLLs)等)已经在预定的工艺节点中设计、布局并经过验证。这些单元经过完全表征,并在逻辑、时序、物理和电气模型方面进行了定义,并正确地打包在…

npm install报错一堆sass gyp ERR!

执行npm install &#xff0c;出现一堆gyp含有sass错误的情况下。 解决办法&#xff1a; 首页可能是node版本问题&#xff0c;太高或者太低&#xff0c;也会导致npm install安装错误&#xff08;不会自动生成node_modules文件&#xff09;&#xff0c;本次试验&#xff0c;刚开…

刷题 - 分治

面试经典 150 题 - 分治 148. 排序链表⭐️⭐️⭐️ - 快慢指针找中间节点 - 归并排序 伪代码&#xff1a; 将链表拆分成两半&#xff0c;返回右半边头节点&#xff08;左半边头节点就是原始链表头节点&#xff09;对左边进行排序并返回左边头节点对右边进行排序返回右边头节…