0%

字节码增强技术,作为Java程序员来说,是个既熟悉又陌生的词,熟悉的是几乎我们无时无刻不再使用着字节码增强技术,陌生则是因为对于开发人员尤其是业务开发来说,实在是没有机会或需求要用到字节码增强相关的技术手段。为此,本文通过简单的介绍来揭开字节码增强技术的神秘面纱,简化起见,本文不对相关技术工具进行深入的介绍,只做综述性质的介绍,旨在让读者有个宏观的认识从而减少初入门时查多方资料的辛苦。

(名词解释: 字节码增强,英文是Bytecode Instrumentation, 那如何理解单词Instrumentation?(本人对这个单词迷惑了很久后,终于有了自己的理解:)) 从词根instrument看,instrument名词形态是工具、器械,动词形态是给什么装上器械,那么instrumentation可以理解为instrument动词形态的名词形式,也就是装上器械、工具,可以理解为要起飞了,所以中文翻译成增强还是很贴切的。)

技术分类

前面也提到了,我们在开发过程中,无时无刻的使用着字节码增强技术,例如简化代码的Lombok,最常用的AOP技术以及各类框架类工具,如代码覆盖率Jacoco、链路追踪的Skywalking、性能分析工具Arthas等等。
整体分类图

虽然这些工具都统称用到了字节码增强技术,然而其实用到的技术都各有差异,为此我们对不同工具的用途与使用时机进行一次全面的梳理。

从狭义来讲,字节码增强讲的就是对已经是字节码的class文件进行操作,那么主要有两种工具,一个是ASM,另一个是Javassit,ASM是纯粹的对字节码按照java的规范就行字节码理解范畴内的进行修改操作,可以说门槛很高; Javassit则可以理解为是一个提供了对字节码操作API的框架,来简化字节码操作的门槛,让字节码操作像面向对象编程一样简单;因此,由想而知,ASM要比Javassit性能要好,为此,为了鱼和熊掌兼得,我们从ASM基础上又衍生出了CGLib,虽然功能没有ASM强悍,但使用相对简单了很多。

那么从广义上来讲,所有让代码具备原本不具有的功能,那种类似魔法效果的技术,都统称为字节码增强。
字节码生命周期

从字节码生命周期看,除了字节码class文件外,还包括从源代码生成字节码的过程,以及字节码进入运行时后classloader加载前,以及加载后的运行。

正如AOP技术可以分为静态织入和动态织入(参见会用就行了?你知道 AOP 框架的原理吗?),字节码也可以分为编译前、编译后、加载前、加载后几个阶段。
字节码使用阶段

类加载器限制

正如前面介绍的字节码操作工具,ASM、Javassit等可以对class进行操作修改,那是不是可以对一个已经加载的类进行修改并重载呢? 答案是不能。因为Classloader在加载类的时候会进行校验,对于已经加载过的类是不允许重复加载的。那如果字节码增强技术只能用在加载之前的范围,那可以使用的范围就特别有限了,好在Java提供了Instrument技术,也称agent技术,让这些字节码工具能够在类加载后也能进行修改,给字节码技术带来了广阔的使用范围和应用空间。

详细介绍

下面分别对几个字节码增强技术进行分别的介绍。

APT

APT (Annotation Processing Tool) 注解处理工具是Java提供的在编译时针对源代码内注解暴露开放点,进行额外处理。在源代码编译成字节码的过程中,编译器会首先把Source Code解析成抽象语法树AST,最终再编译成字节码。在这个过程中,APT可以根据注解,来对AST语法树进行修改,从而实现最终字节码的修改。以Lombok为例,在整个编译过程中,当遇到属于Lombok的注解时,编译器就会执行Lombok的Handler来实现功能增强。
APT处理过程

ASM、Javassit

ASM、Javassit以及CGLib都是针对是字节码的文件进行操作修改,这几个的区别已经在前文讲述,这里不再赘述,至于具体如何使用,不再本文范围内,有兴趣的可以参考此文:https://www.infoq.cn/article/kzmlUsizYFlw7F9t5jPO

Instrument

