ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

使用NTP完成对主机的时钟同步

2022-06-26 10:33:04  阅读:169  来源: 互联网

标签:int NTP new static 主机 import com public 时钟


使用NTP完成对主机的时钟同步

项目简介

网络时间协议,英文名称:Network Time Protocol(NTP)是用来使计算机时间同步化的一种协议,它可以使计算机对其服务器或时钟源(如石英钟,GPS 等等)做同步化。它建立在 UDP 协议上,端口号为123,在无序的 Internet 环境中提供了精确和健壮的时间服务。

NTP 的实现原理并不是本文章讨论的主要问题。读者有兴趣的话,可以自行搜索其原理实现。

理论上来讲,只要在目标服务器上安装 NTP 服务,任何接入互联网的其他主机都能够直接与该服务器进行时钟同步。

而笔者最近介入的项目中,甲方想要通过该协议,完成其客户的服务器主机定时与甲方服务器主机的时钟同步。

 

 

该方案中,后端采用 Vert.x 框架,提供强大的异步事件驱动功能,核心功能是前端发起的手工同步(客户服务器向甲方服务器发起的时钟同步请求),而在此基础上的自动同步则直接使用线程池的定时功能(ScheduledExecutorService)调用该接口。为了保证该功能的安全性,搭配一套登录验证功能以及 Vert.x 框架自带的抵御 CSRF 攻击的 CSRFHandler。

值得一提的是,由于NTP会修改本机服务器的时钟,而 Quartz 的定时任务严重依赖于本地时间,因此并不适合用 Quartz 来为 NTP 同步设置定时任务。事实上,用 ScheduledExecutorService 足矣。

这篇文章,主要是简单描述该核心功能在 Linux 服务器上的 Java 实现。

功能实现

1. 系统命令调用

系统命令调用是实现该功能最简单的方法。直接调用 java.lang.Runtime 类中的 exec() 方法即可:

Process process = Runtime.getruntime().exec(cmd);

在 Linux 服务器上直接执行的命令 cmd 可以直接放入 exec() 方法参数中。比如,想要查询 Linux 服务器的本地时间,可以通过以下命令实现:

而是用 exec() 方法执行的 Java demo 的演示如下:

Process process = Runtime.getRuntime().exec("date");
// 等待该子进程运行结束。returnValue为0表示该进程正常结束
int returnValue = process.waitFor();
System.out.println("returnValue = " + returnValue);

InputStream inputStream = process.getInputStream();
// 通过字符流读取缓冲池的内容
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
String line = null;
while ((line = br.readLine()) != null) {
 System.out.println("line = " + line);
}

将该 demo 打成 jar 包丢到服务器上运行,可以得到如下结果:

关于打 jar 包的方法,有一篇实践简单但操作性比较强的博客供读者参考:https://blog.csdn.net/kelekele111/article/details/123047189

有了以上的实践基础,我们可以拓展出我们所需要的功能实现。

