【WebSocket】使用ws搭建一个简单的在线聊天室

前言

什么是WebSockets?

WebSockets 是一种先进的技术。它可以在用户的浏览器和服务器之间打开交互式通信会话。使用此 API,你可以向服务器发送消息并接收事件驱动的响应,而无需通过轮询服务器的方式以获得响应。
webscokets 包括webscoket接口、CloseEvent接口 和MessageEvent接口。

什么是WebSocket?

WebSocket是一种基于TCP的全双工通信协议,可以更好的节省资源和带宽实现即时通讯,是一种持久化的协议。

什么是ws?

ws 是一个简单易用、速度极快、经过全面测试的 WebSocket 客户端和服务器实现库。

搭建简易WebSocket服务器

安装 ws

npm安装

npm install ws

使用ws

ws使用之前需要导入。

ws属性介绍

new WebSocketServer(options[, callback])

  • options {Object} 配置对象。
    • backlog {Number} 待处理连接队列的最大长度。
    • clientTracking {Boolean} 指定是否跟踪客户。
    • handleProtocols {Function} 用于处理 WebSocket 子协议的函数。请参见下面的说明。
    • host {String} 绑定服务器的主机名。
    • maxPayload {Number} 允许的最大报文大小(字节)。默认为 100 MiB(104857600 字节)。
    • noServer {Boolean} 启用无服务器模式。
    • path {String} 只接受与此路径匹配的连接。
    • perMessageDeflate {Boolean|Object} 启用/禁用 permessage-deflate 功能。
    • port {Number} 绑定服务器的端口。
    • server {http.Server | https.Server} 预先创建的 Node.js HTTP/S 服务器。
    • skipUTF8Validation {Boolean} 指定是否跳过文本和关闭信息的 UTF-8 验证。默认为 false。只有在客户端受信任时才设为 true。
    • verifyClient {Function} 用于验证输入连接的函数。请参阅下文说明。(不鼓励使用)。
    • WebSocket {Function} 指定要使用的 WebSocket 类。它必须是从原始 WebSocket 扩展而来。默认为 WebSocket。
  • callback {Function} 回调函数,将添加到 http 服务’listening’事件上。

创建一个新的服务器实例。必须提供端口、服务器或 noServer 中的一个,否则会出错。如果设置了端口,HTTP 服务器将自动创建、启动和使用。若要使用外部 HTTP/S 服务器,请仅指定服务器或 noServer。在这种情况下,必须手动启动 HTTP/S 服务器。无服务器 "模式允许 WebSocket 服务器与 HTTP/S 服务器完全分离。例如,这样就可以在多个 WebSocket 服务器之间共享一个 HTTP/S 服务器。

事件模型

  • Event:‘close’ : 服务器关闭时发出。只有在内部创建 HTTP 服务器时,该事件才会依赖于 HTTP 服务器的 "关闭 "事件。在其他所有情况下,该事件都是独立发生的。
  • Event: ‘connection’ : 握手完成时发出。请求是客户端发送的 http GET 请求。用于解析权限头、cookie 头和其他信息。
    • 参数 1 websocket {WebSocket}
    • 参数 2 request {http.IncomingMessage}
  • Event: ‘error’ : 当底层服务器出现错误时发出。
    • 参数 1 error {Error}
  • Event: ‘headers’ : 在作为握手的一部分将响应标头写入套接字之前发出。这样,您就可以在发送前检查/修改标头。
    • 参数 1 headers {Array}
    • 参数 2 request {http.IncomingMessage}
  • Event: ‘listening’ : 当底层服务器已绑定时发出。
  • Event: ‘wsClientError’:在建立 WebSocket 连接前发生错误时发出。socket 和 request 分别是发生错误的套接字和 HTTP 请求。该事件的监听器负责关闭套接字。当 "wsClientError "事件发生时,没有 http.ServerResponse 对象,因此任何 HTTP 响应(包括响应标头和正文)都必须直接写入套接字。如果没有该事件的监听器,套接字将以包含描述性错误信息的默认 4xx 响应关闭。
    • error {Error}
    • socket {net.Socket|tls.Socket}
    • request {http.IncomingMessage}

WebSocket

它扩展了 EventEmitter.WebSocket 类。客户端的可以用这个连接 websocket 服务,在 websocket 服务中也需要用它来发送消息。

