Netty权威指南

来源:互联网 发布:查询父级部门 mysql 编辑:程序博客网 时间:2024/05/22 04:55

Chapter1.java I/O演进之路

1.1I/O基础入门

在java 1.4之前,java程序员在开发高性能I/O程序的时候,会面临的问题主要有:

1.没有数据缓冲区,I/O性能存在问题
2.没有c或者c++中的Channel概念,只有输入和输出流
3.同步阻塞式I/O通信(BIO),通常会导致通信线程被长时间阻塞
4.支持的字符集有限,硬件可移植性不好

1.1.1Linux网络I/O模型简介

根据UNIX网络编程对I/O模型的分类,UNIX提供了5种I/O模型:

1.阻塞I/O模型
2.非阻塞I/O模型
3.I/O复用模型
4.信号驱动I/O模型
5.异步I/O

对于大多数java程序员来说,不需要了解网络编程的底层细节,只需要知道,对于操作系统而言,底层是支持异步I/O通信的,只不过在很长一段时间java并没有提供异步I/O通信的类库。

java NIO的核心类库多路复用器Selector就是基于epoll的多路复用技术实现的。

1.1.2I/O多路复用技术

与传统的多线程/多进程模型比,I/O多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降低了系统的维护工作量。

支持I/O多路复用的系统调用select,pselect,poll,epoll,epoll相比于他们,有了很大的改进,主要有:

1.支持一个进程打开的socket描述符(FD)不受限制(仅受限于操作系统的最大文件句柄数)
关于这个最大文件句柄数,可以通过cat /proc/sys/fs/file-max查看。

2.I/O效率不会随着FD数目的增加而线性下降

3.使用mmap加速内核与用户空间的消息传递
无论是select,poll还是epoll都需要内核把FD消息传递给用户空间,如何避免不必要的内存复制就显得非常重要了,epoll是通过内核和用户空间mmap同一块内存实现。

4.epoll的API更简单
包括创建一个epoll描述符,添加监听事件,阻塞等待所监听的事件发生,关闭epoll描述符等。

1.2Java的I/O演进

在JDK1.4推出Java NIO之前,基于Java的所有Socket通信都采用了同步阻塞模式(BIO),这种一请求一应答的通信模型简化了上层的应用开发,但是在性能和可靠性方面存在着巨大的瓶颈。

Chapter2.NIO入门

2.1传统的BIO编程

网络编程的基本模型是client/server模型,也就是两个进程之间进行相互通信,其中服务端提供位置信息(绑定的IP地址和监听端口),客户端通过连接操作向服务端监听的地址发起连接请求,通过三次握手建立连接,如果连接建立成功,双方就可以通过Socket进行通信。

在基于传统同步阻塞模型开发中,ServerSocket负责绑定IP地址,启动监听端口;Socket负责发起连接操作,连接成功后,双方通过输入和输出流进行同步阻塞通信。

2.1.1BIO通信模型图

采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,它接受到连接请求后,为每一个客户端创建一个新的处理线程进行链路处理,处理完成之后,通过输出流返回给客户端,线程销毁。

该模型最大的问题是,缺乏弹性伸缩能力,当客户端并发访问量增加时,服务端的线程个数和客户端并发访问数呈1:1的关系,系统性能下降。

为了改进一线程一连接的模型,后来又演进出了一种通过线程池或者消息队列实现1个或者多个线程处理N个客户端的模型,由于它的底层通信机制依然使用同步阻塞I/O,所以被称为“伪异步”。

2.2伪异步I/O编程

后端通过一个线程池来处理多个客户端的请求接入,形成客户端个数M:线程池最大线程数N的比例关系,其中M可以远远大于N,通过线程池可以灵活的调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。

2.2.1伪异步I/O模型图

当有新的客户端接入的时候,将客户端的Socket封装成一个Task(该任务实现java.lang.Runnable接口),投递到后端的线程池中进行处理,JDK的线程池维护一个消息队列和N个活跃线程对消息队列中的任务进行处理。由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。

伪异步I/O通信框架采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成线程资源耗尽问题。但是由于它底层的通信依然采用同步阻塞模型,因此无法从根本上解决问题。

2.2.3伪异步I/O弊端分析

要深入分析,需要看两个Java同步I/O的API说明:

InputStram中read方法//this method blocks until input datais available,end of file is detected,or an exception is thrown.public int read(byte[] b)

解释,当堆Socket的输入流进行读取操作的时候,它会一直阻塞下去,知道发生下列三种事件:

