从sftp下载大文件到浏览器

news2025/1/10 3:25:21

从sftp下载大文件到浏览器

    • 问题
    • 方案
    • 相关依赖包
    • 相关代码片段(后端)
      • 文件信息缓存工具类-FileChunkCache
      • 文件信息对象-FileDetail
      • sftp传输进度监控-FileProgressMonitor
      • 切片工具类-ChunkService
      • 文件下载服务-AsyncDownloadService

问题

近期遇到直接使用sftp下载文件到前端,前端下载一部分后就会卡住,分析可能是response缓存原因,是故采取切片分式下载到前端,即分多个请求去下载大文件,最终在前端对多个切片进行合并大文件。

方案

查询资料,没有了解到jsch sftp有相关切片下载知识,所以考虑先将文件下载到后端(使用线程异步下载),再下载到前端,具体设计思路如下:
在这里插入图片描述

相关依赖包

        <dependency>
            <groupId>com.jcraft</groupId>
            <artifactId>jsch</artifactId>
            <version>0.1.55</version>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.5</version>
        </dependency>

相关代码片段(后端)

文件信息缓存工具类-FileChunkCache

用于缓存下载文件的信息

public class FileChunkCache {
    private FileChunkCache() {
    }

    /**
     * 缓存下载文件对象信息(3小时)
     */
    private static final TimedCache<String, FileDetail> TIMED_CACHE;

    static {
        // 缓存下载文件对象信息(3小时)
        TIMED_CACHE = new TimedCache<>(1000 * 60 * 60 * 3L);
    }

    public static FileDetail get(String fileKey) {
        return TIMED_CACHE.get(fileKey, false);
    }

    public static void put(String fileKey, FileDetail fileDetail) {
        TIMED_CACHE.put(fileKey, fileDetail);
    }

    public static void remove(String fileKey) {
        TIMED_CACHE.remove(fileKey);
    }

    public static void clear() {
        TIMED_CACHE.clear();
    }

    public static boolean containsKey(String fileKey) {
        return TIMED_CACHE.containsKey(fileKey);
    }

    public static int size() {
        return TIMED_CACHE.size();
    }
}

文件信息对象-FileDetail

记录文件下载状态、分片信息、连接通道

public class FileDetail {
    /**
     * 文件名
     */
    private String fileName;
    /**
     * 文件路径(本地保存路径)
     */
    private String filePath;
    /**
     * 文件大小
     */
    private long fileSize;
    /**
     * 分片大小
     */
    private long chunkSize;
    /**
     * 分片数量
     */
    private long chunkNum;
    /**
     * 文件对象key标识
     */
    private String fileKey;
    /**
     * 连接session
     */
    @JsonIgnore
    private Session session;
    /**
     * sftp通道
     */
    @JsonIgnore
    private ChannelSftp channelSftp;
    /**
     * 下载状态(枚举类,自行定义)
     */
    private DownloadStatus downloadStatus = DownloadStatus.NOT_DOWNLOADED;

    public FileDetail() {
    }

    public void connectSession() throws JSchException {
        this.session.connect();
    }

    public void connectChannelSftp() throws JSchException {
        this.channelSftp.connect();
    }

    public void closeConnect() {
        if (this.session != null) {
            this.session.disconnect();
        }
        if (this.channelSftp != null) {
            this.channelSftp.disconnect();
        }
    }

    public void setDownloadStatus(DownloadStatus downloadStatus) {
        this.downloadStatus = downloadStatus;
        FileChunkCache.put(this.fileKey, this);
    }
}

sftp传输进度监控-FileProgressMonitor

实时sftp监控下载进度

@Slf4j
public class FileProgressMonitor implements SftpProgressMonitor {
    /**
     * 默认间隔时间为2秒
     */
    private static final long PROGRESS_INTERVAL = 2000;

    /**
     * 记录传输是否结束
     */
    private boolean isEnd = false;
    /**
     * 记录已传输的数据总大小
     */
    private long transfer = 0;
    /**
     * 记录文件总大小
     */
    private final long fileSize;
    /**
     * 记录文件路径
     */
    private final String filePath;
    /**
     * 记录文件信息
     */
    private final FileDetail fileDetail;
    /**
     * 定时器对象
     */
    private ScheduledExecutorService scheduledExecutorService;
    /**
     * 记录是否已启动记时器
     */
    private boolean isScheduled = false;


