自己动手实现DNS协议
1. 主要内容
不说废话,直接进入正题。先说说本文本文的主要内容,好让你决定是否看下去:
- 介绍DNS是干什么的;
- 介绍DNS是如何工作的;
- 介绍DNS请求与响应的消息格式;
- 编程实现一个简单的DNS服务器;
2. DNS是啥
关于DNS是啥,想必学过计算机网络的应该都知道,它是Domain Name System的简写,中文翻译过来就是域名系统,是用来将主机名转换为ip的。事实上,除了进行主机名到IP地址的转换外,DNS通常还提供主机名到以下几项的转换服务:
- 主机命名(host aloasing)。有着复杂规范主机名(canonical hostname)的主机可能有一个或多个别名,通常规范主机名较复杂,而别名让人更容易记忆。应用程序可以调用DNS来获得主机别名对应的规范主机名,以及主机的ip地址。
- 邮件服务器别名(mail server aliasing)。DNS也能完成邮件服务器别名到其规范主机名以及ip地址的转换。
- 负载均衡(load distribution)。DNS可用于冗余的服务器之间进行负载均衡。一个繁忙的站点,如abc.com,可能被冗余部署在多台具有不同ip的服务器上。在该情况下,在DNS数据库中,该主机名可能对应着一个ip集合,但应用程序调用DNS来获取该主机名对应的ip时,DNS通过某种算法从该主机名对应的ip集合中,挑选出某一ip进行响应。
问:为什么会有DNS,或者说为什么要弄出两种方式(主机名和IP地址)来标识一台主机呢?
答:这是因为主机名便于人的记忆,而IP地址便于计算机网络设备的处理,于是需要DNS来做前者到后者的转换。
3. DNS工作原理
DNS实际上是由一个分层的DNS服务器实现的分布式数据库和一个让主机能够查询分布式数据库的应用层协议组成。因此,要了解DNS的工作原理,需要从以上两个方便入手。
3.1 DNS的分布式架构
先来了解DNS的分布式架构。
DNS服务器根据域名命名空间(domian name space)组织成如下图所示的树形结构(当然,只给出部分DNS服务器,只为显示出DNS服务器的层次结构):
在图中,根节点代表的是根DNS服务器,因特网上共有13台,编号从A到M;根DNS服务器之下的一层被称为顶级DNS服务器;再往下一层被称为权威DNS服务器。
当一个应用要通过DNS来查询某个主机名,比如www.google.com的ip时,粗略地说,查询过程是这样的:它先与根服务器之一联系,根服务器根据顶级域名com,会响应命名空间为com的顶级域服务器的ip;于是该应用接着向com顶级域服务器发出请求,com顶级域服务器会响应命名空间为google.com的权威DNS服务器的ip地址;最后该应用将请求命名空间为google.com的权威DNS服务器,该权威DNS服务器会响应主机名为www.google.com的ip。
实际上,除了上图层次结构中所展示的DNS外,还有一类与我们接触更为密切的DNS服务器,它们是本地DNS服务器,我们经常在电脑上配置的DNS服务器通常就是此类。它们一般由某公司,某大学,或某居民区提供,比如Google提供的DNS服务器8.8.8.8;比如常被人诟病的114.114.114.114等。
加入了本地DNS的查询过程跟之前的查询过程基本上是一致的,查询流程如下图所示:
在实际工作中,DNS服务器是带缓存的。即DNS服务器在每次收到DNS请求时,都会先查询自身数据库包括缓存中有无要查询的主机名的ip,若有且没有过期,则直接响应该ip,否则才会按上图流程进行查询;而服务器在每次收到响应信息后,都会将响应信息缓存起来;
3.2 DNS应用层协议
3.2.1 DNS资源记录
在介绍DNS层协议之前,先了解一下DNS服务器存储的资源记录(Resource Records,RRs),一条资源记录(RR)记载着一个映射关系。每条RR通常包含如下表所示的一些信息:
字段 | 含义 |
---|---|
NAME | 名字 |
TYPE | 类型 |
CLASS | 类 |
TTL | 生存时间 |
RDLENGTH | RDATA所占的字节数 |
RDATA | 数据 |
NAME和RDATA表示的含义根据TYPE的取值不同而不同,常见的:
- 若TYPE=A,则name是主机名,value是其对应的ip;
- 若TYPE=NS,则name是一个域,value是一个权威DNS服务器的主机名。该记录表示name域的域名解析将由value主机名对应的DNS服务器来做;
- 若TYPE=CNAME,则value是别名为name的主机对应的规范主机名;
- 若TYPE=MX,则value是别名为name的邮件服务器的规范主机名;
- ……
TYPE实际上还有其他类型,所有可能的type及其约定的数值表示如下:
TYPE | value | meaning |
---|---|---|
A | 1 | a host address |
NS | 2 | an authoritative name server |
MD | 3 | a mail destination (Obsolete - use MX) |
MF | 4 | a mail forwarder (Obsolete - use MX) |
CNAME | 5 | the canonical name for an alias |
SOA | 6 | marks the start of a zone of authority |
MB | 7 | a mailbox domain name (EXPERIMENTAL) |
MG | 8 | a mail group member (EXPERIMENTAL) |
MR | 9 | a mail rename domain name (EXPERIMENTAL) |
NULL | 10 | a null RR (EXPERIMENTAL) |
WKS | 11 | a well known service description |
PTR | 12 | a domain name pointer |
HINFO | 13 | host information |
MINFO | 14 | mailbox or mail list information |
MX | 15 | mail exchange |
TXT | 16 | text strings |
3.2.2 整体及Header部分
下面介绍第二个方面,DNS协议。
DNS请求与响应的格式是一致的,其整体分为Header、Question、Answer、Authority、Additional5部分,如下图所示:
Header部分是一定有的,长度固定为12个字节;其余4部分可能有也可能没有,并且长度也不一定,这个在Header部分中有指明。Header的结构如下:
下面说明一下各个字段的含义:
- ID:占16位。该值由发出DNS请求的程序生成,DNS服务器在响应时会使用该ID,这样便于请求程序区分不同的DNS响应。
- QR:占1位。指示该消息是请求还是响应。0表示请求;1表示响应。
- OPCODE:占4位。指示请求的类型,有请求发起者设定,响应消息中复用该值。0表示标准查询;1表示反转查询;2表示服务器状态查询。3~15目前保留,以备将来使用。
- AA(Authoritative Answer,权威应答):占1位。表示响应的服务器是否是权威DNS服务器。只在响应消息中有效。
- TC(TrunCation,截断):占1位。指示消息是否因为传输大小限制而被截断。
- RD(Recursion Desired,期望递归):占1位。该值在请求消息中被设置,响应消息复用该值。如果被设置,表示希望服务器递归查询。但服务器不一定支持递归查询。
- RA(Recursion Available,递归可用性):占1位。该值在响应消息中被设置或被清除,以表明服务器是否支持递归查询。
- Z:占3位。保留备用。
- RCODE(Response code):占4位。该值在响应消息中被设置。取值及含义如下:
- 0:No error condition,没有错误条件;
- 1:Format error,请求格式有误,服务器无法解析请求;
- 2:Server failure,服务器出错。
- 3:Name Error,只在权威DNS服务器的响应中有意义,表示请求中的域名不存在。
- 4:Not Implemented,服务器不支持该请求类型。
- 5:Refused,服务器拒绝执行请求操作。
- 6~15:保留备用。
- QDCOUNT:占16位(无符号)。指明Question部分的包含的实体数量。
- ANCOUNT:占16位(无符号)。指明Answer部分的包含的RR(Resource Record)数量。
- NSCOUNT:占16位(无符号)。指明Authority部分的包含的RR(Resource Record)数量。
- ARCOUNT:占16位(无符号)。指明Additional部分的包含的RR(Resource Record)数量。
3.2.3 Question部分
Question部分的每一个实体的格式如下图所示:
- QNAME:字节数不定,以0x00作为结束符。表示查询的主机名。注意:众所周知,主机名被"."号分割成了多段标签。在QNAME中,每段标签前面加一个数字,表示接下来标签的长度。比如:api.sina.com.cn表示成QNAME时,会在"api"前面加上一个字节0x03,"sina"前面加上一个字节0x04,"com"前面加上一个字节0x03,而"cn"前面加上一个字节0x02;
- QTYPE:占2个字节。表示RR类型,见以上RR介绍;
- QCLASS:占2个字节。表示RR分类,见以上RR介绍。
3.2.4 Answer、Authority、Additional部分
Answer、Authority、Additional部分格式一致,每部分都由若干实体组成,每个实体即为一条RR,之前有过介绍,格式如下图所示:
- NAME:长度不定,可能是真正的数据,也有可能是指针(其值表示的是真正的数据在整个数据中的字节索引数),还有可能是二者的混合(以指针结尾)。若是真正的数据,会以0x00结尾;若是指针,指针占2个字节,第一个字节的高2位为11。
- TYPE:占2个字节。表示RR的类型,如A、CNAME、NS等,见以上RR介绍;
- CLASS:占2个字节。表示RR的分类,见以上RR介绍;
- TTL:占4个字节。表示RR生命周期,即RR缓存时长,单位是秒;
- RDLENGTH:占2个字节。指定RDATA字段的字节数;
- RDATA:即之前介绍的value,含义与TYPE有关,见以上RR介绍。
DNS协议是工作在应用层的,运输层依赖的是UDP协议。下面尝试使用Python3.6来实现一个简单的DNS服务器。
在此之前先用Wireshark抓一下DNS包,验证一下上面的DNS协议的格式,也便于之后的实现。Wireshark的用法就不做介绍了,相信装好随便点点就知道怎么用了。先打开监听,添加过滤条件,然后用nslookup命令发送一个DNS包,比如我们尝试查询www.baidu.com的ip:
nslookup www.baidu.com
然后可以在Wireshark中看到如下图所示的请求数据包:
响应数据如下图所示:
4. 实现一个简单的DNS服务器
下面用Python来实现一个非常简单的DNS服务器。
4.1 代理功能
首先,它应该具有最基本的“代理”功能,即我们的DNS服务器在接到DNS请求后,直接将请求转发到某DNS服务器(如114.114.114.114)上,然后再将那台DNS的响应结果返回给DNS客户端:
import threading
import socket
import socketserver
class Handler(socketserver.BaseRequestHandler):
def handle(self):
request_data = self.request[0]
# 将请求转发到 114 DNS
redirect_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
redirect_socket.sendto(request_data, ('114.114.114.114', 53))
response_data, address = redirect_socket.recvfrom(1024)
# 将114响应响应给客户
client_socket = self.request[1]
client_socket.sendto(response_data, self.client_address)
class Server(socketserver.ThreadingMixIn, socketserver.UDPServer):
pass
if __name__ == "__main__":
# 一下ip需换成自己电脑的ip
server = Server(('172.16.42.254', 53), Handler)
with server:
server_thread = threading.Thread(target=server.serve_forever)
server_thread.daemon = True
server_thread.start()
print('The DNS server is running at 172.16.42.254...')
server_thread.join()
现在我们的DNS服务器就可以进行转发工作了。运行以上程序(需root权限),然后用nsloop命令,向我们的服务器发送DNS请求,一切OK:
$ nslookup baidu.com 172.16.42.254
Server: 172.16.42.254
Address: 172.16.42.254#53
Non-authoritative answer:
Name: baidu.com
Address: 123.125.114.144
Name: baidu.com
Address: 180.149.132.47
Name: baidu.com
Address: 111.13.101.208
Name: baidu.com
Address: 220.181.57.217
4.2 缓存功能
如果仅仅做一下代理转发,那也太无聊了。现在我们再加上缓存功能,即它能够将其他DNS服务器的响应结果缓存起来。当收到请求时,若请求主机号在缓存中,且没有过期,则直接响应缓存结果;否则进行上一功能中的操作。这一功能的关键在于对DNS消息的解析,代码如下:
class Message:
u"""All communications inside of the domain protocol are carried in a single format called a message"""
def __init__(self, header, question=None, answer=None, authority=None, additional=None):
self.header = header
self.question = question
self.answer = answer
self.authority = authority
self.additional = additional
@classmethod
def from_bytes(cls, data):
scanner = Scanner(data)
# 读取header
header = dict()
header['ID'] = scanner.next_bytes(2)
header['QR'] = scanner.next_bits(1)
header['OPCODE'] = scanner.next_bits(4)
header['AA'] = scanner.next_bits(1)
header['TC'] = scanner.next_bits(1)
header['RD'] = scanner.next_bits(1)
header['RA'] = scanner.next_bits(1)
header['Z'] = scanner.next_bits(3)
header['RCODE'] = scanner.next_bits(4)
header['QDCOUNT'] = scanner.next_bytes(2)
header['ANCOUNT'] = scanner.next_bytes(2)
header['NSCOUNT'] = scanner.next_bytes(2)
header['ARCOUNT'] = scanner.next_bytes(2)
print('header:', header)
# 读取question
questions = list()
for _ in range(header['QDCOUNT']):
question = dict()
question['QNAME'] = scanner.next_bytes_until(lambda current, _: current == 0)
scanner.next_bytes(1) # 跳过0
question['QTYPE'] = scanner.next_bytes(2)
question['QCLASS'] = scanner.next_bytes(2)
questions.append(question)
print('questions:', questions)
message = Message(header)
# 读取answer、authority、additional
rrs = list()
for i in range(header['ANCOUNT'] + header['NSCOUNT'] + header['ARCOUNT']):
rr = dict()
rr['NAME'] = cls.handle_compression(scanner)
rr['TYPE'] = scanner.next_bytes(2)
rr['CLASS'] = scanner.next_bytes(2)
rr['TTL'] = scanner.next_bytes(4)
rr['RDLENGTH'] = scanner.next_bytes(2)
# 处理data
if rr['TYPE'] == 1: # A记录
r_data = scanner.next_bytes(rr['RDLENGTH'], False)
rr['RDATA'] = reduce(lambda x, y: y if (len(x) == 0) else x + '.' + y,
map(lambda num: str(num), r_data))
elif rr['TYPE'] == 2 or rr['TYPE'] == 5: # NS与CNAME记录
rr['RDATA'] = cls.handle_compression(scanner, rr['RDLENGTH'])
rrs.append(rr)
answer, authority, additional = list(), list(), list()
for i, rr in enumerate(rrs):
if i < header['ANCOUNT']:
answer.append(rr)
elif i < header['ANCOUNT'] + header['NSCOUNT']:
authority.append(rr)
else:
additional.append(rr)
print('answer:', answer)
print('authority:', authority)
print('additional:', additional)
return message
@classmethod
def handle_compression(cls, scanner, length=float("inf")):
"""
The compression scheme allows a domain name in a message to be represented as either:
- a pointer
- a sequence of labels ending in a zero octet
- a sequence of labels ending with a pointer
"""
byte = scanner.next_bytes()
if byte >> 6 == 3: # a pointer
pointer = (byte & 0x3F << 8) + scanner.next_bytes()
return cls.handle_compression(Scanner(scanner.data, pointer))
data = scanner.next_bytes_until(lambda current, offset: current == 0 or current >> 6 == 3 or offset > length)
if scanner.next_bytes(move=False) == 0: # a sequence of labels ending in a zero octet
scanner.next_bytes()
return data
# a sequence of labels ending with a pointer
result = data + '.' + cls.handle_compression(Scanner(scanner.data, *scanner.position()))
scanner.next_bytes(2) # 跳过2个字节的指针
return result
其中用到了一个自定义的Scanner类,用来帮助我们从bytes中按字节或位读取数据,其定义如下:
class Scanner:
"""scan bytes"""
__mark_offset_byte, __mark_offset_bit = 0, 0
def __init__(self, data: bytes, offset_byte=0, offset_bit=0):
self.data = data
self.__offset_byte = offset_byte
self.__offset_bit = offset_bit
def next_bits(self, n=1):
if n > (len(self.data) - self.__offset_byte) * 8 - self.__offset_bit:
raise RuntimeError('剩余数据不足{}位'.format(n))
if n > 8 - self.__offset_bit:
raise RuntimeError('不能跨字节读取读取位')
result = self.data[self.__offset_byte] >> 8 - self.__offset_bit - n & (1 << n) - 1
self.__offset_bit += n
if self.__offset_bit == 8:
self.__offset_bit = 0
self.__offset_byte += 1
return result
def next_bytes(self, n=1, convert=True, move=True):
if not self.__offset_bit == 0:
raise RuntimeError('当前字节不完整,请先读取完当前字节的所有位')
if n > len(self.data) - self.__offset_byte:
raise RuntimeError('剩余数据不足{}字节'.format(n))
result = self.data[self.__offset_byte: self.__offset_byte + n]
if move:
self.__offset_byte += n
if convert:
result = int.from_bytes(result, 'big')
return result
def next_bytes_until(self, stop, convert=True):
if not self.__offset_bit == 0:
raise RuntimeError('当前字节不完整,请先读取完当前字节的所有位')
end = self.__offset_byte
while not stop(self.data[end], end - self.__offset_byte):
end += 1
result = self.data[self.__offset_byte: end]
self.__offset_byte = end
if convert:
if result:
result = reduce(lambda x, y: y if (x == '.') else x + y,
map(lambda x: chr(x) if (31 < x < 127) else '.', result))
else:
result = ''
return result
然后需要做的是当收到114 DNS服务器的响应消息后,将消息缓存起来:
# 缓存响应结果
message = Message.from_bytes(response_data)
message.save()
以上save方法就是将message中包含的各条RR保存起来,可以直接用一个集合来保存,也可以保存在一些专业的缓存设施中,比如redis。需要注意的是TTL的处理,若用redis缓存,它自带了TTL功能,可以直接使用。若是自己实现的,需要在保存的时候记录当前的时间,以便取出的时候能够判断是否过期。这些应该很容易实现,但是本人比较懒,这里就不写了……
4.3 添加记录功能
最后,它需要具备能够读取我们自定义的记录,并将记录加入缓存。。这个也不想写了……
另外,Message类还应该有一个to_bytes
方法,它能将一个Message对象转换为bytes对象,用于将 从缓存中取出的数据(即RR记录)转换为bytes,返回给用户。这个其实就是from_bytes
的逆过程,但实现起来应该比from_bytes
简单许多,因为你可以不使用指针来压缩数据,这样处理起来就没什么难度了。同样不想写了……
最后稍微做一下测试,算是做个结束:
if __name__ == "__main__":
server = Server('172.16.42.254')
server.start()
使用nsloop发送DNS请求到我们自己写的服务器上,响应结果如下:
$ nslookup api.sina.com.cn 172.16.42.254
Server: 172.16.42.254
Address: 172.16.42.254#53
Non-authoritative answer:
api.sina.com.cn canonical name = common6.dpool.sina.com.cn.
Name: common6.dpool.sina.com.cn
Address: 123.126.56.253
在运行的控制台中,打印出了从114 DNS返回的数据的解析结果:
$ sudo python3 dns.py
The DNS server is running at 172.16.42.254...
header: {'ID': 25835, 'QR': 1, 'OPCODE': 0, 'AA': 0, 'TC': 0, 'RD': 1, 'RA': 1, 'Z': 0, 'RCODE': 0, 'QDCOUNT': 1, 'ANCOUNT': 2, 'NSCOUNT': 4, 'ARCOUNT': 4}
questions: [{'QNAME': 'api.sina.com.cn', 'QTYPE': 1, 'QCLASS': 1}]
answer: [{'NAME': 'api.sina.com.cn', 'TYPE': 5, 'CLASS': 1, 'TTL': 56, 'RDLENGTH': 16, 'RDATA': 'common6.dpool.sina.com.cn'}, {'NAME': 'common6.dpool.sina.com.cn', 'TYPE': 1, 'CLASS': 1, 'TTL': 34, 'RDLENGTH': 4, 'RDATA': '123.126.56.253'}]
authority: [{'NAME': 'dpool.sina.com.cn', 'TYPE': 2, 'CLASS': 1, 'TTL': 26753, 'RDLENGTH': 6, 'RDATA': 'ns1.sina.com.cn'}, {'NAME': 'dpool.sina.com.cn', 'TYPE': 2, 'CLASS': 1, 'TTL': 26753, 'RDLENGTH': 6, 'RDATA': 'ns3.sina.com.cn'}, {'NAME': 'dpool.sina.com.cn', 'TYPE': 2, 'CLASS': 1, 'TTL': 26753, 'RDLENGTH': 6, 'RDATA': 'ns2.sina.com.cn'}, {'NAME': 'dpool.sina.com.cn', 'TYPE': 2, 'CLASS': 1, 'TTL': 26753, 'RDLENGTH': 6, 'RDATA': 'ns4.sina.com.cn'}]
additional: [{'NAME': 'ns1.sina.com.cn', 'TYPE': 1, 'CLASS': 1, 'TTL': 26674, 'RDLENGTH': 4, 'RDATA': '202.106.184.166'}, {'NAME': 'ns2.sina.com.cn', 'TYPE': 1, 'CLASS': 1, 'TTL': 26652, 'RDLENGTH': 4, 'RDATA': '61.172.201.254'}, {'NAME': 'ns3.sina.com.cn', 'TYPE': 1, 'CLASS': 1, 'TTL': 26509, 'RDLENGTH': 4, 'RDATA': '123.125.29.99'}, {'NAME': 'ns4.sina.com.cn', 'TYPE': 1, 'CLASS': 1, 'TTL': 26497, 'RDLENGTH': 4, 'RDATA': '121.14.1.22'}]
以上完整代码,见这里
5. 参考资料
- 计算机网络,自顶向下方法(原书第6版)/(美)库罗斯(Kurose,J.F.),(美)罗斯(Ross,K.W.)著;陈鸣译.——北京:机械工业出版社,2014.9
- RFC 1034
- RFC 1035
- BlackHole
- 结合Wireshark分析DNS 协议
自己动手实现DNS协议的更多相关文章
- 利用WireShark进行DNS协议分析
一.准备工作 系统是Windows 8.1Pro 分析工具是WireShark1.10.8 Stable Version 使用系统Ping命令发送ICMP报文. 二.开始工作 打开CMD.exe键入: ...
- DNS协议 实践
根据DNS协议发送UDP请求,然后获取IP地址 头文件: #ifndef __DNS__ #define __DNS__ #include <stdio.h> #include <s ...
- 计算机网络——DNS协议的学习与实现
1. 主要内容 不说废话,直接进入正题.先说说本文本文的主要内容,好让你决定是否看下去: 介绍DNS是干什么的: 介绍DNS是如何工作的: 介绍DNS请求与响应的消息格式: 编程实现一个简单的DNS服 ...
- 网络协议 16 - DNS 协议:网络世界的地址簿
[前五篇]系列文章传送门: 网络协议 11 - Socket 编程(下):眼见为实耳听为虚 网络协议 12 - HTTP 协议:常用而不简单 网络协议 13 - HTTPS 协议:加密路上无尽头 网络 ...
- IP、TCP、DNS协议
·······················································IP协议························· 位于网络层,作用是把数据包传送 ...
- 简单谈谈DNS协议
DNS协议也可以称为DNS服务,全称是Domain Name System,即域名系统,和HTTP协议一样,也是一个位于应用层的协议(服务),它是基于运输层的UDP协议的,关于网络协议的分层介绍,见这 ...
- DNS协议工作过程;DNS的安全隐患
DNS协议工作过程 下面以域名为m.xyz.com的主机欲通过另一个主机的域名y.abc.com的IP地址为例,简述DNS协议过程. 主机m.xyz.com先向其本地服务器dns.xyz.com进 ...
- DNS协议详解
DNS协议详解 简介 DNS(Domain Name System)域名系统,主要实现的功能是将域名转换成ip地址的一个服务.它是由一个分层的DNS服务器实现的分布式数据库,同时.他也是一个使得主机能 ...
- 应用层协议FTP、DNS协议、HTTP协议分析
分析所用软件下载:Wireshark-win32-1.10.2.exe 一.阅读导览 1.分析FTP协议 2.分析DNS协议 3. 分析HTTP协议 二.分析要求 (1)ftp部分: 学习 Serv- ...
随机推荐
- Beego学习笔记5:MVC-VC
MVC-VC 1> 新建一个user.go控制器,其代码如下: package controllers import ( "webapp/models" " ...
- js学习之数据结构和算法
js中的数据结构 1.列表 待办事项列表.购物清单.最佳十名榜单等等. 适用: 1)数据结构较为简单, 2)不需要在一个长序列中查找元素,或者对其进行排序 2.栈 一摞盘子 ----- 添加删除只能从 ...
- ThinkPHP5 Nginx通用配置
项目配置内容如下: server { #配置监听端口 listen 80; listen [::]:80; #设置网站根目录作为变量root set $root /var/www/web; #设置网站 ...
- Cheat Engine 特征码
打开游戏 引用自动注入 搜索特征码 特征码,是游戏运行中,唯一的汇编代码 验证,特征码是游戏中唯一代码 添加特征码 添加特征码,以及金钱只增不减代码 进行激活测试 特征码,只有在游戏过程中才会执行到 ...
- Linux 下的7种文件类型
普通文件类型 (-)Linux中最多的一种文件类型, 包括 纯文本文件(ASCII):二进制文件(binary):数据格式的文件(data);各种压缩文件.第一个属性为 [-] ,这些文件一般是用一些 ...
- Java 浮点数的精确度问题
题目:分别获取数字的整数部分.小数部分,如15.12,整数部分为15,小数部分为0.12 package my_package; public class Divide { public static ...
- Solr基础理论【倒排索引,模糊查询】
一.简介 现有的许多不同类型 的技术系统,如关系型数据库.键值存储.操作磁盘文件的map-reduce[映射-规约]引擎.图数据库等,都是为了帮助用户解决颇具挑战性的数据存储与检索问题而设计的.而搜索 ...
- java 获取当前方法的被调用信息(被那个方法那个类那一行调用)
public void testMethod(){ Test1 t1 = new Test1(); t1.my(); } public static void main(String[] args) ...
- js检测页面触底
<script> function getDocumentTop() { var scrollTop = 0, bodyScrollTop = 0, documentScrollTop = ...
- tp5 模型中配置数据库连接信息
namespace app\api\model; use think\Model; class BaseModel extends Model { protected $connection = [ ...