java代码大全及剖析说明

前言

Web应用开发架构技术不断的演化,从基于通用网关接口CGI开发的能够在操作系统上运行的独立组件,到专门的隔离运行的Servlet,一直到到现在我们对复杂应用开发使用Java EE技术或者Spring框架系列,其实其底层的核心逻辑基本上没有什么变化。

从总体上说,我们的基于网络数据流处理的应用程序开发,从单个组件到组合应用再到今天我们将组合应用垂直分割,使其功能趋于过去单组件状态。

这种从分到合在到分,但是底层技术还是原来的对等点通信。

在从单个组件到集成容器管理多组件并采用前端控制模式的演化过程中,正是由于高级语言运行环境应用比如JVM等的出现,让我们的组件开发进入了一个受托管,受限制时代。

如此也给了我们一个对运行的操作系统环境进行封装和隔离管理时代,正是这种封装隔离管理,让开发人员在使用像Java这类高级语言时,能够不用去关心要编写的应用程序跟不同的操作系统资源之间的交互。

应用程序与计算机硬件

这里简单讲一下,如果我们要开发一个应用程序需要跟系统的哪些资源进行交互和使用它们呢?

很明显,我们编写的任何代码都是通过CPU和内存来执行的,它们是任何应用程序都必须与之打交道的资源。

除了它们还有,我们的应用程序都是以某种文件结构存储的,所以需要跟操作系统的文件系统和文件的输入输出来打交道。

如果我们编写的是Web应用,也就是基于网络数据流通信的应用,那么就需要跟计算机的网络资源交互。

而这些资源的使用本质就是数据在这些资源的驱动程序的作用下由操作系统来调度来回的进出拷贝操作。

高级语言数据类型与计算机资源抽象

理解了我们应用程序是如何使用计算机资源后,我们就想明白了,其实我们所有的应用程序无非就是处理数据通过跟操作系统交互来通过操作系统管理的系统资源来进行各类数据处理操作。

首先是对于CPU资源的利用,当然必须配合内存空间的分配和利用。这些资源根据操作系统的实现和管理规则,我们将他们抽象成一些重要的描述类型,比如Thread,Buffer等。我们操作系统的重要环境参数也都出现在比如System等类或者包里。

而对于数据,我们一般将其或分为两大类,一类是存储在磁盘上的数据,由操作系统提供的文件系统来管理,它的特点是以固定大小的块来存储,所以操作系统在调度处理它时也是以块为基础进行的。而另外一类则是来自于外部动态的数据,这类数据以字节流的方式呈现给操作系统,比如我们的网络访问数据流。

为了对这些资源进行管理和控制操作,高级语言抽象出流Stream的概念,这个流最基础的是字节流也就是比特数据流,它也是我们网络硬件层传输的数据格式。

关于数据流模型

这个流实质上是对内存一个空间的操作抽象,在对Stream的抽象中其基本的操作读read和写write,基本的属性是容量Capacity,位置Position和限制Limit,其中read和write方法就是告诉CPU去做输入还是输出操作。

我们在内存中开辟一块总长度为Capacity的空间,这块空间可以放东西的起始位置Position是多少,从这个位置开始有Limit的空间可用。每次操作都是由某个指针来计数标示的。当然我们可以通过其它一些指针标示来记录这个空间的状态信息,还可以恢复到某个状态等。

我们知道操作系统管理的文件系统中存储的数据都是以固定块大小存储在磁盘上的,所以我们一般会将输入输出的单位定义成磁盘块大小的倍数。

如此来方便从磁盘向内存的输入和输出操作。操作系统会将有限的物理内存地址通过分页的形式映射到磁盘固定地址区域分为,通过中断机制不断的将磁盘映射的内容调入到物理内存中已提供给CPU计算使用,这就是我们说的计算机虚拟内存技术。

我们在用Java这类高级语言开发应用程序时,涉及到对于文件的读取和写入操作,对于网络数据流的读取和写入操作时,一般都是必须先创建这样一个流模型。

就是在内存中开辟一个缓存的空间,将磁盘文件或者网络数据流写入到该缓存空间中,如果还需要进一步的处理,可以开辟更多的缓存空间,在空间转移过程中对数据进行各类的处理,比如根据某种规则对数据的长度和标记位的规定来编解码,对数据内容进行过滤和安全校验等。

高级语言类型与数据流动模型

从我们计算机的硬件结构上来说,CPU,寄存器,主内存,输入输出控制器缓存,硬盘,键盘,网卡等它们之间通过各类数据总线进行数据输入和输出活动。

正是这些数据在各个硬件中间的进出交流才构成了我们应用程序的逻辑计算实现。为了能够描述它们之间的数据流动,高级语言抽象出了IO模型,从最初的阻塞式输入输出模型,比如Java IO,到后来的非阻塞式IO,比如Java NIO/NIO.2以及AIO等。

