windows服务器下java程序健康检测及假死崩溃后自动重启应用、开机自动启动

前两天由于项目需要,一个windows上的批处理任务(kitchen.bat),需要接到mq的消息通知后执行,为了快速实现这里我们通过springboot写了一个jar程序,用于接收mq的消息,并调用bat文件。

本程序需要实现的功能

  • 调用windows的批处理脚本bat,并支持传参
  • 可根据配置设置并发,同时消费多个mq消息调用多个批处理脚本
  • 确保java程序能一直正常运行(如果有假死或者宕机了可以自动重启)
  • 批处理脚本执行失败了,则再将信息重新放回到mq的队列尾部,等待下次执行

需要用的技术

  • Java的java.lang.Runtime类 用于调用windows服务器命令
  • 通过环境变量配置程序运行的参数,如mq信息、和执行的批处理脚本命令路径、并发等
  • 通过rabbitmq的手工ack来确定消息是否处理成功,及并发实现
  • 通过actuator来判断java程序是否健康
  • 通过windows定时任务来定时检查java程序是否正常提供服务,如果不正常则触发重启jar应用
  • 通过maven+ant打包程序,将可执行程序jar及相关脚本打包成一个zip文件,方便发给使用方使用

主要实现逻辑

开发环境:jdk1.8 + maven3.x + rabbitmq
运行环境:windows + jre1.8

Java调用bat批处理文件

package cn.iccboy.kitchen.common;

import lombok.extern.slf4j.Slf4j;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

/**
 * @author iccboy
 */@Slf4j
public class CmdUtil {

    /**
     * 处理执行进程的流
     *
     * @param inputStream
     * 			  InputStream 执行进程的流
     * @param tag
     * 			  int 标志:1--InputStream;2--ErrorStream
     */
    private static void processStreamHandler(final InputStream inputStream, int tag) {
        // 处理流的线程
        new Thread(() -> {
            String line;
            try (InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
                 BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) {
                while ((line = bufferedReader.readLine()) != null) {
                    if(tag == 1) {
                        log.info(line);
                    } else {
                        log.error(line);
                    }
                }
            } catch (Exception e) {
                log.error("【异常】命令执行异常:{}", e.getMessage());
            }
        }).start();
    }

    public static int exec(String command, String... args) throws IOException {
        String cmd = StrUtil.splicingWithSpace(command, args);
        log.info("执行命令:{}", cmd);
        int ret = 99;
        Process process = Runtime.getRuntime().exec(cmd);

        processStreamHandler(process.getInputStream(), 1);
        processStreamHandler(process.getErrorStream(), 2);

        try {
            ret = process.waitFor();
        } catch (InterruptedException e) {
            log.error("【异常】process.waitFor:{}" , e.getMessage());
        }
        log.info("执行命令:{}, 返回状态码={}", cmd, ret);
        return ret;
    }
}

上面的程序中,一定要注意的是process.getErrorStream()process.getInputStream() 一定要将命令行执行输出的信息(输出流)和错误信息(错误流)都从缓冲区读取出来,不然会导致程序执行阻塞。

process的阻塞: 在runtime执行大点的命令中,输入流和错误流会不断有流进入存储在JVM的缓冲区中,如果缓冲区的流不被读取被填满时,就会造成runtime的阻塞。所以在进行比如:大文件复制等的操作时,需要不断的去读取JVM中的缓冲区的流,防止Runtime的死锁阻塞。

程序健康检查

这里通过actuator来实现,首先程序集成actuator,由于是springboot项目,所以很方便。然后通过一个简单的java程序(CheckActuator)来访问actuator的http地址,通过返回值来判断jar程序是否运行正常,然后通过windows的脚本(checkHealth.bat)来调用CheckActuator,根据返回值在进行java程序的重启等操作。

1. pom.xml增加actuator及prometheus的配置

	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-actuator</artifactId>
	</dependency>
	<dependency>
		<groupId>io.micrometer</groupId>
		<artifactId>micrometer-registry-prometheus</artifactId>
	</dependency>