注意到 Linux 上执行 NTP 同步的命令为(注意,这里只是使用到了其最基础的功能,读者可以自行搜索 ”ntpdate“ 命令,或者在命令行中输入”man ntpdate“,或者”info ntpdate“,来获取更多参数设置方法。推荐阅读:https://www.tutorialspoint.com/unix_commands/ntpdate.htm):

# ntpdate "IP"
ntpdate asia.pool.ntp.org
ntpdate "17.253.84.253"

该命令的返回结果如下所示:

这里的 offset 是通过一定的公式推导得到的结果,表示的是本地服务器时钟与目标服务器时钟之间的时间差,单位为秒(如下图所示)。想要了解该公式推导过程的读者,可以自行搜索了解(推荐阅读:https://www.eecis.udel.edu/~mills/time.html)。

值得注意的是,NTP 只会给请求发起的主机返回 offset,而不会直接提供同步之后的时间戳。因此,如果想要手动通过相关系统调用来修改本地时钟,设置的目标时间戳应该是当前时间戳加上 offset:

Long goalDate = System.currentTimeMillis() + offset;
Date date = new Date(goalDate);

而 ”ntpdate“ 命令这个 Linux 的内核调用,已经将上述步骤囊括其中,不需要再进行额外操作。也就是说,只要执行 ”ntpdate“ 命令,便可直接完成时钟同步。

实际生产中,一般可以将 offset 单独提取出来,用来作为是否显示同步日志的判断根据。这个 offset 的提取,通过解析该命令的返回值实现(后面有代码展示)。

因为,如果每次同步都要给前端返回同步日志,显然会导致同步日志次数过多而导致用户产生错误的判断,认为程序运行存在问题。可以设置一个阈值,比如 20 ms。当 offset 大于该阈值时,同步日志才会返回给前端;否则则不返回。注意:该阈值的设置,与是否同步是没有任何关系的。

结合以上的知识,可以将该程序代码展示如下:

package com.max.runtime.solution;

import java.io.IOException;
import java.io.InputStream;

/**
* @author siyuan
* @description 执行本地Linux命令的工具类
*/
public class LinuxCommandUtil {

   /**
    * @description 默认执行本地Linux命令
    */
   public static String executeLocalLinuxCommand(String[] command) {
       Runtime runtime = Runtime.getRuntime();
       StringBuilder result = new StringBuilder();

       try {
           Process process = runtime.exec(command);
         try {
           // 等待,在该线程上阻塞,直至该线程执行完毕
           // 也可以获取该方法的返回值的int型变量,该变量为0时表示正常结束
           process.waitFor();
        } catch (InterruptedException e) {
           e.printStackTrace();
        }
         // 获取子进程的输入流
         InputStream inputStream = process.getInputStream();
           byte[] data = new byte[1024];
           while (inputStream.read(data) != -1) {
               result.append(new String(data, "UTF-8"));
          }
           if (result.toString().equals("")) {
             // 获取子进程的错误流
               InputStream errorStream = process.getErrorStream();
               while (errorStream.read(data) != -1) {
                   result.append(new String(data, "UTF-8"));
              }
          }
         // 子进程运行结束,将它摧毁
           process.destroy();
           return result.toString();
      } catch (IOException e) {
           e.printStackTrace();
      }
       return null;
  }
}
package com.max.runtime.solution;

import org.apache.commons.lang3.StringUtils;

import java.util.Arrays;
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class Main {

 private final static int CORE_POOL_SIZE = 2;
 private final static long INITIAL_DELAY = 0;
 private final static long PERIODIC = 10;

 // 以向域名为"asia.pool.ntp.org"的服务器进行同步为例
 private final static String[] COMMAND = {"ntpdate", "asia.pool.ntp.org"};

 public static void main(String[] args) {

   final ScheduledExecutorService scheduledExecutorService = Executors
      .newScheduledThreadPool(CORE_POOL_SIZE);

   if (StringUtils.containsIgnoreCase(System.getProperty("os.name"), "Linux")) {
     System.out.println("该程序在Linux操作系统上运行");
     // 调用scheduledAtFixedRate()方法执行定时任务,该定时任务每10 s进行一次时钟同步
     scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
       public void run() {

         String res = LinuxCommandUtil.executeLocalLinuxCommand(COMMAND);
         System.out.println(res);
         if (res == null) {
           throw new RuntimeException("Linux执行命令: " + Arrays.toString(COMMAND) + "失败.");
        }
         Long gap = ntpGetGap(res);
         if (gap == null) {
           throw new RuntimeException("偏移量offset解析失败.");
        }

         System.out.println("服务器时钟与本地时钟之间时间差为: " + (gap >= 0 ? gap : -gap) + "ms");
         Date serverTime = new Date(System.currentTimeMillis() + gap);
         System.out.println("本地时间为: " + new Date());
         System.out.println("服务器时间为: " + serverTime);
      }

    }, INITIAL_DELAY, PERIODIC, TimeUnit.SECONDS);
  } else {
     throw new RuntimeException("该程序暂不支持在Windows操作系统上运行!");
  }
}

 /**
    * @description 解析命令返回的String类型的参数
    */
 private static Long ntpGetGap(String res) {
   String[] s;
   try {
     s = res.split(" ");

  } catch (Exception e) {
     throw new RuntimeException("Linux命令执行结果解析失败!");
  }
   String stringGap;
   Long gap;
   try {
     // 截取返回结果的倒数第二个字段,即所谓的偏移量offset
     stringGap = s[s.length - 2].trim();
     Number num = 1000 * Float.parseFloat(stringGap);
     gap = (long) num.intValue();
  } catch (Exception e) {
     throw new RuntimeException("解析时间差offset失败!");
  }
   return gap;
}
}