就绪状态常量
常量描述
CONNECTING0连接尚未打开。
OPEN1连接已打开,随时可以进行通信。
CLOSING2连接正在关闭。
CLOSED3连接已关闭。
相关事件模型
  • Event: ‘close’ : 连接关闭时发出。code 是一个数值,表示状态代码,说明连接被关闭的原因。reason 是一个缓冲区,包含一个可由人工读取的字符串,用于解释连接被关闭的原因。
    • 参数 1 code {Number}
    • 参数 2 reason {Buffer}
  • Event: ‘error’ : 发生错误时发出。
    • 参数 1 error {Error}
  • Event: ‘message’ : 收到信息时发出。data 是信息内容,isBinary 指定信息是否为二进制。
    • 参数 1 data {Buffer|ArrayBuffer|Buffer[]}
    • 参数 2 isBinary {Boolean}
  • Event: ‘open’: 连接建立时发出。
  • Event: ‘ping’ : 收到 ping 时发出。
    • data {Buffer}
  • Event: ‘pong’ : 收到 pong 时发出。
    • data {Buffer}

websocket服务器实现

这是一个简单的ws服务器,他主要功能是,记录连接在线的用户,以及向所以在线状态的客户端广播转发收到的消息。
运行命令:
假如你服务文件名是server.js,则:

	node  server.js
const { WebSocketServer } = require("ws");

const userSet = new Set(); //  用户列表
//  ws服务器
const server = new WebSocketServer(
  {
    port: 8080,
    perMessageDeflate: {
      zlibDeflateOptions: {
        // See zlib defaults.
        chunkSize: 1024,
        memLevel: 7,
        level: 3,
      },
      zlibInflateOptions: {
        chunkSize: 10 * 1024,
      },
      // Other options settable:
      clientNoContextTakeover: true, // Defaults to negotiated value.
      serverNoContextTakeover: true, // Defaults to negotiated value.
      serverMaxWindowBits: 10, // Defaults to negotiated value.
      // Below options specified as default values.
      concurrencyLimit: 10, // Limits zlib concurrency for perf.
      threshold: 1024, // Size (in bytes) below which messages
      // should not be compressed if context takeover is disabled.
    },
  },
  () => {
    console.log("创建成功");
  }
);

server.on("open", function open() {
  console.log("打开 connected");
});

server.on("close", function close(e) {
  console.log("关闭连接 disconnected", e);
});

server.on("connection", (ws, req) => {
  const query = getQuery(req.url);
  console.log(query, "req::");
  if (userSet.has(query.username)) {
    sendMsg(ws, {
      type: "error",
      data: "用户名重复",
    });

    ws.close();
  } else {
    userSet.add(query.username);
    sendMsg(ws, {
      type: "success",
      data: "连接成功",
    });
    sendMsg(ws, {
      type: "user",
      data: Array.from(userSet),
    });
  }
  const ip = req.socket.remoteAddress;
  const port = req.socket.remotePort;
  const clientName = ip + port;

  console.log("%s 连接 ", clientName);
  ws.username = query.username;
  ws.on("message", function incoming(message) {
    let msg = handleMessage(message);

    server.clients.forEach((client) => {
      console.log("username::", client.username);
      //  广播消息
      if (client.readyState === ws.OPEN) {
        //  发送用户列表
        if (msg.type === "tips" || msg.type === "close") {
          sendMsg(client, {
            type: "user",
            data: Array.from(userSet),
          });
        }

        sendMsg(client, msg);
      }
    });
  });
  ws.on("close", function close(e) {
    console.log("关闭", e);
  });
});

function handleMessage(data) {
  let message;
  let msg;
  if (typeof data === "string") {
    message = data;
  } else {
    message = JSON.parse(data.toString());
  }

  if (typeof message === "string") {
    msg = message;
  } else if (typeof message === "object" && message.type) {
    console.log(message, "data::");

    switch (message.type) {
      case "name":
        msg = {
          type: "tips",
          data: `用户${message.data}连接了`,
        };

        break;
      case "info":
        msg = {
          type: "info",
          data: message.data,
        };
        break;
      case "close":
        msg = {
          type: "tips",
          data: `用户${message.data}退出`,
        };
        userSet.delete(message.data);
        break;
    }
  } else {
    msg = "暂不识别的内容";
  }
  return msg;
}

