第 4 篇 : Netty客户端互发图片和音/视频

news2024/11/26 23:50:07

说明

因为图片和音/视频不能确定其具体大小, 故引入MinIO。客户端之间只发送消息, 通过上传/下载来获取额外信息

1. MinIO搭建(参考前面文章), 并启动

2. 登录MinIO创建3个Bucket: image、voice、video

创建桶
3个桶

3. 客户端改造

3.1 修改 pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.hahashou.netty</groupId>
    <artifactId>client</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>client</name>
    <description>Netty Client Project For Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.100.Final</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.58</version>
        </dependency>
        <dependency>
            <groupId>com.hahashou</groupId>
            <artifactId>minio</artifactId>
            <version>2023.11.16</version>
        </dependency>
        <!-- 此包在引入minio时必须添加-->
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>4.9.3</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

3.2 修改 application.yml, 限制单文件大小为128M以内, 总大小为256M以内

server:
  port: 32001

logging:
  level:
    com.hahashou.netty: info

spring:
  servlet:
    multipart:
      max-file-size: 128MB
      max-request-size: 256MB

userCode: Aa

minio:
  endpoint: http://127.0.0.1:9000
  accessKey: root
  secretKey: root123456

3.3 config包下新增 MinIOConfig类

package com.hahashou.netty.client.config;

import io.minio.MinioClient;
import io.minio.http.HttpUtils;
import okhttp3.OkHttpClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @description: MinIO配置
 * @author: 哼唧兽
 * @date: 9999/9/21
 **/
@Configuration
public class MinIOConfig {

    @Value("${minio.endpoint}")
    private String endpoint;

    @Value("${minio.accessKey}")
    private String accessKey;

    @Value("${minio.secretKey}")
    private String secretKey;

    @Bean
    public MinioClient minioClient() {
        OkHttpClient okHttpClient = HttpUtils.newDefaultHttpClient(60 * 1000,
                50 * 1000, 30 * 1000);
        MinioClient minioClient = MinioClient.builder()
                .endpoint(endpoint)
                .credentials(accessKey, secretKey)
                .httpClient(okHttpClient)
                .build();
        return minioClient;
    }
}

3.4 新增utils包, 放入 MinioUtils类

package com.hahashou.netty.client.utils;

import io.minio.*;
import io.minio.errors.MinioException;
import io.minio.http.Method;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.TimeUnit;

/**
 * @description:
 * @author: 哼唧兽
 * @date: 9999/9/21
 **/
@Component
@Slf4j
public class MinioUtils {

    @Resource
    private MinioClient minioClient;

    /**
     * 文件上传
     * @param bucketName
     * @param fileName
     * @param multipartFile
     * @return
     */
    public void upload(String bucketName, String multiLevelFolders, String fileName, MultipartFile multipartFile) {
        try {
            boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
            if (!found) {
                minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
            }
            String suffix = fileName.substring(fileName.lastIndexOf("."));
            File file = File.createTempFile(fileName, suffix);
            multipartFile.transferTo(file);
            UploadObjectArgs objectArgs = UploadObjectArgs.builder()
                    .bucket(bucketName)
                    .object(StringUtils.hasText(multiLevelFolders) ? multiLevelFolders + fileName : fileName)
                    .filename(file.getAbsolutePath())
                    .contentType(multipartFile.getContentType())
                    .build();
            minioClient.uploadObject(objectArgs);
        } catch (MinioException | InvalidKeyException | IOException | NoSuchAlgorithmException exception) {
            log.error("MinIO文件上传异常 : {}", exception.getMessage());
        }
    }

    /**
     * 预览地址
     * @param fileName
     * @param bucketName
     * @return
     */
    public String preview(String fileName, String bucketName){
        // 查看文件地址
        GetPresignedObjectUrlArgs build = GetPresignedObjectUrlArgs
                .builder()
                .bucket(bucketName)
                .expiry(30, TimeUnit.MINUTES)
                .object(fileName)
                .method(Method.GET)
                .build();
        try {
            return minioClient.getPresignedObjectUrl(build);
        } catch (Exception exception) {
            log.error("获取MinIO预览地址异常 : {}", exception.getMessage());
        }
        return null;
    }
}

3.5 修改 Message类

package com.hahashou.netty.client.config;

import com.alibaba.fastjson.JSON;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.util.CharsetUtil;
import lombok.Data;
import lombok.Getter;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * @description:
 * @author: 哼唧兽
 * @date: 9999/9/21
 **/
@Data
public class Message {

    /** 发送者用户code */
    private String userCode;

    /** 接收者用户code */
    private String friendUserCode;