有一个问题,在 main 方法中,为什么命令 COMMAND 以字符串数组的形式表示呢?直接写成如下的字符串是否也可行?

private final static String COMMAND = "ntpdate asia.pool.ntp.org";

实践表明,这样写是没问题的。实际上,Runtime.getRuntime.exec() 有六个重载方法,其中的两个,参数分别是 String 与 String[]。关于这些重载方法的用法,以及 Runtime 类其他方法的使用,读者可以自行在源码中查看、了解。

将上述代码打成 jar 包丢到服务器上运行,读取日志截图如下:

为了验证该程序的确在正常运行,可以手动修改一下本地时间(注意:该命令的执行必须有 root 权限,否则无法成功修改时间,因此需加上 sudo):

sudo date -s "2022-01-01 00:00:00"

显然,”ntpdate“ 命令能够正常工作。

到此为止,一个简单的 NTP 同步 demo 就完成了。

2. JNA调用

上述 demo,实现起来非常简单。”ntpdate“ 命令进行系统调用,在获取偏移量 offset 的同时,直接将本地时钟与目标服务器时钟同步,该实现对于 Java 程序员是透明的。而且,在生产环境中直接通过 Runtime.getRuntime.exec() 运行命令,是有一定风险的。甲方也禁止这么操作。既然如此,我们能否自己写一段代码,使得同步过程为我们所掌控呢?

