Vue3 + Express 实现大文件分片上传、断点续传、秒传

目录

    • 前言
    • 原理
    • 实现
      • 项目搭建
      • 获取文件
      • 文件分片
      • hash计算
      • 文件上传
      • 文件合并
      • 秒传&断点续传
    • 完整代码

前言

在日常开发中,文件上传是常见的操作之一。文件上传技术使得用户可以方便地将本地文件上传到Web服务器上,这在许多场景下都是必需的,比如网盘上传、头像上传等。 但是当我们需要上传比较大的文件的时候,容易碰到以下问题:

  1. 上传时间比较久;
  2. 中间一旦出错就需要重新上传;
  3. 一般服务端会对文件的大小进行限制。

这几个问题会导致上传时候的用户体验是很不好的,针对存在的这些问题,我们可以通过分片上传来解决。

原理

分片上传的原理就像是把一个大蛋糕切成小块一样。

首先,我们将要上传的大文件分成许多小块,每个小块大小相同,比如每块大小为1MB。然后,我们逐个上传这些小块到服务器。上传的时候,可以同时上传多个小块,也可以一个一个地上传。上传每个小块后,服务器会保存这些小块,并记录它们的顺序和位置信息。

所有小块上传完成后,服务器会把这些小块按照正确的顺序拼接起来,还原成完整的大文件。最后,我们就成功地上传了整个大文件。

在这里插入图片描述

分片上传的好处在于它可以减少上传失败的风险。如果在上传过程中出现了问题,只需要重新上传出错的那个小块,而不需要重新上传整个大文件。此外,分片上传还可以加快上传速度。因为我们可以同时上传多个小块,充分利用网络的带宽。这样就能够更快地完成文件的上传过程。

实现

项目搭建

要实现大文件上传,还需要后端的支持,所以我们就用nodejs来写后端代码。

前端:vue3 + vite

后端:express 框架,用到的工具包:nodemoncorsconnect-multipartyfs-extrabody-parser

获取文件

通过监听 input 标签的 change 事件,当选择本地文件后,可以在回调函数中获取对应的文件。(也可以使用第三方UI库的上传组件获取到对应的文件)

<script>
// 绑定上传事件
const handleUpload = async (e: Event) => {
  const file = (e.target as HTMLInputElement).files?.[0];
  if (!file) return;
};
</script>

<template>
  <h1>大文件上传</h1>
  <input type="file" @change="handleUpload" />
</template>

文件分片

文件分片的核心是用Blob对象的slice方法,我们在上一步获取到选择的文件是一个File对象,它是继承于Blob,所以我们就可以用slice方法对文件进行分片。

// 绑定上传事件
const handleUpload = async (e: Event) => {
  ...
  
+  // 创建文件分片
+  const chunks = createChunks(file);
};
const CHUNK_SIZE = 1024 * 1024;  // 1MB

// 创建文件分片
const createChunks = (file: File) => {
  let start = 0;
  const chunks = [];
  while (start < file.size) {
    chunks.push(file.slice(start, start + CHUNK_SIZE));
    start += CHUNK_SIZE;
  }
  return chunks;
};

hash计算

先来思考一个问题,在向服务器上传文件时,怎么去区分不同的文件呢?如果根据文件名去区分的话可以吗?

答案是不可以,因为文件名我们可以是随便修改的,所以不能根据文件名去区分。但是每一份文件的文件内容都不一样,我们可以根据文件的内容去区分,具体怎么做呢?

可以根据文件内容生产一个唯一的hash值,大家应该都见过用webpack打包出来的文件的文件名都有一串不一样的字符串,这个字符串就是根据文件的内容生成的hash值,文件内容变化,hash值就会跟着发生变化。我们在这里,也可以用这个办法来区分不同的文件。而且通过这个办法,我们还可以实现秒传的功能,怎么做呢?

