第一章 深入Web请求过程
1.1 B/S网站架构概述
HTTP协议采用无状态的短连接的通信方式。通常一次请求就完成一次数据交互,通常也对应一个业务逻辑。
当在浏览器里输入一个URL,首先会请求DNS把域名解析成为IP地址,根据IP地址找到对应的服务器,向服务器发送请求,服务器返回数据资源给访问的用户。
服务器端的其他操作:负载均衡平均分配所有用户的请求。请求数据可能存储在分布式缓存、静态文件、数据库里。静态资源会发起额外的HTTP请求,这些请求可能在CDN上,CDN服务器处理这个请求。
互联网应用原则:
- 互联网上所有资源用一个URL(统一资源定位符)表示。
- 必须基于HTTP协议与服务端交互。
- 数据展示必须在浏览器中进行。
1.2 如何发起一个请求
浏览器在建立Socket连接之前,必须根据地址栏里的URL的域名DNS解析出IP,根据IP地址和默认80端口与远程服务器建立Socket连接,浏览器根据URL组装一个GET类型的HTTP请求头,通过outputStream.write发送到目标服务器,服务器等待inputStream.read返回数据,最后断开连接。
发起一个HTTP请求的过程就是建立一个Socket通信的过程。
1.3 HTTP协议解析
B/S网络架构中核心是HTTP协议。 HTTP协议中最重要的是HTTP Header。
HTTP Header控制着用户浏览器渲染行为和服务器的执行逻辑。
浏览器缓存机制:
Ctrl+F5刷新页面一定能够请求没有缓存的页面。 Ctrl+F5刷新页面,浏览器直接向目标URL发送请求,不使用浏览器缓存。
Ctrl+F5刷新页面,HTTP请求头增加一些请求头,Pragma:no-cache
,Cache-Control:no-cache
。
1.4 DNS域名解析
- 浏览器检查缓存是否有域名对应的IP。
- 浏览器查找操作系统是否有对应的DNS解析成果(hosts文件)。
- (如果前2步没有得到IP)操作系统把域名发给LDNS,本地区的域名服务器。(大约80%的域名解析到这里就完成了)
- LDNS没有,直接到Root Server域名服务器请求解析。LDNS承担了主要的域名解析工作。
- 根域名服务器返回给本地域名服务器一个所查询域的主域名服务器gTLD Server地址。gTLD国际顶级域名服务器,只有13台左右。
- 本地域名服务器(Local DNS Server)向gTLD Server发送请求。
- 接受请求的gTLD Server查找返回此对应域名的Name Server域名服务器地址。你注册的域名服务器,某台服务商申请域名,域名解析交给域名服务器。
- Name Server域名服务器地址查询域名和IP映射关系表,连同TTL值返回给DNS Server域名服务器。
- 返回该域名对应的IP和TTL值,Local DNS Server缓存这个域名和IP对应关系,缓存时间TTL值控制。
- 解析结果给用户,用户根据TTL值缓存到本地系统中,域名解析结束。
1.5 CDN工作机制
内容分发网络(Content Delivery Network)。CDN=镜像+缓存+整体负载均衡。
CDN以缓存网站中的静态数据为主。
用户从主站服务器请求到动态内容后,再从CDN上下载这些静态数据,从而加速网页数据内容的下载速度。
步骤:
访问静态文件,先向Local DNS服务器发起请求,经过迭代到达域名注册服务器解析,
公司DNS把请求重新CNAME解析到另外的一个域名,这个域名指向CDN的DNS负载均衡服务器,由这个GTM分配用户距离最近的CDN节点。
有了DNS的解析结果,用户就可以去这个CDN节点访问这个静态文件了。
如果在这个CDN节点中,所请求的文件不存在,会再回源站获取文件,返回给用户。
负载均衡:
负载均衡对工作任务进行平衡、分摊到多个操作单元上执行。
通常有三种负载均衡架构:链路负载均衡、集群负载均衡、操作系统负载均衡。
链路负载均衡:由DNS解析成不同的IP。
集群负载均衡:分为硬件负载均衡和软件负载均衡。
硬件负载均衡:价格贵。
软件负载均衡:使用最普遍的一种负载方式。缺点是一次访问要经过多次代理服务器,会增加网络延时。
操作系统负载均衡:利用操作系统级别的软中断和硬件中断来达到负载均衡。
CDN动态加速:
原理:在CDN的DNS解析中通过动态的链路探测来寻找回源最好的一条路径,通过DNS的调度将所有请求调度到选定的路径上回源,从而加速用户访问的效率。
第二章 Java I/O工作机制
2.1 Java的I/O类库的基本架构
Java的I/O操作在包java.io下,大概可以分成四组:
基于字节操作的I/O接口:InputStream和OutputStream。
基于字符操作的I/O接口:Writer和Reader。
基于磁盘操作的I/O接口:File。
基于网络操作的I/O接口:Socket。
前两组主要是传输数据的数据格式,后两组主要是传输数据的方式。(Socket类不在java.io包)
不管是磁盘还是网络传输,最小的存储单元都是字节,不是字符。仅是为了操作方便提供一个直接写字符、读字符的I/O接口。
字节与字符的转换接口:
数据持久化或网络传输都是以字节进行的,所以必须要有从字符到字节或从字节到字符的转化。
InputStreamReader类是从字节到字符的转化桥梁
InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, "GBK");
2.2 磁盘I/O工作机制
操作系统为了保护系统本身的运行安全,将内核程序运行使用的内存空间和用户程序运行的内存空间进行隔离。
这虽然保证了内核程序运行的安全性,但是也必然存在 数据可能需要从内核空间向用户空间复制的问题 。
操作系统为了加速I/O访问,在内核程序的内存空间使用缓存机制 。
标准访问文件的方式:
读取:当应用程序调用read()接口时,操作系统检查在内核的高速缓存中有没有需要数据,如果已经缓存了,那么直接返回,没有就从磁盘中读取,并缓存在操作系统的缓存中。
写入:应用程序调用write()接口将数据从用户地址空间复制到内核地址空间的缓存中。什么时候写入到磁盘由操作系统决定,除非显示调用sync同步命令。
直接I/O的方式:
所谓直接I/O的方式就是应用程序直接访问磁盘数据,而不经过操作系统内核数据缓冲区。目的是 减少一次从内核缓冲区到用户程序缓存的数据复制。 如数据库管理系统。
缺点:如果访问的数据不在应用程序缓存中,那么每次数据都会直接从磁盘进行加载,这种直接加载会 非常缓慢 。
通常直接I/O与异步I/O结合使用,会得到较好性能。
同步访问文件的方式:
数据的读取和写入都是同步操作,与标准访问文件的不同是: 只有当数据被成功写到磁盘时才返回给应用程序成功的标志。
性能比较差,在一些对安全性要求较高的场景中使用。
异步访问文件的模式:
当访问数据的线程发出请求之后,线程会接着去处理其他事情,而不是阻塞等待,当请求的数据返回后继续处理下面的操作。
可以明显地提高应用程序的效率,但不会改变访问文件的效率。
内存映射的方式:
操作系统将内存中的某一块区域与磁盘中的文件关联起来,当要访问内存中的一段数据时,转换为访问文件的某一段数据。
减少数据从内核空间缓存到用户空间缓存的数据复制操作,因为 这两个空间的数据时共享的 。
Java序列化技术:
Java序列化:将一个对象转化成一串二进制表示的字节数组,通过保存或转移这些字节数据来达到持久化的目的。
需要持久化,对象必须实现java.io.Serializable接口。
反序列化,则是将这个字节数组再重新构造成对象。
下面是对一些复杂的对象情况的总结:
当父类继承Serializable接口时,所有子类都 可以 被序列化。
子类实现Serializable接口,父类没有,父类中的属性 不能 序列化(不报错,数据会丢失),但是在子类中属性仍能正确序列化。
如果序列化的属性是对象,则这个对象也必须实现Serializable接口,否则会报错。
在反序列化时,如果对象的属性有修改或删除,则修改的部分属性会丢失,但不会报错。
在反序列化时,如果serialVersionUID被修改,则反序列化时会失败。
2.3 网络I/O工作机制
将一份数据从一个地方正确地传输到另一个地方所需的时间称为 响应时间 ,影响因素:
网络带宽
传输距离
TCP拥塞控制
建立通信链路:
客户端:
创建一个Socket实例
为这个Socket实例分配没有被使用的本地端口号
创建一个包含本地地址、远程地址和端口号的套接字数据结构(这个结构保存在系统中直到这个连接关闭)
TCP三次握手,完成后,SOcket实例对象创建完成。
服务端:
创建一个ServerSocket 实例,只要端口号没有被占用,就会完成。
同时,为ServerSocket实例创建一个底层数据结构,通常监听所有地址。
之后当调用accept()方法时,进入阻塞状态,等待客户端的请求。
当一个新的请求到来时,将为这个连接创建一个新的套接字数据结构。
这个新创建的数据结构将会关联到ServerSocket实例的一个未完成的连接数据结构列表中。
注意,这时的服务端的与之对应的Socket实例并没有完成创建,而要等到与客户端的3次握手完成后,这个服务端的Socket实例才会返回,并将这个Socket实例对应的数据结构从未完成列表中移到已完成列表。
数据传输:
数据的读入和读取都是通过缓存区完成的。
2.4 NIO的工作方式
BIO带来挑战:
BIO即阻塞I/O,不管是磁盘I/O还是网络I/O,数据在写入OutputStream或从InputStream读取时都有可能会阻塞,一旦有阻塞,线程将会失去CPU的使用权,这在 当前的大规模访问量和有幸能要求的情况下是不能被接受的 。
网络I/O的一些解决办法:
一个客户端对应一个处理线程,出现阻塞时只是一个线程阻塞而不会影响其他线程工作。
采用线程池来减少线程的创建和回收的成本。
但一些场景无法解决:
一些需要大量HTTP长连接的情况,如淘宝的Web旺旺
想给某些客户端更高的服务优先级时,很难通过设计线程的优先级来完成。
每个客户端的请求在服务端可能需要访问一些竞争资源,这些客户端在不同线程,需要同步。
NIO工作机制:
Selector可以同时监听一组通信信道(Channel)上的I/O状态,前提是这个Selector已经注册到这些通信信道中。
Selector可以调用select()方法检查已经注册的通信信道上I/O是否已经准备好,如果没有至少一个信道I/O状态有变化,那么select()方法会阻塞等待或在超时时间后返回0;
如果有多个信道有数据,那么将会把这些数据分配到对应的数据Buffer中。
有一个线程来处理所有连接的数据交互,每个连接的数据交互都不是阻塞方式,所有可以同时处理大量的连接请求。
Buffer:
Buffer可以理解为一组基本数据类型的元素列表,它通过几个变量来保存这个数据的当前位置状态,也就是4个索引。
通过Channel获取的I/O数据首先要经过操作系统的Socket缓冲区,在将数据复制到Buffer中,这个操作系统缓冲区就是底层的TCP所关联的RecvQ或SendQ队列。
从操作系统缓冲区到用户缓冲区复制数据比较耗性能,Buffer提供另外一种 直接操作操作系统缓冲区的方式 , 即ByteBuffer.allocateDirector(size),这个方法返回的DirectByteBuffer就是与底层存储空间关联的缓冲区,它通过Native代码操作费JVM堆的内存空间。
NIO的数据访问方式:
NIO提供了比传统的文件访问方式更好的方法:一个是FileChannel.transferTo、FileChannel.transferFrom;另一个是FileChannel.map
FileChannel.transferXXX可以减少数据从内核到用户空间的复制,数据直接在内核空间中移动。
FileChannel.map将文件按照一定大小块映射为内存区域,当程序访问这个内存区域时将直接操作这个文件数据,这个方式 省去了数据从内核空间向用户空间复制的损耗。 适合大文件的只读性操作,如大文件的MD5校验。
2.5 I/O调优
磁盘I/O调优:
提高I/O性能:
增加缓存,减少磁盘访问次数。
优化磁盘的管理系统设计最优的磁盘方式策略,以及磁盘的寻址方式。底层操作系统层面
设计合理的磁盘存储数据块,以及访问这些数据库的策略。应用层面。
应用合理的RAID策略提升磁盘I/O。
网络I/O调优:
减少网络交互的次数:在网络交互的两端设置缓存;合并访问请求。
减少网络传输数据量的大小:将数据压缩后再传输;设计简单的协议,尽量通过读取协议头来获取有用的价值信息。
尽量减少编码:尽量直接以字节形式发送,也就是尽量提前将字符转化为字节,或减少字符到字节的转化过程。
2.6 设计模式解析之适配器模式
把一个类的接口变换成客户端所能接受的另一种接口,从而使两个接口不匹配而无法在一起工作的两个类能够在一起工作。
Java的I/O类库中有许多这样的需求,如将字符串数据转变成字节数据保存到文件中,将字节数据转变成流数据等。
以InputStreamReader和OutputStreamWriter类为例。InputStreamReader和OutputStreamWriter类分别继承了Reader和Writer接口,但是要创建它们的对象必须在构造函数中传入一个InputStream和OutputStream的实例。
InputStreamReader和OutputStreamWriter的作用也就是将InputStream和OutputStream适配到Reader和Writer。
2.7 设计模式解析之装饰器模式
InputStream类就是以抽象组件存在的;
而FileInputStream就是具体组件,它实现了抽象组件的所有接口;
FileterInputStream类无疑就是装饰角色,它实现了InputStream类的所有接口,并且持有InputStream的对象实例的引用;
BufferedInputStream是具体的装饰器实现者。这个装饰器类的作用就是使用InputStream读取的数据保存在内存中,而提高读取的性能。
2.8 适配器模式与装饰器模式的区别
装饰器与适配器模式都有一个别名就是 包装模式(Wrapper) 。
适配器模式的意义是要将一个接口转变成另一个接口,目的是 通过改变接口来达到重复使用的目的 ;
装饰器模式不是要改变被装饰对象的接口,而是恰恰要保持原有的接口,但是 增强原有对象的功能或者改变原有对象的处理方法而提升性能 。