function getQuery(url) {
  let paramsStr = decodeURIComponent(url.split("?")[1]);
  let paramsArr = paramsStr.split("&");
  let params = {};
  paramsArr.forEach((str) => {
    let attr = str.split("=");
    params[attr[0]] = attr[1];
  });
  return params;
}

function sendMsg(ws, msg) {
  if (!ws) {
    console.error("没有连接");
    return;
  }
  ws.send(JSON.stringify(msg));
}

前端实现

我页面是使用的是html实现的websocket。没有依赖ws,所以直接浏览器打开即可。
使用前需运行ws服务器。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    .box {
      margin: 32px 10%;
      border-radius: 16px;
      border: 1px solid #000;
      overflow: hidden;
    }

    .box h1 {
      margin: 32px;
      text-align: center;
    }

    .info-box {
      padding: 32px;
      height: 88px;
      background-color: #eee;
    }

    .info-box .name-box {
      display: flex;
      align-items: center;
    }

    .info-box .name-box .edit-name {
      border: 0;
      color: skyblue;
      outline: none;
    }

    .chat-box {
      display: flex;
      margin-top: 32px;
      height: 50vh;
      border-top: 1px solid #000;
    }

    .user-box {
      flex-shrink: 0;
      min-width: 44px;
      max-width: 200px;
      border-right: 1px solid #000;
    }

    .chat-frame {
      flex: 1;
    }

    .message-box {
      display: flex;
      flex-direction: column;
      width: 100%;
      height: 80%;
      border-bottom: 1px solid #000;
      overflow-y: auto;
    }

    .message-box .tips-box {
      text-align: center;
      color: #999;
      font-size: 14px;
    }

    .msg-box {
      align-self: flex-start;
    }

    .msg-box-top {
      font-size: 16px;
      color: skyblue;
    }

    .msg-box-top span {
      font-size: 12px;
      color: #006eff;
      margin-right: 16px;
    }

    .msg-box-content {
      display: inline-block;
      width: 100%;
      padding: 5px 10px;
      line-height: 20px;
      background-color: skyblue;
      font-size: 18px;
      color: #fff;
      border-radius: 7px 7px 12px 7px;
    }

    .msg-box-right {
      align-self: flex-end;
    }

    .msg-box-right .msg-box-content {
      display: inline-block;
      width: 100%;
      padding: 5px 10px;
      line-height: 20px;
      background-color: rgb(35, 185, 35);
      font-size: 18px;
      color: #fff;
      border-radius: 7px 7px 7px 12px;
    }

    .send-box {
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 16px;
      height: 20%;
    }

    .send-box textarea {
      width: 80%;
      resize: none;
    }

    .send-box .send-btn {
      flex-shrink: 0;
      width: 10%;
      min-width: 32px;
      height: 66px;
    }

    .hide {
      display: none !important;
    }

    .show {
      display: block !important;
    }
  </style>
</head>

