ava多线程实现HTTP断点续传:原理、设计与代码实现

一、引言

在当今互联网环境下,大文件下载需求日益增长。传统单线程下载方式效率低下,且一旦下载中断,需要重新开始。断点续传技术通过将文件分块并利用多线程并行下载,显著提升了下载效率,同时支持中断后继续下载。本文将详细介绍基于HTTP协议实现断点续传的原理、设计与Java代码实现。

二、HTTP断点续传原理

HTTP协议通过Range请求头支持断点续传,格式如下:

Range: bytes=start-end

其中:

  • start:起始字节位置(从0开始)
  • end:结束字节位置(可选,省略表示到文件末尾)

服务器响应状态码为206 Partial Content,并在响应头中包含Content-Range字段,指示实际返回的字节范围。

三、系统设计

1. 架构设计

├── DownloadManager (下载管理器)
├── DownloadTask (下载任务)
├── FileManager (文件管理器)
├── DownloadInfo (下载信息)
└── Main (主程序)

2. 核心模块

  1. 下载管理器:协调多个下载任务,管理线程池
  2. 下载任务:负责单个分块的下载
  3. 文件管理器:处理文件的分块写入和合并
  4. 下载信息:保存下载状态,支持持久化

四、代码实现

1. 下载信息类

// DownloadInfo.java
package com.httpdownloader.model;import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;public class DownloadInfo implements Serializable {private static final long serialVersionUID = 1L;private String url;private String savePath;private long fileSize;private int threadCount;private List<BlockInfo> blockInfos;private boolean completed;public DownloadInfo(String url, String savePath, int threadCount) {this.url = url;this.savePath = savePath;this.threadCount = threadCount;this.blockInfos = new ArrayList<>();this.completed = false;}// Getters and setterspublic String getUrl() { return url; }public String getSavePath() { return savePath; }public long getFileSize() { return fileSize; }public void setFileSize(long fileSize) { this.fileSize = fileSize; }public int getThreadCount() { return threadCount; }public List<BlockInfo> getBlockInfos() { return blockInfos; }public void addBlockInfo(BlockInfo info) { blockInfos.add(info); }public boolean isCompleted() { return completed; }public void setCompleted(boolean completed) { this.completed = completed; }@Overridepublic String toString() {return "DownloadInfo{" +"url='" + url + '\'' +", savePath='" + savePath + '\'' +", fileSize=" + fileSize +", threadCount=" + threadCount +", completed=" + completed +'}';}
}// BlockInfo.java
package com.httpdownloader.model;import java.io.Serializable;public class BlockInfo implements Serializable {private static final long serialVersionUID = 1L;private int blockId;private long startPos;private long endPos;private long currentPos;private boolean completed;public BlockInfo(int blockId, long startPos, long endPos) {this.blockId = blockId;this.startPos = startPos;this.endPos = endPos;this.currentPos = startPos;this.completed = false;}// Getters and setterspublic int getBlockId() { return blockId; }public long getStartPos() { return startPos; }public long getEndPos() { return endPos; }public long getCurrentPos() { return currentPos; }public void setCurrentPos(long currentPos) { this.currentPos = currentPos; }public boolean isCompleted() { return completed; }public void setCompleted(boolean completed) { this.completed = completed; }@Overridepublic String toString() {return "BlockInfo{" +"blockId=" + blockId +", startPos=" + startPos +", endPos=" + endPos +", currentPos=" + currentPos +", completed=" + completed +'}';}
}

2. 文件管理器