(以下内容译自 GitHub 上的项目介绍:https://github.com/java-native-access/jna

JNA 是 Java Native Access 的缩写,它使得 Java 程序可以轻松访问本地共享库,无需编写 Java 代码以外的任何内容——不需要 JNI 或原生代码。此功能可与 Windows 的 Platform/Invoke 和 Python 的 ctypes 相媲美。

JNA 允许开发者使用原汁原味的 Java 方法调用,以直接调用原生函数——这种调用,跟直接调用 Java 方法一样简单。大多数调用不需要特殊的处理或配置,也不需要样板文件或生成的代码(generated code)。

JNA 使用小型 JNI 库存根(stub)来动态调用原生代码。开发人员使用 Java 接口来描述目标本地库中的函数和结构。这使得利用本机平台特性变得非常容易,同时不会因为多个平台配置和构建 JNI 代码而产生较高开销。

点击同一页面上的 Getting Started,我们可以看到 JNA 在调用标准 C 库的一段代码:

package com.sun.jna.examples;

import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Platform;

/** Simple example of JNA interface mapping and usage. */
public class HelloWorld {

// This is the standard, stable way of mapping, which supports extensive
// customization and mapping of Java to native types.

public interface CLibrary extends Library {
CLibrary INSTANCE = (CLibrary)
Native.load((Platform.isWindows() ? "msvcrt" : "c"),
CLibrary.class);

void printf(String format, Object... args);
}

public static void main(String[] args) {
CLibrary.INSTANCE.printf("Hello, World\n");
for (int i=0;i < args.length;i++) {
CLibrary.INSTANCE.printf("Argument %d: %s\n", i, args[i]);
}
}
}

显然,这里调用的是 libc 中的 printf 函数。

在 Linux 操作系统上,对于 C 语言标准库函数的调用核心代码:

CLibrary INSTANCE = (CLibrary) Native.load("c", CLibrary.class);

我们这里只讨论 Linux 操作系统,Windows 操作系统也可以通过 JNA 进行调用。有兴趣的读者可以结合搜索引擎,参考以下代码学习:

package com.sun.jna.examples.win32;

import com.sun.jna.*;

// kernel32.dll uses the __stdcall calling convention (check the function
// declaration for "WINAPI" or "PASCAL"), so extend StdCallLibrary
// Most C libraries will just extend com.sun.jna.Library,
public interface Kernel32 extends StdCallLibrary {
// Method declarations, constant and structure definitions go here
Kernel32 INSTANCE = (Kernel32)Native.load("kernel32", Kernel32.class);
// Optional: wraps every call to the native library in a
// synchronized block, limiting native calls to one at a time
Kernel32 SYNC_INSTANCE = (Kernel32)
Native.synchronizedLibrary(INSTANCE);
}

在 Linux 系统中,设置本地时间的函数是 settimeofday。与之搭配使用的是 gettimeofday。我们可以通过这两个函数,完成对于 Linux 系统本地时间的设置。实现的代码如下:

package com.max.method.call.impl;

import com.max.method.call.JNative;
import java.util.Date;

import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.NativeLong;
import com.sun.jna.Structure;

public class LinuxImpl implements JNative {

public static class TM extends Structure{

public static class ByReference extends TM implements Structure.ByReference{}
public static class ByValue extends TM implements Structure.ByValue{}

public int tm_sec;//seconds 0-61
public int tm_min;//minutes 1-59
public int tm_hour;//hours 0-23
public int tm_mday;//day of the month 1-31
public int tm_mon;//months since jan 0-11
public int tm_year;//years from 1900
public int tm_wday;//days since Sunday, 0-6
public int tm_yday;//days since Jan 1, 0-365
public int tm_isdst;//Daylight Saving time indicator
}

// 第一个参数,对应结构体timeval
public static class TimeVal extends Structure{
public static class ByReference extends TimeVal implements Structure.ByReference{}
public static class ByValue extends TimeVal implements Structure.ByValue{}

public NativeLong tv_sec; /* 秒数 */
public NativeLong tv_usec; /* 微秒数 */
}

// 第二个参数,对应结构体timezone
public static class TimeZone extends Structure{
public static class ByReference extends TimeZone implements Structure.ByReference{}
public static class ByValue extends TimeZone implements Structure.ByValue{}

public int tz_minuteswest;
public int tz_dsttime;
}

// 调用本地共享库函数的接口
public interface CLibrary extends Library{

int gettimeofday(TimeVal.ByReference tv, TimeZone.ByReference tz);
int settimeofday(TimeVal.ByReference tv, TimeZone.ByReference tz);

}

public static CLibrary cLibraryInstance = null;

public LinuxImpl(){
cLibraryInstance = (CLibrary)Native.loadLibrary("c", CLibrary.class);
}

public int setLocalTime(Date date) {
long ms = date.getTime();

long s = ms / 1000; //秒
long us = (ms % 1000) * 1000; //微秒

TimeVal.ByReference tv = new TimeVal.ByReference();
TimeZone.ByReference tz = new TimeZone.ByReference();
cLibraryInstance.gettimeofday(tv, tz);

tv.tv_sec = new NativeLong(s);
tv.tv_usec = new NativeLong(us);
// 返回值为0时,表示成功;为-1时表示失败
return cLibraryInstance.settimeofday(tv, tz);
}
}
package com.max.method.call;

import java.util.Date;

public interface JNative {
/**
* 设置系统时间的方法
* @param date Date
*/
int setLocalTime(Date date);
}
package com.max.method.call.factory;

import com.max.method.call.JNative;
import com.max.method.call.impl.LinuxImpl;
import com.sun.jna.Platform;

/**
* 根据操作系统的不同,生成对应的、调用本地方法的工厂
*/
public class NativeFactory {

public static JNative newNative() {
if (Platform.isLinux()) {
return new LinuxImpl();
} else {
// 暂不讨论 Windows 系统
return null;
}
}
}

创建一个调用本地共享库的接口 JNative ,并由 LinuxImpl 去实现它。在 LinuxImpl中,我们只需要关注两个内部类,即 TimeVal 与 TimeZone,和一个内部接口 CLibrary。这两个内部类都继承自 Structure 类,分别对应于 libc 中 settimeofday函数与 gettimeofday 函数中的俩参数(以 gettimeofday 为例):

int gettimeofday ( struct timeval *tp ,  struct timezone *tz )

第一个参数为结构体 timeval,在 <sys/time>.h 头文件中声明如下:

struct timeval {
time_t tv_sec; //used for seconds
suseconds_t tv_usec; //used for microseconds
}

注意到 TimeVal 中的两个成员变量,都是 NativeLong 类型的。因为 timeval 结构体中俩参数的数据类型就是 long。

第二个参数为结构体 timezone。由于该结构体已经过时,因此一般默认缺省为 null ——该结构体的存在,只是考虑到向后兼容性。

内部接口 CLibrary 调用了 gettimeofday 与 settimeofday 这两个函数,声明之后可以直接使用。

我们关注的重点是 setLocalTime() 方法:它通过 gettimeofday 函数获取本地时间,再通过 settimeofday 函数将传入的参数 date 赋值给本地时间,从而完成本地时间的设置。如果没有 gettimeofday,只能喜提 ”time out“ 的异常了。

读者可能注意到了 LinuxImpl 类中没有使用到的一个内部类 TM。它同样继承了 Structure 类,原型是 libc 的结构体 tm。它是否也能用于设置 Linux 的本地时间呢?

实际上,它也可以用来设置 Linux 的本地时间。但它的使用稍微复杂些,而且不是我们讨论的重点,此处只贴出使用 libc 的 mktime 函数(将结构体 tm 存储的时间转换为自纪元(the Epoch)以来的秒数)调用方法供参考:

public long getLocalTime(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);

TM.ByReference byReference = new TM.ByReference();
byReference.tm_year = calendar.get(Calendar.YEAR);
byReference.tm_mon = calendar.get(Calendar.MONTH) + 1;
byReference.tm_mday = calendar.get(Calendar.DATE);
byReference.tm_hour = calendar.get(Calendar.HOUR_OF_DAY);
byReference.tm_min = calendar.get(Calendar.MINUTE);
byReference.tm_sec = calendar.get(Calendar.SECOND);

return cLibraryInstance.mktime(byReference);
}