    private Date startTime;

    private Date endTime;

    /**
     * 构造方法中初始化文件大小
     */
    public FileProgressMonitor(long fileSize, String filePath, FileDetail fileDetail) {
        this.fileSize = fileSize;
        this.filePath = filePath;
        this.fileDetail = fileDetail;
    }


    /**
     * 输出当前传输进度信息
     */
    public void outCurrentDetails() {
        // 判断传输是否已结束
        if (!isEnd()) {
            log.info("文件:{} 传输中...", filePath);
            long transmissionSize = getTransfer();
            if (transmissionSize != fileSize) {
                // 判断当前已传输数据大小是否等于文件总大小
                log.info("当前传输:{} bytes", transmissionSize);
                sendProgressMessage(transmissionSize);
            } else {
                log.info("文件已传输完成。");
                // 如果当前已传输数据大小等于文件总大小,说明已完成,设置end
                setEnd(true);
            }
        } else {
            log.info("文件:【{}】传输完成。关闭进度监视器", filePath);
            stop();
        }
    }

    /**
     * 启动监视器
     */
    public void start() {
        log.info("尝试启动进度监视器。");
        if (scheduledExecutorService == null) {
            scheduledExecutorService = new ScheduledThreadPoolExecutor(1,
                    new BasicThreadFactory.Builder().namingPattern("sftp-schedule-pool-%d").daemon(true).build());
        }
        scheduledExecutorService.scheduleAtFixedRate(this::outCurrentDetails, 1000, PROGRESS_INTERVAL, TimeUnit.MILLISECONDS);
        isScheduled = true;
        log.info("进度监视器启动。");
    }

    /**
     * 关闭监视器
     */
    public void stop() {
        log.info("尝试停止进度监视器。");
        if (scheduledExecutorService != null) {
            scheduledExecutorService.shutdownNow();
            scheduledExecutorService = null;
            isScheduled = false;
        }
        log.info("进度监视器停止了。");
    }


    /**
     * 输出进度条信息
     *
     * @param transmissionSize 当前已传输数据大小
     */
    private void sendProgressMessage(long transmissionSize) {
        if (fileSize != 0) {
            double d = ((double) transmissionSize * 100) / (double) fileSize;
            DecimalFormat df = new DecimalFormat("#.##");
            log.info("文件【{}】已传输进度:{}", filePath, df.format(d) + "%");
        } else {
            log.info("进度消息,文件:{},文件总大小:{},当前传输大小:{}", filePath, fileSize, transmissionSize);
        }
    }

    /**
     * 记录已传输数据大小
     *
     * @param count 当次传输数据大小
     */
    private synchronized void add(long count) {
        transfer = transfer + count;
    }

    /**
     * 获取当前已传输数据大小
     *
     * @return 当前已传输数据大小
     */
    private synchronized long getTransfer() {
        return transfer;
    }

    /**
     * 设置传输是否结束
     *
     * @param isEnd 是否结束
     */
    private synchronized void setEnd(boolean isEnd) {
        this.isEnd = isEnd;
    }

    /**
     * 判断传输是否结束
     *
     * @return 是否结束
     */
    private synchronized boolean isEnd() {
        return isEnd;
    }

    @Override
    public void init(int op, String src, String dest, long max) {
        log.info("开始传输文件:{},文件大小:{}", filePath, max);
        this.startTime = new Date();
        this.fileDetail.setDownloadStatus(DownloadStatus.BE_DOWNLOADING);
    }

    /**
     * 实现SftpProgressMonitor接口的count方法
     */
    @Override
    public boolean count(long count) {
        if (isEnd()) {
            return false;
        }
        if (!isScheduled) {
            start();
        }
        add(count);
        return true;
    }


    /**
     * 实现了SftpProgressMonitor接口的end方法
     */
    @Override
    public void end() {
        this.endTime = new Date();
        setEnd(true);
        stop();
        // 计算耗时
        long time = endTime.getTime() - startTime.getTime();
        this.fileDetail.setDownloadStatus(DownloadStatus.DOWNLOAD_SUCCESS);
        log.info("文件:{},传输结束,耗时:{}ms", filePath, time);
    }
}

切片工具类-ChunkService

对本地文件做切片处理

@Service
@Slf4j
public class ChunkService {