为支持字节码技术能对已加载的类进行操作,Java提供了JVMTI的接口,最常见的就是debug功能,其启动命令是:-agentlib:jdwp=transport=dt_socket,address=7085,server=y,suspend=n, 可以看到利用```agentlib``这个参数就能注入agent相关代码,从而进行插桩。

Agent技术提供了两种使用方式,一个是在启动的时候,正如前面的debug命令一样,在启动时添加agentlib参数,另一个则是在运行时动态attach的方式,通过 Attach API,将模块(jar 包)动态地 Attach 到指定进程 id 的 Java 进程内。为此,也提供了两个入口方法:

1
2
public static void premain(String agentArgs, Instrumentation inst);
public static void agentmain(String agentArgs, Instrumentation inst);

分别对应两种不同的启动方式,当启动的时候,则调用premain方法,当运行中attach的时候,则是调用agentmain方法.

最早的agent lib实现方式都是采用Native方式实现,即c/c++的方式,这对于Java开发者来说门槛很高,为此,从Java 5之后,提供了java语言实现的方式,即大名鼎鼎的Instrument包,启动参数是: -javaagent,例如Aspectj动态方式的运行命令是:-javaagent:path/aspectjweaver.jar
那么问题来了,JVMTI接口使用的是Native方式,那java是怎么运行JAVA语言实现的agent包呢?

这里用到了一个名叫instrument.so的动态链接库, 通过它我们实现了将JVMTI接口与instrument包的java接口得到了打通。即-javaagent命令相当于-agentlib:instrument

重要知识

前面简单快速的介绍了Java字节码增强技术的概览,但作者在研究学习的时候,遇到了好几个重要的且令人疑惑的知识点,这里也进行一个重要阐述,与各位进行分享。

侵入/非侵入 v.s. 固化/非固化

使用agent技术的时候,我们常常说实现了非侵入的特性,但其实并不准确,我不使用agent技术,例如使用注解或者AOP等方式,也称作是非侵入的方式。从前面的介绍来看,显然agent技术的非侵入是与我们常说的非侵入不是一个概念了,为此,引出了一个固化与非固化的概念。

利用agent技术,agent携带的功能是可以随时加载或卸载的,也就是功能不是固化的,但类似AOP等方式,虽然是非侵入的,但具备的功能是早就固化了,不再能够卸载或者增加。

因此,我们可以细分下概念:

  • 侵入: 引入的功能是否会侵入到业务代码里,讨论的对象是“业务代码”。
  • 固化: 引入的功能是否可以动态的添加和卸载,是完全与运行的程序独立的,这里讨论的对象是“整个程序”。

Spring AOP v.s. Aspectj AOP

众所周知Spring AOP采用的是代理的模式,利用采用Java Dynamic Proxy或者CGLib的方式来实现,其原理是创建一个新的代理类来代理目标类,通过对代理类增加功能来实现AOP的切面。而Aspectj则有所不同,是直接通过修改目标类,在目标类里“织入”代码来实现切面。因此,这也是为什么Spring AOP会存在”Self Invocation”的问题,而Aspectj却存在的原因。

特别说明:
动态织入的AOP属于加载前还是加载后是有点模糊的地带,从AOP角度看,不管是用代理方式(Spring AOP)还是织入的方式(Aspectj Agent)都是属于加载程序之后了,但从字节码技术角度看,严格来说Spring AOP是属于加载前的技术,因为Spring AOP不管是JDK动态代理还是CGLib,都是生成一个新的代理类,并未修改原始的class,所以并不属于加载后阶段。

总结

本文以相对简短并快速的方式来简明扼要的介绍了字节码增强技术的相关知识,包括工具使用的所属阶段以及工具的分类,最后对侵入/非侵入,固化/非固化,Spring AOP,Aspectj AOP一些相对模糊的概念进行了重点讲解。

参考

译者注: 本系列总共有5篇,对于软件开发人员讲的很通俗易懂,有助于我们对进程内存的理解,故拿来翻译一下,希望对大家有帮助。

  1. 内存类型
  2. 理解进程内存
  3. 管理内存
  4. 自定义内存分配器
  5. debugging工具

引言

在Intersec公司我们选择使用C语言作为编程语言,因为C语言对于我们所做的事情上给予了全部的控制力,可以达到一个很高的性能水平。对于大多数人,性能是尽可能的减少CPU指令。但是,在现代硬件上是远远要比单纯的CPU复杂的多,任何一个算法都要考虑内存、cpu、硬盘以及网络IO的处理,这些都增加了算法的代价。只有适当的理解了这些,才能保证算法的性能和稳定性。

cpu对于性能以及磁盘、网络对于延迟的影响都能很容易被理解,但是对于内存的影响就没那么容易被理解。在我们与客户打交道中发现,即使是被广泛使用的工具,例如top命令,大多数系统管理员也无法很明确的理解这些工具的输出内容。

本文是本系列的第一篇(总共五篇),我们将介绍这些主题——内存的定义、内存是如何被管理的、如何理解相关工具的输出结果等等,这些应该会是开发和系统管理员都感兴趣的。虽然绝对大多数的规则都适用于大多数现代操作系统,但我们具体只针对Linux系统和C语言来介绍。本文不是第一个写关于内存的,我们强烈推荐这篇高质量的文章(作者:Ulricht Drepper)—— What every programmer should know about memory.(翻译文:https://www.oschina.net/translate/what-every-programmer-should-know-about-memory-part1?lang=chs&

本系列第一篇将介绍内存的定义。 我们假设读者已经对内存地址和进程有了一定基础的理解,包括系统调用、用户态与内核态的区别。你还需要知道,进程是运行在内核之上的,进程要获取资源,需要通过系统调用(system call),由内核与硬件交互。更多关于系统调用的细节,可以通过阅读相关的手册。

虚拟内存(Virtual Memory)

现代操作系统里,每一个进程都是活动到自己的内存空间中。操作系统并不是直接把内存地址映射到硬件地址的,作为一个硬件抽象层,操作系统为每一个进程提供了独有的内存空间。操作系统为每一个进程在内核中维护了一个地址转换表,cpu通过这张表实现了虚拟地址与物理内存地址的转换(每次内核进行进程切换,都会改变这个地址转换表)。
译者注:这个地址转换表在linux中就是页表,进程间发生context switch,就会切换整个页目标的基址,以实现不同进程的同一个虚拟地址映射到不同的物理内存。

虚拟内存有多个目标。首先是让进程间能够相互隔离。一个进程在用户态中访问内存是以虚拟地址来表达的,因此,它只能访问自己空间中的内存,而不能访问到其他进程的内存空间(除非是显示的声明为共享)。

第二个目标是对硬件的抽象。内核能够随意的将一个物理地址指向一个虚拟地址,同时也能选择对一个虚拟内存不申请真实的物理内存,直到真正的需要被使用为止。此外,当占用的内存长时间不使用或者内存不够的时候,还能将内存交换到磁盘,来释放内存占用。这样对于内核使用硬件而言就很自由,唯一的约束就是要确保读的时候是之前写的同一块内存。

第三个目标是对于那些非内存的硬件资源也能用类似的地址方式来访问,这是mmap和文件映射背后的原理。通过一个与文件映射的虚拟地址,你能够用访问内存的方式通过内存缓冲(Memery Buffer)来访问该文件。这是个非常有用的抽象,能够使得代码非常的简单,尤其是在64位的操作系统上,有了一个巨大的虚拟地址空间,只要你想,你能够映射整个磁盘。

第四个目标是共享。由于内核知道每个进程的虚拟地址映射的情况,内核知道如何避免同样的物理资源被加载多次。为此,内核采用写时复制(Copy-on-Write, COW)的方式来实现共享。即,当两个进程共享同一块数据时,如果有其中一个进程要修改而另一个又不希望看到该变化(译者注:对于进程而言这块内存是私有的),内核会对修改的数据做一份copy。最近,操作系统更具有了对不同地址空间中侦测同一内存的能力,能够自动让这些内存映射到同一个物理地址,在Linux中称为KSM技术(Kernel SamePage Merging)。

fork()

写时复制(COW)最常见的例子就是fork()函数了。 在类Unix的系统中,fork()是一个系统调用,利用复制当前进程来创建一个新进程。当fork()函数返回时,两个父子进程会在同样的代码位置、同样的文件打开表以及同样的内存空间下继续执行。有了COW,当fork一个进程时,fork()函数不再需要重新复制一遍进程的内存,只有当父子进程对数据有修改时,才需要复制。由于大多数fork()的使用是伴随着exec函数的使用,该函数会使得当前的虚拟内存空间整个无效,所以COW的机制能够很好的避免内存复制的浪费。

fork()函数的另外一个效果是可以没有任何代价的创建一个进程的快照。如果你想对进程的内存进行一些操作,但又不想对当前进程产生数据修改的风险,而且也不想使用代价高且容易产生错误的锁机制方式,那么就请使用fork,将你的操作在fork出来的子进程上操作,最后将计算结果返回给父进程,例如通过return code,文件,共享内存,管道等等。只要你的计算足够快,使得大部分的内存仍然是父子进程共享的,那么这种方式是特别好的。它能使得你的代码很简单,将隐藏的复杂性交给了内核的虚拟内存而不是你。

内存页(Pages)

虚拟内存被划分为一个个页(page),页是它的最小单位。页的大小受CPU影响,通常是4KB。当需要更多内存的时候,内核会给你一页或更多的内存页。同样,释放内存的时候也是按页为单位。对于每一个已分配的内存页,内核都保持了一组权限,即该页是否可读、可写以及可执行。这些权限要么是在分配内存的时候设置或者是之后调用mprotect()函数来设置。内存页在没被分配的时候是不能被访问的,当你对内存页做一个不被允许的操作时,会触发一个段错误(Segmentation Fault),例如对一个没有读权限的内存页进行读操作。

内存类型(Memory Types)

在虚拟内存中,并不是所有的内存是一样的。我们可以用横纵坐标来区分内存类型,横轴是内存是私有还是共享,纵轴是内存是否是基于文件(file-backed)的,对于不是基于文件的内存,我们称为匿名(anonymous)内存。


PRIVATE SHARED
ANONYMOUS stack
malloc()
mmap(ANON, PRIVATE)
brk()/sbrk()
mmap(ANON, SHARED)
FILE-BACKED mmap(fd, PRIVATE)
binary/shared libraries
mmap(fd, SHARED)

私有内存(Private Memory)

私有内存,顾名思义,就是该内存是进程独有的。在我们程序中,大多数内存都是私有内存。因为在私有内存上的修改是对其他进程不可见的,所以采用的是写时复制(COW)。因此,基于COW的方式,即使内存是私有的,几个进程仍然可以通过同一块物理内存来共享数据,特别是共享库和二进制文件。一个常见的错误认识是,KDE因为每个进程都会加载Qt以及KDElibs而会消耗大量的内存。但实际上不会,因为有COW机制,所以所有的进程只会访问这些共享库只读部分所在的同一份物理内存。

共享内存(Shared Memory)

共享内存是设计作为进程间通信用的。它只能通过显示的mmap或者专用的shm*函数调用来创建。当一个进程在共享内存上写数据时,数据的修改能够被所有与该内存相关的进程看到。
如果内存是基于文件的,任何映射了该文件的进程都能看到文件的修改,因为修改会通过文件来传递。

匿名内存(Anonymous Memory)

匿名内存是只存在于RAM的内存。对于匿名内存,内核只有当进行写操作时,才会真的分配物理地址给该内存。因此,匿名内存在被真正使用之前,是不会对内核产生任何代价的。这使得进程可以在虚拟内存中保留很多内存而又不需要真正使用物理内存。也因此,内核允许进程拥有比实际内存更多的虚拟内存。这种方式通常被称为内存过量使用,over-commit (memory overcommitment)。

基于文件的内存和交换区(File-backed and Swap)

当一个内存类型是基于文件的,那么表示该内存的数据是从磁盘加载而来。大多数情况,都是按需加载的,但也可以主动告知内核,使其预先加载。当你知道具体访问方式的时候,比如顺序访问,那么预先加载有助于你的程序跑的更快。为了避免占用太多内存,你也可以告知内核来处理内存的卸载。这些都可以通过madvise()的系统调用来实现。

当系统缺少物理内存时,内核会试着移动一些内存到磁盘里。如果内存是基于文件且共享的,那么是相当容易的。因为文件才是数据的源头,所以只需要从RAM中移出即可,等到下次需要读的时候,再加载回内存。

对于匿名或者私有的内存,内核也能够从RAM中移出,通过将数据写入到指定的磁盘区域中来实现,这块区域叫做交换区(swap),这种移出方式称为换出(swap-out)。在Linux中,swap区是一个特定的磁盘分区,而其他系统可能是特别的文件。它的工作方式和文件一样,当需要访问的时候再次从swap区中加载。

有了虚拟地址空间,内存页的换进/换出(swap in/out)对于进程而言都是透明的,唯一的区别的是从磁盘访问的延时较长。

总结

本文主要讲述了一些关于内存的重要概念。虽然,我们已经谈论到了物理内存以及与保留的地址空间的区别,但还未涉及实际进程的内存处理。在下一篇文章中,我们会讲述与这个话题并介绍一些工具让你明白一个进程对内存的占用。

Project Reactor提供了很多创建Mono/Flux的静态方法,而最常用的就是Mono#create方法,通过该方法能把以前命令式的程序转化为Reactive的编程方式。
众所周知,Reactive Programming是一种Pull-Push模型,其中Pull用于实现back-pressure,push则是常见的推模型,也是reactive programming的重点(这里不再深入讲解pull/push模型两者的区别)。下面以一个常见的Pull模型迭代器Iterator来说明如何将传统代码转为Reactive的代码。

Iterator -> Flux

1
2
3
4
5
6
7
8
//创建一个迭代器
Iterator it = Arrays.asList<>(1,2,3).iterator();

//使用迭代器
while(it.hasNext()) {
//模拟业务逻辑 —— 这里直接打印value
System.out.println(it.next());
}

上面是一个常见的迭代器使用方式,下面看看是如何将迭代器转换成Flux的:

1
2
3
4
5
6
7
8
9
10
11
12
//创建迭代器
Iterator it = Arrays.asList<>(1,2,3).iterator();

Flux<Integer> iteratorFlux = Flux.create(sink -> {
while (it.hasNext()) {
sink.next(it.next()); //利用FluxSink实现data的Push
}
sink.complete(); //发送结束的Signal
});

//进行订阅,进行业务逻辑操作
iteratorFlux.subscribe(System.out::println);

MonoCreate常见的两者使用方式

传统命令式编程除了Iterator的Pull模式外,通常还有Observable以及Callback这两种Push模式,下面分别举例讲讲这两种模式。

Observable -> MonoCreate

Observable原始代码举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Observable observable = new Observable() {
//需要重写Observable,默认是setChanged与notifyObservers分离,实现先提交再通知的效果
//这里为了简单起见,将通知与提交放在了一起
@Override
public void notifyObservers(Object arg) {
setChanged();
super.notifyObservers(arg);
}
};
Observer first = (ob,value) -> {
System.out.println("value is " + value);
};
observable.addObserver(first);
observable.notifyObservers("42");

// after some time, cancel observer to dispose resource
observable.deleteObserver(first);

MonoCreate的转化示例:

1
2
3
4
5
6
7
8
9
Mono<Object> observableMono = Mono.create(sink -> {
Observer first = (ob, value) -> {
sink.success(value);
};
observable.addObserver(first);
observable.notifyObservers("42");
sink.onDispose(() -> observable.deleteObserver(first));
});
observableMono.subscribe(v -> System.out.println("value is " + v));

Callback -> MonoCreate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//callback example
FutureCallback<HttpResponse> callback = new FutureCallback<HttpResponse>() {
@Override
public void completed(HttpResponse result) {
System.out.println("Response: " + result.getStatusLine());
}

@Override
public void failed(Exception ex) {
System.out.println("Fail in " + ex);
}

@Override
public void cancelled() {
System.out.println("Cancelled");
}
};

CloseableHttpAsyncClient httpclient = HttpAsyncClients.createDefault();
httpclient.start();

HttpGet request = new HttpGet("http://www.example.com/");
httpclient.execute(request, callback);

MonoCreate的转化示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
CloseableHttpAsyncClient httpclient = HttpAsyncClients.createDefault();
httpclient.start();

Mono<HttpResponse> responseMono = Mono.create(monoSink -> {
//创建response callback的处理类,并传入monoSink供使用
CallbackHandler callbackHandler = new CallbackHandler(monoSink);
HttpGet getRequest = new HttpGet("http://www.example.com/");
httpclient.execute(getRequest, callbackHandler.getResponseCallback());
});
responseMono.subscribe(response -> System.out.println("Response: " + response.getStatusLine()));

@Data
static class CallbackHandler {
private MonoSink monoSink;
private FutureCallback<HttpResponse> responseCallback;

public CallbackHandler(MonoSink monoSink) {
this.monoSink = monoSink;
responseCallback = new FutureCallback<HttpResponse>() {
@Override
public void completed(HttpResponse result) {
monoSink.success(result);
}

@Override
public void failed(Exception ex) {
monoSink.error(ex);
}

@Override
public void cancelled() {
monoSink.onDispose(() -> System.out.println("cancelled"));
}
};
}
}

MonoSink

从前面已经可以看到,将传统代码转变为Reactive方式的关键是在于sink,在创建Mono/FluxCreate的时候,Mono/Flux都会提供相应的sink给使用方来使用。MonoSink相比FluxSink要简单的多,为了简单起见,我们先从MonoSink来了解sink的运行原理(FluxSink会专门另开一篇来说明)。下面就来探探Mono下的MonoSink究竟到底是什么。

再深入MonoSink之前,我们先来看看MonoCreate是怎么使用MonoSink的,对于Reactor来说,所有的入口都是subscribe方法,所以先来看看MonoCreate的subscribe方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void subscribe(CoreSubscriber<? super T> actual) {
//1. 创建MonoSink实例,供MonoCreate来使用
//如变量名字emitter一样,MonoSink的作用其实就是信号的发射器(signal emitter)
DefaultMonoSink<T> emitter = new DefaultMonoSink<>(actual);

//2. emitter除了是sink外,也实现了subscription,供Subscriber使用
//这一步,调用Subscriber的onSubscribe方法,其内部则会调用subscription的request方法 (后续会重点说DefaultMonoSink的request方法)
actual.onSubscribe(emitter);

try {
//3. callback就是在Mono.create时候传入的Mono构造器
//此步骤即调用Mono构造器函数,并将sink传入
callback.accept(emitter);
}
catch (Throwable ex) {
emitter.error(Operators.onOperatorError(ex, actual.currentContext()));
}
}

从上面的源代码可以看出,整个MonoCreate订阅过程很简单,主要是分为三个步骤:

  1. 创建DefaultMonoSink (通过这一步可以看出,一个Subscriber是独占一个MonoSink的)
  2. 实现Subscriber的onSubscribe的方法
  3. 调用Mono#create的构造器函数

以上三个步骤是从整体视角来看的,我们再进一步进入DefaultMonoSink,以它的内部视角,来看看到底作为signal emitter的MonoSink做了些什么。

MonoSink 内部状态

MonoSink内部主要有4个状态:

1
2
3
4
5
volatile int state; //初始默认状态0,即未调用Request且未赋值

static final int NO_REQUEST_HAS_VALUE = 1; //未调用Request但已经赋值
static final int HAS_REQUEST_NO_VALUE = 2; //调用了Request但还未赋值
static final int HAS_REQUEST_HAS_VALUE = 3; //调用了Request且已经赋值了

这三个状态主要取决于request和success(或者error)的调用时机,调用了request方法则会是HAS_REQUEST,调用了success(或者error)方法则会是HAS_VALUE,其中request方法调用是由Subscriber#onSubscribe调用的,success或者error则是由具体使用者来调用的,如Callback。由于success或者error调用时机往往不可能确定(通常是异步的),所以才产生了上述4种状态。

以同步的角度思考,通常是先调用request然后再调用success或者error方法,其中success会对应调用Subscriber的onNext与onComplete方法,error方法则会调用对应的Subscriber#onError方法。但事情往往没这么简单,就如前面提到的,request方法与success/error方法是乱序的,很有可能在request的时候,success/error方法已经调用结束了。为了解决这个问题,每个方法都引入了for-loop加CAS的多线程操作,变得相对复杂了,但只要知道其内部原理,再复杂的代码看起来就都有线索了,下面以request方法为例,来讲讲是MonoSink是如何解决多线程问题的。

MonoSink request方法解释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public void request(long n) {
if (Operators.validate(n)) {
LongConsumer consumer = requestConsumer;
//1. 如果传入了requestConsumer,则调用
//requestConsumer是通过onRequest方法传入的
if (consumer != null) {
consumer.accept(n);
}
//2. 进入for loop来实现自旋
for (; ; ) {
int s = state;
//2.1 HAS_Request: 已经调用过了,直接退出
if (s == HAS_REQUEST_NO_VALUE || s == HAS_REQUEST_HAS_VALUE) {
return;
}
if (s == NO_REQUEST_HAS_VALUE) {
// 2.2 double check 是否已经有值
// 如果是,执行onNext/onComplete方法,并设置完成状态: HAS_REQUEST_HAS_VALUE
// 如果不是,double check失败,直接退出,说明有别的线程已经执行了该方法了
if (STATE.compareAndSet(this, s, HAS_REQUEST_HAS_VALUE)) {
try {
actual.onNext(value);
actual.onComplete();
}
finally {
//释放资源 - 具体调用的disposable对象由onDisposable方法赋值
disposeResource(false);
}
}
return;
}
//2.3 正常流程,值没有被赋值,设置为HAS_REQUEST_NO_VALUE
if (STATE.compareAndSet(this, s, HAS_REQUEST_NO_VALUE)) {
return;
}
}
}
}

MonoSink回调方法

MonoSink除了request、success、error方法外,还提供了几个回调函数,以供使用者使用,主要有:

1
2
3
4
5
6
7
8
//request的时候会被调用,获取request的数量N
MonoSink<T> onRequest(LongConsumer consumer);

//Subscriber调用subscription.cancel是会调用该Disposable方法
MonoSink<T> onCancel(Disposable d);

//与onCancel类似,区别是,除了onCancel方法,在onComplete以及onError也会调用该Disposable方法
MonoSink<T> onDispose(Disposable d);

这里简单讲一下Reactor的代码命名规范,对于回调函数都是以onXXX方式命名,注意调用该onXXX方式的时候,并不是直接调用,而只是传入该回调方法,待对应的事件信号发生时,才会真的被调用。这也是声明式编程的一个特色,先声明再执行。

总结

本文首先描述了传统命令式的代码如何转换为Reactive方式的代码,然后就其内部MonoSink就行了深入的了解,重点讲解了其实现形式,通过对MonoSink的剖析,能够更具体的对Mono整体的使用方式的了解。

本文介绍dig的常规使用,以及以iqiyi.com域名为例的dns记录类型举例。之所以用iqiyi.com为例,是因为本人在此公司任职:)。DNS记录类型介绍可以查看DNS记录类型

dig命令

1
dig [type] [@resolver-server] [+short] [+trace] target
  • type指定记录类型如A、NS、MX等,具体可参考我之前的文章。默认查询为A记录。
  • @resolver-server指定域名解析的本地服务器,默认是网关,但也可以手动指定,如8.8.8.8google的通用域名解析器。
  • +short 简化应答形式,只显示ip结果
  • +trace 显示整个域名解析的迭代过程,从根域名到顶级域名到次级域名直到主机域名。

dig举例

dig查询

执行命令:dig iqiyi.com,得到结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//第一段
; <<>> DiG 9.10.6 <<>> iqiyi.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 6831
;; flags: qr rd ra; QUERY: 1, ANSWER: 5, AUTHORITY: 0, ADDITIONAL: 0

//第二段
;; QUESTION SECTION:
;iqiyi.com. IN A

//第三段
;; ANSWER SECTION:
iqiyi.com. 350 IN A 101.227.188.172
iqiyi.com. 350 IN A 101.227.188.170
iqiyi.com. 350 IN A 101.227.188.174
iqiyi.com. 350 IN A 101.227.188.176
iqiyi.com. 350 IN A 101.227.188.178

//第四段
;; Query time: 7 msec
;; SERVER: 192.168.3.1#53(192.168.3.1)
;; WHEN: Sun Dec 29 17:45:11 CST 2019
;; MSG SIZE rcvd: 107
  • 结果说明
    • 第一段 是查询命令说明以及结果统计信息展示
    • 第二段 查询内容
    • 第三段 查询结果内容,iqiyi.com的A记录有4个,350TTL(表明缓存时间是350s)。
    • 第四段 DNS服务器信息,192.168.3.1#53是本地DNS服务器192.168.3.1(家里的网关ip),53是端口号,DNS服务的默认端口。107是收到结果的大小107个字节。

dig +short 简单查询

1
2
3
4
5
101.227.188.170
101.227.188.174
101.227.188.172
101.227.188.176
101.227.188.178

只返回查询结果的ip地址。

dig分级查询

执行命令:dig iqiyi.com +trace,得到结果如下:

1
2
3
; <<>> DiG 9.10.6 <<>> iqiyi.com +trace
;; global options: +cmd
;; Received 28 bytes from 192.168.3.1#53(192.168.3.1) in 3 ms

发现什么都没有返回,应该是本地服务器192.168.3.1网关不支持trace的查询。

所以改用8.8.8.8服务器来查询,命令:dig iqiyi.com +trace @8.8.8.8,结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
; <<>> DiG 9.10.6 <<>> iqiyi.com +trace @8.8.8.8
;; global options: +cmd
//首次查询 根域名服务器查询 (查询本地服务器)
. 51069 IN NS f.root-servers.net.
. 51069 IN NS h.root-servers.net.
. 51069 IN NS e.root-servers.net.
. 51069 IN NS l.root-servers.net.
. 51069 IN NS c.root-servers.net.
. 51069 IN NS b.root-servers.net.
. 51069 IN NS m.root-servers.net.
. 51069 IN NS a.root-servers.net.
. 51069 IN NS d.root-servers.net.
. 51069 IN NS i.root-servers.net.
. 51069 IN NS k.root-servers.net.
. 51069 IN NS j.root-servers.net.
. 51069 IN NS g.root-servers.net.
. 51069 IN RRSIG NS 8 0 518400 20200110170000 20191228160000 22545 . O+lE7aOii9IvLdYuWpMrTY0RkPpbc0yJLAhg/pwdxh8qZiACwS4TxuYo vxZrvoB0sJ7RyDgycViUEt++avEWx1JjzluiOXj1R0jqZQ7EXO+L+acP o88jV9F3Hqeuudj4u3ZZvM55eLnWfJaJzap/H3xi87rt5obw3zMd5QZE M2zQXSnrCiI2rSelaTeeHx6Mu+8yVigaAwRAH/8QdiAs2y2VuLbdl+C7 mHrbc3blraXSC6dzlGAEmryReOS5WOaMkSJZBctFbnX8KMmSdd83zBOv CVtdkPKGCwtUTWbbNZqrndYGqfCHf8NVLugz5R+jY3uzMvaQvdXBKwFT dcImog==
;; Received 525 bytes from 8.8.8.8#53(8.8.8.8) in 41 ms

//第一次迭代 顶级域名(TLD)查询 (查询根域名服务器)
com. 172800 IN NS l.gtld-servers.net.
com. 172800 IN NS b.gtld-servers.net.
com. 172800 IN NS c.gtld-servers.net.
com. 172800 IN NS d.gtld-servers.net.
com. 172800 IN NS e.gtld-servers.net.
com. 172800 IN NS f.gtld-servers.net.
com. 172800 IN NS g.gtld-servers.net.
com. 172800 IN NS a.gtld-servers.net.
com. 172800 IN NS h.gtld-servers.net.
com. 172800 IN NS i.gtld-servers.net.
com. 172800 IN NS j.gtld-servers.net.
com. 172800 IN NS k.gtld-servers.net.
com. 172800 IN NS m.gtld-servers.net.
com. 86400 IN DS 30909 8 2 E2D3C916F6DEEAC73294E8268FB5885044A833FC5459588F4A9184CF C41A5766
com. 86400 IN RRSIG DS 8 1 86400 20200111050000 20191229040000 22545 . YT51a7sayHoEdZByf40buEQfUYzapxyvAwfPV12AwfWRh4crg9jIVcY6 V79GO4Yb+ezclS4ZTvT+WZ9yLdwuWnzAGVTD0fd9RLvK03nk45ZK42LP MNSHwwUOjv338vqcubwqNOyjxpEukQF3TPXgKAV/ltpGzQYmnDofCd+S uLAssjpag59wPWruFItrIvE6qD7xaDXv+oVsO/bTp7pVb7NOi+KOCpMI D8aP4xm+624JWxLZ59YXOLOy3q1YVfLiVCe4ghtJS4/6BIuRhQ3CAOmj w4QfJVrTDnyn/RY3z41BnRT8K6CkUyuDc5Nc4NlU5KX3HxdiphW1w6JM oWNrPQ==
;; Received 1169 bytes from 192.203.230.10#53(e.root-servers.net) in 170 ms

//第二次迭代 次级域名查询(查询TLD)
iqiyi.com. 172800 IN NS ns1.iqiyi.com.
iqiyi.com. 172800 IN NS ns2.iqiyi.com.
iqiyi.com. 172800 IN NS ns3.iqiyi.com.
iqiyi.com. 172800 IN NS ns4.iqiyi.com.
CK0POJMG874LJREF7EFN8430QVIT8BSM.com. 86400 IN NSEC3 1 1 0 - CK0Q1GIN43N1ARRC9OSM6QPQR81H5M9A NS SOA RRSIG DNSKEY NSEC3PARAM
CK0POJMG874LJREF7EFN8430QVIT8BSM.com. 86400 IN RRSIG NSEC3 8 2 86400 20200102054825 20191226043825 12163 com. J8V3FpilA7JdIt7GBym3CCORYjgGlHAazZlLNBiJ0bFa92n4PrX0hPYo oUHtAA4lEaw9eSJjOIVXhnKq9AR7EgQFfMxcT8OvbBVJ4eErF1vBjd1B x4EkZM2IHIVPPv8XlziufAhiSVMnYHcZnuO8BpDaXrasvlW3U9vv/VQU dCs79XwjQR/XkFvJKvldj2EZd3FXLlRDdnwESxhlpLZmIg==
CDJHMJ049AHN95A56GE5FPTIT6CK3TVA.com. 86400 IN NSEC3 1 1 0 - CDJIFERNDE19197KLS7DLE5N5008MQB2 NS DS RRSIG
CDJHMJ049AHN95A56GE5FPTIT6CK3TVA.com. 86400 IN RRSIG NSEC3 8 2 86400 20200103053214 20191227042214 12163 com. LQOPbhYOUqtzkl49A0Sg7IVpJ8HVep5FwE5ILJ0cK/5uGsvKk1bbrM4A s0M5iiaVnQ0BwTt9FRNdYRGUVUL6YSJATaouDomYj3o/h+0kGxvrJxnF jRuDsQS56c6LDALzd+2Hv4xwiOURhv0Nl4v3rycokglC6IjN1VpGrgWN bldR9ixluPAQsBo+m3TdieQyb4zc10Ks3BAJg4UKmNQSLg==
;; Received 723 bytes from 192.26.92.30#53(c.gtld-servers.net) in 171 ms

//第三次迭代 主机域名查询 (查询权威域名服务器)
iqiyi.com. 600 IN A 101.227.188.172
iqiyi.com. 600 IN A 101.227.188.174
iqiyi.com. 600 IN A 101.227.188.176
iqiyi.com. 600 IN A 101.227.188.178
iqiyi.com. 600 IN A 101.227.188.170
;; Received 118 bytes from 43.225.84.1#53(ns3.iqiyi.com) in 28 ms

根据每次迭代返回结果的服务器信息,我们可以发现DNS查询整体流程是这样的:

  • 首次查询 从本地服务器(local dns server)获取根域名服务器地址,然后由本地服务器开始迭代dns查询过程。
  • 第一次迭代 由本地服务器访问根域名服务器,根域名服务器返回关于顶级域名.com的NS记录。
  • 第二次迭代 由本地服务器访问上一步获得的.com地址的顶级域名服务器,并从中获得关于iqiyi.com的NS记录。
  • 第三次迭代 由本地服务器访问由上一步获得的iqiyi.comNS的服务器地址,即iqiyi.com的权威服务器,然后获得iqiyi.com的A记录。
  • 最终,本地服务器返回关于iqiyi.com的IP地址给客户端,然后客户端再访问目标服务器。

记录类型举例

  1. SOA记录 dig iqiyi.com soa
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    ; <<>> DiG 9.10.6 <<>> iqiyi.com soa
    ;; global options: +cmd
    ;; Got answer:
    ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 20850
    ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

    ;; OPT PSEUDOSECTION:
    ; EDNS: version: 0, flags:; udp: 4096
    ;; QUESTION SECTION:
    ;iqiyi.com. IN SOA

    ;; ANSWER SECTION:
    iqiyi.com. 86400 IN SOA ns1.iqiyi.com. dnsadmin.iqiyi.com. 2019122905 1800 600 1209600 600

    ;; Query time: 33 msec
    ;; SERVER: 192.168.3.1#53(192.168.3.1)
    ;; WHEN: Sun Dec 29 22:46:03 CST 2019
    ;; MSG SIZE rcvd: 87
  2. NS记录 dig iqiyi.com ns
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    ; <<>> DiG 9.10.6 <<>> iqiyi.com ns
    ;; global options: +cmd
    ;; Got answer:
    ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 21342
    ;; flags: qr rd ra; QUERY: 1, ANSWER: 4, AUTHORITY: 0, ADDITIONAL: 1

    ;; OPT PSEUDOSECTION:
    ; EDNS: version: 0, flags:; udp: 4096
    ;; QUESTION SECTION:
    ;iqiyi.com. IN NS

    ;; ANSWER SECTION:
    iqiyi.com. 1132 IN NS ns2.iqiyi.com.
    iqiyi.com. 1132 IN NS ns3.iqiyi.com.
    iqiyi.com. 1132 IN NS ns4.iqiyi.com.
    iqiyi.com. 1132 IN NS ns1.iqiyi.com.

    ;; Query time: 11 msec
    ;; SERVER: 192.168.3.1#53(192.168.3.1)
    ;; WHEN: Sun Dec 29 22:47:10 CST 2019
    ;; MSG SIZE rcvd: 110
  3. ns3.iqiyi.com的A记录 dig ns3.iqiyi.com
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    ; <<>> DiG 9.10.6 <<>> ns3.iqiyi.com
    ;; global options: +cmd
    ;; Got answer:
    ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 11375
    ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

    ;; QUESTION SECTION:
    ;ns3.iqiyi.com. IN A

    ;; ANSWER SECTION:
    ns3.iqiyi.com. 296 IN A 43.225.84.1

    ;; Query time: 51 msec
    ;; SERVER: 192.168.3.1#53(192.168.3.1)
    ;; WHEN: Sun Dec 29 22:49:07 CST 2019
    ;; MSG SIZE rcvd: 47
  4. MX记录 dig iqiyi.com mx
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    ; <<>> DiG 9.10.6 <<>> iqiyi.com mx
    ;; global options: +cmd
    ;; Got answer:
    ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 16303
    ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

    ;; QUESTION SECTION:
    ;iqiyi.com. IN MX

    ;; ANSWER SECTION:
    iqiyi.com. 300 IN MX 10 mx1.iqiyi.com.

    ;; Query time: 9 msec
    ;; SERVER: 192.168.3.1#53(192.168.3.1)
    ;; WHEN: Sun Dec 29 22:50:26 CST 2019
    ;; MSG SIZE rcvd: 47
  5. TXT记录 dig iqiyi.com txt
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    ; <<>> DiG 9.10.6 <<>> iqiyi.com txt
    ;; global options: +cmd
    ;; Got answer:
    ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 28756
    ;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

    ;; OPT PSEUDOSECTION:
    ; EDNS: version: 0, flags:; udp: 4096
    ;; QUESTION SECTION:
    ;iqiyi.com. IN TXT

    ;; ANSWER SECTION:
    iqiyi.com. 600 IN TXT "92qpmvb8qgzqbvndvbgtrn9nlys14bxh"
    iqiyi.com. 600 IN TXT "v=spf1 ip4:202.108.14.100 a mx ~all"

    ;; Query time: 64 msec
    ;; SERVER: 192.168.3.1#53(192.168.3.1)
    ;; WHEN: Sun Dec 29 22:51:45 CST 2019
    ;; MSG SIZE rcvd: 131
  6. SRV、PTR
    SRV与PRT不常用,所以iqiyi.com的域名服务器并没有配置这两项。

参考

DNS 原理入门

DNS是WWW万维网中重要的一环,内部涉及到多种数据类型,dns的数据称为记录(record),平时我们涉及到最多的可能只有IP解析服务的A记录,但深入了解下去,发现DNS有多种用于不同用途的数据类型,常见的主要有:

  • A (Host address)
  • AAAA (IPv6 host address)
  • CNAME (Canonical name for an alias)
  • MX (Mail eXchange)
  • NS (Name Server)
  • PTR (Pointer)
  • SOA (Start Of Authority)
  • SRV (location of service)
  • TXT (Descriptive text)

更多的记录类型可以参考:https://simpledns.plus/help/dns-record-types

Zone文件

要想清楚明白记录类型,就不得不去深入了解Zone文件。DNS服务器是采用Zone文件来进行数据管理的,每个Zone相当于一个独立的管理单元,一个DNS可以管理多个zone文件,一个zone文件也可以被多个单独的dns服务器管理(如主、从、缓存服务器)。

zone文件结构

一个域名对应着一个zong文件,以abc.com为例,zone文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$TTL 6h //第1行
$ORIGIN abc.com. //第2行
@ 3600 IN SOA ns1.ddd.com. root.ddd.com.( //第3行
929142851 ; Serial //第4行
1800 ; Refresh //第5行
600 ; Retry //第6行
2w ; Expire //第7行
300 ; Minimum //第8行
)
@ 2d IN NS ns1.ddd.com. //第9行
@ 2d IN NS ns2.ddd.com. //第10行
@ 2d IN NS ns3.ddd.com. //第11行
ns1 3600 IN A 120.172.234.27 //第12行
ns2 3600 IN A 120.172.234.28 //第13行
ns3 3600 IN A 120.172.234.29 //第14行
a 3600 IN A 120.172.234.27 //第15行
b 3600 IN CNAME a.abc.com. //第16行
@ 3600 IN MX a.abc.com. //第17行
@ 3600 IN TXT "TXT" //第18行

文件解释

  • 第1行,这行内容给出了该域名(abc.com.)各种记录的默认TTL值,这里为6小时。即如果该域名的记录没有特别定义TTL,则默认TTL为有效值。
  • 第2行,这行内容标识出该ZONE文件是隶属那个域名的,这里为abc.com.
  • 第3行,从这行开始到第8行为该域名的SOA记录部分,这里的@代表域名本身。ns1.ddd.com表示该域名的主权威DNS。root.ddd.com表示该主权威DNS管理员邮箱,等价于root@ddd.com
  • 第4行,Serial部分,这部分用来标记ZONE文件更新,如果发生更新则Serial要单增,否则MASTER不会通知SLAVE进行更新。
  • 第5行,Refresh部分,这个标记SLAVE服务器多长时间主动(忽略MASTER的更新通知)向MASTER复核Serial是否有变,如有变则更新之。
  • 第6行,Retry部分,如Refresh过程不能完成,重试的时间间隔。
  • 第7行,Expire部分,如SLAVE无法与MASTER取得联系,SLAVE继续提供DNS服务的时间,这里为2W(两周时间)。Expire时间到期后SLAVE仍然无法联系MASTER则停止工作,拒绝继续提供服务。Expire的实际意义在于它决定了MASTER服务器的最长下线时间(如MASTER迁移,DOWN机等)。
  • 第8行,Minimum部分,这个部分定义了DNS对否定回答(NXDOMAIN即访问的记录在权威DNS上不存在)的缓存时间。
  • 第9-11行,定义了该域名的3个权威DNS服务器。NS记录表明要想知道该域名的ip解析,就要向该地址的服务器请求访问,这里的域名是abc.com.,@表示本域名。那NS服务的具体地址是什么呢,由对应的ns域名的A记录来指定,如第12-14行。通常NS记录的TTL大些为宜,这里为2天。设置过小只会增加服务器无谓的负担,同时解析稳定性会受影响。
  • 第15-18行是常用的几个记录类型,详细请参考下一节。

数据类型

SOA记录

一个zone文件的第一个记录(Start Of Authority),记录了整个zone文件(权威域)的全局配置,如主域名、管理员邮箱、重试时间、刷新时间等等。

A、AAAA记录

DNS中最常用的记录类型,用于IP解析功能,A记录用于IPv4的解析,AAAA用于IPv6的解析。

NS记录

NS(NameServer)域名服务记录,是除A记录外必需的记录,用于记录指定域名的服务器解析地址。

CNAME记录

CNAME相关于别名功能,对于多个不同的域名,采用同一个CNAME来方便配置。

1
2
3
4
www IN CNAME name.abc.com.
web IN CNAME name.abc.com.
home IN CNAME name.abc.com.
name IN A 110.10.1.2

例如三个不同的域名www.abc.com、web.abc.com、home.abc.com可以用同一个CNAME name.abc.com来表示,方便配置与修改。

MX记录

MX(Mail Exchange)邮箱服务器地址,用于记录邮件地址对应的服务器地址,如mailname@abc.com的邮箱地址,邮件系统会根据abc.com域名的MX记录来找到指定的邮箱服务器地址。

PTR

PTR记录可以理解为是A记录的反解,A记录是根据域名来获取ip地址,而PTR记录则是根据ip可以反向查出ip地址对应的域名,例如给定的ip属于www.abc.com。

1
2
3
4
; 50 是 10.0.1.50  4个数字中的最后一个
; www.abc.com 必须是 FQDN

50 IN PTR www.abc.com.

SRV

DNS SRV是DNS记录中一种,用来指定服务地址。与常见的A记录、cname不同的是,SRV中除了记录服务器的地址,还记录了服务的端口,并且可以设置每个服务地址的优先级和权重。访问服务的时候,本地的DNS resolver从DNS服务器查询到一个地址列表,根据优先级和权重,从中选取一个地址作为本次请求的目标地址。例如:

1
2
3
4
5
6
7
8
9
10
_ldap._tcp.example.com TTL Class SRV Priority Weight Port Target
Service: 服务名称,前缀“_”是为防止与DNS Label(普通域名)冲突。
Proto: 服务使用的通信协议,_TCP、_UDP、其它标准协议或者自定义的协议。
Name: 提供服务的域名。
TTL: 缓存有效时间。
CLASS: 类别
Priority: 该记录的优先级,数值越小表示优先级越高,范围0-65535。
Weight: 该记录的权重,数值越高权重越高,范围0-65535。
Port: 服务端口号,0-65535。
Target: host地址。

一个能够支持SRV的LDAP client可以通过查询域名,得知LDAP服务的IP地址和服务端口。

TXT

TXT记录用于DNS一些扩展功能,最多可记录65536个字节。通常主要用于域名拥有权验证(如google-site-verification),SPF反垃圾邮箱验证等等。后续会对TXT记录做详细的介绍。

参考文献

基础概念介绍

1. 一个简单服务器的基本流程

graph TB
C --建立--> D
C --建立--> D1
subgraph Socket生命周期
D1(收到客户端连接完成) --> E1(生成普通Socket资源)
E1 --> F1
F1(Socket读写) --while loop --> F1
F1 --> G1((关闭))
end
subgraph Socket生命周期
D(收到客户端连接完成) --> E(生成普通Socket资源)
E --> F
F(Socket读写) --while loop --> F
F --> G((关闭))
end
subgraph 创建Server Socket
A(创建服务Socket资源) --> B(绑定服务端口)
B --> C(开始监听Accept)
end

2. 一切皆资源

在Linux世界里,所有的资源都用描述符来表示,且对于IO资源都虚拟化为了文件,所以IO的描述符都叫文件描述即fd (file descriptor)。

在简单服务器里,主要有两类资源,即服务端Server Socket以及普通连接Socket。

  • ServerSocket负责服务端口监听,当有请求进来时建立与客户端通信的socket连接。
  • 普通Socket负责与客户端的读写通信。

例如Http是80端口,服务前监听80端口,然后每接收到一个请求,则建立一个普通的Socket。建立完Socket后,此后与客户端通信都只与这个socket有关,与ServerSocket无关了。

3. Bio v.s. Nio

Bio: 顾名思义,在等待资源ready的时候都会阻塞,例如accept以及read和write。操作系统默认行为就是阻塞的,此时如果资源没有准备好,就会阻塞当前线程。

由于IO会阻塞线程,所以对于BIO而言,就会有one connection per thread模型,即一个连接(connection)需要配一个线程。这种模式在大量连接时会存在缺陷,例如有一万个用户连接,就需要创建一万个线程,而线程也是要耗不少资源的,假如一个线程占内存512K,则一万个线程需要5G内存,所以导致难以支持一万个连接,更多细节请查看C10K问题。 (一个衍生:创建一个线程需要多少资源)

为了解决C10K问题,为此引入了nio。顾名思义,nio是指IO不会阻塞,但nio重点并不是为了非阻塞,为的是通过非阻塞进而引入的线程模型的变化,即可以通过利用一个线程来管理多个连接(Connection)资源,即multiple connection per thread模型

Java Nio

先以一个简单的EchoServer为例,来介绍下Java Nio涉及到的核心知识。

1. 简单EchoServer示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
package com.gaocher.learning.server.javanio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

/**
* @author Isaac Gao
* @Date 2019/12/19
*/
public class EchoServer {
private static Selector selector;
public static void main(String[] args) {
try {
selector = Selector.open();
// We have to set connection host, port and non-blocking mode
ServerSocketChannel socket = ServerSocketChannel.open();
ServerSocket serverSocket = socket.socket();
serverSocket.bind(new InetSocketAddress("localhost", 8089));
socket.configureBlocking(false);
int ops = socket.validOps();
SelectionKey register = socket.register(selector, ops, null);
while (true) {
selector.select();
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> i = selectedKeys.iterator();

while (i.hasNext()) {
SelectionKey key = i.next();

if (key.isAcceptable()) {
// New client has been accepted
handleAccept(socket, key);
} else if (key.isReadable()) {
// We can run non-blocking operation READ on our client
String data = handleRead(key);
handleWrite( data, (SocketChannel)key.channel());
}
i.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}

private static void handleAccept(ServerSocketChannel mySocket,
SelectionKey key) throws IOException {

System.out.println("Connection Accepted...");

// Accept the connection and set non-blocking mode
SocketChannel client = mySocket.accept();
client.configureBlocking(false);

// Register that client is reading this channel
client.register(selector, SelectionKey.OP_READ);
}

private static String handleRead(SelectionKey key)
throws IOException {
System.out.println("Reading...");
// create a ServerSocketChannel to read the request
SocketChannel client = (SocketChannel) key.channel();

// Create buffer to read data
ByteBuffer buffer = ByteBuffer.allocate(1024);
client.read(buffer);
// Parse data from buffer to String
String data = new String(buffer.array()).trim();
if (data.length() > 0) {
System.out.println("Received message: " + data);
if (data.equalsIgnoreCase("exit")) {
client.close();
System.out.println("Connection closed...");
}
}
return data;
}

private static void handleWrite(String data, SocketChannel client) {
byte[] bytes = data.getBytes();
ByteBuffer wrap = ByteBuffer.wrap(bytes);
try {
client.write(wrap);
} catch (IOException e) {
e.printStackTrace();
}
}

}

2. java nio 核心概念

为了支持nio,java提供了一套抽象模型,即Selector、Channel、Buffer,除此之外,为了方便编程,还提供了一个组合体SelectionKey。

  • Selector: 对多个Connection进行管理,负责对感兴趣的事件(interested ops)进行注册监听
  • Channel:对Socket的抽象,即代表连接,读写操作都在此进行。
  • Buffer:读写缓冲,用户态的内存用于与内核态的数据buffer进行交换。
  • SelectionKey:用于表示一个Selector与Channel,重点是attach的对象,可以方便的用于与Channel相关的Context存储

具体关系如下:
Java Nio关系图

一个channel为什么会有多个key?一个channel可被多个selector注册监听,所以需要用数组来保存keys。

除了上面几个核心概念外,还有一个,就是ops,表示要关注的IO操作。

虽然在关注的事件用Ops(Operation操作)来表示,但其实应该用事件event更贴切,其实Nio就是一个基于事件的网络模型。为了简化,Java将事件总共分为四种:

1
2
3
4
5
6
7
public static final int OP_READ = 1 << 0;

public static final int OP_WRITE = 1 << 2;

public static final int OP_CONNECT = 1 << 3;

public static final int OP_ACCEPT = 1 << 4;

其中serversocketChannel只有Accept,另外三个属于普通的SocketChannel,为了方便,java通过Channel.validOps()硬编码了这个知识。

3. 基本流程

  1. 创建相关资源

    1
    2
    3
    4
    5
    selector = Selector.open();
    ServerSocketChannel socket = ServerSocketChannel.open();
    ServerSocket serverSocket = socket.socket();
    serverSocket.bind(new InetSocketAddress("localhost", 8089));
    socket.configureBlocking(false);
  2. 注册channel到selector

    1
    2
    int ops = socket.validOps();
    SelectionKey register = socket.register(selector, ops, null);
  3. 开启循环监听

    1
    2
    3
    4
    5
    while(true) {
    selector.select(); //若无事件产生,则阻塞
    Set<SelectionKey> selectedKeys = selector.selectedKeys(); //获取已经ready的SelectionKey
    //... 进行读写、关闭等操作
    }
  4. 处理事件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    SelectionKey key = i.next();

    if (key.isAcceptable()) {
    // New client has been accepted
    handleAccept(socket, key);
    } else if (key.isReadable()) {
    // We can run non-blocking operation READ on our client
    String data = handleRead(key);
    handleWrite( data, (SocketChannel)key.channel());
    }

    注意,这里写数据的时候并没有使用Nio,而是直接调用channel.write(Bio的方式)的方式来实现,因为写数据往往都是ready的,除非是缓冲区已满无法写入。所以对于写操作而言,用Bio方式更快一些。

  5. 删除事件

    1
    2
    3
    Iterator<SelectionKey> i = selectedKeys.iterator();
    //...
    i.remove();

    Selector不会对selectedKeys做删除,当有事件触发后,则key会一直存在,所以需要手动删除。

4. doSelect源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
protected int doSelect(long timeout) throws IOException {
if (closed)
throw new ClosedSelectorException();
processDeregisterQueue();
try {
begin();
pollWrapper.poll(timeout);
} finally {
end();
}
processDeregisterQueue(); //对于需要cancel的key进行deregister
int numKeysUpdated = updateSelectedKeys();
if (pollWrapper.interrupted()) {
// Clear the wakeup pipe
pollWrapper.putEventOps(pollWrapper.interruptedIndex(), 0);
synchronized (interruptLock) {
pollWrapper.clearInterrupted();
IOUtil.drain(fd0);
interruptTriggered = false;
}
}
return numKeysUpdated;
}
private int updateSelectedKeys() {
int entries = pollWrapper.updated;
int numKeysUpdated = 0;
for (int i=0; i<entries; i++) {
int nextFD = pollWrapper.getDescriptor(i);
SelectionKeyImpl ski = fdToKey.get(Integer.valueOf(nextFD));
// ski is null in the case of an interrupt
if (ski != null) {
int rOps = pollWrapper.getEventOps(i);
if (selectedKeys.contains(ski)) {
if (ski.channel.translateAndSetReadyOps(rOps, ski)) {
numKeysUpdated++;
}
} else {
ski.channel.translateAndSetReadyOps(rOps, ski);
if ((ski.nioReadyOps() & ski.nioInterestOps()) != 0) {
selectedKeys.add(ski);
numKeysUpdated++;
}
}
}
}
return numKeysUpdated;
}
  • processDeregisterQueue()

有注册就有解注册,在主循环里,除了返回感兴趣的事件外,也要对不需要的key进行删除,例如channel已经关闭,则与该channel相关的key就需要删除,否则selector关注的key会越来越多,而导致性能变慢。

  • updateSelectedKeys

根据操作系统底层返回的描述符fd,利用fdToKey获取java层面的SelectionKey

  • translateAndSetReadyOps

将底层IO操作转义为java的Nio操作。由于Nio要支持多种协议,不单单只是tcp,所以要将其他IO操作转义为Java定义的4种IO操作。

  • 为什么需要ski.nioReadyOps() & ski.nioInterestOps()进行对比

因为write事件是一直ready的,若不和interestOps比较,会频繁触发该Key。

5. selector成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected Set<SelectionKey> selectedKeys = new HashSet();
protected HashSet<SelectionKey> keys = new HashSet();
private Set<SelectionKey> publicKeys;
private Set<SelectionKey> publicSelectedKeys;

protected SelectorImpl(SelectorProvider var1) {
super(var1);
if (Util.atBugLevel("1.4")) {
this.publicKeys = this.keys;
this.publicSelectedKeys = this.selectedKeys;
} else {
this.publicKeys = Collections.unmodifiableSet(this.keys);
this.publicSelectedKeys = Util.ungrowableSet(this.selectedKeys);
}

}
  • keys: 所有用户感兴趣的SelectionKey,每个channel只注册一个key到selector上,重复注册无效
  • selectedKeys: 监听到对应事件的keys而返回。selectedKeys不会做删除,当有事件触发后,则key会一直存在。所以需要手动删除。即需要selectedKeys.iterator().remove()
  • public*: 用户可访问的key,对原始的key进行不可修改或不可增长的封装
  • selectedKeys.iterator().remove():只是从selectedKeys列表里去除,并没有删除key本身,key本身仍然在channel里被引用,所以无需担心会重新创建而有影响。

遗留问题

本文只是初略的概括了Nio在java中的使用,但并未深入到在真实案例中的使用,例如nio在tomcat与netty下的具体实现,后续会继续展开描述,nio在tomcat与netty下的实战。除此以外,还有以下细节并未探索:

  1. LT模式 v.s. ET模式
    • LT模式 —— 如何利用de-register来优化selector监听
    • ET模式时,Write事件会一直有吗?
  2. de-register后,何时重新注册ops
  3. 同样的端口,操作系统如何区分ServerSocket与普通socket。
  4. PollSelector初始化详细过程
  5. Buffer为什么需要flip(读写共用)
  6. 如何知道connection已经断开

主要链接

  1. Hexo安装 https://hexo.io/docs/setup
  2. Hexo发布到GitHub https://hexo.io/docs/github-pages
  3. Next主题安装 https://github.com/theme-next/hexo-theme-next
  4. Next主题使用教程 http://theme-next.iissnan.com/
  5. 添加sitemap供搜索引擎搜索 https://eericzeng.github.io/2019/07/14/hexo%E5%8D%9A%E5%AE%A2%E7%AB%99%E7%82%B9sitemap%E7%9A%84%E4%BD%BF%E7%94%A8/
  6. 添加版权信息 http://blog.amdoing.com/the-post-copyright-in-hexo-next/
  7. 添加评论 https://yashuning.github.io/2018/06/29/hexo-Next-%E4%B8%BB%E9%A2%98%E6%B7%BB%E5%8A%A0%E8%AF%84%E8%AE%BA%E5%8A%9F%E8%83%BD/
  8. 添加阅读次数 https://www.zyjdn.com/2020/02/05/Hexo-NexT%20%E4%BD%BF%E7%94%A8%20leancloud%20%E9%98%85%E8%AF%BB%E6%AC%A1%E6%95%B0/

添加插件

  1. 站长工具 cnzz
    目前v7.6.0版本安装cnzz特别简单,只需要将创建的cnzz的id写入_config.yml里cnzz_siteid即可,无需添加其他任何文件。
    如何申请友盟,请参考https://www.jianshu.com/p/3025b0e221bf
  2. 添加mermaid画图
    • npm install hexo-filter-mermaid-diagrams –save
    • 修改themes/next下的_config.yml:
      1
      2
      3
      4
      5
      6
      7
      # Mermaid tag
      mermaid:
      enable: false
      # Available themes: default | dark | forest | neutral
      theme: forest
      cdn: //cdn.jsdelivr.net/npm/mermaid@8/dist/mermaid.min.js
      #cdn: //cdnjs.cloudflare.com/ajax/libs/mermaid/8.0.0/mermaid.min.js
      具体请参考 https://rogersnowing.cn/post/38b5106c.html

问题解决

  1. 当博客域名是子URL时,例如https://xxx.github.io/blog,则修改站点配置文件如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # URL
    ## If your site is put in a subdirectory, set url as 'http://yoursite.com/child' and root as '/child/'
    url: https://gaocher.github.io/blog
    root: /blog/
    permalink: :year/:month/:day/:title/
    permalink_defaults:
    pretty_urls:
    trailing_index: true # Set to false to remove trailing 'index.html' from permalinks
    trailing_html: true # Set to false to remove trailing '.html' from permalinks

参考文章:

https://www.ezlippi.com/blog/2016/02/jekyll-to-hexo.html
https://github.com/EZLippi/hexo-theme