上的版本会根据springboot对应版本自动集成
2. 配置actuator
在application.yml中增加如下配置

management:
  health:
    rabbit:
      enabled: true
  endpoints:
    web:
      exposure:
        include: ["prometheus","health"]
  endpoint:
    health:
      show-details: always
  metrics:
    export:
      prometheus:
        enabled: true
      jmx:
        enabled: true

3. 编写CheckActuator.java程序
当然也可以通过windows的批处理命令直接访问actuator的地址,来判断服务是否正常。

/**
 * 注意:该类不能删除!!!! 不能改名!!!!不能移动位置!!!!
 */
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;


/**
 * ===================================================
 * 注意:该类不能删除!!!! 不能改名!!!!不能移动位置!!!!
 *
 * 该类用于检查程序是否健康(通过actuator进行判断是否健康)
 *
 * 主要供脚本checkHealth.bat进行调用
 * ===================================================
 */
public class CheckActuator {
	private static final String HEALTH_FLAG = "\"status\":\"UP\"";

    public static void main(String[] args) {
		String url = "http://127.0.0.1:8000/actuator/health";
		if(args != null && args.length != 0) {
			url = args[0];
		}
		testUrlWithTimeOut(url);
    }

	public static void testUrlWithTimeOut(String urlString){
		int timeOutMillSeconds = 2000;
		URL url;
		try {
			url = new URL(urlString);
			URLConnection conn =  url.openConnection();
			conn.setConnectTimeout(timeOutMillSeconds);
			conn.connect();
			InputStream in = conn.getInputStream();
			BufferedReader reader = new BufferedReader(  new InputStreamReader(in));
			String line;
			StringBuilder sb = new StringBuilder();
			while ((line = reader.readLine()) != null) {
				sb.append(line);
			}
			boolean healthFlag = sb.toString().contains(HEALTH_FLAG);
			if(healthFlag) {
				System.exit(0);
			} else {
				System.out.println("健康检查异常:" + sb);
				System.exit(1);
			}
		} catch (Exception e) {
			System.out.println("网络连接异常: e=" + e.getMessage());
			System.exit(1);
		}
	}
}

我将上面的CheckActuator.java文件放到maven项目的test/java/跟目录下,后面会通过ant命令将.class移动到指定位置

  1. 健康检测脚本checkHealth.bat

上面的springboot项目会通过http服务,其运行的端口是8000,下面脚本会通过8000端口来获取对应的进程pid

::存活监控!
@echo off
set strPath=%~dp0
echo %strPath%
mkdir %strPath%log
set "yMd=%date:~0,4%-%date:~5,2%-%date:~8,2% %time:~0,8%"
set strFile=%strPath%log/checkHealth-%date:~0,4%%date:~5,2%%date:~8,2%.log
java -classpath %strPath% CheckActuator
if ERRORLEVEL 1 (goto err) else (goto ok)
 
:err
echo %yMd% 程序连接失败,进行重启! >> %strFile%
set port=8000
for /f "tokens=1-5" %%i in ('netstat -ano^|findstr ":%port%"') do (
    echo kill the process %%m who use the port 
    taskkill /pid %%m -t -f   
)
goto start
exit 
 
:ok
echo %yMd% 程序运行正常 >> %strFile%
exit 
:start
chcp 65001
setlocal enabledelayedexpansion
set filename=""
for /f %%a in ('dir strPath *.jar /o-d /tc /b ') do (
    set filename=%%~na%%~xa
    echo 文件名: !filename!, 最新创建时间: %%~ta >> %strFile%
    if not !filename! == ""  (
        goto startjar
    )
)
:startjar
rem 注释:查找最新文件结束,最新文件名为:%filename%
java -jar %strPath%%filename%

windows定时任务配置

  1. 新增-健康检查定时任务.bat
@echo off
set strPath=%~dp0
set checkBat=%strPath%checkHealth.bat
schtasks /create /tn xxx-health-check /tr %checkBat% /sc minute /mo 2
pause