就是服务器在处理上传文件的请求的时候,要先判断下对应文件的 hash值有没有记录,如果A和B先后上传一份内容相同的文件,所以这两份文件的 hash值是一样的。当A上传的时候会根据文件内容生成一个对应的hash值,然后在服务器上就会有一个对应的文件,B再上传的时候,服务器就会发现这个文件的hash值之前已经有记录了,说明之前已经上传过相同内容的文件了,所以就不用处理B的这个上传请求了,给用户的感觉就像是实现了秒传。

那么怎么计算文件的hash值呢?可以通过一个工具:spark-md5,所以我们得先安装它。

const fileHash = ref(""); // 文件hash

// 绑定上传事件
const handleUpload = async (e: Event) => {
  ...

  // 创建文件分片
  const chunks = createChunks(file);

+  // 计算文件内容hash值
+  fileHash.value = await calculateHash(file);
};
// 计算文件内容hash值
const calculateHash = (file: File): Promise<string> => {
  return new Promise((resolve) => {
    const fileReader = new FileReader();
    fileReader.readAsArrayBuffer(file);
    fileReader.onload = function (e) {
      const spark = new SparkMD5.ArrayBuffer();
      spark.append((e.target as FileReader).result as ArrayBuffer);
      resolve(spark.end());
    };
  });
};

文件上传

前端实现

前面已经完成了上传的前置操作,接下来就来看下如何去上传这些切片。

我们以1G的文件来分析,假如每个分片的大小为1M,那么总的分片数将会是1024个,如果我们同时发送这1024个分片,浏览器肯定处理不了,原因是切片文件过多,浏览器一次性创建了太多的请求。这是没有必要的,拿chrome浏览器来说,默认的并发数量只有6,过多的请求并不会提升上传速度,反而是给浏览器带来了巨大的负担。因此,我们有必要限制前端请求个数。

怎么做呢,我们要创建最大并发数的请求,比如6个,那么同一时刻我们就允许浏览器只发送6个请求,其中一个请求有了返回的结果后我们再发起一个新的请求,依此类推,直至所有的请求发送完毕。

上传文件时一般还要用到FormData对象,需要将我们要传递的文件还有额外信息放到这个FormData对象里面。

// 绑定上传事件
const handleUpload = async (e: Event) => {
  ...

+  // 上传文件分片
+  uploadChunks(chunks);
};
// 上传文件分片
const uploadChunks = async (
  chunks: Array<Blob>,
) => {
  const formDatas = chunks
    .map((chunk, index) => ({
      fileHash: fileHash.value,
      chunkHash: fileHash.value + "-" + index,
      chunk,
    }))
    .map((item) => {
      const formData = new FormData();
      formData.append("fileHash", item.fileHash);
      formData.append("chunkHash", item.chunkHash);
      formData.append("chunk", item.chunk);
      return formData;
    });

  const taskPool = formDatas.map(
    (formData) => () =>
      fetch("http://localhost:3000/upload", {
        method: "POST",
        body: formData,
      })
  );

  // 控制请求并发
  await concurRequest(taskPool, 6);
};
// 控制请求并发
const concurRequest = (
  taskPool: Array<() => Promise<Response>>,
  max: number
): Promise<Array<Response | unknown>> => {
  return new Promise((resolve) => {
    if (taskPool.length === 0) {
      resolve([]);
      return;
    }

    const results: Array<Response | unknown> = [];
    let index = 0;
    let count = 0;

    const request = async () => {
      if (index === taskPool.length) return;
      const i = index;
      const task = taskPool[index];
      index++;
      try {
        results[i] = await task();
      } catch (err) {
        results[i] = err;
      } finally {
        count++;
        if (count === taskPool.length) {
          resolve(results);
        }
        request();
      }
    };

    const times = Math.min(max, taskPool.length);
    for (let i = 0; i < times; i++) {
      request();
    }
  });
};

后端实现

后端我们处理文件时需要用到connect-multiparty这个工具,所以也是得先安装,然后再引入它。