    /**
     * 获取分片数据(从正在下载的文件中返回)
     *
     * @param fileKey        文件key
     * @param chunkSize      分片大小
     * @param resultFileName 文件名
     * @param offset         分片偏移量
     * @return 分片数据
     */
    public byte[] getChunkOnDownloadFile(String fileKey, Integer chunkSize, String resultFileName, long offset, HttpServletResponse response) {
        File file = new File(resultFileName);
        // 重试次数
        int retryCount = 0;
        // 当前文件大小
        long currentFileSize = file.length();
        // 当前文件大小是否达到分片大小
        while (offset + chunkSize > currentFileSize) {
            // 重试次数大于100次则退出
            if (retryCount > 120) {
                throw new RuntimeException("重试达到最大值");
            }
            FileDetail fileDetail = FileChunkCache.get(fileKey);
            if (DownloadStatus.DOWNLOAD_FAILED.equals(fileDetail.getDownloadStatus())) {
                throw new RuntimeException("当前文件sftp下载失败");
            }
            // 休眠1秒后再次获取
            try {
                log.info("文件大小未达到分片大小,休眠1秒后再次获取,分片大小:{},偏移量:{},文件大小:{},当前文件下载状态:{}", chunkSize, offset, currentFileSize, fileDetail.getDownloadStatus().getLabel());
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException("线程休眠异常", e);
            }
            retryCount++;
            // 重新获取文件大小
            file = new File(resultFileName);
            currentFileSize = file.length();
        }
        return getChunk(chunkSize, resultFileName, offset, response);
    }

    /**
     * 获取分片数据
     *
     * @param chunkSize      分片大小
     * @param resultFileName 文件名
     * @param offset         分片偏移量
     * @return 分片数据
     */
    public byte[] getChunk(Integer chunkSize, String resultFileName, long offset, HttpServletResponse response) {
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(resultFileName, "r")) {
            // 定位到该分片的偏移量
            randomAccessFile.seek(offset);
            //读取
            byte[] buffer = new byte[chunkSize];
            randomAccessFile.read(buffer);
            return buffer;
        } catch (IOException e) {
            throw new RuntimeException("读取文件分片数据失败", e);
        }
    }

}

文件下载服务-AsyncDownloadService

@Slf4j
@Service
public class AsyncDownloadService {

    // 本地下载目录
    private String localFilePath = "/data/test/";
    // 文件分片大小
    private long fileChunkSize=1024*1024*100L;

    @Autowired
    private ChunkService chunkService;