上面的xxx-health-check是定时任务的名字; /sc minute /mo 2 表示每2分钟执行一次命令。上面是通过命令配置的定时任务,也可以通过windows的图形管理界面【计划任务】配置。

  1. 移除-健康检查定时任务.bat
@echo off
pause
schtasks /delete /tn xxx-health-check /f
pause
  1. 查看-健康检查定时任务.bat
@echo off
schtasks /query /V /FO LIST   /tn xxx-health-check
pause

通过windows环境变量设置java程序的配置

application.yml 部分配置如下:

server:
  port: ${K_PORT:8000}
  servlet:
    context-path: /
spring:
  application:
    name: xxx
  rabbitmq:
    host: ${K_MQ_HOST:172.18.1.100}
    password: ${K_MQ_PASSWORD:123456}
    port: ${K_MQ_PORT:5672}
    username: ${K_MQ_USERNAME:mq}
    connection-timeout: 15000
    listener:
      simple:
        acknowledge-mode: manual #开启手动ACK
        concurrency: ${K_WORKS:1} # 并发
        max-concurrency: ${K_WORKS:1} # 最大并发
        prefetch: 1 # 每个消费每次预去取几个消息
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    time-zone: GMT+8

shell:
  paths: ${K_BAT_PATHS:C:\invoke.bat}

可通过设置系统的环境变量来改变配置,可设置的变量包含:

变量说明默认值
K_PORT程序运行的http服务端口8000
K_MQ_HOSTrabbitmq 服务ip172.18.1.100
K_MQ_PORTrabbitmq 服务端口5672
K_MQ_USERNAMErabbitmq 用户名mq
K_MQ_PASSWORDrabbitmq 密码123456
K_BAT_PATHSbat脚本路径,可以配置多个,通过英文逗号分隔,配置多个就会启动多个消费者,如:C:\invoke_1.bat,C:\invoke_2.batC:\invoke.bat
K_WORKS每个消费者的并发数。如:K_BAT_PATHS配置了3个命令,K_WORKS 配置了 2 ,这表示有3*2=6个消费者1

消费mq消息并执行bat文件

package cn.iccboy.kitchen.mq;

import cn.iccboy.kitchen.common.CmdUtil;
import cn.iccboy.kitchen.common.ThreadUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.MessageProperties;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.annotation.Headers;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Map;

import static cn.iccboy.kitchen.mq.TopicRabbitMqConfig.EXCHANGE_DATA;
import static cn.iccboy.kitchen.mq.TopicRabbitMqConfig.KEY_INDEX_PROCESS;

/**
 * @author iccboy
 * @date 2023-08-05 15:35
 */
@Slf4j
public class CmdMqReceive {

    @Setter
    private String batPath;

    @Setter
    private Integer seq;

    @RabbitListener(queues = TopicRabbitMqConfig.QUEUE_INDEX_PROCESS)
    public void receive(Message<String> message, @Headers Map<String,Object> headers, Channel channel) throws IOException {
        long deliveryTag = (long) headers.get(AmqpHeaders.DELIVERY_TAG);
        try {
            log.info("[start]第{}执行器,消息内容:{}", seq, message.getPayload());
            int status = CmdUtil.exec(batPath, message.getPayload());
            if(status != 0) {
                log.info("[err_1]第{}执行器,消息内容:{}加工脚本执行异常,状态码={}",seq, message.getPayload(), status);
                throw new RuntimeException("脚本执行异常");
            }
            log.info("[end]第{}执行器执行完成:{}", seq, message.getPayload());
        } catch (Exception e) {
            ThreadUtil.sleep(1000);
            log.error("[err]第{}执行器,执行异常重新进入队列:{}", seq, message.getPayload(), e);
            //channel.basicNack(deliveryTag, false, true);
            // 将处理错误的消息放到重新队列尾部
            channel.basicPublish(EXCHANGE_DATA,
                    KEY_INDEX_PROCESS, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getPayload().getBytes(StandardCharsets.UTF_8));

        } finally {
            // 确认已处理
            channel.basicAck(deliveryTag,false);
        }
    }

}

