Java DNS查询内部实现
源码分析
在Java中,DNS相关的操作都是通过通过InetAddress提供的API实现的。比如查询域名对应的IP地址:
String dottedQuadIpAddress = InetAddress.getByName( "blog.arganzheng.me" ).getHostAddress();
或者反过来IP对应域名:
InetAddress[] addresses = InetAddress.getAllByName("8.8.8.8"); // ip or DNS name
for (int i = 0; i < addresses.length; i++) {
String hostname = addresses[i].getHostName();
System.out.println(hostname);
}
输出:
google-public-dns-a.google.com
那么InetAddress是如何实现DNS解析的呢?让我们深入代码一步步挖掘下去:
import java.net.UnknownHostException; public class InetAddress extends java.net.InetAddress implements java.io.Serializable { public static InetAddress getByName(String host)
throws UnknownHostException {
return InetAddress.getAllByName(host)[0];
} public static InetAddress[] getAllByName(String host)
throws UnknownHostException {
return getAllByName(host, null);
} private static InetAddress[] getAllByName(String host, InetAddress reqAddr)
throws UnknownHostException { // ... 省略对于IPV6地址判断,HostName或者IP地址判断
// hostname, resolve it
return getAllByName0(host, reqAddr, true);
} private static InetAddress[] getAllByName0(String host, InetAddress reqAddr, boolean check)
throws UnknownHostException { /* If it gets here it is presumed to be a hostname */
/* Cache.get can return: null, unknownAddress, or InetAddress[] */ /* make sure the connection to the host is allowed, before we
* give out a hostname
*/
if (check) { // 安全检查
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkConnect(host, -1);
}
} // 从DNS Cache中获取
InetAddress[] addresses = getCachedAddresses(host); /* If no entry in cache, then do the host lookup */
if (addresses == null) {
addresses = getAddressesFromNameService(host, reqAddr);
} if (addresses == unknown_array)
throw new UnknownHostException(host); return addresses.clone();
}
这里需要注意,JVM会先查询DNS缓存。有一个问题:默认的缓存时间是永远!这个是JDK实现中比较坑的地方。
InetAddress Caching
The InetAddress class has a cache to store successful as well as unsuccessful host name resolutions. By default, when a security manager is installed, in order to protect against DNS spoofing attacks, the result of positive host name resolutions are cached forever. When a security manager is not installed, the default behavior is to cache entries for a finite (implementation dependent) period of time. The result of unsuccessful host name resolution is cached for a very short period of time (10 seconds) to improve performance.
If the default behavior is not desired, then a Java security property can be set to a different Time-to-live (TTL) value for positive caching. Likewise, a system admin can configure a different negative caching TTL value when needed.
Two Java security properties control the TTL values used for positive and negative host name resolution caching:
networkaddress.cache.ttl Indicates the caching policy for successful name lookups from the name service. The value is specified as as integer to indicate the number of seconds to cache the successful lookup. The default setting is to cache for an implementation specific period of time. A value of -1 indicates "cache forever".
networkaddress.cache.negative.ttl (default: 10) Indicates the caching policy for un-successful name lookups from the name service. The value is specified as as integer to indicate the number of seconds to cache the failure for un-successful lookups. A value of 0 indicates "never cache". A value of -1 indicates "cache forever".
如果Cache miss,那么就会调用配置的nameServices执行真正DNS查询:
private static InetAddress[] getAddressesFromNameService(String host, InetAddress reqAddr)
throws UnknownHostException
{
InetAddress[] addresses = null;
boolean success = false;
UnknownHostException ex = null; // Check whether the host is in the lookupTable.
// 1) If the host isn't in the lookupTable when
// checkLookupTable() is called, checkLookupTable()
// would add the host in the lookupTable and
// return null. So we will do the lookup.
// 2) If the host is in the lookupTable when
// checkLookupTable() is called, the current thread
// would be blocked until the host is removed
// from the lookupTable. Then this thread
// should try to look up the addressCache.
// i) if it found the addresses in the
// addressCache, checkLookupTable() would
// return the addresses.
// ii) if it didn't find the addresses in the
// addressCache for any reason,
// it should add the host in the
// lookupTable and return null so the
// following code would do a lookup itself.
if ((addresses = checkLookupTable(host)) == null) {
try {
// This is the first thread which looks up the addresses
// this host or the cache entry for this host has been
// expired so this thread should do the lookup. /*
* 这里可以看到nameServices是链状的,这是JDK7+的逻辑。
* 插入自定义nameservice的逻辑就在这里。
*/
for (NameService nameService : nameServices) {
try {
/*
* Do not put the call to lookup() inside the
* constructor. if you do you will still be
* allocating space when the lookup fails.
*/ addresses = nameService.lookupAllHostAddr(host);
success = true;
break;
} catch (UnknownHostException uhe) {
if (host.equalsIgnoreCase("localhost")) {
InetAddress[] local = new InetAddress[] { impl.loopbackAddress() };
addresses = local;
success = true;
break;
}
else {
addresses = unknown_array;
success = false;
ex = uhe;
}
}
} // More to do?
if (reqAddr != null && addresses.length > 1 && !addresses[0].equals(reqAddr)) {
// Find it?
int i = 1;
for (; i < addresses.length; i++) {
if (addresses[i].equals(reqAddr)) {
break;
}
}
// Rotate
if (i < addresses.length) {
InetAddress tmp, tmp2 = reqAddr;
for (int j = 0; j < i; j++) {
tmp = addresses[j];
addresses[j] = tmp2;
tmp2 = tmp;
}
addresses[i] = tmp2;
}
}
// Cache the address.
cacheAddresses(host, addresses, success); if (!success && ex != null)
throw ex; } finally {
// Delete host from the lookupTable and notify
// all threads waiting on the lookupTable monitor.
updateLookupTable(host);
}
} return addresses;
}
这里有一段非常关键的代码:
for (NameService nameService : nameServices) {
try {
/*
* Do not put the call to lookup() inside the
* constructor. if you do you will still be
* allocating space when the lookup fails.
*/
addresses = nameService.lookupAllHostAddr(host);
success = true;
break;
} catch (UnknownHostException uhe) {
if (host.equalsIgnoreCase("localhost")) {
InetAddress[] local = new InetAddress[] { impl.loopbackAddress() };
addresses = local;
success = true;
break;
}
else {
addresses = unknown_array;
success = false;
ex = uhe;
}
}
}
这是真正执行DNS查询的地方,而且可以看到是通过NameService链来依次解析的。其中nameServices是InetAddress的一个成员变量:
/* Used to store the name service provider */
private static List<NameService> nameServices = null;
通过static块进行初始化的:
static {
// create the impl
impl = InetAddressImplFactory.create(); // get name service if provided and requested
String provider = null;;
String propPrefix = "sun.net.spi.nameservice.provider.";
int n = 1;
nameServices = new ArrayList<NameService>();
provider = AccessController.doPrivileged(
new GetPropertyAction(propPrefix + n));
while (provider != null) {
NameService ns = createNSProvider(provider);
if (ns != null)
nameServices.add(ns); n++;
provider = AccessController.doPrivileged(
new GetPropertyAction(propPrefix + n));
} // if not designate any name services provider,
// create a default one
if (nameServices.size() == 0) {
NameService ns = createNSProvider("default");
nameServices.add(ns);
}
}
因为是通过InetAddress的static块初始化的,所以必须在使用InetAddress之前配置sun.net.spi.nameservice.provider.<n>=<default|dns,sun|...>
配置项。否则不会生效。具体可以参见SO上的这篇帖子:Clean DNS server in JVM。
另外需要注意的是在JDK7之前,只有第一个成功加载的nameservice参与解析。其他的nameservice并不起作用。
sun.net.spi.nameservice.provider.<n>=<default|dns,sun|...>
Specifies the name service provider that you can use. By default, Java will use the system configured name lookup mechanism, such as file, nis, etc. You can specify your own by setting this option. takes the value of a positive number, it indicates the precedence order with a small number takes higher precendence over a bigger number. Aside from the default provider, the JDK includes a DNS provider named "dns,sun".
Prior to JDK 7, the first provider that was successfully loaded was used. In JDK 7, providers are chained, which means that if a lookup on a provider fails, the next provider in the list is consulted to resolve the name.
不过我看一下OpenJDK6的代码,貌似也是链状的。
然后我们来看看NameService的定义:
package sun.net.spi.nameservice; import java.net.UnknownHostException; public interface NameService {
public java.net.InetAddress[] lookupAllHostAddr(String host) throws UnknownHostException;
public String getHostByAddr(byte[] addr) throws UnknownHostException;
}
其实是个很简单的接口。默认有两个实现:
- 匿名类:就是provider名称为default的实现类
DNSNameService
private static NameService createNSProvider(String provider) { if (provider == null) return null; NameService nameService = null;
if (provider.equals("default")) {
// initialize the default name service
nameService = new NameService() {
public InetAddress[] lookupAllHostAddr(String host)
throws UnknownHostException {
return impl.lookupAllHostAddr(host);
}
public String getHostByAddr(byte[] addr)
throws UnknownHostException {
return impl.getHostByAddr(addr);
}
};
} else {
final String providerName = provider;
try {
nameService = java.security.AccessController.doPrivileged(
new java.security.PrivilegedExceptionAction<NameService>() {
public NameService run() {
Iterator<NameServiceDescriptor> itr =
ServiceLoader.load(NameServiceDescriptor.class)
.iterator();
while (itr.hasNext()) {
NameServiceDescriptor nsd = itr.next();
if (providerName.
equalsIgnoreCase(nsd.getType()+","
+nsd.getProviderName())) {
try {
return nsd.createNameService();
} catch (Exception e) {
e.printStackTrace();
System.err.println(
"Cannot create name service:"
+providerName+": " + e);
}
}
} return null;
}
}
);
} catch (java.security.PrivilegedActionException e) {
}
} return nameService;
}
默认的NameService其实将域名解析操作委托给了impl变量:
static InetAddressImpl impl;
InetAddressImpl本身也是一个接口:
package java.net;
import java.io.IOException;
/*
* Package private interface to "implementation" used by
* {@link InetAddress}.
* <p>
* See {@link java.net.Inet4AddressImp} and
* {@link java.net.Inet6AddressImp}.
*
* @since 1.4
*/
interface InetAddressImpl { String getLocalHostName() throws UnknownHostException;
InetAddress[]
lookupAllHostAddr(String hostname) throws UnknownHostException;
String getHostByAddr(byte[] addr) throws UnknownHostException; InetAddress anyLocalAddress();
InetAddress loopbackAddress();
boolean isReachable(InetAddress addr, int timeout, NetworkInterface netif,
int ttl) throws IOException;
}
有两个实现:
- Inet4AddressImpl
- Inet6AddressImpl
这里我们仅仅挑选Inet4AddressImpl说明。
package java.net;
import java.io.IOException; /*
* Package private implementation of InetAddressImpl for IPv4.
*
* @since 1.4
*/
class Inet4AddressImpl implements InetAddressImpl {
public native String getLocalHostName() throws UnknownHostException;
public native InetAddress[]
lookupAllHostAddr(String hostname) throws UnknownHostException;
public native String getHostByAddr(byte[] addr) throws UnknownHostException;
private native boolean isReachable0(byte[] addr, int timeout, byte[] ifaddr, int ttl) throws IOException; public synchronized InetAddress anyLocalAddress() {
if (anyLocalAddress == null) {
anyLocalAddress = new Inet4Address(); // {0x00,0x00,0x00,0x00}
anyLocalAddress.holder().hostName = "0.0.0.0";
}
return anyLocalAddress;
} public synchronized InetAddress loopbackAddress() {
if (loopbackAddress == null) {
byte[] loopback = {0x7f,0x00,0x00,0x01};
loopbackAddress = new Inet4Address("localhost", loopback);
}
return loopbackAddress;
} public boolean isReachable(InetAddress addr, int timeout, NetworkInterface netif, int ttl) throws IOException {
byte[] ifaddr = null;
if (netif != null) {
/*
* Let's make sure we use an address of the proper family
*/
java.util.Enumeration<InetAddress> it = netif.getInetAddresses();
InetAddress inetaddr = null;
while (!(inetaddr instanceof Inet4Address) &&
it.hasMoreElements())
inetaddr = it.nextElement();
if (inetaddr instanceof Inet4Address)
ifaddr = inetaddr.getAddress();
}
return isReachable0(addr.getAddress(), timeout, ifaddr, ttl);
}
private InetAddress anyLocalAddress;
private InetAddress loopbackAddress;
}
可以看到这个类基本没有实现什么逻辑,主要实现都是通过JNI调用native方法了。native方法其实就是系统调用了。这里就不展开了,逻辑不外乎就是查看/etc/resolv.conf下配置的nameserver和/etc/hosts下面的配置,然后使用DNS协议查询。
其他NameService通过NameServiceDescriptor来创建相应。NameServiceDescriptor本身也是一个接口:
package sun.net.spi.nameservice; public interface NameServiceDescriptor {
/**
* Create a new instance of the corresponding name service.
*/
public NameService createNameService () throws Exception ; /**
* Returns this service provider's name
*
*/
public String getProviderName(); /**
* Returns this name service type
* "dns" "nis" etc
*/
public String getType();
}
这里我们先看看JDK自带一个实现——DNSNameService。
package sun.net.spi.nameservice.dns; /*
* A name service provider based on JNDI-DNS.
*/
public final class DNSNameService implements NameService { // List of domains specified by property
private LinkedList<String> domainList = null; // JNDI-DNS URL for name servers specified via property
private String nameProviderUrl = null; // Per-thread soft cache of the last temporary context
private static ThreadLocal<SoftReference<ThreadContext>> contextRef =
new ThreadLocal<>(); // Simple class to encapsulate the temporary context
private static class ThreadContext {
private DirContext dirCtxt;
private List<String> nsList; ...
} }
看一下构造函数:
public DNSNameService() throws Exception { // default domain
String domain = AccessController.doPrivileged(
new GetPropertyAction("sun.net.spi.nameservice.domain"));
if (domain != null && domain.length() > 0) {
domainList = new LinkedList<String>();
domainList.add(domain);
} // name servers
String nameservers = AccessController.doPrivileged(
new GetPropertyAction("sun.net.spi.nameservice.nameservers"));
if (nameservers != null && nameservers.length() > 0) {
nameProviderUrl = createProviderURL(nameservers);
if (nameProviderUrl.length() == 0) {
throw new RuntimeException("malformed nameservers property");
}
} else {
// no property specified so check host DNS resolver configured
// with at least one nameserver in dotted notation.
//
List<String> nsList = ResolverConfiguration.open().nameservers();
if (nsList.isEmpty()) {
throw new RuntimeException("no nameservers provided");
}
boolean found = false;
for (String addr: nsList) {
if (IPAddressUtil.isIPv4LiteralAddress(addr) ||
IPAddressUtil.isIPv6LiteralAddress(addr)) {
found = true;
break;
}
}
if (!found) {
throw new RuntimeException("bad nameserver configuration");
}
}
}
可以看到它主要是解析下面两个配置项,得到nameserver:
sun.net.spi.nameservice.domain=<domainname>
sun.net.spi.nameservice.nameservers=<server1_ipaddr,server2_ipaddr ...>
如果 sun.net.spi.nameservice.nameservers 没有配置,那么会使用 ResolverConfiguration 得到系统配置的nameserver:
List<String> nsList = ResolverConfiguration.open().nameservers();
ResolverConfiguration的实现逻辑是加载/etc/resolv.conf
配置文件中的nameserver。具体参见:ResolverConfigurationImpl。这里就不赘述。
得到NameServer,DNS的解析就很简单了,对NameServer分别执行DNS查询就可以了。具体代码大家可以参见 DNSNameService。
NOTE 从代码可以看出,系统配置的nameserver和通过SystemProperty配置的nameserver是或的关系,所以如果配置了sun.net.spi.nameservice.nameservers,那么相当于绕过了系统配置的nameserver了。
前面说过,非默认的NameService是需要通过相应的NameServiceDescriptor传教的,所以DNSNameService也需要有一个对应的NameServiceDescriptor,就是DNSNameServiceDescriptor:
package sun.net.spi.nameservice.dns; import sun.net.spi.nameservice.*; public final class DNSNameServiceDescriptor implements NameServiceDescriptor {
/**
* Create a new instance of the corresponding name service.
*/
public NameService createNameService() throws Exception {
return new DNSNameService();
} /**
* Returns this service provider's name
*
*/
public String getProviderName() {
return "sun";
} /**
* Returns this name service type
* "dns" "nis" etc
*/
public String getType() {
return "dns";
}
}
可以看到DNSNameService的type为dns,name为sun。所以在配置的时候应该配置为dns,sun
。
但是NameServiceDescriptor本身是怎么加载的呢?回到上面的代码:
Iterator<NameServiceDescriptor> itr = ServiceLoader.load(NameServiceDescriptor.class).iterator();
原来是是通过java.util.ServiceLoader。这个类是专门用来加载service-provider的。这个类有非常详细的JavaDoc。其中有一段说明怎么告诉ServiceLoader加载自定义的ServiceProvider:
A service provider is identified by placing a provider-configuration file in the resource directory META-INF/services. The file's name is the fully-qualified binary name of the service's type. The file contains a list of fully-qualified binary names of concrete provider classes, one per line. Space and tab characters surrounding each name, as well as blank lines, are ignored. The comment character is '#' ('\u0023', NUMBER SIGN); on each line all characters following the first comment character are ignored. The file must be encoded in UTF-8.
Example Suppose we have a service type com.example.CodecSet which is intended to represent sets of encoder/decoder pairs for some protocol. In this case it is an abstract class with two abstract methods:
public abstract Encoder getEncoder(String encodingName);
public abstract Decoder getDecoder(String encodingName);Each method returns an appropriate object or null if the provider does not support the given encoding. Typical providers support more than one encoding. If com.example.impl.StandardCodecs is an implementation of the CodecSet service then its jar file also contains a file named META-INF/services/com.example.CodecSet This file contains the single line:
com.example.impl.StandardCodecs # Standard codecsThe CodecSet class creates and saves a single service instance at initialization:
private static ServiceLoader<CodecSet> codecSetLoader = ServiceLoader.load(CodecSet.class);To locate an encoder for a given encoding name it defines a static factory method which iterates through the known and available providers, returning only when it has located a suitable encoder or has run out of providers.
public static Encoder getEncoder(String encodingName) {
for (CodecSet cp : codecSetLoader) {
Encoder enc = cp.getEncoder(encodingName);
if (enc != null)
return enc;
}
return null;
}A getDecoder method is defined similarly.
所以对于DNSNameServiceDescriptor,在sun/net/spi/nameservice/dns/META-INF/services
目录下有一个名称为sun.net.spi.nameservice.NameServiceDescriptor
的文件:
# dns service provider descriptor
sun.net.spi.nameservice.dns.DNSNameServiceDescriptor
这种机制后来广泛用于Spring的自定义便签(Creating a Custom Spring 3 XML Namespace),估计是受这个影响。
实际应用
知道这个有什么用呢?比如说我们想让我们的应用走谷歌的公共DNS服务器:8.8.8.8。但是我们又不想让用户去修改系统配置,这对用户是个负担,而且我们不希望影响到其他应用。怎么处理呢?只需要在应用启动之前简单配置一下:
System.setProperty("sun.net.spi.nameservice.provider.1", "dns,sun");
System.setProperty("sun.net.spi.nameservice.nameservers", "8.8.8.8");
System.setProperty("sun.net.spi.nameservice.provider.2", "default");
再比如,如果你只想针对某些域名做特殊的解析,那么你可以自定义一个NameServiceProvider,实现对应的NameServiceDescriptor,还有相应的META-INF说明。然后在应用启动的时候配置一下:
System.setProperty("sun.net.spi.nameservice.provider.1", "dns,yourProviderName");
System.setProperty("sun.net.spi.nameservice.provider.2", "default");
参考文章
- DNS: Java Glossary 深入浅出的一篇介绍性文章,强烈推荐。
- Understanding host name resolution and DNS behavior in Java
- Networking Properties
- Local Managed DNS (Java)
Java DNS查询内部实现的更多相关文章
- Java内部DNS查询实现和参数设置
一.Java内部DNS查询 Java使用域名查询时,用的自己内部的域名实现机制,最后都是交给InetAddress去做DNS解析. 源码分析参考:http://blog.arganzheng.me/p ...
- DNS查询相关
本文同时发表在https://github.com/zhangyachen/zhangyachen.github.io/issues/45 一种简单的设计方式是在因特网上使用一个DNS服务器,该服务器 ...
- python dns查询与DNS传输漏洞查询
前言: 昨天晚上在看DNS协议,然后想到了 DNS传输漏洞.便想写一个DNS查询与DNS传输漏洞查询 DNS传输漏洞介绍: DNS传输漏洞:若DNS服务器配置不当,可能导致匿名用户获取某个域的所有记录 ...
- JAVA语言基础内部测试题(50道选择题)
JAVA语言基础内部测试题 选择题(针对以下题目,请选择最符合题目要求的答案,针对每一道题目,所有答案都选对,则该题得分,所选答案错误或不能选出所有答案,则该题不得分.)(每题2分) 没有注明选择几项 ...
- Elasticsearch由浅入深(六)批量操作:mget批量查询、bulk批量增删改、路由原理、增删改内部原理、document查询内部原理、bulk api的奇特json格式
mget批量查询 批量查询的好处就是一条一条的查询,比如说要查询100条数据,那么就要发送100次网络请求,这个开销还是很大的如果进行批量查询的话,查询100条数据,就只要发送1次网络请求,网络请求的 ...
- 域名dns查询_查询域名dns ip地址
最近有部分用户反应管理的天气网站打开偏慢,决定从每一个可以出现的问题点查起!首先就是dns! 通过360dns监控对比发现,同一组域名,15tianqi.cn的dns响应时间比较长,在300-700间 ...
- 转载 DNS查询流程简介
转载请注明出处:http://blog.csdn.net/luotuo44/article/details/45545059 DNS(domain name system),读者们或多或少都听过,就是 ...
- 【Python Network】分解DNS查询结果
针对DNS查询records,通过NS.PTR.CNAME和MX类别不同,返回数据将包含另外主机名.为了解最终的IP地址,通过将返回信息分解.继续使用PyDNS获取详细信息. #! /usr/bin/ ...
- DNS查询的工作原理
二.DNS查询的工作原理 1.DNS查询过程按两部分进行 1.名称查询从客户端计算机开始, 并传送给本机的DNS客户服务程序进行解析 2.如果不能再本机解析查询, 可根据设定的查询DN ...
随机推荐
- 实现代理设置proxy
用户在哪些情况下是需要设置网络代理呢? 1. 内网上不了外网,需要连接能上外网的内网电脑做代理,就能上外网:多个电脑共享上外网,就要用代理: 2.有些网页被封,通过国外的代理就能看到这被封的网站:3. ...
- 带你实现开发者头条APP(三) 首页实现
title: 带你实现开发者头条APP(三) 首页实现 tags: 轮播广告,ViewPager切换,圆形图片 grammar_cjkRuby: true --- 一.前言 今天实现开发者头条APP的 ...
- Opserver开源的服务器监控系统(ASP.NET)
Opserver是Stack Exchange下的一个开源监控系统,系统本身由C#语言开发的ASP.NET(MVC)应用程序,无需任何复杂的应用配置,入门很快.下载地址:https://github. ...
- 谈一谈NOSQL的应用,Redis/Mongo
1.心路历程 上年11月份来公司了,和另外一个同事一起,做了公司一个移动项目的微信公众号,然后为了推广微信公众号,策划那边需要我们做一些活动,包括抽奖,投票.最开始是没有用过redis的,公司因为考虑 ...
- 介绍一款原创的四则运算算式生成器:CalculateIt2
家里小朋友读一年级了,最近每天都有一些10以内的加减法口算练习,作为程序员爸爸,自然也是想办法能够偷懒,让电脑出题,给小朋友做些练习.于是,自己在业余时间开发了一个四则运算算式生成器,名为:Calcu ...
- MVC还是MVVM?或许VMVC更适合WinForm客户端
最近开始重构一个稍嫌古老的C/S项目,原先采用的技术栈是『WinForm』+『WCF』+『EF』.相对于现在铺天盖地的B/S架构来说,看上去似乎和Win95一样古老,很多新入行的,可能就没有见过经典的 ...
- springmvc 多数据源 SSM java redis shiro ehcache 头像裁剪
获取下载地址 QQ 313596790 A 调用摄像头拍照,自定义裁剪编辑头像 B 集成代码生成器 [正反双向](单表.主表.明细表.树形表,开发利器)+快速构建表单; 技术:31359679 ...
- Flexible 弹性盒子模型之CSS flex-basis 属性
实例 设置第二个弹性盒元素的初始长度为 80 像素: div:nth-of-type(2){flex-basis:80px;} 效果预览 浏览器支持 表格中的数字表示支持该属性的第一个浏览器的版本 ...
- jQuery 的选择器常用的元素查找方法
jQuery 的选择器常用的元素查找方法 基本选择器: $("#myELement") 选择id值等于myElement的元素,id值不能重复在文档中只能有一个id值是myE ...
- Prometheus 系统监控方案 一
最近一直在折腾时序类型的数据库,经过一段时间项目应用,觉得十分不错.而Prometheus又是刚刚推出不久的开源方案,中文资料较少,所以打算写一系列应用的实践过程分享一下. Prometheus 是什 ...