<body>
  <div class="box">
    <h1>在线聊天室</h1>
    <!-- 个人信息模态框 -->
    <div class="info-box">
      <div>
        <label for="name1">请输入昵称:</label>
        <input name="name1" class="name-input" type="text" placeholder="请输入聊天昵称"> <button class="ok-name">确定</button>
      </div>
      <div class="hide">
        <div class=" name-box">
          <div class="name-show"></div>
          <button class="edit-name">#修改</button>

        </div>
        <button class="link-btn">连接聊天室</button>
        <button class="out-btn hide">退出聊天室</button>

      </div>

    </div>
    <!-- 在线聊天模态框 -->
    <div class="chat-box hide">
      <div class="user-box">
        <h2>用户列表</h2>
        <div class="user-list"></div>
      </div>
      <div class="chat-frame">
        <div class="message-box"></div>
        <div class="send-box">

          <textarea class="send-content" name="message" id="" rows="5" maxlength="150"></textarea>
          <button class="send-btn">发送</button>
        </div>
      </div>

    </div>
  </div>
  <script>

    let ws;
    let nickName; //  昵称

    const okNameBtn = document.querySelector('.ok-name')
    const nameInput = document.querySelector('.name-input')
    const editNameBtn = document.querySelector('.edit-name')
    let userDom = document.querySelector('.user-list')
    let msgDom = document.querySelector('.message-box')
    const linkBtn = document.querySelector('.link-btn')
    const outBtn = document.querySelector('.out-btn')
    const chatBox = document.querySelector('.chat-box')
    const nameShowDiv = document.querySelector('.name-show')
    const sendBtn = document.querySelector('.send-btn')
    const sendContentDom = document.querySelector('.send-content')
    okNameBtn.onclick = function () {
      if (!nameInput.value || nameInput.value === '') return
      nickName = nameInput.value
      nameShowDiv.innerHTML = nickName
      nameShowDiv.parentElement.parentElement.classList.remove('hide')
      nameInput.parentElement.classList.add('hide')

    }
    editNameBtn.onclick = function () {
      nameInput.value = nickName
      nameInput.parentElement.classList.remove('hide')
      nameShowDiv.parentElement.parentElement.classList.add('hide')
    }
    linkBtn.onclick = function () {
      linkWs()
      linkBtn.classList.add('hide')
      editNameBtn.classList.add('hide')
      outBtn.classList.remove('hide')
      chatBox.classList.remove('hide')
    }
    outBtn.onclick = function () {
      outBtn.classList.add('hide')
      chatBox.classList.add('hide')
      linkBtn.classList.remove('hide')
      editNameBtn.classList.remove('hide')

      sendMsg({
        type: 'close',
        data: nickName
      })
      ws.close()
    }
    sendBtn.onclick = function () {
      let content = sendContentDom.value
      if (!content || content === '' || !ws) return
      sendMsg({
        type: 'info',
        data: {
          date: Date.now(),
          content: content,
          author: nickName
        }
      })
    }
    /**
     *  连接websocket服务器
     * */
    function linkWs() {
      ws = new WebSocket(`ws://localhost:8080?username=${nickName}`)
      ws.addEventListener("open", (e) => {

      })
      ws.addEventListener("message", function (e) {
        let msg = JSON.parse(e.data)
        //  处理消息
        handleMessage(msg)
      })
      ws.addEventListener("close", function (e) {
        console.log('关闭连接后');
        outBtn.classList.add('hide')
        chatBox.classList.add('hide')
        linkBtn.classList.remove('hide')
        editNameBtn.classList.remove('hide')
        //  隐藏聊天框
        alert('聊天室断开连接')
      })
    }

    function handleMessage(msg) {
      switch (msg.type) {
        case 'user':
          //  用户列表填充数据


          let domStr = ''

          msg.data.forEach(item => {
            console.log(item, 'i');
            domStr += `<div> ${item} </div>`
          })
          userDom.innerHTML = domStr
          break;
        case 'tips':
          //  提示信息
          msgDom.innerHTML += `<div class="tips-box">${msg.data} </div>`

          break;
        case 'info':
          //  信息列表
          const { content, author, date } = msg.data
          msgDom.innerHTML += `<div class="${author === nickName ? 'msg-box-right' : 'msg-box'}">
              <div class="msg-box-top">${author} <span>${transTime(date)}</span></div>
              <div class="msg-box-content"> ${content}</div>
            </div>`

          break;
        case 'close':
          //  关闭连接
          if (msg.data) {
            sendMsg({
              type: 'close',
              data: nickName
            })
            ws.close()
          }
          break;
        case 'error':
          //  关闭连接
          if (msg.data) {
            alert(msg.data)
            ws.close()
          }
          break;
        case 'success':
          //  关闭连接
          if (msg.data) {
            alert(msg.data)

            sendMsg({
              type: 'name',
              data: nickName
            })
          }
          break;
        default:
          break;
      }
    }

    function sendMsg(msg) {
      if (!ws) {
        console.error('没有连接');
        return
      }
      ws.send(JSON.stringify(msg))

    }

    window.addEventListener("beforeunload", (e) => {
      if (ws) {
        sendMsg({
          type: 'close',
          data: nickName
        })
        ws.close()
      }
    })

    function transTime(time) {
      let date = new Date(time)
      let year = date.getFullYear()
      let month = date.getMonth() + 1
      let day = date.getDate()
      let hour = date.getHours()
      let minute = date.getMinutes()
      let second = date.getSeconds()
      return `${year}/${month}/${day} ${hour}:${minute}:${second}`
    }
  </script>