我们在处理每个上传的分片的时候,应该先将它们临时存放到服务器的一个地方,方便我们合并的时候再去读取。为了区分不同文件的分片,我们就用文件对应的那个hash为文件夹的名称,将这个文件的所有分片放到这个文件夹中。

const multipart = require("connect-multiparty");
const multipartMiddleware = multipart();

// 所有上传的文件存放在该目录下
const UPLOADS_DIR = path.resolve("uploads");

/**
 * 上传
 */
app.post("/upload", multipartMiddleware, (req, res) => {
  const { fileHash, chunkHash } = req.body;

  // 如果临时文件夹(用于保存分片)不存在,则创建
  const chunkDir = path.resolve(UPLOADS_DIR, fileHash);
  if (!fse.existsSync(chunkDir)) {
    fse.mkdirSync(chunkDir);
  }

  // 如果临时文件夹里不存在该分片,则将用户上传的分片移到临时文件夹里
  const chunkPath = path.resolve(chunkDir, chunkHash);
  if (!fse.existsSync(chunkPath)) {
    fse.moveSync(req.files.chunk.path, chunkPath);
  }

  res.send({
    success: true,
    msg: "上传成功",
  });
});

写完前后端代码后就可以来试下看看文件能不能实切片的上传,如果没有错误的话,服务器的uploads文件夹下应该就会多一个文件夹,这个文件夹里面就是存储的所有文件的分片了。

文件合并

上一步我们已经实现了将所有切片上传到服务器了,上传完成之后,我们就可以将所有的切片合并成一个完整的文件了,下面就一块来实现下。

前端实现

前端只需要向服务器发送一个合并的请求,并且为了区分要合并的文件,需要将文件的hash值给传过去。

+  const fileName = ref(""); // 文件名称

  // 绑定上传事件
  const handleUpload = async (e: Event) => {
    const file = (e.target as HTMLInputElement).files?.[0];
    if (!file) return;

+    fileName.value = file.name;

    ...
  };
// 上传文件分片
const uploadChunks = async (
  chunks: Array<Blob>,
) => {
  ...

+  // 合并分片请求
+  mergeRequest();
};
// 合并分片请求
const mergeRequest = () => {
  fetch("http://localhost:3000/merge", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      fileHash: fileHash.value,
      fileName: fileName.value,
    }),
  });
};

后端实现

在之前已经可以将所有的切片上传到服务器并存储到对应的目录里面去了,合并的时候需要从对应的文件夹中获取所有的切片,然后利用文件的读写操作,就可以实现文件的台并了。合并完成之后,我们将生成的文件以hash值命名存放到对应的位置就可以了。

/**
 * 合并
 */
app.post("/merge", async (req, res) => {
  const { fileHash, fileName } = req.body;

  // 最终合并的文件路径
  const filePath = path.resolve(UPLOADS_DIR, fileHash + path.extname(fileName));
  // 临时文件夹路径
  const chunkDir = path.resolve(UPLOADS_DIR, fileHash);

  // 读取临时文件夹,获取该文件夹下“所有文件(分片)名称”的数组对象
  const chunkPaths = fse.readdirSync(chunkDir);

  // 读取临时文件夹获得的文件(分片)名称数组可能乱序,需要重新排序
  chunkPaths.sort((a, b) => a.split("-")[1] - b.split("-")[1]);

  // 遍历文件(分片)数组,将分片追加到文件中
  const pool = chunkPaths.map(
    (chunkName) =>
      new Promise((resolve) => {
        const chunkPath = path.resolve(chunkDir, chunkName);
        // 将分片追加到文件中
        fse.appendFileSync(filePath, fse.readFileSync(chunkPath));
        // 删除分片
        fse.unlinkSync(chunkPath);
        resolve();
      })
  );
  await Promise.all(pool);
  // 等待所有分片追加到文件后,删除临时文件夹
  fse.removeSync(chunkDir);

  res.send({
    success: true,
    msg: "合并成功",
  });
});