注意:tm_mon 的取值范围是 0-11,因此要表示月份的时候,必须再加上 1。

到此为止,我们已经学会了如何使用函数调用的方式设置 Linux 系统本地时间。

而 NTP 实现的时钟同步代码,需要我们手动实现。我们已经在”系统命令调用“一节,讲清了 NTP 时钟同步的逻辑了:获取与服务器之间的时间差 offset,由此设置同步后的本地时间:

Long goalDate = System.currentTimeMillis() + offset;
Date date = new Date(goalDate);

同样地,我们在 main 函数中写一个定时任务来实现 NTP 同步:

package com.max.method.call;

import com.max.runtime.solution.NTPUtil;
import com.max.method.call.factory.NativeFactory;
import com.sun.jna.Native;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.apache.commons.net.ntp.NTPUDPClient;
import org.apache.commons.net.ntp.TimeInfo;

public class Main {

private final static int CORE_POOL_SIZE = 2;
private final static long INITIAL_DELAY = 0;
private final static long PERIODIC = 10;

private static JNative localImpl = NativeFactory.newNative();

private static TimeInfo timeInfo;
private static Long offset;

public static void main(String[] args) {
final ScheduledExecutorService scheduledExecutorService = Executors
.newScheduledThreadPool(CORE_POOL_SIZE);
final NTPUDPClient client = NTPUtil.getClient();
// 过期时间为10 s
client.setDefaultTimeout(10 * 1000);
InetAddress inetAddress = null;
try {
String SERVER_NAME = "asia.pool.ntp.org";
inetAddress = InetAddress.getByName(SERVER_NAME);
} catch (UnknownHostException e) {
e.printStackTrace();
}
final InetAddress finalInetAddress = inetAddress;

// 线程池定时任务
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
public void run() {
try {
timeInfo = client.getTime(finalInetAddress);
} catch (IOException e) {
e.printStackTrace();
}
// computeDetails之后获得offset
timeInfo.computeDetails();
if (timeInfo.getOffset() != null) {
offset = timeInfo.getOffset();
} else {
throw new RuntimeException("无法获取offset!");
}
System.out.println("本地时钟与服务器时钟之间时间差为: " + offset + " ms");
Date currentDate = new Date();
System.out.println("本地时间为: " + currentDate);
long thisTime = System.currentTimeMillis() + offset;
Date now = new Date(thisTime);
System.out.println("服务器时间为: " + now);
// 通过 JNA,设置本地时间
try {
int returnValue = localImpl.setLocalTime(now);
if (returnValue != 0) {
// 如果返回值为-1,说明设置本地时间失败。一般来说,此时返回的错误码应该是1,表示权限不够。
throw new RuntimeException("设置本地时间失败!失败码为" + Native.getLastError());
}
System.out.println("本次时钟同步成功!");
} catch (Exception e) {
e.printStackTrace();
}
}
}, INITIAL_DELAY, PERIODIC, TimeUnit.SECONDS);
}
}

