MinIO分片上传超大文件(纯服务端)

news2024/12/27 3:23:05

目录

  • 一、MinIO快速搭建
    • 1.1、拉取docker镜像
    • 1.2、启动docker容器
  • 二、分片上传大文件到MinIO
    • 2.1、添加依赖
    • 2.2、实现MinioClient
    • 2.3、实现分片上传
      • 2.3.0、初始化MinioClient
      • 2.3.1、准备分片上传
      • 2.3.2、分片并上传
        • 2.3.2.1、设置分片大小
        • 2.3.2.2、分片
      • 2.3.3、分片合并
  • 三、测试
    • 3.1、完整测试代码
    • 3.2、运行日志和效果

一、MinIO快速搭建

这里简单介绍一下通过docker方式快速搭建MinIO的大体流程。

1.1、拉取docker镜像

首先直接尝试拉取:

docker pull minio/minio

如果拉不到,试图更改docker镜像源:

echo '{
    "registry-mirrors": [
        "https://4xxwxhl6.mirror.aliyuncs.com",
        "https://mirror.iscas.ac.cn",
        "https://docker.rainbond.cc",
        "https://docker.nju.edu.cn",
        "https://6kx4zyno.mirror.aliyuncs.com",
        "https://mirror.baidubce.com",
        "https://docker.m.daocloud.io",
        "https://dockerproxy.com"
    ]
}' | sudo tee /etc/docker/daemon.json > /dev/null

接着重启docker服务,使新配置生效:

sudo systemctl restart docker

最后再次拉取即可。

1.2、启动docker容器

首先创建配置和数据目录:

mkdir -p /opt/minio/config
mkdir -p /opt/minio/data

接着启动:

docker run -p 9000:9000 -p 9001:9001 --net=host --name minio -d --restart=always -e "MINIO_ACCESS_KEY=minio" -e "MINIO_SECRET_KEY=minio123" -v /opt/minio/data:/data -v /opt/minio/config:/root/.minio minio/minio server /data --console-address ":9001" -address ":9000"

最后进入MinIO控制台http://192.168.2.195:9001,简单做点存储桶、用户、用户组等配置即可。比如创建新用户名minioUser,密码minioUser123。

二、分片上传大文件到MinIO

2.1、添加依赖

这里需要注意minio 8.3.3必须依赖okhttp的版本不小于4.8.1。

// minio 8.3.3 Must use okhttp >= 4.8.1
implementation 'io.minio:minio:8.3.3'
implementation 'com.squareup.okhttp3:okhttp:4.12.0'

2.2、实现MinioClient

参考S3官方文档https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpuoverview.html#mpu-process,大文件的分片上传,主要分三步实现:

  1. initMultiPartUpload创建一个大文件分片上传任务
  2. uploadMultiPart逐个上传分片
  3. mergeMultipartUpload合并分片

通过继承默认的MinioClient,将一些相关的重要方法暴露出来,以便使用。

package com.szh.minio;

import com.google.common.collect.Multimap;
import io.minio.*;
import io.minio.errors.*;
import io.minio.messages.Part;

import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

public class CustomMinioClient extends MinioClient {

    /**
     * 继承父类
     */
    public CustomMinioClient(MinioClient client) {
        super(client);
    }

    /**
     * 初始化分片上传即获取uploadId
     */
    public String initMultiPartUpload(String bucket, String region, String object, Multimap<String, String> headers, Multimap<String, String> extraQueryParams) throws IOException, InvalidKeyException, NoSuchAlgorithmException, InsufficientDataException, ServerException, InternalException, XmlParserException, InvalidResponseException, ErrorResponseException {
        CreateMultipartUploadResponse response = this.createMultipartUpload(bucket, region, object, headers, extraQueryParams);
        return response.result().uploadId();
    }