到这里,我们就已经实现了大文件的分片上传的基本功能了,但是我们没有考虑到如果上传相同的文件的情况,而且如果中间网络断了,我们就得重新上传所有的分片,这些情况在大文件上传中也都需要考虑到,下面,我们就来解决下这两个问题。

秒传&断点续传

我们在上面有提到,如果内容相同的文件进行hash计算时,对应的hash值应该是一样的,而且我们在服务器上给上传的文件命名的时候就是用对应的hash值命名的,所以在上传之前是不是可以加一个判断,如果有对应的这个文件,就不用再重复上传了,直接告诉用户上传成功,给用户的感觉就像是实现了秒传。接下来,就来看下如何实现的。

前端实现

前端在上传之前,需要将对应文件的hash值告诉服务器,看看服务器上有没有对应的这个文件,如果有,就直接返回,不执行上传分片的操作了。

// 校验文件、文件分片是否存在
const verify = (fileHash: string, fileName: string) => {
  return fetch("http://localhost:3000/verify", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      fileHash,
      fileName,
    }),
  }).then((res) => res.json());
};
// 绑定上传事件
const handleUpload = async (e: Event) => {
  ...
  
  // 计算文件内容hash值
  fileHash.value = await calculateHash(file);

+  // 校验文件、文件分片是否存在
+  const verifyRes = await verify(fileHash.value, fileName.value);
+  const { existFile, existChunks } = verifyRes.data;
+  if (existFile) return;

  ...
};

后端实现

因为我们在合并文件时,文件名是根据该文件的hash值命名的,所以只需要看看服务器上有没有对应的这个hash值的文件就可以判断了。

/**
 * 校验
 */
app.post("/verify", (req, res) => {
  const { fileHash, fileName } = req.body;

  // 判断服务器上是否存在该hash值的文件
  const filePath = path.resolve(UPLOADS_DIR, fileHash + path.extname(fileName));
  const existFile = fse.existsSync(filePath);

  res.send({
    success: true,
    msg: "校验文件",
    data: {
      existFile,
    },
  });
});

完成上面的步骤后,当我们再上传相同的文件,即使改了文件名,也会提示我们秒传成功了,因为服务器上已经有对应的那个文件了。上面我们解决了重复上传的文件,但是对于网络中断需要重新上传的问题没有解决,那该如何解决呢?

如果我们之前已经上传了一部分分片了,我们只需要再上传之前拿到这部分分片,然后再过滤掉是不是就可以避免去重复上传这些分片了,也就是只需要上传那些上传失败的分片,所以,再上传之前还得加一个判断。

前端实现

我们还是在那个/verify的接口中去获取已经上传成功的分片,然后在上传分片前进行一个过滤。

// 绑定上传事件
const handleUpload = async (e: Event) => {
  ...

  // 上传文件分片
-  uploadChunks(chunks);
+  uploadChunks(chunks, existChunks);
};
// 上传文件分片
const uploadChunks = async (
  ...
+  existChunks: Array<string>
) => {
  const formDatas = chunks
    .map((chunk, index) => ({
      fileHash: fileHash.value,
      chunkHash: fileHash.value + "-" + index,
      chunk,
    }))
+    .filter((item) => !existChunks.includes(item.chunkHash))
    .map((item) => {
      const formData = new FormData();
      formData.append("fileHash", item.fileHash);
      formData.append("chunkHash", item.chunkHash);
      formData.append("chunk", item.chunk);
      return formData;
    });
    
  ...
};

后端实现

只需要在/verify这个接口中加上已经上传成功的所有切片的名称就可以,因为所有的切片都存放在以文件的hash值命名的那个文件夹,所以需要读取这个文件夹中所有的切片的名称就可以。

/**
 * 校验
 */