    /** 连接时专用 */
    private String channelId;

    /** 消息类型 */
    private Integer type;

    public enum TypeEnum {

        TEXT(0, "文字", "", new ArrayList<>()),
        IMAGE(1, "图片", "image", Arrays.asList("bmp", "gif", "jpeg", "jpg", "png")),
        VOICE(2, "语音", "voice", Arrays.asList("mp3", "amr", "flac", "wma", "aac")),
        VIDEO(3, "视频", "video", Arrays.asList("mp4", "avi", "rmvb", "flv", "3gp", "ts", "mkv")),

        ;

        @Getter
        private Integer key;

        @Getter
        private String describe;

        @Getter
        private String bucketName;

        @Getter
        private List<String> formatList;

        TypeEnum(int key, String describe, String bucketName, List<String> formatList) {
            this.key = key;
            this.describe = describe;
            this.bucketName = bucketName;
            this.formatList = formatList;
        }

        public static TypeEnum select(String format) {
            TypeEnum result = null;
            for (TypeEnum typeEnum : TypeEnum.values()) {
                if (typeEnum.getFormatList().contains(format)) {
                    result = typeEnum;
                    break;
                }
            }
            return result;
        }
    }

    /** 文字或文件的全路径名称 */
    private String text;

    public static ByteBuf transfer(Message message) {
        return Unpooled.copiedBuffer(JSON.toJSONString(message), CharsetUtil.UTF_8);
    }

    /**
     * 生成指定长度的随机字符串
     * @param length
     * @return
     */
    public static String randomString (int length) {
        if (length > 64) {
            length = 64;
        }
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            list.add(i + "");
        }
        for (char i = 'A'; i <= 'Z'; i++) {
            list.add(String.valueOf(i));
        }
        for (char i = 'a'; i <= 'z'; i++) {
            list.add(String.valueOf(i));
        }
        list.add("α");
        list.add("ω");
        Collections.shuffle(list);
        String string = list.toString();
        return string.replace("[", "")
                .replace("]", "")
                .replace(", ", "")
                .substring(0, length);
    }
}

3.6 新增service包, 放入 ClientService接口

package com.hahashou.netty.client.service;

import com.hahashou.netty.client.config.Message;

import javax.servlet.http.HttpServletRequest;

/**
 * @description:
 * @author: 哼唧兽
 * @date: 9999/9/21
 **/
public interface ClientService {

    /**
     * 上传
     * @param userCode
     * @param httpServletRequest
     * @return
     */
    Message upload(String userCode, HttpServletRequest httpServletRequest);

    /**
     * 下载链接
     * @param fileName
     * @return
     */
    String link(String fileName);
}

3.7 service包下新建impl包, 放入 ClientServiceImpl类

package com.hahashou.netty.client.service.imol;

import com.hahashou.netty.client.config.Message;
import com.hahashou.netty.client.service.ClientService;
import com.hahashou.netty.client.utils.MinioUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.*;

/**
 * @description:
 * @author: 哼唧兽
 * @date: 9999/9/21
 **/
@Service
@Slf4j
public class ClientServiceImpl implements ClientService {

    @Resource
    private MinioUtils minioUtils;

    @Override
    public Message upload(String userCode, HttpServletRequest httpServletRequest) {
        Message result = new Message();
        MultipartHttpServletRequest multipartHttpServletRequest = (MultipartHttpServletRequest) httpServletRequest;
        List<MultipartFile> multipartFileList = multipartHttpServletRequest.getFiles("file");
        if (!CollectionUtils.isEmpty(multipartFileList)) {
            MultipartFile multipartFile = multipartFileList.get(0);
            String originalFilename = multipartFile.getOriginalFilename();
            String suffix = originalFilename.substring(originalFilename.lastIndexOf(".") + 1);
            Message.TypeEnum typeEnum = Message.TypeEnum.select(suffix);
            if (typeEnum != null) {
                String fileName = generateFileName(suffix);
                String multiLevelFolders = userCode + "/"
                        + LocalDate.now().toString().replace("-", "") + "/";
                minioUtils.upload(typeEnum.getBucketName(), multiLevelFolders, fileName, multipartFile);
                result.setType(typeEnum.getKey());
                result.setText(multiLevelFolders + fileName);
            }
        }
        return result;
    }

    public static String generateFileName(String suffix) {
        LocalTime now = LocalTime.now();
        return now.toString().replace(":", "")
                .replace(".", "-") + Message.randomString(4) + "." + suffix;
    }