    /**
     * 上传单个分片
     */
    public UploadPartResponse uploadMultiPart(String bucket, String region, String object, Object data,
                                              long length,
                                              String uploadId,
                                              int partNumber,
                                              Multimap<String, String> headers,
                                              Multimap<String, String> extraQueryParams) throws IOException, InvalidKeyException, NoSuchAlgorithmException, InsufficientDataException, ServerException, InternalException, XmlParserException, InvalidResponseException, ErrorResponseException {
        return this.uploadPart(bucket, region, object, data, length, uploadId, partNumber, headers, extraQueryParams);
    }

    /**
     * 合并分片
     */
    public ObjectWriteResponse mergeMultipartUpload(String bucketName, String region, String objectName, String uploadId, Part[] parts, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws IOException, NoSuchAlgorithmException, InsufficientDataException, ServerException, InternalException, XmlParserException, InvalidResponseException, ErrorResponseException, ServerException, InvalidKeyException {
        return this.completeMultipartUpload(bucketName, region, objectName, uploadId, parts, extraHeaders, extraQueryParams);
    }

    public void cancelMultipartUpload(String bucketName, String region, String objectName, String uploadId, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws ServerException, InsufficientDataException, ErrorResponseException, NoSuchAlgorithmException, IOException, InvalidKeyException, XmlParserException, InvalidResponseException, InternalException {
        this.abortMultipartUpload(bucketName, region, objectName, uploadId, extraHeaders, extraQueryParams);
    }

    /**
     * 查询当前上传后的分片信息
     */
    public ListPartsResponse listMultipart(String bucketName, String region, String objectName, Integer maxParts, Integer partNumberMarker, String uploadId, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, ServerException, XmlParserException, ErrorResponseException, InternalException, InvalidResponseException {
        return this.listParts(bucketName, region, objectName, maxParts, partNumberMarker, uploadId, extraHeaders, extraQueryParams);
    }
}

2.3、实现分片上传

2.3.0、初始化MinioClient

连接到minio,并确保存储桶的存在。

static CustomMinioClient minioClient = new CustomMinioClient(MinioClient.builder()
        .endpoint("http://192.168.2.195:9000")
        .credentials("minioUser", "minioUser123")
        .build());
// 测试桶
static String bucketName = "test";
static {
    try {
        boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
        if (!found) {
            minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
        }
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

2.3.1、准备分片上传

创建一个大文件分片上传任务。

String contentType = "application/octet-stream";
HashMultimap<String, String> headers = HashMultimap.create();
headers.put("Content-Type", contentType);
String uploadId = minioClient.initMultiPartUpload(bucketName, null, file.getName(), headers, null);
System.out.println("uploadId: " + uploadId);

2.3.2、分片并上传

本文是使用纯服务端进行分片和上传,而实际项目中更推荐由后端首先调用minio的接口getPresignedObjectUrl,逐个生成每个分片的签名后的上传url,然后前端直接以此上传到minio,即可省去后端服务的网络IO开销。

📢 后者方案请见:MinIO分片上传超大文件(非纯服务端)

2.3.2.1、设置分片大小

一方面,需要注意单个分片大小最小5MB,如果每个分片设置小于5MB,则minio或S3底层在合并时报错:code = EntityTooSmall, message = Your proposed upload is smaller than the minimum allowed object size

另一方面,在调整分片大小时,需要注意minio或S3底层允许的分片范围[1,10000]

2.3.2.2、分片

一方面,为了保证分片的效率,借助线程池的并发,以及RandomAccessFile的文件随机访问能力,更快地完成分片的流程。当然,可控制并发数和分片大小以防止并发分片中的OOM。

另一方面,考虑到分片全部完成之后,还有最后的合并操作,所以借助CountDownLatch来确保所有分片上传之后,再去执行合并。

2.3.3、分片合并

合并所有已上传的分片。

Part[] parts = new Part[(int) chunkCount];
// 查询上传后的分片数据。S3最大允许10000,且从1开始
ListPartsResponse partResult = minioClient.listMultipart(bucketName, null, file.getName(), 10000, 0, uploadId, null, null);
int partNumber = 1;
for (Part part : partResult.result().partList()) {
    parts[partNumber - 1] = new Part(partNumber, part.etag());
    partNumber++;
}
ObjectWriteResponse objectWriteResponse = minioClient.mergeMultipartUpload(bucketName, null, file.getName(), uploadId, parts, null, null);

三、测试

3.1、完整测试代码

package com.szh.minio;

import com.google.common.collect.HashMultimap;
import io.minio.*;
import io.minio.messages.Part;
import lombok.Getter;
import lombok.Setter;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Setter
@Getter
public class MinioMain {
static CustomMinioClient minioClient = new CustomMinioClient(MinioClient.builder()
        .endpoint("http://192.168.2.195:9000")
        .credentials("minioUser", "minioUser123")
        .build());
        // 测试桶
        static String bucketName = "test";
        static {
            try {
                boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
                if (!found) {
                    minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

    // 需要被分片上传的大文件
    static String filePath = "C:\\tmp\\psi_result.csv";
    static File file = new File(filePath);

    // 单个分片大小5MB,如果每个分片设置小于5MB,则minio或S3底层在合并时报错:
    // code = EntityTooSmall, message = Your proposed upload is smaller than the minimum allowed object size.
    static final long CHUNK_SIZE = 5 * 1024 * 1024;

    // 当前分片号,minio或S3底层允许的分片范围[1,10000]
    // https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpuoverview.html#mpu-process
    private int chunkIndex;

    // 用于得知所有分片都传输成功后的时刻,进而进行合并
    private static CountDownLatch countDownLatch;

    public static void main(String[] args) throws Exception {
        // 第一步:准备分片上传
        String contentType = "application/octet-stream";
        HashMultimap<String, String> headers = HashMultimap.create();
        headers.put("Content-Type", contentType);
        String uploadId = minioClient.initMultiPartUpload(bucketName, null, file.getName(), headers, null);
        System.out.println("uploadId: " + uploadId);

        // 第二步:分片并上传
        // ps:实际项目中可由后端先getPresignedObjectUrl逐个生成每个分片的签名后的上传url,前端直接以此上传到minio,即可省去后端服务的网络开销
        long totalLength = file.length();
        System.out.println("totalLength: " + totalLength + " Byte");
        // 计算分片数量
        long chunkCount = (totalLength + CHUNK_SIZE - 1) / CHUNK_SIZE;
        System.out.println("chunkCount: " + chunkCount);
        countDownLatch = new CountDownLatch((int) chunkCount);
        // 5个核心线程并发上传分片
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
        for (long i = 0; i < chunkCount; i++) {
            long position = i * CHUNK_SIZE;
            int bytesRead = (int) Math.min(CHUNK_SIZE, totalLength - position);

            MinioMain minioMain = new MinioMain();
            // S3分片号从1开始
            minioMain.setChunkIndex((int) i + 1);
            fixedThreadPool.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        // 上传分片
                        minioMain.processChunk(filePath, position, bytesRead, uploadId);
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
            });
        }
        countDownLatch.await();
        fixedThreadPool.shutdownNow();

        // 第三步:合并分片
        System.out.println("ready to merge <" + file.getName() + " - " + uploadId + " - " + bucketName + ">");
        Part[] parts = new Part[(int) chunkCount];
        // 查询上传后的分片数据。S3最大允许10000,且从1开始
        ListPartsResponse partResult = minioClient.listMultipart(bucketName, null, file.getName(), 10000, 0, uploadId, null, null);
        int partNumber = 1;
        for (Part part : partResult.result().partList()) {
            parts[partNumber - 1] = new Part(partNumber, part.etag());
            partNumber++;
        }
        ObjectWriteResponse objectWriteResponse = minioClient.mergeMultipartUpload(bucketName, null, file.getName(), uploadId, parts, null, null);
        System.out.println("mergeMultipartUpload resp etag: " + objectWriteResponse.etag());
        StatObjectResponse statObjectResponse = minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(file.getName()).build());
        System.out.println("etag: " + statObjectResponse.etag() + " size: " + statObjectResponse.size() + " lastModified: " + statObjectResponse.lastModified());
    }

    private void processChunk(String filePath, long position, int bytesRead, String uploadId) {
        // 可控制并发数和分片大小以防止OOM
        byte[] buffer = new byte[bytesRead];
        RandomAccessFile raf = null;
        try {
            int chunkIndex = this.getChunkIndex();
            raf = new RandomAccessFile(filePath, "r");
            // 定位到指定位置
            raf.seek(position);
            // 读取bytesRead字节长度作为分片
            raf.readFully(buffer);
            String contentType = "application/octet-stream";
            HashMultimap<String, String> headers = HashMultimap.create();
            headers.put("Content-Type", contentType);
            UploadPartResponse uploadPartResponse = minioClient.uploadMultiPart(bucketName, null, file.getName(),
                    buffer, bytesRead,
                    uploadId, chunkIndex, headers, null);
            System.out.println("chunk[" + chunkIndex + "] buffer size: [" + buffer.length + " Byte] upload etag: [" + uploadPartResponse.etag() + "]");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (raf != null) {
                try {
                    raf.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            countDownLatch.countDown();
        }
    }
}

3.2、运行日志和效果

运行日志如下:

uploadId: MzFiMWRmZjctMDg0Yy00YzMyLTk5NTYtMjRkZGZiMDZlYjJhLmUwZmFkNzFiLWEwZTctNDU1Yi04ZWFjLWFhODQyZjBiMmIyOXgxNzI3MzQwMjUzMTA2Njc5MTEz
totalLength: 3576974860 Byte
chunkCount: 683
chunk[1] buffer size: [5242880 Byte] upload etag: [97096e510d1dcda56646608345de08ea]
chunk[3] buffer size: [5242880 Byte] upload etag: [d8102f80f10eb79f600cdf2d378ae8fe]
chunk[4] buffer size: [5242880 Byte] upload etag: [b74f9b8fa2025580b4fc00449c66e271]
chunk[5] buffer size: [5242880 Byte] upload etag: [e77603ee49cc3f7d229f124ecd9a3f38]
chunk[2] buffer size: [5242880 Byte] upload etag: [b148b311ccd2b3fcd4777d56a8758c3d]
chunk[6] buffer size: [5242880 Byte] upload etag: [94abe5a7a2117b612d9805029398cfd9]
chunk[7] buffer size: [5242880 Byte] upload etag: [433b52aed0d1b1486df07a2259932a83]
chunk[8] buffer size: [5242880 Byte] upload etag: [2c242bd205f9b3c4546454fe2d0abef4]
...
chunk[679] buffer size: [5242880 Byte] upload etag: [8492b0573cc74ec55cb6d2a86aee0f69]
chunk[678] buffer size: [5242880 Byte] upload etag: [4aa5c01b4f7aea95952ec62d71ee9996]
chunk[681] buffer size: [5242880 Byte] upload etag: [ac0b739044bfd2644fc8da97fc03a1a9]
chunk[680] buffer size: [5242880 Byte] upload etag: [d95ee210ac774b3ca26e091941c66e20]
chunk[682] buffer size: [5242880 Byte] upload etag: [75e78df64c1fad0839ba8a1583cd93ec]
chunk[683] buffer size: [1330700 Byte] upload etag: [2f30c8d65e23d266c7f10f051854bc6a]
ready to merge <psi_result.csv - MzFiMWRmZjctMDg0Yy00YzMyLTk5NTYtMjRkZGZiMDZlYjJhLmUwZmFkNzFiLWEwZTctNDU1Yi04ZWFjLWFhODQyZjBiMmIyOXgxNzI3MzQwMjUzMTA2Njc5MTEz - test>
mergeMultipartUpload resp etag: "ff6ebd330b3cb224ade84463dd14df82-683"
etag: ff6ebd330b3cb224ade84463dd14df82-683 size: 3576974860 lastModified: 2024-09-26T09:09Z

上传后的控制台:
MinioConsole

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

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

相关文章

Windows命令行执行cmake

生成Win32 工程并编译 cmake ../../ -G "Visual Studio 16 2019" -A Win32set pathC:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin;%path%msbuild VideoNetOptimization.sln /p:ConfigurationRelWithDebInfo /p:PlatformWin3…

MSYS2+GCC 安装与应用保姆手册

msys2 提供可在Windows下使用 GCC 编译器&#xff1b;并且&#xff0c;借助 Linux 包管理功能&#xff0c;可轻松下载丰富的可在Windows下直接使用的 C/C 开发包&#xff0c;包括编译好的二进制包。 网络库asio、准标准库boost、zip解压缩、json格式处理、引擎 SDL……十八般兵…

2025年软考高项(信息系统项目管理师)包过班靠谱吗?

随着新一轮软考的到来&#xff0c;出现了很多“软考包过”的信息&#xff0c;但需要注意的是&#xff0c;“软考包过”根本不可信&#xff01; 因此不要想着依靠不正当手段来取得证书&#xff0c;要知道&#xff0c;如今 软考 由笔试变为机考&#xff0c;很难作弊&#xff0c;…

​速卖通、敦煌卖家备战双11+黑五前,怎么增加店铺曝光?

在速卖通这个竞争激烈的跨境电商平台上&#xff0c;店铺曝光率是决定销售成败的关键因素之一。为了在众多商家中脱颖而出&#xff0c;增加速卖通店铺曝光显得尤为重要。速卖通怎么增加店铺曝光&#xff1f; 一、速卖通怎么增加店铺曝光? 1、优化产品列表 速卖通的产品列表是…

需求9——通过一个小需求来体会service层的作用

昨天在完成了睿哥的需求验收之后&#xff0c;暂时没有其他任务&#xff0c;因此今天可能会比较有空闲时间。趁着这个机会&#xff0c;我打算把之前完成的一些需求进行总结&#xff0c;方便以后复习和参考。 在8月份的时候&#xff0c;我负责了一个需求&#xff0c;该需求的具体…

白色简洁大方公司企业网站源码 WordPress主题2款

WordPress白色简洁大方公司企业网站主题2款 白色整洁风格wordpress主题是一款比较新颖的国际设计范风格 简洁而大方的 WordPress 主题&#xff0c;适合个人博客、企业和工作室用。 完美支持下拉菜单的wordpress企业主题。 wordpress简白企业模板是一款适合企业站以及工作室…

c++基础知识复习(1)

前期知识准备 1 构造函数 &#xff08;1&#xff09;默认构造函数&#xff1a;没有参数传入&#xff0c;也没有在类里面声明 &#xff08;2&#xff09;手动定义默认构造函数&#xff1a;没有参数传入&#xff0c;但是在类里面进行了声明 可以在类外实现或者类内实现 以下案…

IDM6.42下载器最新版本,提速你的网络生活!

&#x1f680;【速度与激情&#xff0c;IDM 6.42来袭&#xff01;】&#x1f4a3; Hey, 亲爱的下载达人们&#xff01;&#x1f44b; 今天我要给你们安利一个神器——Internet Download Manager&#xff08;简称IDM&#xff09;&#xff0c;版本6.42&#xff0c;这可不是普通的…

猿人学 — 第1届第13题(解题思路附源码)

猿人学 — 第1届第13题&#xff08;解题思路附源码&#xff09; 发现在翻页过程中&#xff0c;只要中途有几秒的间隔&#xff0c;那么就会显示拉取数据失败&#xff0c;然后网页重新加载回到刚进来显示的第一页的情况 重新加载时&#xff0c;会发送一系列的请求&#xff0c;发…

threejs-基础材质设置

一、介绍 主要内容&#xff1a;基础材质(贴图、高光、透明、环境、光照、环境遮蔽贴图) 主要属性&#xff1a; side: three.DoubleSide, //设置双面 color: 0xffffff, //颜色 map: texture, //纹理 transparent: true, // 透明度 aoMap: aoTexture, //ao贴图 aoMapIntensity: 1…

JAVA海外短剧国际版系统小程序源码

海外短剧国际版系统——连接世界的剧情舞台 &#x1f30d; 引言&#xff1a;跨越国界的情感共鸣 在这个全球化的时代&#xff0c;文化的边界越来越模糊&#xff0c;而艺术成为了连接不同国家和地区人民心灵的桥梁。今天&#xff0c;我要向大家介绍一个令人兴奋的平台——“海…

【必看!!!】Python—requests模块详解!(文末附带无偿大礼包)

1、模块说明 requests是使用Apache2 licensed 许可证的HTTP库。 用python编写。 比urllib2模块更简洁。 Request支持HTTP连接保持和连接池&#xff0c;支持使用cookie保持会话&#xff0c;支持文件上传&#xff0c;支持自动响应内容的编码&#xff0c;支持国际化的URL和POS…

Word页眉内容自动填充为章节标题

Word页眉内容自动填充为章节标题 在写毕业论文的过程中&#xff0c;通常要求将页眉设置为章节标题&#xff0c;例如这样 通常&#xff0c;页眉内容我们都是手敲上去的&#xff0c;其实在Word中可以设置为自动引用章节标题&#xff0c;以下为设置方法&#xff0c;仅供参考&…

2024互联网下载神器IDM6.42你值得拥有

&#x1f525; 互联网下载神器大揭秘&#xff01;IDM6.42你值得拥有 &#x1f680; Hey&#xff0c;各位小伙伴们&#xff0c;今天我要给你们安利一款我超爱的软件——Internet Download Manager 6.42&#xff08;简称IDM&#xff09;&#xff0c;这款下载器简直就是下载界的“…

HarmonyOS NEXT应用元服务开发按钮标注场景

对于用户可点击等操作的任何按钮&#xff0c;如果不是文本类控件&#xff0c;则须通过给出标注信息&#xff0c;包括用户自定义的控件中的虚拟按钮区域&#xff0c;否则可能会导致屏幕朗读用户无法完成对应的功能。 此类控件在进行标注时&#xff0c;标注文本不要包含控件类型、…

视频格式不支持播放怎么办?几招教你转换成mp4格式

视频已成为我们生活中不可或缺的一部分&#xff0c;无论是学习、娱乐还是工作交流&#xff0c;视频都扮演着重要角色。然而&#xff0c;在享受视频带来的便利时&#xff0c;我们时常会遇到一个令人头疼的问题——视频格式不支持播放。不同设备、平台和软件对视频格式的支持各不…

推荐一个物联网平台,支持源代码交付

ThingsKit物联网平台概述&#xff1a; ThingsKit是一个开箱即用的物联网平台&#xff0c;它支持通过行业标准的物联网协议&#xff08;如MQTT、TCP、UDP、CoAP和HTTP&#xff09;实现设备连接。这个平台能够帮助用户快速实现物联网的数据收集、分析处理、可视化和设备管理&…

『网络游戏』服务器向客户端分发消息【21】

新建缓存层文件夹 创建脚本&#xff1a;CacheSvc 编写服务器脚本&#xff1a;CacheSvc 修改服务器脚本&#xff1a;LoginSys.cs 修改服务器脚本&#xff1a;PEProtocol.cs 服务器编写完成 - 测试运行服务端 修改客户端脚本&#xff1a;NetSvc.cs 修改客户端脚本&#xff1a;Cli…

跟《经济学人》学英文:2024年10月05日这期 Workouts for the face are a growing business

Workouts for the face are a growing business They may not help much in the quest for eternal youth 原文&#xff1a; The FaceGym studio in central London looks more like a hair salon than a fitness studio. Customers recline on chairs while staff pummel t…

路径跟踪之导航向量场——二维导航向量场

今天带来一期轨迹跟踪算法的讲解&#xff0c;首先讲解二维平面中的导航向量场[1]。该方法具有轻量化、计算简便、收敛性强等多项优点。该方法根据期望的轨迹函数&#xff0c;计算全局位置的期望飞行向量&#xff0c;将期望飞行向量转为偏光角&#xff0c;输入底层控制器&#x…