-
java.io流
BIO(同步阻塞 IO)
客户端一个请求对应一个线程。客户端上来一个请求(最开始的连接以及后续的IO请求),服务端新建一个线程去处理这个请求,由于线程总数是有限的(操作系统对线程总数的限制或者线程池的大小),所以,当达到最大值时给客户端的反馈就是无法响应,阻塞体现在服务端接收客户端连接请求被阻塞了,还有一种阻塞是在单线程处理某一个连接时,需要一直等待IO操作完成。同步体现在单个线程处理请求时调用read(write)方法需等待读取(写)操作完成才能返回。
缺点
- 默认单线程有阻塞,不支持并发。(两个阻塞)
- 设置成多线程的话,浪费资源。(有多少连接,就需要多少线程,并且大部分只是连接但是不做实际操作)
NIO(同步非阻塞 IO)
客户端一个IO请求对应一个线程。过程是这样的,每个客户端一开始上来的连接会注册到selector中,selector会轮询注册上来的连接是否有IO请求,如果有IO请求,就创建一个线程处理该连接上的该次请求。非阻塞体现在服务端能够无限量(相对于BIO)的接收客户端的连接请求。同步体现在单个线程处理请求时调用read(write)方法需等待读取(写)操作完成才能返回。这种模式下,如果后端应用处理遇到资源争夺(数据库操作)而阻塞等,为提高请求的处理速度,可以在后端设立资源池或队列等,把对应的请求数据以及现场(哪个连接的哪个请求等)放入队列,前台线程立即返回处理别的IO请求。
改进一:
- 单线程情况下,解阻塞。ServerSocketChannel
- 存储链接信息,遍历是否有客户端发送消息
仍然具有缺点:性能瓶颈。连接数量超级多的时候,遍历很费时间。优化方案:由jvm交给os操作系统执行。
改进二:
-
交给底层操作系统执行(c,c++执行)
- 即jni实现了java调用os操作系统函数的功能。 JNI是Java Native Interface的缩写
- java关键字:native ,如果有native则会调用操作系统的方法。
- 常见的有selector, epoll。 重点是epoll
-
注意:不同系统源码不一样。(所以下载的时候才会有版本区分,至少是原因之一吧)
- windows版本的java调用的是select
- linux版本java调用的是epoll
NIO方法
ServerSocketChannel
类似于ServerSocket,重点在于提供了非阻塞的方法。configureBlocking(false).则建立的连接不是阻塞的
SocketChannel
类似于Socket。用于客户端和服务器端的交互。
并且区别在于socket通过获取inputStream和outputStream来实现交互(理解成单向的),
但是SocketChannel是双向的。可以并且只能通过xxxBuffer实现读写。
ByteBuffer我理解的是一个容器类。里面有一个很有意思的方法:flip,用于切换读写状态。
AIO(NIO2:异步非阻塞IO)
客户端一个IO请求对应一个线程。
过程同NIO,只是在读写IO时略有差异,对于read,方法调用后会立即返回,返回对应中有个回调方法,调用时java会告知操作系统缓冲区大小以及地址,操作系统把流里面的内容读入缓冲区后回调刚刚read返回的回调方法。对应write,方法调用后会立即返回,返回对应中有个回调方法,调用时将数据放入缓存区,操作系统往流里面写完数据后同样会回调刚刚write返回的回调方法。
阻塞是因为此时是通过select系统调用来完成的,而select函数本身的实现方式是阻塞的,而采用select函数有个好处就是它可以同时监听多个文件句柄,从而提高系统的并发性请求。
异步步体现在单个线程处理请求时调用read(write)方法会立即返回,返回对象有回调方法,底层OS完成IO操作后会回调该方法。
异步主要体现
-
AsynchronousSocketChannel
- 类似SocketChannel
-
AsynchronousServerSocketChannel
- 类似ServerSocketChannel
- AsynchronousFileChannel
- AsynchronousDatagramChannel
在AIO socket编程中,服务端通道是AsynchronousServerSocketChannel,这个类提供了一个open()静态工厂,一个bind()方法用于绑定服务端IP地址(还有端口号),另外还提供了accept()用于接收用户连接请求。在客户端使用的通道是AsynchronousSocketChannel,这个通道处理提供open静态工厂方法外,还提供了read和write方法。
在AIO编程中,发出一个事件(accept read write等)之后要指定事件处理类(回调函数),AIO中的事件处理类是CompletionHandler<V,A>,这个接口定义了如下两个方法,分别在异步操作成功和失败时被回调。
void completed(V result, A attachment);
void failed(Throwable exc, A attachment);
demo
开启和绑定
回调方法需要具体实现
accept回调
read回调
write回调
NETTY
参考
netty介绍
netty概述
reactor模式
新手入门 《重点推荐》
源码进阶
优点
- 设计优雅:适用于各种传输类型的统一 API 阻塞和非阻塞 Socket;基于灵活且可扩展的事件模型,可以清晰地分离关注点;高度可定制的线程模型 - 单线程,一个或多个线程池;真正的无连接数据报套接字支持(自 3.1 起)。
- 使用方便:详细记录的 Javadoc,用户指南和示例;没有其他依赖项,JDK 5(Netty 3.x)或 6(Netty 4.x)就足够了。
- 高性能、吞吐量更高:延迟更低;减少资源消耗;最小化不必要的内存复制。
- 安全:完整的 SSL/TLS 和 StartTLS 支持。、
使用场景
-
互联网行业:在分布式系统中,各个节点之间需要远程服务调用,高性能的 RPC 框架必不可少,Netty 作为异步高性能的通信框架,往往作为基础通信组件被这些 RPC 框架使用。典型的应用有:阿里分布式服务框架 Dubbo 的 RPC 框架使用 Dubbo 协议进行节点间通信,Dubbo 协议默认使用 Netty 作为基础通信组件,用于实现各进程节点之间的内部通信。
-
游戏行业:无论是手游服务端还是大型的网络游戏,Java 语言得到了越来越广泛的应用。Netty 作为高性能的基础通信组件,它本身提供了 TCP/UDP 和 HTTP 协议栈。
-
非常方便定制和开发私有协议栈,账号登录服务器,地图服务器之间可以方便的通过 Netty 进行高性能的通信。
-
大数据领域:经典的 Hadoop 的高性能通信和序列化组件 Avro 的 RPC 框架,默认采用 Netty 进行跨界点通信,它的 Netty Service 基于 Netty 框架二次封装实现。
Reactor模式
使用netty,不得不了解reactor模式(别问我为什么知道 = =)
在处理web请求时,通常有两种体系结构,分别为:thread-based architecture(基于线程)、event-driven architecture(事件驱动)
高性能设计
Netty 作为异步事件驱动的网络,高性能之处主要来自于其 I/O 模型和线程处理模型,前者决定如何收发数据,后者决定如何处理数据
I/O 模型
线程处理模型
thread-based architecture
基于线程的体系结构通常会使用多线程来处理客户端的请求,每当接收到一个请求,便开启一个独立的线程来处理。
请求很多的时候,线程开销很大。造成服务器负担
event-driven architecture
事件驱动体系结构是目前比较广泛使用的一种。这种方式会定义一系列的事件处理器来响应事件的发生,并且将服务端接受连接与对事件的处理分离。其中,事件是一种状态的改变。比如,tcp中socket的new incoming connection、ready for read、ready for write。
reactor
reactor设计模式是event-driven architecture的一种实现方式,处理多个客户端并发的向服务端请求服务的场景。每种服务在服务端可能由多个方法组成。reactor会解耦并发请求的服务并分发给对应的事件处理器来处理。目前,许多流行的开源框架都用到了reactor模式,如:netty、node.js等,包括java的nio。
netty架构
模块组件
Bootstrap、ServerBootstrap
Bootstrap 类是客户端程序的启动引导类,ServerBootstrap 是服务端启动引导类
Channel和Pipeline
Netty 网络通信的组件,能够用于执行网络 I/O 操作
提供一下channel类型:
在 Netty 中每个 Channel 都有且仅有一个 ChannelPipeline 与之对应,它们的组成关系如下:
ChannelPipeline是一个双向链表。入站事件会从链表 head 往后传递到最后一个入站的 handler,出站事件会从链表 tail 往前传递到最前一个出站的 handler,两种类型的 handler 互不干扰。
HandlerAdapter适配器
Selector
Netty 基于 Selector 对象实现 I/O 多路复用,通过 Selector 一个线程可以监听多个连接的 Channel 事件。
当向一个 Selector 中注册 Channel 后,Selector 内部的机制就可以自动不断地查询(Select) 这些注册的 Channel 是否有已就绪的 I/O 事件(例如可读,可写,网络连接完成等),这样程序就可以很简单地使用一个线程高效地管理多个 Channel 。
ByteBuf
对比NIO.ByteBuffer的优势:
- Pooling (池化,这点减少了内存复制和GC,提升效率)
- 可以自定义缓冲类型
- 通过一个内置的复合缓冲类型实现零拷贝
- 扩展性好,比如 StringBuffer
- 不需要调用 flip()来切换读/写模式
- 读取和写入索引分开
- 方法链
- 引用计数
创建和释放
原理:通过release方法减去 heapBuffer 的使用计数,Netty 会自动回收 heapBuffer
手动获取与释放ByteBuf
获取Java 堆中的缓冲区
释放缓冲区(不管手动还是自动,其原理都是release)
自动获取和释放 ByteBuf
方式一:TailHandler 自动释放
方式二:SimpleChannelInboundHandler 自动释放
方式三:HeadHandler 自动释放
总结
- 入站处理流程中,如果对原消息不做处理,默认会调用 ctx.fireChannelRead(msg) 把原消息往下传,由流水线最后一棒 TailHandler 完成自动释放。
- 如果将原消息转化为新的消息并调用 ctx.fireChannelRead(newMsg)往下传,那必须把原消息release掉。
- 或者是继承SimpleChannelInboundHandler自动处理。
- 出站处理过程中,申请分配到的 ByteBuf,通过 HeadHandler 完成自动释放。
- 多层的异常处理机制,有些异常处理的地方不一定准确知道ByteBuf之前释放了没有,可以在释放前加上引用计数大于0的判断避免异常;
- 总之,只要是在传递过程中,没有传递下去的ByteBuf就需要手动释放,避免不必要的内存泄露。
缓冲区 Allocator 分配器
Netty提供了ByteBufAllocator的两种实现:PoolByteBufAllocator和UnpooledByteBufAllocator。前者将ByteBuf实例放入池中,提高了性能,将内存碎片减少到最小。这个实现采用了一种内存分配的高效策略,称为 jemalloc。它已经被好几种现代操作系统所采用。后者则没有把ByteBuf放入池中,每次被调用时,返回一个新的ByteBuf实例
缓冲区内存的类型
使用:
ByteBuf 的四个逻辑部分
第一个部分是已经丢弃的字节,这部分数据是无效的;
第二部分是可读字节,这部分数据是 ByteBuf 的主体数据, 从 ByteBuf 里面读取的数据都来自这一部分;
第三部分的数据是可写字节,所有写到 ByteBuf 的数据都会写到这一段。
第四部分的字节,表示的是该 ByteBuf 最多还能扩容的大小。
ByteBuf 的三个指针
-
readerIndex(读指针)
每读取一个字节,readerIndex 自增1 。一旦 readerIndex 与 writerIndex 相等,ByteBuf 不可读 。
-
writerIndex(写指针)
每写一个字节,writerIndex 自增1。一旦增加到 writerIndex 与 capacity() 容量相等,表示 ByteBuf 已经不可写了
-
maxCapacity(最大容量)
指示可以 ByteBuf 扩容的最大容量。当向 ByteBuf 写数据的时候,如果容量不足,可以进行扩容。
capacity()扩容超过最大限度 maxCapacity 就会报错
ByteBuf 的引用计数
如果计数为0,则需要看是pooled还是Unpooled,如果是pool则放入池子,反之由GC回收。
- 默认情况下,当创建完一个 ByteBuf 时,它的引用为1
- 每次调用 retain()方法, 它的引用就加 1
- 每次调用 release() 方法,是将引用计数减 1
ByteBuf 的浅层复制
ByteBuf 的浅层复制分为两种,有切片slice 浅层复制,和duplicate 浅层复制。
slice 只copy可读部分
duplicate copy全部
slice 切片浅层复制
输出:
duplicate() 浅层复制
工作架构图
BossGroup NioEventLoop 循环执行的任务包含 3 步:
1)轮询 Accept 事件;
2)处理 Accept I/O 事件,与 Client 建立连接,生成 NioSocketChannel,并将 NioSocketChannel 注册到某个 Worker NioEventLoop 的 Selector 上;
3)处理任务队列中的任务,runAllTasks。任务队列中的任务包括用户调用 eventloop.execute 或 schedule 执行的任务,或者其他线程提交到该 eventloop 的任务。
WorkerGroup NioEventLoop 循环执行的任务包含 3 步:
1)轮询 Read、Write 事件;
2)处理 I/O 事件,即 Read、Write 事件,在 NioSocketChannel 可读、可写事件发生时进行处理;
3)处理任务队列中的任务,runAllTasks。
通信协议(补充知识)
TCP
优点:
可靠,稳定 TCP的可靠体现在TCP在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制,在数据传完后,还会断开连接用来节约系统资源。
缺点:
慢,效率低,占用系统资源高,易被攻击 TCP在传递数据之前,要先建连接,这会消耗时间,而且在数据传递时,确认机制、重传机制、拥塞控制机制等都会消耗大量的时间,而且要在每台设备上维护所有的传输连接,事实上,每个连接都会占用系统的CPU、内存等硬件资源。 而且,因为TCP有确认机制、三次握手机制,这些也导致TCP容易被人利用,实现DOS、DDOS、CC等攻击。
使用场景:
当对网络通讯质量有要求的时候,比如:整个数据要准确无误的传递给对方,这往往用于一些要求可靠的应用,比如HTTP、HTTPS、FTP等传输文件的协议,POP、SMTP等邮件传输的协议。 在日常生活中,常见使用TCP协议的应用如下: 浏览器,用的HTTP FlashFXP,用的FTP Outlook,用的POP、SMTP Putty,用的Telnet、SSH QQ文件传输 …………
UDP
优点:
快,比TCP稍安全 UDP没有TCP的握手、确认、窗口、重传、拥塞控制等机制,UDP是一个无状态的传输协议,所以它在传递数据时非常快。没有TCP的这些机制,UDP较TCP被攻击者利用的漏洞就要少一些。但UDP也是无法避免攻击的,比如:UDP Flood攻击
缺点:
不可靠,不稳定 因为UDP没有TCP那些可靠的机制,在数据传递时,如果网络质量不好,就会很容易丢包。
使用场景:
当对网络通讯质量要求不高的时候,要求网络通讯速度能尽量的快,这时就可以使用UDP。 比如,日常生活中,常见使用UDP协议的应用如下: QQ语音 QQ视频 TFTP
RTP
是用于Internet上针对多媒体数据流的一种传输层协议。它是建立在 UDP 协议上的. RTP 不像http和ftp可完整的下载整个影视文件,它是以固定的数据率在网络上发送数据,客户端也是按照这种速度观看影视文件,当影视画面播放过后,就不可以再重复播放
RTCP
RTSP
RTSP 是一种双向实时数据传输协议,它允许客户端向服务器端发送请求,如回放、快进、倒退等操作
WebRTC
RTMP(Real Time Messaging Protocol)
基于TCP不会丢失
HLS
HLS 点播,基本上就是常见的分段HTTP点播,不同在于,它的分段非常小
__EOF__