    @Override
    public String link(String fileName) {
        String suffix = fileName.substring(fileName.lastIndexOf(".") + 1);
        Message.TypeEnum typeEnum = Message.TypeEnum.select(suffix);
        if (typeEnum != null) {
            return minioUtils.preview(fileName, typeEnum.getBucketName());
        }
        return null;
    }
}

3.8 修改 ClientController类

package com.hahashou.netty.client.controller;

import com.alibaba.fastjson.JSON;
import com.hahashou.netty.client.config.Message;
import com.hahashou.netty.client.config.NettyClientHandler;
import com.hahashou.netty.client.service.ClientService;
import io.netty.channel.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;

/**
 * @description:
 * @author: 哼唧兽
 * @date: 9999/9/21
 **/
@RestController
@RequestMapping("/client")
@Slf4j
public class ClientController {

    @Resource
    private ClientService clientService;

    @PostMapping("/send")
    public String send(@RequestBody Message dto) {
        Channel channel = NettyClientHandler.CHANNEL;
        if (channel == null) {
            return "服务端已下线";
        }
        channel.writeAndFlush(Message.transfer(dto));
        return "success";
    }

    @GetMapping("/upload/{userCode}")
    public String upload(@PathVariable String userCode, final HttpServletRequest httpServletRequest) {
        if (StringUtils.isEmpty(userCode)) {
            return "userCode is null";
        }
        Message upload = clientService.upload(userCode, httpServletRequest);
        return JSON.toJSONString(upload);
    }

    @GetMapping("/link")
    public String link(@RequestParam String fileName) {
        // 如果Bucket包含多级目录, fileName为Bucket下文件的全路径名
        if (StringUtils.isEmpty(fileName)) {
            return "fileName is null";
        }
        return clientService.link(fileName);
    }
}

4. 服务端改造

4.1 复制客户端的 Message类

4.2 修改 NettyServerHandler类

package com.hahashou.netty.server.config;

import com.alibaba.fastjson.JSON;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @description:
 * @author: 哼唧兽
 * @date: 9999/9/21
 **/
@Component
@ChannelHandler.Sharable
@Slf4j
public class NettyServerHandler extends ChannelInboundHandlerAdapter {

    /** key: 用户code; value: channelId */
    public static Map<String, String> USER_CHANNEL = new ConcurrentHashMap<>(32);

    /** key: channelId; value: Channel */
    public static Map<String, Channel> CHANNEL = new ConcurrentHashMap<>(32);

    /** 用户离线消息 */
    public static Map<String, List<Message>> USER_MESSAGE = new ConcurrentHashMap<>(32);

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        Channel channel = ctx.channel();
        String channelId = channel.id().asLongText();
        log.info("有客户端连接, channelId : {}", channelId);
        CHANNEL.put(channelId, channel);
        Message message = new Message();
        message.setChannelId(channelId);
        channel.writeAndFlush(Message.transfer(message));
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) {
        log.info("有客户端断开连接, channelId : {}", ctx.channel().id().asLongText());
        CHANNEL.remove(ctx.channel().id().asLongText());
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        if (msg != null) {
            Message message = JSON.parseObject(msg.toString(), Message.class);
            String userCode = message.getUserCode(),
                    channelId = message.getChannelId();
            if (StringUtils.hasText(userCode) && StringUtils.hasText(channelId)) {
                connect(userCode, channelId);
            } else if (StringUtils.hasText(message.getText())) {
                if (StringUtils.hasText(message.getFriendUserCode())) {
                    sendOtherClient(message);
                } else {
                    sendAdmin(ctx.channel(), message);
                }
            }
        }
    }

    /**
     * 建立连接
     * @param userCode
     * @param channelId
     */
    private void connect(String userCode, String channelId) {
        log.info("客户端 {} 连接", userCode);
        USER_CHANNEL.put(userCode, channelId);
    }

    /**
     * 发送给其他客户端
     * @param message
     */
    private void sendOtherClient(Message message) {
        String friendUserCode = message.getFriendUserCode();
        String queryChannelId = USER_CHANNEL.get(friendUserCode);
        if (StringUtils.hasText(queryChannelId)) {
            Channel channel = CHANNEL.get(queryChannelId);
            if (channel == null) {
                offlineMessage(friendUserCode, message);
                return;
            }
            channel.writeAndFlush(Message.transfer(message));
        } else {
            offlineMessage(friendUserCode, message);
        }
    }

    /**
     * 离线消息存储
     * @param friendUserCode
     * @param message
     */
    public void offlineMessage(String friendUserCode, Message message) {
        List<Message> messageList = USER_MESSAGE.get(friendUserCode);
        if (CollectionUtils.isEmpty(messageList)) {
            messageList = new ArrayList<>();
        }
        messageList.add(message);
        USER_MESSAGE.put(friendUserCode, messageList);
    }

    /**
     * 发送给服务端
     * @param channel
     * @param message
     */
    private void sendAdmin(Channel channel, Message message) {
        message.setUserCode("ADMIN");
        message.setText(LocalDateTime.now().toString());
        channel.writeAndFlush(Message.transfer(message));
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        log.info("有客户端发生异常, channelId : {}", ctx.channel().id().asLongText());
    }
}