app.post("/verify", (req, res) => {
  const { fileHash, fileName } = req.body;

  // 判断服务器上是否存在该hash值的文件
  const filePath = path.resolve(UPLOADS_DIR, fileHash + path.extname(fileName));
  const existFile = fse.existsSync(filePath);
  
+  // 获取已经上传到服务器的文件分片
+  const chunkDir = path.resolve(UPLOADS_DIR, fileHash);
+  const existChunks = [];
+  if (fse.existsSync(chunkDir)) {
+    existChunks.push(...fse.readdirSync(chunkDir));
+  }

  res.send({
    success: true,
    msg: "校验文件",
    data: {
      existFile,
+      existChunks,
    },
  });
});

完整代码

前端部分

<script setup lang="ts">
import { ref } from "vue";
import SparkMD5 from "spark-md5";

const CHUNK_SIZE = 1024 * 1024; // 1MB
const fileName = ref(""); // 文件名称
const fileHash = ref(""); // 文件hash

// 创建文件分片
const createChunks = (file: File) => {
  let start = 0;
  const chunks = [];
  while (start < file.size) {
    chunks.push(file.slice(start, start + CHUNK_SIZE));
    start += CHUNK_SIZE;
  }
  return chunks;
};

// 计算文件内容hash值
const calculateHash = (file: File): Promise<string> => {
  return new Promise((resolve) => {
    const fileReader = new FileReader();
    fileReader.readAsArrayBuffer(file);
    fileReader.onload = function (e) {
      const spark = new SparkMD5.ArrayBuffer();
      spark.append((e.target as FileReader).result as ArrayBuffer);
      resolve(spark.end());
    };
  });
};

// 控制请求并发
const concurRequest = (
  taskPool: Array<() => Promise<Response>>,
  max: number
): Promise<Array<Response | unknown>> => {
  return new Promise((resolve) => {
    if (taskPool.length === 0) {
      resolve([]);
      return;
    }

    const results: Array<Response | unknown> = [];
    let index = 0;
    let count = 0;

    const request = async () => {
      if (index === taskPool.length) return;
      const i = index;
      const task = taskPool[index];
      index++;
      try {
        results[i] = await task();
      } catch (err) {
        results[i] = err;
      } finally {
        count++;
        if (count === taskPool.length) {
          resolve(results);
        }
        request();
      }
    };

    const times = Math.min(max, taskPool.length);
    for (let i = 0; i < times; i++) {
      request();
    }
  });
};

// 合并分片请求
const mergeRequest = () => {
  fetch("http://localhost:3000/merge", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      fileHash: fileHash.value,
      fileName: fileName.value,
    }),
  });
};

// 上传文件分片
const uploadChunks = async (
  chunks: Array<Blob>,
  existChunks: Array<string>
) => {
  const formDatas = chunks
    .map((chunk, index) => ({
      fileHash: fileHash.value,
      chunkHash: fileHash.value + "-" + index,
      chunk,
    }))
    .filter((item) => !existChunks.includes(item.chunkHash))
    .map((item) => {
      const formData = new FormData();
      formData.append("fileHash", item.fileHash);
      formData.append("chunkHash", item.chunkHash);
      formData.append("chunk", item.chunk);
      return formData;
    });

  const taskPool = formDatas.map(
    (formData) => () =>
      fetch("http://localhost:3000/upload", {
        method: "POST",
        body: formData,
      })
  );

  // 控制请求并发
  await concurRequest(taskPool, 6);

  // 合并分片请求
  mergeRequest();
};

// 校验文件、文件分片是否存在
const verify = (fileHash: string, fileName: string) => {
  return fetch("http://localhost:3000/verify", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      fileHash,
      fileName,
    }),
  }).then((res) => res.json());
};