通过批处理命令配置个数,动态生成对应个数消费者

package cn.iccboy.kitchen.mq;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotationMetadata;

import java.util.List;

@Slf4j
@Configuration
@Import(DynamicBuildMqReceiveBean.ImportConfig.class)
public class DynamicBuildMqReceiveBean {
    public static class ImportConfig implements ImportBeanDefinitionRegistrar, EnvironmentAware {
        private List<String> batPaths;

        @Override
        public void setEnvironment(Environment environment) {
            try {
                batPaths = environment.getProperty("shell.paths", List.class);
            } catch (Exception ex) {
                log.error("参数绑定", ex);
            }
        }

        @Override
        public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
            int seq = 0;
            for (String batPath : batPaths) {
                seq++;
                // 注册bean
                RootBeanDefinition beanDefinition = new RootBeanDefinition();
                beanDefinition.setBeanClass(CmdMqReceive.class);
                MutablePropertyValues values = new MutablePropertyValues();
                values.addPropertyValue("batPath", batPath);
                values.addPropertyValue("seq", seq);
                beanDefinition.setPropertyValues(values);
                registry.registerBeanDefinition(CmdMqReceive.class.getName() + "#" + seq, beanDefinition);
            }
        }
    }
}

上面通过ImportBeanDefinitionRegistrar的方式 实现了动态bean的生成

通过maven的ant插件实现打包

在项目的 pom.xml文件中增加如下配置

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-antrun-plugin</artifactId>
				<version>1.8</version>
				<executions>
					<execution>
						<id>clean</id>
						<phase>clean</phase>
						<configuration>
							<target>
								<delete file="${basedir}/shell/CheckActuator.class"/>
							</target>
						</configuration>
						<goals>
							<goal>run</goal>
						</goals>
					</execution>
					<execution>
						<id>test-compile</id>
						<phase>test-compile</phase>
						<configuration>
							<target>
								<copy overwrite="true" file="${project.build.directory}/test-classes/CheckActuator.class"
									  todir="${basedir}/shell" />
							</target>
						</configuration>
						<goals>
							<goal>run</goal>
						</goals>
					</execution>
					<execution>
						<id>package</id>
						<phase>package</phase>
						<configuration>
							<target>
								<delete dir="${project.build.directory}/kitchen-mq-bin"/>
								<mkdir dir="${project.build.directory}/kitchen-mq-bin"/>
								<copy todir="${project.build.directory}/kitchen-mq-bin" overwrite="true">
									<fileset dir="${basedir}/shell" erroronmissingdir="false">
										<include name="*"/>
									</fileset>
								</copy>
								<copy overwrite="true" file="${project.build.directory}/${project.name}-${project.version}.jar" todir="${project.build.directory}/kitchen-mq-bin" />
								<zip destfile="${basedir}/kitchen-mq-bin.zip">
									<fileset dir="${project.build.directory}/kitchen-mq-bin">
										<include name="*"/>
									</fileset>
								</zip>
							</target>
						</configuration>
						<goals>
							<goal>run</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>

项目结构如下图:
在这里插入图片描述
获取执行包

  1. 执行打包命令mvn clean package
  2. 上面命令执行完成后,在项目的跟目录会产生一个压缩包kitchen-mq-bin.zip,将压缩包直接拷贝到目标服务器,解压即可。
  3. 解压后,直接执行新增-健康检查定时任务.bat即可。2分钟后就会启动程序。

下图是执行命令后,多出的 zip文件包,以及包里面的文件
在这里插入图片描述在这里插入图片描述

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

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

相关文章

MFC140.dll缺失的修复方法,安装MFC140.dll文件

大家好&#xff0c;今天我要和大家分享的是如何正确安装和使用MFC140.dll。MFC140.dll是一种常见的动态链接库文件&#xff0c;它是Microsoft Foundation Classes(MFC)的一部分&#xff0c;被广泛应用于Windows操作系统中的各种应用程序中。在本文中&#xff0c;我们将详细介绍…