这些数据模型最基本的是流Stream分为输入数据流和输出数据流,它们原生的数据流就是二进制比特流,为了能够符合我们人类世界的内容表示,我们对其长度和固定数位进行了一些结构化的抽象,也就是我们常用的各种数据类型,包括一些常见数据类型的组合和嵌套,更抽象出对象世界。

其本质就是对于二进制数据流的数位长度规定,数位固定意义的赋予这些操作。

Java语言的I/O模型

随着Java语言的发展,到了1.4后,开始提出了一个新的输入输出模型NIO,它跟原来IO模型的最大区别在于引入了通道Channel和选择器Selector概念,不再是直接操作流Stream了而是在流的基础上抽象出通道的概念,基本上就是在系统级别上独立出一个管理线程,不断的监控多个内存空间,如果哪个空间数据准备就绪,就会分配CPU去处理该空间的数据。如果没有,CPU可以不理具体的IO操作,并且现代计算机硬件中提供DMA技术,一些输入输出操作可能都不需要CPU的参与。

这样做的好处是使用极小的CPU线程资源可以管理多个输入输出的内存空间资源。

而不是想IO模型中,当我们应用需要一个IO操作时,整个IO操作线程会被阻塞等待IO操作的完成,如果应用程序涉及到大量的输入输出操作,由于IO的速度跟CPU跟其高级缓存之间的数据交换速度完全不在一个数量级上,所以这种线程阻塞等待会极大的降低CPU的利用率。

NIO模型最重要的是进一步抽象各种数据流为通道,并且增加了一个独立的选择器线程,并且将在数据流动过程中可能出现的比如连接,发送,接收,中断异常,关闭等行为封装成事件,通过事件的触发来告诉CPU对其处理,从而避免了具体IO线程对于CPU资源的长期占用,只借助输入输出控制设备就能完成很多工作。

数据流分类

当然上面谈到的数据流并没有进一步划分基于什么的数据流,其实我们知道基于文件块数据流和基于流的数据流是有差别的。

我们在使用Java语言编程时,要操作某个文件首先需要建立一个基于该文件的数据流,然后在这个流的基础上创建一个通道,对通道进行指令操作。

不管是读取还是写入,其另一端必然是一个内存中开辟的缓冲区,这个内存区域一般是在JVM管理的堆上的,由我们应用程序控制和管理的,但是真正的输入输出操作是需要操作系统来完成的,所以操作系统会有自己的缓存空间来进行数据的拷贝,然后将其在拷贝到我们应用程序管理的空间里,在操作系统缓存到我们的应用程序管理空间的数据转移就会涉及到编解码问题。

在过去基于较低级别语言比如C/C++的编程中,我们是需要自己去定义不同的内存空间,并在这些内存空间之间进行数据转移,而在较低级别语言中内存是属于系统内存,也就是说我们操作的内存就是操作系统使用的内存,如果不对其进行有效的自我管理,就很容易造成系统的崩溃问题。

而在像高级语言Java等编程时,这些内存空间时JVM管理的堆空间,在我们不在引用这些数据时,有专门的垃圾收集器来根据负责根据需要清理它们。

网络数据流

同样基于网络的数据是以数据流的方式来处理的,这个数据流我们将其抽象为Socket格式,也就是遵循TCP/IP或者UDP协议,它们规定了分割数据流的长度和标记,以及各部分的意义。

TCP与UDP区别在于一个数据流里有目标地址,另一个没有,一个是定点发送,另一个算是广播。而对于网络中相互关联的对等点计算机是通过InetAddress来抽象的表示的。

而我们的Socket本质就是数据流,它支持读取和写入操作,在新的NIO模型中,它有被抽象出了对应的通道,比如ServerSocketChannel,SocketChannel和DatagramChannel等。

我们编写网络应程序的底层逻辑处理一般就是先使用网络节点主机的抽象信息创建一个网络数据流,即Socket对象,然后通过它来读取或者写入数据,在NIO模型里,我们就可以通过Socket对象来获取其通道对象。

该通道对象分为服务器套接字通道对象和套接字通道对象,服务器套接字通道只是一个具备管理能力的套接字通道类。如此有了套接字通道以后,我们就可是通过通道的相关方法来操作网络数据流。

现在很少有直接通过JDK提供的这些类库来编写网络应用了,目前比较流行的是使用第三方封装这类网络通道模型的功能库比如Netty等。

总结

感觉还没有说多少就又超过可接受的文章长度了,只能先说到这里吧。简单总结一下,其实要想将Web应用程序从最初的系统组件服务到现在的微服务架构的实现历程说明白是很困难的。但是不管如何其底层有一个不变的主线就是数据的输入/输出模型,它是我们开发基于网络数据流的应用程序的根基。所以,一定要清楚I/O模型的前世今生,以及现在流行的反应式流模型,如此才能更好的使用现在流行的框架和内容开发出高性能的网络应用程序。