1.有数据可读
2.可用数据已经读取完毕
3.发生空指针或者I/O异常

这意味着当对方发送请求或者应答消息比较缓慢,或者网络传输较慢时,读取输入流一方的通信线程将被长时间阻塞。如果对方要60s才能够将数据发送完成,读取一方的I/O线程也将会被同步阻塞60s,在此期间,其他接入信息只能在消息队列中排队。

OutputStram中write方法//writes an array of bytes.this method will block until the bytes are actually written.public int write(byte[] b)

当调用OutputStream的write方法写输出流的时候,它将会被阻塞,知道所有要发送的字节全部写入完毕,或者发生异常。

学习过TCP/IP相关知识的都知道,当消息的接收方处理缓慢的时候,将不能及时地从TCP缓冲区读取数据,这将导致发送方的TCP window size不断减小,直到为0,双方处于Keep-Alive状态,消息发送方将不能再向TCP缓冲区写入消息,这时如果采用的是同步阻塞I/O,write操作将会被无限期阻塞,知道TCP window size大于0或者发生I/O异常。

通过对输入和输出流的API文档进行分析,我们了解到读和写操作都是同步阻塞的,阻塞的时间取决于对方I/O线程的处理速度和网络I/O的传输速度。但是,我们无法保证在生产环境中这些都是可控的,这是同步方式的致命弱点。

下面简单分析如果通信对方返回应答时间过长,会引起的级联故障:

1.服务端处理缓慢,返回应答消息耗费60s,平时只需要10ms。
2.采用伪异步I/O的线程正在读取故障服务节点的响应,由于读取输入流是阻塞的,因此,它将会被同步阻塞60s。
3.假如所有的可用线程都被故障服务器阻塞,那后续所有的I/O消息都将在队列中排队。
4.由于线程池采用阻塞队列实现,当队列堆积满之后,后续入队列的操作将被阻塞。
5.由于前端只有一个Acceptor线程接收客户端接入,它被阻塞在线程池的同步阻塞队列之后,新的客户端请求消息将被拒绝,客户端会发生大量的连接超时。
6.由于几乎所有的连接都超时,调用者会认为系统已经崩溃,无法接受新的请求消息。

2.3NIO编程

一般来说,低负载,低并发的应用程序可以选择同步阻塞I/O以降低编程复杂度,但是对于高负载,高并发的网络应用,需要使用NIO的非阻塞模型进行开发。

2.3.1NIO类库简介

1.缓冲区Buffer

Buffer是一个对象,它包含一些要写入或者要读出的数据。
不同:在面向流的I/O中,可以将数据直接写入或者将数据直接读到Stream对象中。在NIO中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的;在写入数据时,写入到缓冲区中。
任何时候访问NIO中的数据,都是通过缓冲区进行操作

除了ByteBuffer,每一个Buffer内都有完全一样的操作,只是它们所处理的数据类型不一样。

2.通道Channel

Channel是一个通道,可以通过它读取和写入数据,网络数据通过Channel读取和写入。通道和流的不同之处在于通道是双向的,流只是在一个方向上移动(一个流必须是InputStream或者OutputStream的子类),而且通道可以用于读,写或者同时用于读写。

因为Channel是全双工的,所以它可以比流更好地映射底层操作系统的API。特别是在UNIX网络编程模型中,底层操作系统的通道都是全双工的,同时支持读写操作。

实际上Channel可以分为两大类:分别是用于网络读写的SelectableChannel和用于文件操作的FileChannel

3.多路复用器Selector

它是Java NIO编程的基础。多路复用器提供选择已经就绪的任务的能力

简单来讲,Selector会不断地轮询注册在其上的Channel,如果某个Channel上面有新的TCP连接接入,读和写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。

一个多路复用器Selector可以同时轮询多个Channel,由于JDK使用了epoll()代替传统的select实现,所以它并没有最大连接句柄1024/2048的限制。这也就意味着只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端,确实是非常大的进步。

NIO编程难度比同步阻塞BIO大很多,这些难度还不包括“半包读”和“半包写”,如果考虑这些,难度更大。但是NIO的优点足以让其值回票价:

1.客户端发起的连接操作是异步的,可以通过在多路复用器注册OP_CONNECT等待后续结果,不需要像之前的客户端那样被同步阻塞。
2.SocketChannel的读写操作都是异步的,如果没有可读写数据它不会同步等待,直接返回,这样I/O通信线程就可以处理其他的链路,不需要同步等待这个链路可用。
3.线程模型的优化:通过epoll实现,没有连接句柄数的限制