    // 线程池
    private static final ThreadFactory FACTORY = new ThreadFactoryBuilder()
            .setNameFormat("sftp-pool-%d").build();
    /**
     * 任务执行线程池
     */
    private static final ExecutorService POOL = new ThreadPoolExecutor(1, 10, 1L,
            TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1),
            FACTORY, new ThreadPoolExecutor.CallerRunsPolicy());

    public String createFileKey() {
        // 创建唯一标识随机字符串
        String s = RandomUtil.randomString(6);
        // 当前时间
        String dateTime = DateUtil.format(new Date(), "yyyyMMddHHmmssSSS");
        // 文件唯一标识(时间戳+随机字符串 保证唯一性)
        return dateTime + s;
    }

    /**
     * 异步从sftp下载文件
     *
     * @param fileName     文件名
     * @param remoteDir    远程文件路径
     * @param sftpHost     sftp主机
     * @param sftpUser     sftp用户名
     * @param sftpPassword sftp密码
     * @param port         sftp端口
     * @return 文件对象
     */
    public FileDetail downloadInSftp(String fileName,
                                     String remoteDir,
                                     String sftpHost,
                                     String sftpUser,
                                     String sftpPassword,
                                     int port) {
        FileDetail fileDetail = new FileDetail();
        fileDetail.setFileName(fileName);
        // 远程文件路径
        String remoteFile = remoteDir + fileName;
        // 获取文件名尾缀
        String suffix = FileUtil.getSuffix(fileName);
        // 获取文件名
        String fileNamePrefix = FileUtil.getPrefix(fileName);

        String fileKey = createFileKey();
        fileDetail.setFileKey(fileKey);
        suffix = StringUtils.isBlank(suffix) ? "" : "." + suffix;
        // 文件名重构
        String fileNameRebuild = fileNamePrefix + "_" + fileKey + suffix;
        String localFile = FilePathUtil.normalizePath(localFilePath + DateUtil.format(new Date(), "yyyyMMdd") + File.separator + fileNameRebuild);
        fileDetail.setFilePath(localFile);

        // 父目录创建
        File file = new File(localFile);
        if (!file.getParentFile().exists()) {
            FileUtil.mkdir(file.getParentFile());
        }

        try {
            JSch jsch = new JSch();
            Session session = null;
            session = jsch.getSession(sftpUser, sftpHost, port);
            session.setPassword(sftpPassword);
            session.setConfig("StrictHostKeyChecking", "no");

            fileDetail.setSession(session);
            fileDetail.connectSession();

            ChannelSftp channelSftp = (ChannelSftp) session.openChannel("sftp");
            fileDetail.setChannelSftp(channelSftp);
            fileDetail.connectChannelSftp();

            SftpATTRS attrs = channelSftp.stat(remoteFile);
            // 文件总大小
            long fileSize = attrs.getSize();
            fileDetail.setFileSize(fileSize);

            // 使用线程池下载文件
            downloadFile(fileDetail, remoteFile, localFile, fileSize);
        } catch (Exception e) {
            log.error("sftp连接失败", e);
            throw new RuntimeException("sftp连接异常", e);
        }
        // 计算分片信息
        setChunk(fileDetail);
        FileChunkCache.put(fileKey, fileDetail);
        return fileDetail;
    }


    /**
     * 线程内部下载文件
     *
     * @param fileDetail 文件对象
     * @param remoteFile 远程文件路径
     * @param localFile  本地文件路径
     * @param fileSize   文件大小
     */
    public void downloadFile(FileDetail fileDetail, String remoteFile, String localFile, long fileSize) {
        POOL.execute(() -> {
            try (// 本地文件输出流
                 FileOutputStream outputStream = new FileOutputStream(localFile)) {
                // 下载文件到输出流
                fileDetail.getChannelSftp().get(remoteFile, outputStream, new FileProgressMonitor(fileSize, localFile, fileDetail), ChannelSftp.OVERWRITE, 0);
            } catch (Exception e) {
                fileDetail.setDownloadStatus(DownloadStatus.DOWNLOAD_FAILED);
                log.error("下载文件失败", e);
            } finally {
                // 关闭连接
                fileDetail.closeConnect();
            }
        });
    }

    /**
     * 设置分片信息
     *
     * @param fileDetail 文件对象
     */
    public void setChunk(FileDetail fileDetail) {
        long fileSize = fileDetail.getFileSize();
        fileDetail.setChunkSize(fileChunkSize);
        long fragmentNum = new BigDecimal(fileSize).divide(new BigDecimal(fileDetail.getChunkSize()), 0, RoundingMode.UP).longValue();
        fileDetail.setChunkNum(fragmentNum);
    }

    /**
     * 分片下载
     *
     * @param fileKey 文件唯一标识
     * @param index   分片索引
     */
    public void fragmentDownload(String fileKey, Integer index, HttpServletResponse response) {
        FileDetail fileDetail = FileChunkCache.get(fileKey);
        if (fileDetail == null) {
            throw new RuntimeException("未存在下载文件信息");
        }
        long fileSize = fileDetail.getFileSize();
        // 分片大小
        long chunkSize = fileDetail.getChunkSize();
        // 分片偏移量
        long offset = index * chunkSize;
        long chunkNum = fileDetail.getChunkNum();
        // 最后一片分片,重新计算分片大小
        if (index == chunkNum - 1) {
            chunkSize = fileSize - offset;
        }
        log.info("下载分片数据,文件:{},文件总大小:{},当前分片索引:{},本次分片大小:{}", fileDetail, fileSize, index, chunkSize);
        byte[] chunk = chunkService.getChunkOnDownloadFile(fileKey, (int) chunkSize, fileDetail.getFilePath(), offset, response);

        // 设置响应头
        try {
            String fileName = URLEncoder.encode(fileDetail.getFileName(), "UTF-8");
            response.addHeader("Content-Disposition", "attachment;filename=" + fileName);
            response.addHeader("Content-Length", "" + (chunk.length));
            response.setContentType("application/octet-stream");

            // 写出数据
            ServletOutputStream outputStream = response.getOutputStream();
            outputStream.write(chunk);
            outputStream.flush();
            outputStream.close();
        } catch (Exception e) {
            response.setStatus(HttpResponseStatus.REQUEST_EXCEPTION.getCode());
            throw new RuntimeException("写回分片信息异常", e);
        }
    }

    public void downloadComplete(String fileKey) {
        FileDetail fileDetail = FileChunkCache.get(fileKey);
        if (fileDetail == null) {
            throw new RuntimeException("未存在下载文件信息");
        }
        // 删除缓存
        FileChunkCache.remove(fileKey);
        // 删除本地文件
        FileUtil.del(fileDetail.getFilePath());
    }
}

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

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