// 绑定上传事件
const handleUpload = async (e: Event) => {
  const file = (e.target as HTMLInputElement).files?.[0];
  if (!file) return;

  fileName.value = file.name;

  // 创建文件分片
  const chunks = createChunks(file);

  // 计算文件内容hash值
  fileHash.value = await calculateHash(file);

  // 校验文件、文件分片是否存在
  const verifyRes = await verify(fileHash.value, fileName.value);
  const { existFile, existChunks } = verifyRes.data;
  if (existFile) return;

  // 上传文件分片
  uploadChunks(chunks, existChunks);
};
</script>

<template>
  <h1>大文件上传</h1>
  <input type="file" @change="handleUpload" />
</template>

后端部分

const express = require("express");
const cors = require("cors");
const bodyParser = require("body-parser");
const fse = require("fs-extra");
const path = require("path");
const multipart = require("connect-multiparty");
const multipartMiddleware = multipart();

const app = express();

app.use(cors());
app.use(bodyParser.json());

// 所有上传的文件存放在该目录下
const UPLOADS_DIR = path.resolve("uploads");

/**
 * 上传
 */
app.post("/upload", multipartMiddleware, (req, res) => {
  const { fileHash, chunkHash } = req.body;

  // 如果临时文件夹(用于保存分片)不存在,则创建
  const chunkDir = path.resolve(UPLOADS_DIR, fileHash);
  if (!fse.existsSync(chunkDir)) {
    fse.mkdirSync(chunkDir);
  }

  // 如果临时文件夹里不存在该分片,则将用户上传的分片移到临时文件夹里
  const chunkPath = path.resolve(chunkDir, chunkHash);
  if (!fse.existsSync(chunkPath)) {
    fse.moveSync(req.files.chunk.path, chunkPath);
  }

  res.send({
    success: true,
    msg: "上传成功",
  });
});

/**
 * 合并
 */
app.post("/merge", async (req, res) => {
  const { fileHash, fileName } = req.body;

  // 最终合并的文件路径
  const filePath = path.resolve(UPLOADS_DIR, fileHash + path.extname(fileName));
  // 临时文件夹路径
  const chunkDir = path.resolve(UPLOADS_DIR, fileHash);

  // 读取临时文件夹,获取该文件夹下“所有文件(分片)名称”的数组对象
  const chunkPaths = fse.readdirSync(chunkDir);

  // 读取临时文件夹获得的文件(分片)名称数组可能乱序,需要重新排序
  chunkPaths.sort((a, b) => a.split("-")[1] - b.split("-")[1]);

  // 遍历文件(分片)数组,将分片追加到文件中
  const pool = chunkPaths.map(
    (chunkName) =>
      new Promise((resolve) => {
        const chunkPath = path.resolve(chunkDir, chunkName);
        // 将分片追加到文件中
        fse.appendFileSync(filePath, fse.readFileSync(chunkPath));
        // 删除分片
        fse.unlinkSync(chunkPath);
        resolve();
      })
  );
  await Promise.all(pool);
  // 等待所有分片追加到文件后,删除临时文件夹
  fse.removeSync(chunkDir);

  res.send({
    success: true,
    msg: "合并成功",
  });
});

/**
 * 校验
 */
app.post("/verify", (req, res) => {
  const { fileHash, fileName } = req.body;

  // 判断服务器上是否存在该hash值的文件
  const filePath = path.resolve(UPLOADS_DIR, fileHash + path.extname(fileName));
  const existFile = fse.existsSync(filePath);

  // 获取已经上传到服务器的文件分片
  const chunkDir = path.resolve(UPLOADS_DIR, fileHash);
  const existChunks = [];
  if (fse.existsSync(chunkDir)) {
    existChunks.push(...fse.readdirSync(chunkDir));
  }

  res.send({
    success: true,
    msg: "校验文件",
    data: {
      existFile,
      existChunks,
    },
  });
});

const server = app.listen(3000, () => {
  console.log(`Example app listening on port ${server.address().port}`);
});

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

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

相关文章

比特币ETF通过为BiFinance带来全新机遇