JDK7升级了NIO类库,被称为NIO2.0,值得注意的是,Java正式提供了1.异步文件I/O操作,同时提供了2.与UNIX网络编程事件驱动I/O对应的AIO

2.4AIO编程

2.5 4种I/O对比

这里写图片描述

2.6.1不选择Java原生NIO编程的原因

1.NIO的类库和API繁杂,使用麻烦,你需要熟练使用Selector,ServerSocketChannel,SocketChannel,ByteBuffer等。
2.需要具备其他的额外技能做铺垫,例如熟悉Java多线程编程。
3.可靠性能力补齐,工作量和难度都非常大。
4.Java NIO的BUG。

Chapter3.Netty入门应用

Chapter4.TCP粘包/拆包问题的解决之道

4.1TCP粘包/拆包

TCP是个“流”协议,所谓流,就是没有界限的一串数据。所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小包封装成一个大的数据包进行发送。

4.1.3粘包问题的解决策略

由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决(比如HTTP协议),根据业界的主流协议的解决方案,可以归纳如下:

1.消息定长,例如每个报文的大小为固定长度200字节,如果不够,空位补空格。
2.在包尾添加回车换行符进行分割,例如FTP协议。
3.将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段,通常涉及思路为消息头的第一个字段使用int32 来表示消息的总长度。
4.更复杂的应用层协议

4.3.4LineBasedFrameDecoder和StringDecoder的原理分析

LineBasedFrameDecoder的工作原理是它依次遍历ByteBuf中的可读字节,判断看是否有”\n”或者”\r\n”,如果有,就以此位置为结束位置,从可读索引到结束位置区间的字节就组成了一行。

StringDecoder的功能非常简单,就是将接收到的对象转换成字符串,然后继续调用后面的handler。LineBasedFrameDecoder+StringDecoder组合就是按行切换的文本解码器,它被设计用来支持TCP的粘包和拆包。

当然,Netty中还提供了很多其他的解决TCP粘包和拆包的解码器,满足不同的需求!

5.3总结

DelimiterBasedFrameDecoder用于对使用分隔符结尾的消息进行自动解码,FixedLengthFrameDecoder用于对固定长度的消息进行自动解码。

Chapter6.编解码技术

Java序列化的目的主要有两个

1.网络传输
2.对象持久化

Java序列化仅仅是Java编解码技术的一种,由于它的种种缺陷,衍生出很多种编解码技术和框架。

6.1Java序列化的缺点

1.无法跨语言
2.序列化后的码流太大
3.序列化性能太低

6.2业界主流的编解码框架

1.Google的Protobuf
2.Facebook的Thrift

Chapter11.WebSocket协议开发

长期以来存在着各种技术让服务器得知有新数据可用时,立即将数据发送到客户端。这些技术种类繁多,例如“推送”或Comet。最常用的是对服务器发起链接创建假象,被称为长轮询。长轮询和其他技术都非常好用,在Gmail聊天等应用中会经常使用它们。

但是,这些解决方案都存在一个共同的问题:由于HTTP协议的开销,导致它们不适用于低延迟应用

为了解决这些问题,WebSocket将网络套接字引入到了客户端和服务端,浏览器和服务器之间可以通过套接字建立持久的连接,双方随时都可以互发数据给对方,而不是之前由客户端控制的一请求一应答模式

11.1HTTP协议的弊端

主要弊端如下:

1.HTTP协议为半双工协议。
2.HTTP消息冗长而繁琐
3.针对服务器推送的黑客攻击,例如长时间轮询。

现在很多网站为了实现消息推送,所用的技术都是轮询。轮询的思想和实现都很简单,但是会占用很多的带宽和服务器资源。

比较新的一种轮询技术是Comet,使用了Ajax。这种技术虽然可达到双向通信,但依然需要发出请求,而且在Comet中,普遍采用了长连接,也会有上述性能问题。

11.2WebSocket入门

WebSocket特点:

1.单一的TCP连接,采用全双工模式通信
2.对代理,防火墙和路由器透明
3.无头部信息,Cookie和身份验证
4.无安全开销
5.通过”ping/pong”帧保持链路激活
6.服务器可以主动传递消息给客户端,不再需要客户端轮询

WebSocket设计出来的目的就是要取代轮询和Comet技术,使客户端浏览器具备像C/S架构下桌面系统一样的实时通信能力。

the end

0 0