Debian10: 安装nut服务器(UPS)

UPS说明&#xff1a; UPS的作用就不必讲了&#xff0c;我选择是SANTAKTGBOX-850&#xff0c;规格为 850VA/510W&#xff0c;可以满足所需&#xff0c;关键是Debian10自带了驱动可以支持&#xff0c;免去安装驱动&#xff0c;将UPS通过USB线连接服务器即可&#xff0c;如下图所示…

大模型基础02:GPT家族与提示学习

大模型基础&#xff1a;GPT 家族与提示学习 从 GPT-1 到 GPT-3.5 GPT(Generative Pre-trained Transformer)是 Google 于2018年提出的一种基于 Transformer 的预训练语言模型。它标志着自然语言处理领域从 RNN 时代进入 Transformer 时代。GPT 的发展历史和技术特点如下: GP…

Java 日期格式(yyyy-MM-dd HH:mm:ss SSS)

常用格式为&#xff1a;yyyy-MM-dd HH:mm:ss 以 2019-12-31 06:07:59:666 时间为例&#xff1a; 字符 含义 Example y&#xff08;小写的y&#xff09; 年 yyyy---->2019 M&#xff08;大写的M&#xff09; 月 MM---->12 d&#xff08;小写的d&#xff09; 一…

论文笔记 Graph Attention Networks

2018 ICLR 1 intro 1.1. GCN的不足 无法完成inductive任务 inductive任务是指&#xff1a; 训练阶段与测试阶段需要处理的graph不同。通常是训练阶段只是在子图上进行&#xff0c;测试阶段需要处理未知的顶点。GGN 的参数依赖于邻接矩阵A/拉普拉斯矩阵L&#xff0c;所以换了…

DTC 19服务学习1

在UDS&#xff08;统一诊断服务&#xff09;协议中&#xff0c;0x19是用于DTC&#xff08;诊断故障代码&#xff09;信息的服务。以下是你提到的子服务的功能和作用&#xff1a; 0x01 - 报告DTC按状态掩码。这个子服务用于获取当前存储在ECU中的DTC列表。状态掩码用于过滤DTC&a…

【Mariadb高可用MHA】

目录 一、概述 1.概念 2.组成 3.特点 4.工作原理 二、案例介绍 1.192.168.42.3 2.192.168.42.4 3.192.168.42.5 4.192.168.42.6 三、实际构建MHA 1.ssh免密登录 1.1 所有节点配置hosts 1.2 192.168.42.3 1.3 192.168.42.4 1.4 192.168.42.5 1.5 192.168.42.6 …

听GPT 讲Prometheus源代码--rules

Prometheus的rules目录主要包含规则引擎和管理规则的文件: engine.go 该文件定义了规则引擎的接口和主要结构,包括Rule,Record,RuleGroup等。它提供了规则的加载、匹配、评估和结果记录的功能。 api.go 定义了用于管理和查询规则的RESTful API,包括获取、添加、删除规则等方法。…

中大型无人机远程VHF语音电台系统方案

方案背景 中大型无人机在执行飞行任务时&#xff0c;特别是在管制空域飞行时地面航管人员需要通过语音与无人机通信。按《无人驾驶航空器飞行管理暂行条例》规定&#xff0c;中大型无人机应当进行适航管理。物流无人机和载人eVTOL都将进行适航管理&#xff0c;所以无人机也要有…

计算机组成与设计 Patterson Hennessy 笔记(二)MIPS 指令集

计算机的语言&#xff1a;汇编指令集 也就是指令集。本书主要介绍 MIPS 指令集。 汇编指令 算数运算&#xff1a; add a,b,c # abc sub a,b,c # ab-cMIPS 汇编的注释是 # 号。 由于MIPS中寄存器大小32位&#xff0c;是基本访问单位&#xff0c;因此也被称为一个字 word。M…