5. 测试

5.1 上传

上传图片
minio文件界面

5.2 获取下载链接, 之后直接在浏览器中打开链接

获取下载链接

5.3 Aa向Bb发送消息, Bb收到后可以通过下载链接获取

Aa向Bb发送图片消息
Bb收到图片消息

5.4 测试音频

音频上传
音频位置

5.5 测试视频

视频上传
视频位置

5.6 MioIO界面Buckets

minio界面buckets

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

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

相关文章

浅谈OpenCV 粗略计算工件轮廓面积和外接圆直径(Emgu.CV)

前言 最近领导在做库房工具管理这块的功能&#xff0c;希望能集成OpenCV 粗略的计算出工具的长度&#xff0c;以方便用户再归还工具的时候&#xff0c;提示用户该放在那种尺寸的盒子里面&#xff0c;这便是这篇文章的由来。 我们的系统是基于.net开发的&#xff0c;所以采用的是…

Memory augment is All You Need for image restoration 论文翻译

目录 一.介绍 二.实际工作 A.图像阴影去除 B.图像去雨 C.存储模块的开发 三.网络结构 A.内存扩充 B.损失函数设计 四.实验 A.与最先进方法的比较 B.MemoryNet消融研究 五.结论 CVPR2023 MemoryNet 记忆增强是图像恢复所需要的一切 论文地址https://arxiv.org/abs/…

就业班 第三阶段(nginx) 2401--4.26 day5 nginx5 nginx https部署实战

三、HTTPS 基本原理 1、https 介绍 HTTPS&#xff08;全称&#xff1a;HyperText Transfer Protocol over Secure Socket Layer&#xff09;&#xff0c;其实 HTTPS 并不是一个新鲜协议&#xff0c;Google 很早就开始启用了&#xff0c;初衷是为了保证数据安全。 近些年&…

大型零售企业,适合什么样的企业邮箱大文件解决方案?

大型零售企业通常指的是在全球或特定地区内具有显著市场影响力和知名度的零售商。这些企业不仅在零售业务收入上达到了惊人的规模&#xff0c;而且在全球范围内拥有广泛的销售网络和实体店铺。它们在快速变化的零售行业中持续创新&#xff0c;通过实体店、电商平台等多种渠道吸…

「C++ 内存管理篇 1」C++动态内存分配

目录 〇、C语言的动态内存分配方式 一、C的动态内存分配方式 1. 什么是C的动态内存分配&#xff1f; 2. 为什么需要C的动态内存分配&#xff1f; a. new的优势 b. new的不足 c. delete的优势 d. 总结 3. 怎么使用new和delete? a. 对于内置类型 b. 对于自定义类型 c. 为什么ne…

python学习笔记----循环语句(四)

一、while循环 为什么学习循环语句 循环在程序中同判断一样&#xff0c;也是广泛存在的&#xff0c;是非常多功能实现的基础&#xff1a; 1.1 while循环语法 while 条件表达式:# 循环体# 执行代码这里&#xff0c;“条件表达式”是每次循环开始前都会评估的表达式。如果条件…

【血泪教训】Altium Designer隐藏覆铜层导致PCB电路板未加工隐藏层

Altium Designer隐藏覆铜层导致PCB电路板未加工隐藏层 血泪教训&#xff01;&#xff01;&#xff01; 事情经过是这样的 测试板PCB Layout完成后&#xff0c;隐藏铺铜层&#xff0c;方便check&#xff0c;隐藏操作如下图所示&#xff0c;选择“隐藏所有”或“隐藏选中铺铜”…

Redis基本數據結構 ― String

Redis基本數據結構 ― String 介紹常用命令範例1. 為字串鍵設值/取得字串鍵的值2. 查看字串鍵的過期時間3. 如何為key設置時間?4. 如何刪除指定key?5. 如何增加value的值?6. 獲取value值的長度 介紹 字串鍵是Redis中最基本的鍵值對類型&#xff0c;這種類型的鍵值對會在數據…

【python技术】使用akshare抓取东方财富所有概念板块,并把指定板块概念的成分股保存excel 简单示例