如果该程序顺利执行,我们将得到下图所示的结果:

实际上,如果我们打成的 jar 包运行在 Ubuntu 系统上,我们会获取以下的报错信息。

如果我们顺着 "invalid ELF header" 这个关键词去查找问题的解决方案,恐怕得不到正确的结论。

帮助我解决这个问题的,是组内的一位 C++ 大佬。他建议我使用链接 librt 而不是 libc,于是我将核心代码修改如下(顺带将类名也作了修改):

public static RTLibrary rtLibraryInstance = null;

public LinuxImpl() {
rtLibraryInstance = (RTLibrary) Native.loadLibrary("rt", RTLibrary.class);
}

修改完成。在自己的服务器上跑通了之后,我又将这段代码整合进项目代码中。公司使用的 Linux 服务器是 Redhat,无论链接 libc 还是 librt,期待出现的运行结果都出现了。类似地,我们依然使用 date -s 指令”捣蛋“:

对于 Ubuntu 和 Redhat 之间表现差异,我忍不住跑去 Oracle 官网上查询,这两个库之间的联系与区别。链接贴在这里:https://docs.oracle.com/cd/E86824_01/html/E54772/index.html

根据库函数列表,我们可以发现, gettimeofday 与 settimeofday 这俩函数仅存在于 libc 中。不过这两个库之间的关系,官网上有一段话介绍:

This functionality(librt) now resides in libc.This library is maintained to provide backward compatibility for both runtime and compilation environments. The shared object is implemented as a filter on libc.so.1. New application development need not specify `–lrt`.

即: librt 现在存在于 libc 中,用于提供运行时与编译时环境的向后兼容,作为 libc.so.1 的过滤器而存在。

再回到 Ubuntu 和 Red Hat 这俩系统间的区别上。在 Ubuntu 系统中,我们进入 libc 所在的目录 /usr/lib/x86_64-linux-gnu ,找到 libc.so 与 librt.so,进入查看:

类似地,Red Hat 系统上的 libc.so 与 librt.so 查到的内容与之相同。

由此看来,尽管内容一样,比起 Red Hat,Ubuntu 上的 libc.so 显然只是一个”假库“。这是否是作为企业版应用的 Red Hat 与免费版的 Ubuntu 之间一个差别呢?

在 Linux 上以非 root 用户运行 jar 包时,同样需要加 sudo,否则我们会发现,本地时间的修改是无法实现的。此时,方法 Native.getLastError() 会给我们返回错误码 1,表示权限不够。

sudo nohup java -jar linux-ntpdate.jar > hup.out

结语

以上便是本次文章分享的所有内容了。写这篇文章贼累,毕竟笔者只是一个”半路出家“的 Java coder,没有认真而系统学过 C/C++ 相关的知识,面对跨语言的函数(方法)调用时经常遇到一些无法理解的问题,表达上可能会有许多不严谨之处。

不管如何,还是希望这篇文章能够帮助到有需要的读者。

标签:int,NTP,new,static,主机,import,com,public,时钟
来源: https://www.cnblogs.com/max2022/p/16413062.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有