AI搜索引擎助力科学家创新

开发者希望通过帮助科学家从大量文献中发现联系从而解放科学家&#xff0c;让他们专注于发现和创新。 图片来源&#xff1a;The Project Twins 对于专注于历史的研究者Mushtaq Bilal来说&#xff0c;他在未来科技中投入了大量时间。 Bilal在丹麦南部大学&#xff08; Universit…

SpringBoot + Vue 前后端分离项目 微人事(九)

职位管理后端接口设计 在controller包里面新建system包&#xff0c;再在system包里面新建basic包&#xff0c;再在basic包里面创建PositionController类&#xff0c;在定义PositionController类的接口的时候&#xff0c;一定要与数据库的menu中的url地址到一致&#xff0c;不然…

[python爬虫] 爬取图片无法打开或已损坏的简单探讨

本文主要针对python使用urlretrieve或urlopen下载百度、搜狗、googto&#xff08;谷歌镜像&#xff09;等图片时&#xff0c;出现"无法打开图片或已损坏"的问题&#xff0c;作者对它进行简单的探讨。同时&#xff0c;作者将进一步帮你巩固selenium自动化操作和urllib…

部署工业物联网可以选择哪些通信方案?

部署工业物联网有诸多意义&#xff0c;诸如提升生产效率&#xff0c;降低管理成本&#xff0c;保障生产品质稳定&#xff0c;应对长期从业劳动力变化趋势等。针对不同行业、场景&#xff0c;工业物联网需要选择不同的通信方案&#xff0c;以达到成本和效益的最佳平衡。本篇就简…

《HeadFirst设计模式(第二版)》第十章代码——状态模式

如下图所示&#xff0c;这是一个糖果机的状态机图&#xff0c;要求使用代码实现&#xff1a; 初始版本&#xff1a; package Chapter10_StatePattern.Origin;/*** Author 竹心* Date 2023/8/19**/public class GumballMachine {final static int SOLD_OUT 0;final static int…

Python学习笔记_进阶篇(二)_django知识(一)

本章简介&#xff1a; Django 简介Django 基本配置Django urlDjango viewDjango 模板语言Django Form Django 简介 Django是一个开放源代码的Web应用框架&#xff0c;由Python写成。采用了MVC的软件设计模式&#xff0c;即模型M&#xff0c;视图V和控制器C。它最初是被开发来…

攻防世界-PHP2

原题 解题思路 这题需要查看页面的phps文件&#xff08;这玩意从没见过&#xff09;。phps的文件是存放php的源代码的&#xff0c;但是不是所有网站都有。 只要让传入的idadmin就可以得到key了。 但是直接传入admin不行。用burp编码。 结果还是不行&#xff1a; 那就再…

【C++】做一个飞机空战小游戏(十)——子弹击落炮弹、炮弹与飞机相撞

[导读]本系列博文内容链接如下&#xff1a; 【C】做一个飞机空战小游戏(一)——使用getch()函数获得键盘码值 【C】做一个飞机空战小游戏(二)——利用getch()函数实现键盘控制单个字符移动【C】做一个飞机空战小游戏(三)——getch()函数控制任意造型飞机图标移动 【C】做一个飞…

Linux笔试题(4)

67、在局域网络内的某台主机用ping命令测试网络连接时发现网络内部的主机都可以连同,而不能与公网连通,问题可能是__C_ A.主机ip设置有误 B.没有设置连接局域网的网关 C.局域网的网关或主机的网关设置有误 D.局域网DNS服务器设置有误 解析&#xff1a;在局域网络内的某台主…

视频怎么转gif高清动图?分享一款视频转gif工具

许多小伙伴都不知道如何将拍摄的短视频转gif图片&#xff0c;本文将分享一款专业的视频转gif工具&#xff0c;打来浏览器即可将视频在线转gif&#xff08;https://www.gif.cn&#xff09;&#xff0c;操作简单&#xff0c;使用方便&#xff0c;下面是详细的步骤。 打开网站&am…