</body>

</html>

效果

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

结语

这只是一个简单的在线聊天室,作学习使用,并没有涉及到用户验证,数据存储和复杂数据处理等逻辑。我认为websocket的侧重点在数据处理上,怎么转发和处理客户端发的数据更为重要。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

AntDesignBlazor示例——创建列表页

本示例是AntDesign Blazor的入门示例&#xff0c;在学习的同时分享出来&#xff0c;以供新手参考。 示例代码仓库&#xff1a;https://gitee.com/known/AntDesignDemo 1. 学习目标 使用Table组件创建列表页面使用DisplayName特性显示中文表头使用模板和Tag组件显示高温数据使…

2023站酷CUBE设计大会,以AIGC赋能创意人

12月6日&#xff0c;2023站酷CUBE设计大会在厦门举行。大会以“AI与热爱”为主题&#xff0c;由美图与站酷联合举办&#xff0c;邀请了多位创意先锋进行分享&#xff0c;旨在构建设计新生态&#xff0c;以AIGC内容生产新范式为创意人持续赋能&#xff0c;共同提升设计价值。 美…

简单自定义vuex的设计思路

vuex集中式存储管理应用所有组件的状态&#xff0c;并以响应的规则保证状态以可预测的方式 发生变化。 步骤&#xff1a; 1.Store类&#xff0c;保存选项&#xff0c;_mutations&#xff0c;_actions&#xff0c;getters 2.响应式状态&#xff1a;new Vue方式设置响应式。 …

电脑开机提示“未正确启动”怎么办?

有时我们在打开电脑时&#xff0c;会出现蓝屏&#xff0c;并提示“电脑未正确启动”&#xff0c;那么&#xff0c;这该怎么办呢&#xff1f;下面我们就来了解一下。 方法一&#xff1a;执行系统还原 我们在上文中提到了Windows无法正确启动的问题可能是由于三方程序或者近期的…

Java利用TCP实现简单的双人聊天

一、创建新项目 首先创建一个新的项目&#xff0c;并命名为聊天。然后创建包&#xff0c;创建两个类&#xff0c;客户端&#xff08;SocketClient&#xff09;和服务器端&#xff08;SocketServer&#xff09; 二、实现代码 客户端代码&#xff1a; package 聊天; import ja…

Spring Boot 3.2项目中使用缓存Cache的正确姿势!!!

你是否曾想过为什么在 Spring Boot 应用中缓存是如此重要&#xff1f;答案在于它通过减少数据检索时间来提高性能。在本文中&#xff0c;我们将深入探讨缓存对微服务模式的影响&#xff0c;并探讨根据操作易用性、速度、可用性和可观测性等因素选择正确缓存的重要性。我们还将探…

[RISCV] 发现一个可以看RISC-V CPU行为的开源项目

最近在浏览某大型程序员交友 网站的时候发现一个好玩的项目,介绍如下: A small program that handles mie, msi, mti and trap interrupts and updates some global variables on interrupts. 重点是他下面还放了一张图: 能看到RISCV CSR的行为太酷啦!!! 下面一起setup一…

Sourcepawn脚本入门(二)命令与事件监听

&#x1f34e;Sourcepawn脚本入门(二)命令与事件监听 &#xff08;控制台&#xff09;命令是常用的插件形式&#xff0c;eg. noclip …等都是常用的命令&#xff0c;在游戏中使用也很容易,souremod可以注册自己的命令。 事件的监听则需要考虑到不同的起源游戏支持的事件不同&am…

中文BERT模型预训练参数总结以及转化为pytorch的方法

1.目前针对中文的bert预训练模型有三家&#xff1a; 谷歌发布的chinese_L-12_H-768_A-12 还有哈工大的chinese-bert-wwm / chinese-bert-wwm-ext 以及HuggingFace上的bert-base-chinese(由清华大学基于谷歌的BERT在中文数据集上训练开发的模型&#xff0c;上传在HuggingFace) …

彻底删除VsCode配置和安装过的插件与缓存

前言 当你准备对 Visual Studio Code&#xff08;VSCode&#xff09;进行重新安装时&#xff0c;可能遇到一个常见问题&#xff1a;重新安装后&#xff0c;新的安装似乎仍然保留了旧的配置信息&#xff0c;这可能会导致一些麻烦。这种情况通常是由于卸载不彻底所致&#xff0c…