2013年7月&#xff0c;Winklevoss兄弟提交了首个比特币交易所交易基金&#xff08;ETF&#xff09;申请&#xff0c;随后多家公司纷纷效仿&#xff0c;但美国证券交易委员会&#xff08;SEC&#xff09;均以“容易受到市场操纵”为由驳回了这些申请。时至2024年伊始&#xff0c…

TCP_拥塞控制

引言 24年春节马上就要到了&#xff0c;作为开车党&#xff0c;最大的期盼就是顺利回家过年不要堵车。梦想是美好的&#xff0c;但现实是骨感的&#xff0c;拥堵的道路让人苦不堪言。 在网络世界中&#xff0c;类似于堵车的问题也存在&#xff0c;而TCP&#xff08;Transmissi…

环形链表找入环点----链表OJ---三指针

https://leetcode.cn/problems/linked-list-cycle-ii/description/?envTypestudy-plan-v2&envIdtop-100-liked 首先&#xff0c;需要判断是否有环&#xff0c;而这里我们不单纯判断是否有环&#xff0c;还要为下一步做准备&#xff0c;需要让slow指针和fast都从头结点开始…

【数据结构1-1】线性表

线性表是最简单、最基本的一种数据结构&#xff0c;线性表示多个具有相同类型数据“串在一起”&#xff0c;每个元素有前驱&#xff08;前一个元素&#xff09;和后继&#xff08;后一个元素&#xff09;。根据不同的特性&#xff0c;线性表也分为数组&#xff08;vector&#…

MySQL 学习记录

基本常识 row-size-limitsblob&#xff1a; BLOB and TEXT columns cannot have DEFAULT values.Instances of BLOB or TEXT columns in the result of a query that is processed using a temporary table causes the server to use a table on disk rather than in memory b…

C++11——新的类功能与可变参数模板

系列文章目录 文章目录 系列文章目录一、新的类功能默认成员函数类成员变量初始化强制生成默认函数的关键字default禁止生成默认函数的关键字delete继承和多态中的final与override关键字 二、可变参数模板递归函数方式展开参数包逗号表达式展开参数包STL容器中的empalce_back与…

写点东西《JWT 与会话身份验证》

写点东西《JWT 与会话身份验证》 身份验证与授权 JWT 与session身份验证 - 基本差异 什么是 JWT&#xff1f; JWT 结构&#xff1a; JWT 工作流程&#xff1a;优势: 安全问题&#xff1a; 处理令牌过期&#xff1a; 基于session的身份验证&#xff08;通常称为基于 cookie 的身…

工程对接大模型流式和非流式对话底层原理解析

文章目录 前言一、非流式输出设计二、stream流式输出设计三、手撸一个流式输出项目总结 前言 之前对接过OpenAi大模型的官方API&#xff0c;可以看到它有一个Stream参数&#xff0c;设置成true的时候就是流式的对话输出&#xff0c;现象就是一段一段的往外崩。 官方手册的地址…

蓝桥杯训练|基础语言Day1 - STL pair vector list stack queue set map容器

学习目标&#xff1a; 博主介绍: 27dCnc 专题 : 算法题入门 &#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d;&#x1f44d; ☆*: .&#xff61;. o(≧▽≦)o .&#xff61…

Python爬虫案例展示:实现花猫壁纸数据采集

嗨喽~大家好呀&#xff0c;这里是魔王呐 ❤ ~! python更多源码/资料/解答/教程等 点击此处跳转文末名片免费获取 环境使用: Python 3.10 Pycharm 模块使用: import requests >>> pip install requests win R 输入cmd 输入安装命令 pip install requests 安装即…

Springboot各种请求参数详解