最近有个想法&#xff0c;分析A股某个概念成分股情况进行分析&#xff0c;第一反应是把对应概念板块的成分股爬取下来。说干就干 下面是简单示例 import akshare as ak import pandas as pddef fetch_and_save_concept_stocks(name):# 获取指定股票概念的成分股&#xff0c;并…

机器学习每周挑战——百思买数据

最近由于比赛&#xff0c;断更了好久&#xff0c;从五一开始不会再断更了。这个每周挑战我分析的较为简单&#xff0c;有兴趣的可以将数据集下载下来试着分析一下&#xff0c;又不会的我们可以讨论一下。 这是数据集&#xff1a; import pandas as pd import numpy as np impo…

Leetcode-面试题 02.02. 返回倒数第 k 个节点

目录 题目 图解 代码 面试题 02.02. 返回倒数第 k 个节点 - 力扣&#xff08;LeetCode&#xff09;https://leetcode.cn/problems/kth-node-from-end-of-list-lcci/description/ 题目 实现一种算法&#xff0c;找出单向链表中倒数第 k 个节点。返回该节点的值。 注意&…

保证接口幂等性的多种实现方式(数据库方案)

1. 幂等性的概念 接口幂等性是指在软件工程和Web服务领域中&#xff0c;一个接口&#xff08;通常是HTTP API&#xff09;无论被调用一次还是多次&#xff0c;其对系统产生的副作用应该是相同的&#xff0c;即结果保持一致&#xff0c;不会因为多次请求而有所不同。换句话说&am…

ES练习项目-酒店搜索

目录 1 需求分析2 酒店搜索和分页2.1 请求和响应分析2.2 定义实体类&#xff0c;接收请求参数的JSON对象2.3 编写controller&#xff0c;接收页面的请求2.4 编写业务实现&#xff0c;利用RestHighLevelClient实现搜索、分页 3. 酒店结果过滤3.1 请求和响应分析3.2 修改请求参数…

上位机图像处理和嵌入式模块部署(树莓派4b设置ftp下载)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 作为一个开发板&#xff0c;最好支持ftp下载&#xff0c;这样文件的上传和下载都会比较方便。虽然目前为止&#xff0c;利用mobaxterm和ssh也能实现…

算法学习(5)-图的遍历

目录 什么是深度和广度优先 图的深度优先遍历-城市地图 图的广度优先遍历-最少转机 什么是深度和广度优先 使用深度优先搜索来遍历这个图的过程具体是&#xff1a; 首先从一个未走到过的顶点作为起始顶点&#xff0c; 比如以1号顶点作为起点。沿1号顶点的边去尝试访问其它未…

用Jenkins实现cherry-pick多个未入库的gerrit编译Android固件

背景: 在做Android固件开发的时候,通常我们可以利用gerrit-trigger插件,开发者提交一笔的时候自动触发jenkins编译,如果提交的这一笔的编译依赖其他gerrit才能编译过,我们可以在commit message中加入特殊字段,让jenkins在编译此笔patch的时候同时抓取依赖的gerrit代码下…

selenium在Pycharm中结合python的基本使用、交互、无界面访问

下载 下载与浏览器匹配的浏览器驱动文件&#xff0c;这里一定注意的是&#xff0c;要选择和浏览器版本号相同的驱动程序&#xff0c;否则后面会有很多问题。 &#xff08;1&#xff09;浏览器&#xff08;以google为例&#xff09;版本号的查询&#xff1a; 我这里的版本号是1…

RT-Thread V5.2.0版本尝鲜

文章目录 配置界面的更新新旧内核生成的二进制文件大小差异旧版本V5.1.0内核旧版本V5.2.0内核 配置界面的更新 尝试将手头RT-Thread工程的OS部分源码进行了更新&#xff0c;发现不少新的变化 配置界面变得更醒目&#xff1a; 配置变更保存提醒界面更新&#xff1a; 新旧内核…

Android Widget开发代码示例详细说明

因为AppWidgetProvider扩展自BroadcastReceiver, 所以你不能保证回调函数完成调用后&#xff0c;AppWidgetProvider还在继续运行。 a. AppWidgetProvider 的实现 /*** Copyright(C):教育电子有限公司 * Project Name: NineSync* Filename: SynWidgetProvider.java * Author(S…

【Node.js工程师养成计划】之原生node开发web服务器

一、使用node创建http服务器 var http require(http);// 获取到服务器实例对象 var server http.createServer() server.listen(8080, function() {console.log(http://127.0.0.1:8080); })server.on(request, function(req, res){console.log(request);res.write(6666666688…