【LVS实战】04 LVS+Keepalived实现负载均衡高可用

一、介绍 Keepalived 是一个用于 Linux 平台的高可用性软件。它实现了虚拟路由器冗余协议 (VRRP) 和健康检查功能&#xff0c;可以用于确保在多台服务器之间提供服务的高可用性。Keepalived 可以检测服务器的故障&#xff0c;并在主服务器宕机时&#xff0c;自动将备份服务器提…

外卖系统源码开发:打造高效智能化餐饮解决方案

在当今数字化时代&#xff0c;外卖系统成为了餐饮业中不可或缺的一部分。为了满足日益增长的外卖需求&#xff0c;我们将深入探讨外卖系统源码开发的关键技术和创新应用。 1. 技术栈选择 在开始外卖系统源码的开发之前&#xff0c;我们首先需要选择适用的技术栈。一个典型的…

【langchain实战】开源项目-RasaGPT

1、概述 RasaGpt是一个建立在 Rasa 和 Langchain 之上的没有显示界面的LMM聊天机器人平台。它是一个Rasa和Telegram这种利用像Langchain这样的LMM库进行索引、检索和上下文注入的样板及参考实现。 开源地址&#xff1a; GitHub - paulpierre/RasaGPT: &#x1f4ac; RasaGPT is…

揭秘:软件测试中Web请求的完整流程!

在软件开发的过程中&#xff0c;测试是一个至关重要的环节。而在现代互联网应用中&#xff0c;Web请求是很常见的一个测试需求。本文将介绍Web请求的完整测试流程&#xff0c;帮助读者更好地理解软件测试的关键步骤。 一、测试准备阶段 在进行Web请求测试之前&#xff0c;测试团…

【CMake入门】第二节——CMake常用指令介绍

系列文章&#xff1a; 【CMake入门】第一节——CMake的安装与简单样例 CMake常用指令介绍 cmake_minimum_required 指定要求最小的cmake版本&#xff0c;如果版本小于该要求&#xff0c;程序终止 project(test) 设置当前项目名称为test CMAKE_BUILD_TYPE 用于设置CMake构…

招商银行薪福:一站式API连接电商平台,实现CRM与客服系统集成

招商银行薪福通的API集成优势 招商银行的SaaS产品薪福通在电商行业迅速崭露头角&#xff0c;它通过一站式API连接&#xff0c;极大地简化了电商平台与CRM及客服系统的集成过程。企业无需深入研究API开发细节&#xff0c;也不必担心代码复杂性&#xff0c;就能实现系统间的高效…

LeetCode力扣每日一题(Java):14、最长公共前缀

一、题目 二、解题思路 1、我的思路 乍一看我的代码量还是比较少&#xff0c;但是提交上去发现时间效率和空间效率都不占优势 讲讲我的思路&#xff1a;首先通过for循环找出数组中长度最短的字符串&#xff0c;并用min储存最短字符串的长度&#xff0c;最长公共前缀不可能比…

SAP MM 中的业务伙伴确定配置

这篇博客文章将概述 SAP MM 供应商帐户组中的合作伙伴确定是什么以及如何在 S/4 系统中配置它。 本文将指导您完成分步过程&#xff0c;并为您提供有关在供应商主数据中使用合作伙伴确定的完整想法。 合作伙伴角色 供应商在 SAP 中扮演着不同类型的角色&#xff0c;让我们通…

基于JavaSwing实现的学生管理系统(课程设计必备)

基于JavaSwing实现的学生管理系统&#xff0c;最全的Java基础学生管理系统&#xff0c;Java课程设计必备 系统一共包括学生信息管理&#xff0c;考勤签到管理&#xff0c;学生成绩管理&#xff0c;学生选课管理系统。不仅有基础功能&#xff0c;还有图表统计等功能。功能是非常…

记录 | chrome下载提速

chrome浏览器下载提速的方法&#xff1a; 谷歌浏览器一直都带有多线程下载模式&#xff0c;只不过默认关闭了&#xff0c;打开后下载速度会提升很多&#xff0c;只要在新标签页输入 chrome://flags/#enable-parallel-downloading &#xff0c;点enter&#xff0c;出现下图点击…