相关文章

Dubbo与SpringBoot整合

1.注意starter版本适配 2.服务提供者 创建Maven项目 boot-user-service-provider 服务提供者 2.1.通用模块依旧照用 2.2.POM <dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</a…

如何在华为OD机试中获得满分?Java实现【IPv4地址转换成整数】一文详解!

✅创作者:陈书予 🎉个人主页:陈书予的个人主页 🍁陈书予的个人社区,欢迎你的加入: 陈书予的社区 🌟专栏地址: Java华为OD机试真题(2022&2023) 文章目录 1. 题目描述2. 输入描述3. 输出描述4. Java算法源码5. 测试6.解题思路1. 题目描述 存在一种虚拟 IPv4<

本地创建的项目托管到git

本地创建的项目托管到git 这里的情况是本地先通过命令在电脑上指定文件夹创建好项目后&#xff0c;需要托管到git上&#xff0c;这里以gitee为例 打开gitee&#xff0c;登录滑动到右上方&#xff0b;&#xff0c;点击新建仓库&#xff0c;跳转到新建仓库页面 填写仓库信息&am…

Ubuntu18.04+RTX3060+TensorFlow2.12.0(GPU版)+Cuda11.1+CuDNN8.6.0安装

前情提要 可以跳过 我在Ubuntu18.04上安装了pytorch的相关环境&#xff0c;配置如图。 Ubuntu18.04RTX3060显卡配置pytorch、cuda、cudnn和miniconda_Toblerone_Wind的博客-CSDN博客之前已经安装成功了&#xff0c;也发了篇博客梳理了整套流程如下。ubuntu18.04安装pytorch、c…

回归预测 | MATLAB实现实现FOA-BP果蝇算法优化BP神经网络多变量输入回归预测模型

回归预测 | MATLAB实现实现FOA-BP果蝇算法优化BP神经网络多变量输入回归预测模型 目录 回归预测 | MATLAB实现实现FOA-BP果蝇算法优化BP神经网络多变量输入回归预测模型效果一览基本介绍程序设计参考资料 效果一览 基本介绍 果蝇算法(FOA)优化BP神经网络回归预测,FOA-BP回归预测…

springboot3.0集成nacos2.2.1(一)

本章节内容是没有开启nacos校验方式进行接入 集成环境&#xff1a; java版本&#xff1a;JDK17 springboot版本&#xff1a;3.0.2 创建spring项目&#xff0c;我这里用到的是spring-cloud全家桶 首先是jar包依赖&#xff1a; <properties><maven.compiler.so…

HTB-Forest(PowerView.ps1使用、嵌套组解析、了解帐户操作员组)

目录 扫描 枚举特定于域控制器的服务 AS-REP烘焙服务帐户svc-alfresco 使用Hashcat破解AS-REP哈希 作为svc-alfresco获得立足点 攻击后的枚举和权限提升 查找指向“Account Operators”组的嵌套组 使用PowerView.ps1枚举组 了解帐户操作员组 寻找有价值的ACE 在Exc…

IDE装上ChatGPT,这款编辑器真的做到可以自动写代码了!

ChatGPT狂飙160天&#xff0c;世界已经不是之前的样子。 我新建了人工智能中文站https://ai.weoknow.com 每天给大家更新可用的国内可用chatGPT资源 Cursor 是集成了 GPT-4 的 IDE 工具&#xff0c;目前免费并且无需 API Key&#xff0c;支持 Win、Mac、Linux 平台&#xff0c;…

C# | 凸包算法之Graham,快速找到一组点最外侧的凸多边形

C#实现凸包算法之Graham 文章目录 C#实现凸包算法之Graham前言示例代码实现思路测试结果结束语 前言 这篇关于凸包算法的文章&#xff0c;本文使用C#和Graham算法来实现凸包算法。 首先消除两个最基本的问题&#xff1a; 什么是凸包呢&#xff1f; 凸包是一个包围一组点的凸多…