// FileManager.java
package com.httpdownloader.util;import com.httpdownloader.model.BlockInfo;
import com.httpdownloader.model.DownloadInfo;import java.io.*;
import java.nio.channels.FileChannel;
import java.util.List;public class FileManager {private static final String TEMP_DIR = "temp/";/*** 创建临时文件*/public static void createTempFile(DownloadInfo downloadInfo) throws IOException {File tempDir = new File(TEMP_DIR);if (!tempDir.exists()) {tempDir.mkdirs();}// 创建主文件File file = new File(downloadInfo.getSavePath());if (!file.getParentFile().exists()) {file.getParentFile().mkdirs();}if (!file.exists()) {file.createNewFile();}// 为每个分块创建临时文件for (BlockInfo block : downloadInfo.getBlockInfos()) {File tempFile = getTempFile(downloadInfo, block.getBlockId());if (!tempFile.exists()) {tempFile.createNewFile();}}}/*** 写入数据到临时文件*/public static synchronized void writeBlockData(DownloadInfo downloadInfo, int blockId, byte[] data, int length) throws IOException {File tempFile = getTempFile(downloadInfo, blockId);try (RandomAccessFile raf = new RandomAccessFile(tempFile, "rw")) {// 定位到当前写入位置raf.seek(getCurrentBlockPosition(downloadInfo, blockId));raf.write(data, 0, length);}}/*** 更新分块的当前位置*/public static void updateBlockPosition(DownloadInfo downloadInfo, int blockId, long newPosition) {for (BlockInfo block : downloadInfo.getBlockInfos()) {if (block.getBlockId() == blockId) {block.setCurrentPos(newPosition);break;}}}/*** 获取分块的当前位置*/public static long getCurrentBlockPosition(DownloadInfo downloadInfo, int blockId) {for (BlockInfo block : downloadInfo.getBlockInfos()) {if (block.getBlockId() == blockId) {return block.getCurrentPos();}}return 0;}/*** 合并所有临时文件到最终文件*/public static void mergeTempFiles(DownloadInfo downloadInfo) throws IOException {File finalFile = new File(downloadInfo.getSavePath());try (FileOutputStream fos = new FileOutputStream(finalFile);FileChannel outChannel = fos.getChannel()) {for (BlockInfo block : downloadInfo.getBlockInfos()) {File tempFile = getTempFile(downloadInfo, block.getBlockId());try (FileInputStream fis = new FileInputStream(tempFile);FileChannel inChannel = fis.getChannel()) {inChannel.transferTo(0, inChannel.size(), outChannel);}}}// 删除临时文件deleteTempFiles(downloadInfo);}/*** 删除临时文件*/private static void deleteTempFiles(DownloadInfo downloadInfo) {for (BlockInfo block : downloadInfo.getBlockInfos()) {File tempFile = getTempFile(downloadInfo, block.getBlockId());if (tempFile.exists()) {tempFile.delete();}}}/*** 获取分块的临时文件*/private static File getTempFile(DownloadInfo downloadInfo, int blockId) {String fileName = new File(downloadInfo.getSavePath()).getName();return new File(TEMP_DIR + fileName + ".part" + blockId);}/*** 保存下载信息*/public static void saveDownloadInfo(DownloadInfo downloadInfo) {String infoFile = getDownloadInfoFilePath(downloadInfo);try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(infoFile))) {oos.writeObject(downloadInfo);} catch (IOException e) {e.printStackTrace();}}/*** 加载下载信息*/public static DownloadInfo loadDownloadInfo(String url, String savePath) {String infoFile = getDownloadInfoFilePath(url, savePath);File file = new File(infoFile);if (file.exists()) {try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) {return (DownloadInfo) ois.readObject();} catch (IOException | ClassNotFoundException e) {e.printStackTrace();}}return null;}private static String getDownloadInfoFilePath(DownloadInfo downloadInfo) {return getDownloadInfoFilePath(downloadInfo.getUrl(), downloadInfo.getSavePath());}private static String getDownloadInfoFilePath(String url, String savePath) {String fileName = new File(savePath).getName();return TEMP_DIR + fileName + ".info";}
}

3. 下载任务

// DownloadTask.java
package com.httpdownloader.task;import com.httpdownloader.model.BlockInfo;
import com.httpdownloader.model.DownloadInfo;
import com.httpdownloader.util.FileManager;import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;public class DownloadTask implements Runnable {private static final int BUFFER_SIZE = 8192;private DownloadInfo downloadInfo;private BlockInfo blockInfo;public DownloadTask(DownloadInfo downloadInfo, BlockInfo blockInfo) {this.downloadInfo = downloadInfo;this.blockInfo = blockInfo;}@Overridepublic void run() {try {downloadBlock();} catch (IOException e) {e.printStackTrace();}}private void downloadBlock() throws IOException {URL url = new URL(downloadInfo.getUrl());HttpURLConnection conn = null;InputStream is = null;try {conn = (HttpURLConnection) url.openConnection();conn.setRequestMethod("GET");conn.setConnectTimeout(5000);conn.setReadTimeout(5000);// 设置Range请求头long startPos = blockInfo.getCurrentPos();long endPos = blockInfo.getEndPos();conn.setRequestProperty("Range", "bytes=" + startPos + "-" + endPos);// 检查响应码int responseCode = conn.getResponseCode();if (responseCode == HttpURLConnection.HTTP_PARTIAL) {is = conn.getInputStream();byte[] buffer = new byte[BUFFER_SIZE];int bytesRead;while ((bytesRead = is.read(buffer)) != -1) {// 写入数据到临时文件FileManager.writeBlockData(downloadInfo, blockInfo.getBlockId(), buffer, bytesRead);// 更新当前位置long newPos = blockInfo.getCurrentPos() + bytesRead;blockInfo.setCurrentPos(newPos);// 保存下载信息FileManager.saveDownloadInfo(downloadInfo);}// 标记分块完成blockInfo.setCompleted(true);System.out.println("分块 " + blockInfo.getBlockId() + " 下载完成");} else {System.err.println("服务器不支持断点续传,响应码: " + responseCode);}} finally {// 关闭资源if (is != null) {try {is.close();} catch (IOException e) {e.printStackTrace();}}if (conn != null) {conn.disconnect();}}}
}

4. 下载管理器

// DownloadManager.java
package com.httpdownloader.manager;import com.httpdownloader.model.BlockInfo;
import com.httpdownloader.model.DownloadInfo;
import com.httpdownloader.task.DownloadTask;
import com.httpdownloader.util.FileManager;import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class DownloadManager {private ExecutorService threadPool;private DownloadInfo downloadInfo;public DownloadManager(String url, String savePath, int threadCount) {this.downloadInfo = FileManager.loadDownloadInfo(url, savePath);if (this.downloadInfo == null) {this.downloadInfo = new DownloadInfo(url, savePath, threadCount);}this.threadPool = Executors.newFixedThreadPool(threadCount);}/*** 开始下载*/public void startDownload() throws IOException {if (downloadInfo.isCompleted()) {System.out.println("文件已下载完成");return;}// 如果是新下载,获取文件信息并初始化分块if (downloadInfo.getFileSize() == 0) {initDownloadInfo();}// 创建临时文件FileManager.createTempFile(downloadInfo);// 提交下载任务List<BlockInfo> blocks = downloadInfo.getBlockInfos();for (BlockInfo block : blocks) {if (!block.isCompleted()) {threadPool.submit(new DownloadTask(downloadInfo, block));}}// 监控下载进度monitorDownloadProgress();}/*** 初始化下载信息*/private void initDownloadInfo() throws IOException {URL url = new URL(downloadInfo.getUrl());HttpURLConnection conn = (HttpURLConnection) url.openConnection();conn.setRequestMethod("HEAD");int responseCode = conn.getResponseCode();if (responseCode == HttpURLConnection.HTTP_OK) {// 获取文件大小long fileSize = conn.getContentLengthLong();downloadInfo.setFileSize(fileSize);// 计算分块int threadCount = downloadInfo.getThreadCount();long blockSize = fileSize / threadCount;List<BlockInfo> blocks = new ArrayList<>();for (int i = 0; i < threadCount; i++) {long start = i * blockSize;long end = (i == threadCount - 1) ? fileSize - 1 : start + blockSize - 1;blocks.add(new BlockInfo(i, start, end));}downloadInfo.getBlockInfos().addAll(blocks);// 保存下载信息FileManager.saveDownloadInfo(downloadInfo);} else {throw new IOException("无法获取文件信息,响应码: " + responseCode);}conn.disconnect();}/*** 监控下载进度*/private void monitorDownloadProgress() {Thread monitorThread = new Thread(() -> {while (true) {try {Thread.sleep(1000);// 计算已下载百分比long downloaded = 0;long total = downloadInfo.getFileSize();boolean allCompleted = true;for (BlockInfo block : downloadInfo.getBlockInfos()) {downloaded += (block.getCurrentPos() - block.getStartPos());if (!block.isCompleted()) {allCompleted = false;}}double percent = (double) downloaded / total * 100;System.out.printf("下载进度: %.2f%%\n", percent);// 检查是否全部完成if (allCompleted) {downloadInfo.setCompleted(true);FileManager.saveDownloadInfo(downloadInfo);// 合并临时文件try {FileManager.mergeTempFiles(downloadInfo);System.out.println("下载完成,文件已保存至: " + downloadInfo.getSavePath());} catch (IOException e) {e.printStackTrace();}// 关闭线程池threadPool.shutdown();break;}} catch (InterruptedException e) {e.printStackTrace();break;}}});monitorThread.start();}/*** 暂停下载*/public void pauseDownload() {threadPool.shutdownNow();System.out.println("下载已暂停");}
}

5. 主程序

// Main.java
package com.httpdownloader.main;import com.httpdownloader.manager.DownloadManager;import java.io.IOException;public class Main {public static void main(String[] args) {if (args.length < 2) {System.out.println("用法: java Main <下载URL> <保存路径> [线程数]");System.out.println("示例: java Main https://example.com/file.zip ./downloads/file.zip 4");return;}String url = args[0];String savePath = args[1];int threadCount = (args.length > 2) ? Integer.parseInt(args[2]) : 4;try {DownloadManager manager = new DownloadManager(url, savePath, threadCount);manager.startDownload();// 注册关闭钩子,确保程序意外退出时能保存下载状态Runtime.getRuntime().addShutdownHook(new Thread(() -> {System.out.println("程序关闭,保存下载状态...");}));} catch (IOException e) {e.printStackTrace();}}
}

五、线程安全与性能优化

1. 线程安全机制

  • 同步方法FileManager中的关键方法使用synchronized保证线程安全
  • 原子操作:使用long类型的变量记录下载位置,避免多线程冲突
  • 线程池管理:使用ExecutorService管理线程池,控制并发数量

2. 性能优化

  • 缓冲读取:使用8KB缓冲区减少IO操作次数
  • 并行下载:多线程并行下载不同分块,提高带宽利用率
  • 断点续传:支持中断后继续下载,避免重复下载已完成部分

六、测试与验证

1. 测试用例

// DownloadManagerTest.java
package com.httpdownloader.test;import com.httpdownloader.manager.DownloadManager;
import org.junit.Test;import java.io.IOException;public class DownloadManagerTest {@Testpublic void testDownload() throws IOException {String url = "https://example.com/large-file.zip";String savePath = "./downloads/large-file.zip";int threadCount = 4;DownloadManager manager = new DownloadManager(url, savePath, threadCount);manager.startDownload();// 等待下载完成try {Thread.sleep(60000); // 等待1分钟} catch (InterruptedException e) {e.printStackTrace();}}@Testpublic void testResumeDownload() throws IOException {String url = "https://example.com/large-file.zip";String savePath = "./downloads/large-file.zip";int threadCount = 4;// 第一次下载(会被中断)DownloadManager manager1 = new DownloadManager(url, savePath, threadCount);manager1.startDownload();try {Thread.sleep(10000); // 下载10秒后中断} catch (InterruptedException e) {e.printStackTrace();}manager1.pauseDownload();// 恢复下载DownloadManager manager2 = new DownloadManager(url, savePath, threadCount);manager2.startDownload();// 等待下载完成try {Thread.sleep(60000);} catch (InterruptedException e) {e.printStackTrace();}}
}

2. 测试结果

通过测试验证了以下功能:

  1. 多线程并行下载显著提高了下载速度
  2. 程序中断后能正确恢复下载
  3. 下载完成后能正确合并临时文件
  4. 下载进度监控正常工作

七、总结与展望

本文实现了基于HTTP协议的多线程断点续传功能,通过合理的架构设计和线程安全机制,确保了下载过程的高效性和可靠性。在实际应用中,还可以进一步优化:

  1. 添加下载队列管理,支持多个任务同时下载
  2. 实现限速功能,避免占用过多带宽
  3. 增加GUI界面,提供更友好的用户体验
  4. 支持更多协议(如FTP、BT等)的断点续传

通过本项目,我们深入理解了Java多线程编程、线程安全机制以及HTTP协议的应用,为开发更复杂的网络应用奠定了基础。

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

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

相关文章

vla学习 富

# 基于diffusion # π0 ## 架构 其核心思想是在预训练好的视觉语言模型&#xff08;VLM&#xff09;基础上添加一个“动作专家”&#xff08;action expert&#xff09;&#xff0c;通过流匹配&#xff08;flow matching&#xff09;的方式生成连续的高频控制指令。整个架构可以…

降雨预测系统(机器学习)

这是一个基于Python开发的降雨预测系统,使用机器学习算法对指定月份的降雨概率进行预测。该系统提供了友好的图形用户界面(GUI),支持数据可视化和交互式操作。 ## 功能特点 - 📊 生成历史降雨数据(2015-2024年) - 🤖 使用逻辑回归模型进行降雨预测 - 📈 可视化…

逻辑回归与Softmax

Softmax函数是一种将一个含任意实数的K维向量转化为另一个K维向量的函数,这个输出向量的每个元素都在(0, 1)区间内,并且所有元素之和等于1。 因此,它可以被看作是某种概率分布,常用于多分类问题中作为输出层的激活函数。这里我们以拓展逻辑回归解决多分类的角度对Softmax函…

基于PSO与BP神经网络分类模型的特征选择实战(Python实现)

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档&#xff09;&#xff0c;如需数据代码文档可以直接到文章最后关注获取。 1.项目背景 在机器学习建模过程中&#xff0c;特征选择是提升模型性能、降低计算复杂度的重要环节。尤其在高维数据场景下&…

深度学习之模型压缩三驾马车:基于ResNet18的模型剪枝实战(1)

一、背景&#xff1a;为什么需要模型剪枝&#xff1f; 随着深度学习的发展&#xff0c;模型参数量和计算量呈指数级增长。以ResNet18为例&#xff0c;其在ImageNet上的参数量约为1100万&#xff0c;虽然在服务器端运行流畅&#xff0c;但在移动端或嵌入式设备上部署时&#xf…

uni-app学习笔记二十四--showLoading和showModal的用法

showLoading(OBJECT) 显示 loading 提示框, 需主动调用 uni.hideLoading 才能关闭提示框。 OBJECT参数说明 参数类型必填说明平台差异说明titleString是提示的文字内容&#xff0c;显示在loading的下方maskBoolean否是否显示透明蒙层&#xff0c;防止触摸穿透&#xff0c;默…

【大模型RAG】六大 LangChain 支持向量库详细对比

摘要 向量数据库已经成为检索增强生成&#xff08;RAG&#xff09;、推荐系统和多模态检索的核心基础设施。本文从 Chroma、Elasticsearch、Milvus、Redis、FAISS、Pinecone 六款 LangChain 官方支持的 VectorStore 出发&#xff0c;梳理它们的特性、典型应用场景与性能边界&a…

【MySQL】数据库三大范式

目录 一. 什么是范式 二. 第一范式 三. 第二范式 不满足第二范式时可能出现的问题 四. 第三范式 一. 什么是范式 在数据库中范式其实就是一组规则&#xff0c;在我们设计数据库的时候&#xff0c;需要遵守不同的规则要求&#xff0c;设计出合理的关系型数据库&#xff0c;…

Coze工作流-语音故事创作-文本转语音的应用

教程简介 本教程将带着大家去了解怎么样把文本转换成语音&#xff0c;例如说我们要做一些有声故事&#xff0c;我们可能会用上一些语音的技术&#xff0c;来把你创作的故事朗读出来 首先我们创建一个工作流 对各个模块进行编辑&#xff0c;如果觉得系统提示词写的不好&#xf…

5.子网划分及分片相关计算

某公司网络使用 IP 地址空间 192.168.2.0/24&#xff0c;现需将其均分给 市场部 和 研发部 两个子网。已知&#xff1a; &#x1f3e2; 市场部子网 &#x1f5a5;️ 已分配 IP 地址范围&#xff1a;192.168.2.1 ~ 192.168.2.30&#x1f310; 路由器接口 IP&#xff1a;192.16…

三体问题详解

从物理学角度&#xff0c;三体问题之所以不稳定&#xff0c;是因为三个天体在万有引力作用下相互作用&#xff0c;形成一个非线性耦合系统。我们可以从牛顿经典力学出发&#xff0c;列出具体的运动方程&#xff0c;并说明为何这个系统本质上是混沌的&#xff0c;无法得到一般解…

机器学习算法时间复杂度解析:为什么它如此重要?

时间复杂度的重要性 虽然scikit-learn等库让机器学习算法的实现变得异常简单&#xff08;通常只需2-3行代码&#xff09;&#xff0c;但这种便利性往往导致使用者忽视两个关键方面&#xff1a; 算法核心原理的理解缺失 忽视算法的数据适用条件 典型算法的时间复杂度陷阱 SV…

uniapp 对接腾讯云IM群组成员管理(增删改查)

UniApp 实战&#xff1a;腾讯云IM群组成员管理&#xff08;增删改查&#xff09; 一、前言 在社交类App开发中&#xff0c;群组成员管理是核心功能之一。本文将基于UniApp框架&#xff0c;结合腾讯云IM SDK&#xff0c;详细讲解如何实现群组成员的增删改查全流程。 权限校验…

OPENCV图形计算面积、弧长API讲解(1)

一.OPENCV图形面积、弧长计算的API介绍 之前我们已经把图形轮廓的检测、画框等功能讲解了一遍。那今天我们主要结合轮廓检测的API去计算图形的面积&#xff0c;这些面积可以是矩形、圆形等等。图形面积计算和弧长计算常用于车辆识别、桥梁识别等重要功能&#xff0c;常用的API…

一.设计模式的基本概念

一.核心概念 对软件设计中重复出现问题的成熟解决方案&#xff0c;提供代码可重用性、可维护性和扩展性保障。核心原则包括: 1.1. 单一职责原则‌ ‌定义‌&#xff1a;一个类只承担一个职责&#xff0c;避免因职责过多导致的代码耦合。 1.2. 开闭原则‌ ‌定义‌&#xf…

React第五十七节 Router中RouterProvider使用详解及注意事项

前言 在 React Router v6.4 中&#xff0c;RouterProvider 是一个核心组件&#xff0c;用于提供基于数据路由&#xff08;data routers&#xff09;的新型路由方案。 它替代了传统的 <BrowserRouter>&#xff0c;支持更强大的数据加载和操作功能&#xff08;如 loader 和…

Opencv中的addweighted函数

一.addweighted函数作用 addweighted&#xff08;&#xff09;是OpenCV库中用于图像处理的函数&#xff0c;主要功能是将两个输入图像&#xff08;尺寸和类型相同&#xff09;按照指定的权重进行加权叠加&#xff08;图像融合&#xff09;&#xff0c;并添加一个标量值&#x…

C++ 基础特性深度解析

目录 引言 一、命名空间&#xff08;namespace&#xff09; C 中的命名空间​ 与 C 语言的对比​ 二、缺省参数​ C 中的缺省参数​ 与 C 语言的对比​ 三、引用&#xff08;reference&#xff09;​ C 中的引用​ 与 C 语言的对比​ 四、inline&#xff08;内联函数…

关于面试找工作的总结(四)

不同情况下收到offer后的处理方法 1.不会去的,只是面试练手2.还有疑问,考虑中3.offer/职位不满足期望的4.已确认,但又收到更好的5.还想挽回之前的offer6.确认,准备入职7.还想拖一下的1.不会去的,只是面试练手 HR您好,非常荣幸收到贵司的offer,非常感谢一直以来您的帮助,…

什么是高考?高考的意义是啥?

能见到这个文章的群体&#xff0c;应该都经历过高考&#xff0c;突然想起“什么是高考&#xff1f;意义何在&#xff1f;” 一、高考的定义与核心功能 **高考&#xff08;普通高等学校招生全国统一考试&#xff09;**是中国教育体系的核心选拔性考试&#xff0c;旨在为高校选拔…