文章目录 请求Postman**为什么需要Postman****什么是Postman****Postman使用教程** 请求参数简单参数实体参数数组参数集合参数![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/eba0ca80e3724412ae4c79af72b859c3.png#pic_center)日期参数json参数路径参数总结 请求…

STM32CubeMX教程31 USB_DEVICE - HID外设_模拟键盘或鼠标

目录 1、准备材料 2、实验目标 3、模拟鼠标实验流程 3.0、前提知识 3.1、CubeMX相关配置 3.1.0、工程基本配置 3.1.1、时钟树配置 3.1.2、外设参数配置 3.1.3、外设中断配置 3.2、生成代码 3.2.0、配置Project Manager页面 3.2.1、设初始化调用流程 3.2.2、外设中…

ESP8266 控制之 : 使用 RingBuffer USART1 和 USART3互传

简介 使用Buffer来避免数据的丢失, 或许你自己在使用串口进行收发时会丢失数据, 现在我们就来简单使用一下RingBuffer创建Rx、Tx的Buffer来避免发送接收丢包或数据丢失问题。 扩展知识 RingBuffer的介绍, 看完大概也就知道了&#xff0c;实在不知道就看看下面的代码 线路连接…

详解操作系统各章大题汇总(死锁资源分配+银行家+进程的PV操作+实时调度+逻辑地址->物理地址+页面置换算法+磁盘调度算法)

文章目录 第三章&#xff1a;死锁资源分配图例一例二 第三章&#xff1a;银行家算法第四章&#xff1a;进程的同步与互斥做题步骤PV操作的代码小心容易和读者写者混 1.交通问题&#xff08;类似读者写者&#xff09;分析代码 2.缓冲区问题&#xff08;第二个缓冲区是复制缓冲区…

实现元素进入界面的平滑效果

先看效果&#xff1a; 实现思路&#xff1a;获取页面中需要加载动画的节点&#xff0c;用元素的animate()方法创建一个动画对象&#xff0c;并传入两个关键帧&#xff0c;接着使用IntersectionObserverAPI创建观察对象&#xff0c;用于观察元素进入页面。当元素进入界面时&…

【数据分享】1929-2023年全球站点的逐年平均气温数据(Shp\Excel\免费获取)

气象数据是在各项研究中都经常使用的数据&#xff0c;气象指标包括气温、风速、降水、湿度等指标&#xff0c;其中又以气温指标最为常用&#xff01;说到气温数据&#xff0c;最详细的气温数据是具体到气象监测站点的气温数据&#xff01;本次我们为大家带来的就是具体到气象监…

CSS--Emmet 语法

Emmet语法的前身是Zen coding,它使用缩写,来提高html/css的编写速度, Vscode内部已经集成该语法. 目录 1. 快速生成HTML结构语法 1.1 快速生成HTML结构语法 2. 快速生成CSS样式语法 2.1 快速生成CSS样式语法 1. 快速生成HTML结构语法 1.1 快速生成HTML结构语法 1. 生成标…

2.【Vue3】Vue 基本使用——局部使用Vue

文章目录 1. 快速入门2. 常用指令2.1 v-for2.2 v-bind2.3 v-if 与 v-show2.4 v-on2.5 v-model 3. 生命周期4. Ajax 函数库 Axios4.1 Axios 基本使用4.2 Axios 请求方式别名 1. 快速入门 现在需要将 “hello vue3” 这样一个字符串渲染到页面上进行展示。 这个需求并不陌生&…

JVM系列——对象管理

JVM对象分布 对象头 第一类是用于存储对象自身的运行时数据&#xff0c;如哈希码&#xff08;HashCode&#xff09;、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等 另外一部分是类型指针&#xff0c;即对象指向它的类型元数据的指针&#xff0c;Java 虚…

【ArcGIS微课1000例】0096:dem三维块状表达(层次地形模型)

文章目录 一、DEM表达方式二、层次模型表达三、注意事项一、DEM表达方式 DEM数字高程模型的表达方式通常有以下4种: 1. 规则格网 2. 不规则三角网 3. 等高线 4. 层次地形模型 作为栅格地理数据,DEM 数据具有2.5维的特征,能够以三维表面的形式进行三维空间表达。但受其数…