IIC协议

1.认识IIC 1、IIC协议概述&#xff1a; IIC&#xff08;Inter-Integrated Circuit&#xff0c;集成电路总线&#xff09;是一种串行通信协议&#xff0c;也被称为I2C协议。它是由荷兰的PHILIPS公司&#xff08;现在philips公司将其半导体部门拆分出来并更名为NXP半导体公司&a…

KVM虚拟化技术学习-KVM管理

二&#xff0c;KVM管理 1.升级配置 1.创建一个空磁盘卷 [rootlocalhost ~]# qemu-img create -f qcow2 /kvm/images/disk2.qcow2 5G Formatting disk2.qcow2, fmtqcow2 size5368709120 encryptionoff cluster_size65536 lazy_refcountsoff 2.修改配置文件 <disk typefi…

SpringCloudAlibaba整合分布式事务Seata

文章目录 1 整合分布式事务Seata1.1 环境搭建1.1.1 Nacos搭建1.1.2 Seata搭建 1.2 项目搭建1.2.1 项目示意1.2.2 pom.xml1.2.2.1 alibaba-demo模块1.2.2.2 call模块1.2.2.3 order模块1.2.2.4 common模块 1.2.3 配置文件1.2.3.1 order模块1.2.3.2 call模块 1.2.4 OpenFeign调用1…

想要成为一个性能测试工程师需要掌握哪些知识?

如果想要成为一个性能测试工程师需要掌握哪些知识&#xff1f; 可以看看下方教程&#xff01; 2023年最新版Jmeter性能测试项目实战讲解&#xff0c;从入门到精通价值8888的实战教程_哔哩哔哩_bilibili2023年最新版Jmeter性能测试项目实战讲解&#xff0c;从入门到精通价值888…

idea不识别yml文件了

添加上这两个就好了

recurdyn实用操作

目录 1.剖视图查看 2.自动重复操作 3.多个面生成FaceSurface 4.查看质心&#xff0c;质量坐标工具Mass 5.履带仿真建立其他特征路面 6.joint单位 7.创建样条插值函数AKISPL 8.导出结果曲线数据 9.后处理各名称含义 1.剖视图查看 取消剖视图需要重新进入&#xff0c;取…

Redis的ZipList和QuickList和SkipList和RedisObject(未完成)

ZipList:压缩列表&#xff0c;为了节省内存而设计的一种数据结构 ZipList是一种特殊的双端链表&#xff0c;是由一系列的特殊编码的连续内存块组成&#xff0c;不需要通过指针来进行寻址来找到各个节点&#xff0c;可以在任意一端进行压入或者是弹出操作&#xff0c;并且该操作…

C# | 凸包算法之Andrew‘s,获取围绕一组点的凸多边形的轮廓点

C#实现凸包算法之Andrew’s 文章目录 C#实现凸包算法之Andrews前言示例代码实现思路测试结果结束语 前言 这篇关于凸包算法的文章&#xff0c;本文使用C#和Andrew’s算法来实现凸包算法。 首先消除两个最基本的问题&#xff1a; 什么是凸包呢&#xff1f; 凸包是一个包围一组…

上海城市开发者社区小聚有感

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是Rockey&#xff0c;不知名企业的不知名Java开发工程师 &#x1f525;如果感觉博主的文章还不错的话&#xff0c;请&#x1f44d;三连支持&#x1f44d;一下博主哦 &#x1f4dd;联系方式&#xff1a;he18339193956&…

JAVAWEB(上)

一、HTML和CSS 1.盒子 2.表单 3.机器人回答&#xff1a; 3.1 label标签 <label>标签用于关联表单元素和文本标签&#xff0c;通过为表单元素定义文本标签&#xff0c;可以使表单更易于使用和访问。它的基本语法如下&#xff1a;<label for"input_id">…

LeetCode高频算法刷题记录9

文章目录 1. 二叉树的最大深度【简单】1.1 题目描述1.2 解题思路1.3 代码实现 2. 对称二叉树【简单】2.1 题目描述2.2 解题思路2.3 代码实现 3. 二叉树的直径【简单】3.1 题目描述3.2 解题思路3.3 代码实现 4. 验证二叉搜索树【中等】4.1 题目描述4.2 解题思路4.3 代码实现 5. …