0203 - 计算机网络
2021-04-18 09:54:41 1 举报
AI智能生成
架构师技术栈-网络编程
作者其他创作
大纲/内容
IO/NIO
基本概念
BIO/NIO对比
1、NIO解决了服务端很多不活跃的连接;当连接不多时,并且每个连接都很活跃时,BIO性能可能比非阻塞要好;
2、NIO所有数据都通过Buffer缓冲区再写入通道,不会像BIO将字节一个一个直接写入流中,减少频繁的I/O操作。
2、NIO所有数据都通过Buffer缓冲区再写入通道,不会像BIO将字节一个一个直接写入流中,减少频繁的I/O操作。
文件描述符FD
操作文件的三个阶段:用户进程(Selector轮询Socket)-----> 内核系统(Epoll轮询FD)
内核监视所有的文件描述,进程对FD的操作都需要进行系统调用
内核监视所有的文件描述,进程对FD的操作都需要进行系统调用
中断程序
中断的位置由“信号”决定,如:网卡输入、硬盘却页中断、断电中断、键盘鼠标点击
I/O模型
阻塞I/O模型
进程阻塞在recvfrom系统调用,直到数据包到达且被复制到用户空间的缓冲区;
recvfrom时阻塞方法,从而导致网络socket的连接accept()、读read()、写write()都是阻塞方法。
recvfrom时阻塞方法,从而导致网络socket的连接accept()、读read()、写write()都是阻塞方法。
非阻塞IO模型
进程轮询调用recvfrom,内核空间缓冲区没有数据的话,就直接返回一个EWOULDBLOCK错误
-------------------------------------------------------------------------------------------------
1、读写请求
2、立马返回结果
3、结果为erro说明内核数据还没有准备好
4、用户线程不断的询问内核数据是否准备好,不交出CPU,占用CPU
5、准备好后再收到询问,立马将数据拷贝给用户线程
-------------------------------------------------------------------------------------------------
1、读写请求
2、立马返回结果
3、结果为erro说明内核数据还没有准备好
4、用户线程不断的询问内核数据是否准备好,不交出CPU,占用CPU
5、准备好后再收到询问,立马将数据拷贝给用户线程
I/O复用模型
经典的Reactor设计模式,有时也称为异步阻塞IO,又被称为“事件驱动”。
---------------------------------------------------------------------------------------------------------------------------------------
原理:IO多路复用技术通过把多个IO的阻塞复用到同一个select的阻塞列表上,它通过记录传入的每一个IO流的状态同时管理多个IO,
从而避免每一个IO都阻塞一个线程。
---------------------------------------------------------------------------------------------------------------------------------------
多路复用使得系统在单线程的情况下可以同时处理多个客户端请求。
---------------------------------------------------------------------------------------------------------------------------------------
原理:IO多路复用技术通过把多个IO的阻塞复用到同一个select的阻塞列表上,它通过记录传入的每一个IO流的状态同时管理多个IO,
从而避免每一个IO都阻塞一个线程。
---------------------------------------------------------------------------------------------------------------------------------------
多路复用使得系统在单线程的情况下可以同时处理多个客户端请求。
I/O多路复用常用的系统调用
select、poll、epoll是用来管理大量文件的文件描述符
select
每个Socket都有一个监视等待队列,线程A的main方法遍历Socket时没有数据需要将线程A添加到每个Socket的等待队列上。
直到某个Socket上有数据,将所有等待队列上的线程A删除,main方法继续执行;
进程受阻于select系统调用,等待一个或多个套接字变为可读;
因为每个Socket都有一个监视等待队列需要管理,所以有很大的系统开销,单个进程所打开的FD是有一定限制的,默认值是1024;
当socket收到数据后,select不知道哪个Socket已经就绪,select/poll需要线性扫描全部的channel集合得到就绪Socket,复杂度
是O(N)。IO效率会随着FD数目的增加而线性下降;
通过内存复制将内核把FD消息通知给用户空间。
直到某个Socket上有数据,将所有等待队列上的线程A删除,main方法继续执行;
进程受阻于select系统调用,等待一个或多个套接字变为可读;
因为每个Socket都有一个监视等待队列需要管理,所以有很大的系统开销,单个进程所打开的FD是有一定限制的,默认值是1024;
当socket收到数据后,select不知道哪个Socket已经就绪,select/poll需要线性扫描全部的channel集合得到就绪Socket,复杂度
是O(N)。IO效率会随着FD数目的增加而线性下降;
通过内存复制将内核把FD消息通知给用户空间。
epoll
原理
将等待队列和阻塞分离,只要监视一个等待队列就好了
进程受阻于epoll_wait系统调用,等待直到注册的事件发生
因为只有一个等待队列,所以没有太大的开销,epoll并没有句柄数限制,它所支持的FD上限是操作系统的最大文件句柄数,这个数字远远大于 1024。在1GB内存的机器上大约是10万个句柄左右,只受限于操作系统的最大句柄数。
当 socket 收到数据后,中断程序会给 eventpoll 的“就绪列表(rdlist)”添加 socket 引用,epoll只会对“活跃”的socket进行轮询处理,复杂度降低到了O(1),epoll不存在随着FD数目的增加而线性下降
使用内存映射技术mmap加速内核与用户空间的消息传递
进程受阻于epoll_wait系统调用,等待直到注册的事件发生
因为只有一个等待队列,所以没有太大的开销,epoll并没有句柄数限制,它所支持的FD上限是操作系统的最大文件句柄数,这个数字远远大于 1024。在1GB内存的机器上大约是10万个句柄左右,只受限于操作系统的最大句柄数。
当 socket 收到数据后,中断程序会给 eventpoll 的“就绪列表(rdlist)”添加 socket 引用,epoll只会对“活跃”的socket进行轮询处理,复杂度降低到了O(1),epoll不存在随着FD数目的增加而线性下降
使用内存映射技术mmap加速内核与用户空间的消息传递
epoll内部的三个系统调用
epoll_create(int size)
进程启动时,生成一个epoll专用的文件描述符。它其实是在内核申请一空间用来存放你想关注的socket fd上是否发生以及发生了什么事件。
size就是你在这个epoll fd上能关注的最大socket fd数。
size就是你在这个epoll fd上能关注的最大socket fd数。
epoll_ctl(epfd, op, fd, event)
每当监听一个fd的事件,都要通过系统调用epoll_ctl()事先注册这个文件描述符,一旦基于这个文件描述符对应的事件就绪时,
内核会采用类似callback的回调机制,迅速激活这个文件描述符
第一个参数是epoll_create()的返回值
第二个参数op是操作类别(宏集合)
EPOLL_CTL_ADD
注册新的fd到epfd中
EPOLL_CTL_MOD
修改已经注册的fd的监听事件
EPOLL_CTL_DEL
从epfd中删除一个fd
第三个参数是需要监听的fd
第四个参数是监听类型(宏集合)
EPOLLIN
文件描述符可以读
EPOLLOUT
文件描述符可以写
EPOLLPRI
文件描述符有紧急的数据可读
EPOLLERR
文件描述符发生错误
EPOLLHUP
文件描述符被挂断
EPOLLET
将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说
EPOLLONESHOT
只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
内核会采用类似callback的回调机制,迅速激活这个文件描述符
第一个参数是epoll_create()的返回值
第二个参数op是操作类别(宏集合)
EPOLL_CTL_ADD
注册新的fd到epfd中
EPOLL_CTL_MOD
修改已经注册的fd的监听事件
EPOLL_CTL_DEL
从epfd中删除一个fd
第三个参数是需要监听的fd
第四个参数是监听类型(宏集合)
EPOLLIN
文件描述符可以读
EPOLLOUT
文件描述符可以写
EPOLLPRI
文件描述符有紧急的数据可读
EPOLLERR
文件描述符发生错误
EPOLLHUP
文件描述符被挂断
EPOLLET
将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说
EPOLLONESHOT
只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
epoll_wait(epfd, events, maxevents, timeout)
阻塞主线程,收集在epoll监控的事件中已经发生的事件,epoll将会把发生的事件赋值到events数组中
maxevents告诉内核这个events数组的大小
参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)
epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可
maxevents告诉内核这个events数组的大小
参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)
epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可
epoll的两种触发模式
水平触发(LT)
当就绪的fd未被用户进程处理后,下一次epoll查询依旧会返回该未处理的fd
epoll默认使用水平触发,通过相应选项可以使用边缘触发
epoll默认使用水平触发,通过相应选项可以使用边缘触发
边缘触发(ET)
无论就绪的fd是否被用户进程处理,下一次epoll查询不再返回
理论上性能更高,但是实现相当复杂,并且任何意外的丢失事件都会造成请求处理错误
理论上性能更高,但是实现相当复杂,并且任何意外的丢失事件都会造成请求处理错误
信号驱动I/O模型
开启套接口信号驱动I/O功能,通过系统调用sigaction,此系统调用时非阻塞的,立即返回;进程不受阻可以继续进行;
当数据准备就绪时,就为改进程生产一个SIGIO信号,通过信号回调通知应用程序调用recvfrom来读取数据, 并通知主
循环函数处理数据。
-------------------------------------------------------------------------------------------------------------------------
1、用户发起一个IO请求
2、给对应的socket注册一个信号函数
3、用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程
4、用户线程接收到信号之后
5、便在信号函数中调用IO读写操作来进行实际的IO请求操作
当数据准备就绪时,就为改进程生产一个SIGIO信号,通过信号回调通知应用程序调用recvfrom来读取数据, 并通知主
循环函数处理数据。
-------------------------------------------------------------------------------------------------------------------------
1、用户发起一个IO请求
2、给对应的socket注册一个信号函数
3、用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程
4、用户线程接收到信号之后
5、便在信号函数中调用IO读写操作来进行实际的IO请求操作
异步I/O模型
经典的Proactor设计模式,也称为异步非阻塞IO。异步I/O当数据准备就绪时,也为改进程生成一个SIGIO信号;
唯一的区别在于应用程序不用调用recvfrom来读取数据,而是由内核线程主动将内核空间缓存复制到用户空间缓存。
----------------------------------------------------------------------------------------------------------------------
1、用户发起一个IO请求,立刻就可以开始去做其它的事情
2、内核数据准备完毕以后,直接拷贝到用户线程
3、结束后内核会给用户发送一个信号,告知已完成
唯一的区别在于应用程序不用调用recvfrom来读取数据,而是由内核线程主动将内核空间缓存复制到用户空间缓存。
----------------------------------------------------------------------------------------------------------------------
1、用户发起一个IO请求,立刻就可以开始去做其它的事情
2、内核数据准备完毕以后,直接拷贝到用户线程
3、结束后内核会给用户发送一个信号,告知已完成
序列号与反序列化
序列化
序列化类型
JDK Serializable 对象序列化
ObjectInputStream、ObjectOutputStream
JSON序列化
Fastjson、Jackson、Gson等库
XML序列化
XStream、JAXB等
序列化协议
反序列化
IO(同步阻塞IO)
字节流/字符流
对象序列化
实现Serializable接口
readObject(ObjectInputStream ois)、writeObject(ObjectOutputStream oos)方法
文件操作(磁盘读写)
创建/删除文件
读/写文件
NIO(同步非阻塞IO,利用多路复用技术)
NIO(同步非阻塞IO,利用多路复用技术)
多路复用,传统IO的流是单向的,不能同时用来进行读写操作,而Channel则是双向的。
Channel(通道、双向)
通过open()静态方法打开一个通道
分类
FileChannel(文件IO)
DatagramChannel(UDP)
SocketChannel(TCP)
ServerSocketChannel(TCP)
Selector(选择器)
Selector 能够检测多个注册的通道上是否有事件发生,如果有事
件发生,便获取事件然后针对每个事件进行相应的响应处理。
件发生,便获取事件然后针对每个事件进行相应的响应处理。
只是用一个单线程就可以管理多个通道,也就是管理多个连接
减少了系统开销,并且避免了多线程之间的上下文切换导致的开销。
通过Selector.open()静态方法选择一个事件进行处理
Buffer(缓存区)
缓冲区,实际上是一个容器,是一个连续数组
Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由Buffer
使用的是堆外内存,不受GC管理,非线程安全
Netty框架
选择Netty的理由
1、NIO的类库和API繁杂,使用麻烦。需要掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等,而Netty使用启动器封装了这些复杂的操作;
2、NIO处理多条链路线程模型需要自己实现,Netty有高效的Reactor线程模型;
3、IO线程处理多条链路,它的调试和跟踪非常麻烦,特别是生成环境我们无法进行有效的调试和跟踪,往往只能靠一些日志来辅助分析,定位难度很大;
4、处理网络的闪断、客户端的重复写入、客户端的安全认证、消息的编解码、半包读写;
5、功能强大,预置了多种编解码功能,支持多种主流协议;
6、灵活的TCP参数配置能力;
7、Netty提供了自己的Channel和其子类实现,不受JVM厂家标准限制,扩展性强;
8、Netty为了解决ByteBuffer缺陷,重写了一个新的数据接口ByteBuf,新增了高级和实用的特性;
9、定制能力强,可以通过ChannelHandler对通信框架进行灵活地扩展;
10、对于NIO非阻塞Socket通信,JDK并没有提供现成可用的类库简化用户开发。Netty通过JDK的SSLEngine以SslHandler的方式提供对SSl/TLS安全传输的支持,极大的简化了用户的开发工具;
11、Netty修复了已经发现的所有JDK NIO BUG,业务开发人员不需要再为NIO的BUG而烦恼;
12、社区活跃、版本迭代周期短、发现的BUG可以被及时修复、同时更多的新功能会加入。
2、NIO处理多条链路线程模型需要自己实现,Netty有高效的Reactor线程模型;
3、IO线程处理多条链路,它的调试和跟踪非常麻烦,特别是生成环境我们无法进行有效的调试和跟踪,往往只能靠一些日志来辅助分析,定位难度很大;
4、处理网络的闪断、客户端的重复写入、客户端的安全认证、消息的编解码、半包读写;
5、功能强大,预置了多种编解码功能,支持多种主流协议;
6、灵活的TCP参数配置能力;
7、Netty提供了自己的Channel和其子类实现,不受JVM厂家标准限制,扩展性强;
8、Netty为了解决ByteBuffer缺陷,重写了一个新的数据接口ByteBuf,新增了高级和实用的特性;
9、定制能力强,可以通过ChannelHandler对通信框架进行灵活地扩展;
10、对于NIO非阻塞Socket通信,JDK并没有提供现成可用的类库简化用户开发。Netty通过JDK的SSLEngine以SslHandler的方式提供对SSl/TLS安全传输的支持,极大的简化了用户的开发工具;
11、Netty修复了已经发现的所有JDK NIO BUG,业务开发人员不需要再为NIO的BUG而烦恼;
12、社区活跃、版本迭代周期短、发现的BUG可以被及时修复、同时更多的新功能会加入。
网络事件
典型的网络事件
链路注册、链路激活、链路断开、链路发生异常、接收到的请求消息、请求消息接收并处理完毕;发送应答信息、发生用户自定义事件
事件通知机制实现原理
轮询
观察者模式
Nett事件
事实上inbound和outbound事件是Netty自身根据事件再pipeline种的流向抽象出来的术语,事件再pipeline种得到传播和处理,它是事件处理的总入口
inbound事件
通常由I/O线程触发,例如TCP链路建立事件、链路关闭事件、读事件、异常通知事件等;
Inbound事件是通知事件,当某件事情已经就绪后,通知上层;
Inbound事件发起者是unsafe,默认的处理者是TailContext,并且其处理方法是空实现;
Inbound事件在Pipeline种传输方向是handContext->tailContext。
Inbound事件是通知事件,当某件事情已经就绪后,通知上层;
Inbound事件发起者是unsafe,默认的处理者是TailContext,并且其处理方法是空实现;
Inbound事件在Pipeline种传输方向是handContext->tailContext。
outbound事件
通常由用户主动发起的网络I/O操作,例如用户发起的连接操作、绑定操作、消息放送等操作;
Outbound事件是请求事件(由Connect发起一个请求,并最终由unsafe处理这个请求);
Outbound事件的发起者是Channel、Outbound事件的处理者是unsafe;
Outbound事件是Pipeline中的传输方向是tailContext->handContext。
Outbound事件是请求事件(由Connect发起一个请求,并最终由unsafe处理这个请求);
Outbound事件的发起者是Channel、Outbound事件的处理者是unsafe;
Outbound事件是Pipeline中的传输方向是tailContext->handContext。
高性能
IO线程模型
2种fd
listenfd:一般情况只有一个,用于监听一个特定的端口(如8080)
connfd:每个连接都会打开一个connfd,用来收发数据
3种事件
listenfd进行accpet阻塞监听,创建一个connfd
用户态内核之间copy数据,每个connfd对应着2个应用缓冲区:readbuf和writebuf
客户端请求的5个阶段
read-接收到请求,读取数据;
decode-解码数据;
compute-业务逻辑处理;
encode-返回数据编码;
send-返送数据;
其中以read和send阶段IO最为频繁。
decode-解码数据;
compute-业务逻辑处理;
encode-返回数据编码;
send-返送数据;
其中以read和send阶段IO最为频繁。
Reacotr
概念
Reactor模型基于事件驱动,来了事件我通知你,你来处理。
三种角色
Reactor
事件监听和响应主线程,通过Select监控客户端请求事件,收到事件后进行dispatch分发,如果连接事件由acceptor线程接受连接,如果是读写事件分发给事件绑定函数Handler处理
Acceptor
接受客户端新连接,创建新的SocketChannel将其注册到Reactor主线程的selector上,并创建事件响应函数handler处理后续事件
Handler
事件响应函数,完成channel的读取数据,完成处理业务逻辑后,负责将结果写入channel发送给客户端,可用资源池来管理。
三种Reactor模型
单Reactor单线程模型
Reactor单主线程所有事件的响应,如果是连接事件分发给Acceptor线程处理,如果是读写事件则Reactor单主线程完成整个Handler处理;
理论上一个Reactor主线程可以独立处理所有IO相关的操作,但一个NIO线程同时处理成百上千的链路,会导致该线程负载过重,处理速度将变慢,性能无法支撑;
Redis是使用单Reactor单进程的模型
理论上一个Reactor主线程可以独立处理所有IO相关的操作,但一个NIO线程同时处理成百上千的链路,会导致该线程负载过重,处理速度将变慢,性能无法支撑;
Redis是使用单Reactor单进程的模型
单Reactor多线程模型
相对于第一个模型来说,Reactor线程在获取到IO的读写事件之后从channel中read数据,处理业务逻辑结果交由Worker线程池来处理(decode-compute-encode),handler收到Worker线程响应后通过send将响应结果返回给客户端。这样可以降低Reactor主线程的性能开销,从而更专注的做事情分发工作了,提升整体的应用的吞吐。
Reactor单主线程虽然只承担所有事情的监听和响应,不承担业务逻辑处理,只要是单线程可能会存在性能问题。
Reactor单主线程虽然只承担所有事情的监听和响应,不承担业务逻辑处理,只要是单线程可能会存在性能问题。
主从Reactor多线程模型
比起第二种模型,它是将Reactor分成两部分mainReactor和subReactor
mainReactor
负载监听serversocket,用来处理网络IO连接建立操作,将建立的socketChannel指定注册给subReactor;
从mainReactor线程池中随机选择一个Reactor线程作为acceptor线程,用于绑定监听端口,接收客户端连接;
acceptor线程接收客户端连接请求之后创建新的SocketChannel,将其注册到mainReactor线程池的其它Reactor线程上,由其负责接入认证、IP黑白名单过滤、握手等操作;
业务层的链路正式建立,将SocketChannel从主线程池的Reactor线程的多路复用上摘除,重新注册到subReactor线程池的线程上;
创建一个Handler用于下面处理其它读写事件。
从mainReactor线程池中随机选择一个Reactor线程作为acceptor线程,用于绑定监听端口,接收客户端连接;
acceptor线程接收客户端连接请求之后创建新的SocketChannel,将其注册到mainReactor线程池的其它Reactor线程上,由其负责接入认证、IP黑白名单过滤、握手等操作;
业务层的链路正式建立,将SocketChannel从主线程池的Reactor线程的多路复用上摘除,重新注册到subReactor线程池的线程上;
创建一个Handler用于下面处理其它读写事件。
subReactor
主要做和建立起来的socket做数据交互和事件业务处理操作,通常subReactor个数上可与CPU个数等同;
当有新的事件发生时,SubReactor会调用已创建的Handler进行响应;
Handler通过read从channel读取数据后,会分发给后面的Worker线程池进行业务处理;
Worker线程池会分配独立的线程完成真正的业务处理,如何将响应结果返回给Handler进行处理;
Handler收到响应结果后通过Send将响应结果返回给Client。
当有新的事件发生时,SubReactor会调用已创建的Handler进行响应;
Handler通过read从channel读取数据后,会分发给后面的Worker线程池进行业务处理;
Worker线程池会分配独立的线程完成真正的业务处理,如何将响应结果返回给Handler进行处理;
Handler收到响应结果后通过Send将响应结果返回给Client。
Nginx、Swoole、Memcached和Netty都是采用这种实现
Proactor
来了事件我来处理,处理完了通知你
理论上proactor比Reactor效率要高一些,异步IO能够充分利用DMA特性,让IO操作与计算重叠,但是实现真正的异步IO,操作系统需要做大量的工作;
目前Window下通过IOCP实现了真正的异步IO,而在Linux系统下的AIO并不完善,因此在Linux下实现高并发网络编程时都是以Reactor模式为主。
理论上proactor比Reactor效率要高一些,异步IO能够充分利用DMA特性,让IO操作与计算重叠,但是实现真正的异步IO,操作系统需要做大量的工作;
目前Window下通过IOCP实现了真正的异步IO,而在Linux系统下的AIO并不完善,因此在Linux下实现高并发网络编程时都是以Reactor模式为主。
多线程
无锁化的串行设计
NioEventLoop读取消息之后,直接调用ChannelPipeline的fireChannelRead(Object msg),只要用户不主动切换线程,一直会由NioEventLoop调用到用户的Handler,期间不进行线程切换。这种串行化处理方式避免了多线程操作导致的锁的竞争,从性能角度看时最优。
高效的并发编程
volatile的大量、正确的使用;
CAS和原子类的广泛使用;
线程安全容器的使用;
通过读写锁提升并发性;
CAS和原子类的广泛使用;
线程安全容器的使用;
通过读写锁提升并发性;
高性能的序列化框架
Netty默认提供了对Google Protobuf的支持,用户通过扩展Netty的编解码接口,可以实现其他的高性能序列化框架,例如Thrift
零拷贝
Netty的接收和发送ByteBuffer采用DIRECT BUFFERS,使用堆外直接内存进行Socket读写;
实现CompositeByteBuf,它对外将多个ByteBuf封装成一个ByteBuf;
Netty文件传输类DefaultFileRegion通过transferTo方法将文件发送到目标Channel中。
实现CompositeByteBuf,它对外将多个ByteBuf封装成一个ByteBuf;
Netty文件传输类DefaultFileRegion通过transferTo方法将文件发送到目标Channel中。
可靠性
网络通信类故障
客户端连接超时
创建连接超时定时任务之后,会由NioEventLoop负责执行,如果已经连接超时,但是服务端仍然没有返回TCP握手应答,则关闭连接;
设置完连接超时之后,Netty在发起连接的时候,会根据超时时候创建ScheduledFuture挂载在Reactor线程上,用于定时监测是否发送连接超时;
Netty的客户端连接超时参数与其他常用的TCP参数一起配置,使用起来非常方便,上层用户不用关心底层的超时实现机制。这即满足了用户的个性需求,由实现了故障的分层隔离。
设置完连接超时之后,Netty在发起连接的时候,会根据超时时候创建ScheduledFuture挂载在Reactor线程上,用于定时监测是否发送连接超时;
Netty的客户端连接超时参数与其他常用的TCP参数一起配置,使用起来非常方便,上层用户不用关心底层的超时实现机制。这即满足了用户的个性需求,由实现了故障的分层隔离。
通信对端强制关闭
由于TCP是全双工的,通信双方都需要关闭和释放Socket句柄才不会发生句柄的泄漏,Netty底层已经自动对该故障进行了处理,及时关闭了句柄;
场景:连接网络闪断,对方进程突然当即或者其他非正常关闭链路事件时,TCP链路就会发生异常,发生编码异常等不可恢复错误时,需要主动关闭连接。
场景:连接网络闪断,对方进程突然当即或者其他非正常关闭链路事件时,TCP链路就会发生异常,发生编码异常等不可恢复错误时,需要主动关闭连接。
链路主动关闭
在实际的NIO编程过程中,经常存在一种误区:认为只有对方关闭连接,就会反生IO异常,捕获IO异常之后再关闭连接即可。实际上,连接的合法关闭不会反生IO异常,它是一种正常场景,如果遗漏了该场景的判断和处理就会导致连接句柄泄漏。
场景:心跳超时,需要主动关闭连接;对于短连接协议,例如HTTP协议,通信双方数据交互完成之后,通常按照双方的约定由服务器关闭连接,客户端获得TCP连接关闭请求之后,关闭自身的Socket连接,双方正式断开连接。
场景:心跳超时,需要主动关闭连接;对于短连接协议,例如HTTP协议,通信双方数据交互完成之后,通常按照双方的约定由服务器关闭连接,客户端获得TCP连接关闭请求之后,关闭自身的Socket连接,双方正式断开连接。
定制I/O故障
客户端的端连重连机制
消息的缓存重发
接口日志中详细记录故障细节
运维相关功能,例如告警、触发邮件/短息等
消息的缓存重发
接口日志中详细记录故障细节
运维相关功能,例如告警、触发邮件/短息等
链路有效性检测
链路的有效性检测需要周期性的心跳对链路进行有效性检测,一旦发生问题,可以及时关闭链路,重建TCP连接
心跳检测机制
心跳检测的目的就是确认当前链路可用,对方活着并且能够正常接收和发送消息。做为高可靠的NIO框架,Netty也提供了心跳检测机制。
三个层面的心跳
TCP层面的心跳检测,即TCP的Keep-Alive机制,它的作用域是整个TCP协议栈
协议层的心跳检测,主要存在于长连接协议中,例如SMPP协议
应用层的心跳检测,它主要由各业务产品通过约定的方式定时给对方发送心跳消息实现
心跳机制
Netty的心跳检测实际上是利用链路空闲检测机制实现
空闲检测机制
读空闲超时--链路持续时间T没有读取到任何消息
写空闲超时--链路持续时间T没有发送任何消息
读写空闲超时--链路持续时间T没有接收或者发送任何消息
两种心跳检测
Ping-Pong型心跳
由通信一方定时发送Ping消息,对方接收到Ping消息之后,立即返回Pong应答消息给对方,属于请求-响应型心跳
Ping-Ping型心跳
不区分心跳请求和应答,由通信双方按照约定定时向对方发送心跳Ping消息,它属于双向心跳
重连机制
为了保证服务端能够有充足的时间释放句柄资源,在首次断连时客户端需要等待INTERVAL时间之后再发起重连,而不是失败后就立即重连;
为了保证句柄资源能够及时释放,无论什么场景下的重连失败,客户端都必须保证自身的资源被及时释放;
重连失败后,需要打印异常堆栈信息,方便后续的问题定位;
当客户端握手成功之后,在链路处于正常状态下,不允许客户端重复登录,以防止客户端在异常的状态下反复重连导致句柄资源被耗尽。
为了保证句柄资源能够及时释放,无论什么场景下的重连失败,客户端都必须保证自身的资源被及时释放;
重连失败后,需要打印异常堆栈信息,方便后续的问题定位;
当客户端握手成功之后,在链路处于正常状态下,不允许客户端重复登录,以防止客户端在异常的状态下反复重连导致句柄资源被耗尽。
内存保护机制
链路总数的控制
每条链路都包含接收和发送缓冲区,链路个数太多容易导致内存溢出
单个缓冲区的上限控制
防止非法长度或者消息过大导致内存溢出;消息解码的时候,对消息长度进行判断,如果超过最大容量上限,则抛出解码异常,拒绝分配内存。
缓冲区内存释放
防止因为缓冲区使用不当导致的内存泄漏;为了防止因为用户遗漏导致内存泄漏,Netty在Pipeline的TailHandler中自动对内存进行释放。
NIO消息发送队列的长度上限控制
Netty的NIO消息发送队列ChannelOutboundBuffer并没有容量上限控制,它会随着消息的积压自动扩展,直到达到Ox7fffffff;
通过启动项的ChannelOption设置发送队列的长度,或者通过-D启动参数配置该长度
通过启动项的ChannelOption设置发送队列的长度,或者通过-D启动参数配置该长度
流量整形
定义
流量整形(Traffic Shaping)是一种主动调整流量输出速率的措施。一个典型应用是基于下游网络结点的TP指标来控制本地流量的输出;流量整形对流量监管中需要丢弃的报文进行缓存-通常是将它们放入缓冲区或队列内,也称流量整形
原理
流量整形原理是对每次读取到的ByteBuf可写字节数进行计算,获取当前的报文流量,然后与流量整形阈值对比。如果已经达到或者超过了阈值,则计算等待时间delay,将当前的ByteBuf放到定时任务Task中缓存,由定时任务线程池在延迟delay之后继续处理该ByteBuf。
区别
流量整形和流控的最大区别在于流控会拒绝消息,流量整形不拒绝和丢弃消息,无论接收量多大,它总能以近似恒定的速度下发消息,根变压器的原理和功能类似
作用
防止由于上下游网元性能不均衡导致下游网元被压垮,业务流程中断;
防止由于通信模块接收消息过块,后端业务线程处理不及时导致的“撑死”问题;
防止由于通信模块接收消息过块,后端业务线程处理不及时导致的“撑死”问题;
分类
全局流量整形
它的作用域针对所有的Channel
单链路流量整形
单链路流量整形与全局流量整形的最大区别就是它以单个链路为作用域,可以对不同的链路设置不同的整形策略
优雅停机接口
Java的优雅停机通常通过注册JDK的ShutDownHook实现,当系统接收到退出指令后,首先标记系统处于退出状态,不再接收新的消息,然后将积压的消息处理完,最后调用资源回收接口将资源销毁,最后各线程退出执行
通常优雅退出有个时间限制,例如30S,如果到达执行时间仍然没有完成退出的操作,则由监控脚本直接kill -9 pid,强制退出
通常优雅退出有个时间限制,例如30S,如果到达执行时间仍然没有完成退出的操作,则由监控脚本直接kill -9 pid,强制退出
安全性
场景
仅限内部使用的RPC通信框架--不需要做握手,黑白名单、SSL/TLS等
对第三方开放的通信框架
在企业网--开放给内部其他模块调用的服务,通常不需要进行安全认证和SSL/TLS传输
被外部其他模块调用的服务,往往需要利用IP黑白名单,握手登录等方式进行安全认证
被外部其他模块调用的服务,往往需要利用IP黑白名单,握手登录等方式进行安全认证
开发给企业外部第三方应用访问--对于敏感的服务往往需要通过SSl/TLS进行安全传输
应用层协议的安全性
Netty需要向上层提供通信层的安全传输,也就是需要支持SSL/TLS
接入认证
Nett握手--握手的发起是在客户端和服务端TCP链路建立成功通道激活时,握手消息的接入和安全认证在服务端处理
IP地址黑名单认证机制
安全认证
SSL安全传输
SSL单向认证
SSL双向认证
第三方CA认证
源码解析
启动类
Builder模式进行对象初始化,门面模式对各种功能进行封装;
ServerBootstrap-创建服务端的辅助启动类ServerBootstrap对象,目的是降低服务端的开发复杂度;
Bootstrap-创建客户端辅助启动类Bootstrap对象。
ServerBootstrap-创建服务端的辅助启动类ServerBootstrap对象,目的是降低服务端的开发复杂度;
Bootstrap-创建客户端辅助启动类Bootstrap对象。
常用方法
group(parentGroup, childGroup)
参数配置Reactor线程模型
channel(Channel)
参数配置Channel类型,工厂模式创建Channel实例
服务端配置为NioServerSocketChannel,客户端配置为NioSocketChannel
option()
参数配置parentGroup的Option
handler(ChannelHandler)
参数配置parentGroup中accpet线程的Handler
childOption(ChannelHandler)
参数配置TCP属性
childAttr(AttributeKey)
参数配置childGroup的Ractor线程Attr
childHandler(ChildChannelHandler)
参数配置childGroup的Reactor线程Handler
bind(ip,port)
服务端监听端口,返回ChannelFuture
connect(ip,port)
客户端连接TCP服务端,返回ChannelFuture
EventLoopGroup
通过适当的参数配置,就可以支持三种Reactor线程模型,类似于web应用中的tomcat
继承关系
NioEventLoopGroup->EventLoopGroup->EventExcutorGroup->ScheduledExcutorService->ExcutorService
EventLoopGroup实际就是EventLoop的数组
线程模型
服务端启动的时候,创建了两个NioEventLoopGroup,他们实际是两个独立的Reactor线程池。线程池的隔离术,一个用于接收客户端的TCP连接,另一个用于处理I/O相关的读写操作,或者执行系统Task、定时任务Task等。
parentGroup
管理accept连接的线程
boss线程池
boosGroup对应的就是主线程池,只接收客户端的连接(注册,初始化逻辑),将链路状态变更事件通知给ChannelPipeline
childGroup
管理I/O读写的线程
worker线程
具体的工作由workerGroup这个从线程池来完成;异步读取通信对端的数据报,发送读事件到ChannelPipeline;异步发送消息到通信对端,调用ChannelPipeline的消息发送接口。
常用方法
shutdownGracefully()
优雅退出操作,通过方法方便的关闭各种资源
EventLoop线程
EventLoop的职责是处理所有注册到本线程多路复用器Selector上的Channel的网络IO事件
Selector的轮询操作由绑定的EventLoop线程run方法驱动
用户自定义Task
通过调用NioEventLoop的execute(Runnable task)方法实现,当IO线程和用户线程同时操作网络资源时,为了防止并发操作异常的锁竞争,将用户线程的操作封装成Task放入到消息队列中,并I/O线程负责执行,这样就实现了局部无锁化。
定时任务
通过调用NioEventLoop的schedule(Runnable comman、long delay、TimeUnit unit)方法实现
Channel
类似web应用的request&response
Netty提供了自己的Channel和其子类实现,NioServerSocketChannel、NioSocketChannel
常用方法
关联引用方法
返回ChannelPipeline,Unsafe--pipeline(),Unfafe()
closeFuture()
返回channel对象中的closeFuture对象
bind()、connect(), read()、write() flush() whth and flush, writeAndFlush
与NIO中Channel对应的方法功能相似,入参可以新增ChannelPromise,返回参数ChannelFuture
获取当前Chance的配置信息,例如:CONNECT_TIMEOUT_MILLS--config()
设置用户自定义的属性,类似于request上的attribute--attract()
unsafe接口
封装了Java底层的socket操作,作为连接netty和Java底层NIO的重要桥梁,channel会初始化unsafe来和Socket打交道
ChannelPipeline
责任链模式,类似于web应用中所有Filter、Servlet行为管理类
ChannelPipeline并不是NIO服务器端必需的
本质是一个负责处理网络事件的责任链,网络事件以事件流的形式在ChannelPipeline中流转
ChannelPipeline并不是NIO服务器端必需的
本质是一个负责处理网络事件的责任链,网络事件以事件流的形式在ChannelPipeline中流转
ChannelPipeline事件处理流程
用户自定义ChannelHandler会插入到head和tail之间,如果是ChannelInboundHandler的回调,根据插入的顺序从左到右进行链式调用,ChannelOutboundHandler则相反
常用方法
获取关联引用方法---返回Channel、ChannelHandlerContext--channel(),context()
触发outbound事件方法:ChannelPipeline拥有channel所有的方法
触发inbound事件方法:返回ChannelPipeline--fireChannelActive(),fireChannelRead(),fireChannelRegistered()
addLast、addFirst、addBefor,addAfter
ChannelHandlerContext
类似于web应用中单个Filter,Servlet的配置类ServletContext,它代表了ChannelPipeline之间的关联,创建于ChannelHandler被配置到ChannelPipeline的时候,每个ChannelHandler都对应一个ChannelHandlerContext
默认处理者
HandContext
HandContext继承AbstractChannelHandlerContxt实现了ChannelOutboundHandler,ChannelInboundHandler,所以其既可以处理inBound事件及可以处理outBound事件
TailContext
tail只是起了指引的作用,其具体处理业务逻辑都是尤其父亲实现,即调用下一个channelhandlerContext,其本身并没有神没实现
常用方法
关联引用方法:返回Channel,ChannelHandler、ChannelPipeline、EventExecutor---channel()、handler()、pipeline()、executor()
触发outbound事件方法:ChannelHandlerContext拥有channel所有方法
触发inbound事件方法:返回fireChannelActive()、firelChannelRead()、fireChannelRegister()
attr():ChannelHandlerContext的自定义属性
ChannleHandler
IO事件的处理类,类似于Web应用中单个Filter,Servlet配置中的实际逻辑类;该注解表示一个ChannelHandler可以属于多个ChannelPipeline,保证线程安全---@Sharable;被Skip注解的方法不会被调用,直接忽略,直接跳到下一个ChannelHandler中执行对应的方法---@Skip
子类
ChannelHandlerAdapter
选择性拦截和处理某个或者某些事件
ChannelInitializer
初始化Server端新接入的SockChannel对象,Channel被注册到Event Loop的时刻会调用--initChange()
ChannelInboundHandler
处理read()输入数据
ChannelOutboundHander
处理write()输出数据
常用方法
生命周期方法
channelRegistered
当前的channel注册到EventLoop
channelUnregistered
当前channel从EventLoop取消注册
channelActive
当前channel激活的时候
channelInactive
当前channel不活跃的时候,也就是当前channel到了它生命周期末
数据处理方法
channelRead
当前channel从远端读取到数据
channelReadComplete
channel read 消费完读取的数据的时候被触发
userEventTriggered
用户事件触发的时候
channelWriteabilityChanged
channel的写状态变化的时候触发
exceptionCaught
当发生异常时,关闭ChannelHandlerContext,释放和ChannelHandlerContext相关联的句柄等资源
预置的ChannelHandler
ByteToMessageCode
系统编解码框架
LengthFieldBasedFrameDecoder
通用基于长度的半包解码器
LoggingHandler
码流日志打印
SslHandler
SSl安全认证
IdleStateHandler
链路空闲检测
ChannelTrafficShapingHandler
流量整形
Base64Decoder和Base64Encoder
Base64编解码
AttributeMap
AttributeMap和Map的格式很像,可以根据AttributeKey找到相应的Attribute
Channel上的AttributeMap就是大家共享并且线程安全,每一个ChannelHandler都能获取到
ChannelHandlerContext上的AttributeMap是每个ChannelHandler独有的
Channel上的AttributeMap就是大家共享并且线程安全,每一个ChannelHandler都能获取到
ChannelHandlerContext上的AttributeMap是每个ChannelHandler独有的
ChannelOption
SO_BACKLOG
对应的是tcp/ip协议listen函数中的backlog参数,函数listen(int socketfd,int backlog)用来初始化服务端可连接队列,服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接,多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog参数指定了队列的大小,默认大小为100。
SO_REUSEADDR
对应于套接字选项中的SO_REUSEADDR,这个参数表示允许重复使用本地地址和端口,比如某个进程非正常退出,该程序占用的端口可能要被占用一段时间才能允许其他进程使用,而且程序死掉以后,内核一需要一定的时间才能够释放此端口,不设置SO_REUSEADDR就无法正常使用该端口。
SO_KEEPALIVE
参数对应于套接字选项中的SO_KEEPALIVE,该参数用于设置TCP连接,当设置该选项以后,连接会测试链接的状态,这个选项用于可能长时间没有数据交流的连接。当设置该选项以后,如果在两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文。
SO_SNDBUF/SO_RCVBUF
参数对应于套接字选项中的SO_SNDBUF,ChannelOption.SO_RCVBUF参数对应于套接字选项中的SO_RCVBUF这两个参数用于操作接收缓冲区和发送缓冲区的大小,接收缓冲区用于保存网络协议站内收到的数据,直到应用程序读取成功,发送缓冲区用于保存发送数据,直到发送成功。
SO_LINGER
对应于套接字选项中的SO_LINGER,Linux内核默认的处理方式是当用户调用close()方法的时候,函数返回,在可能的情况下,尽量发送数据,不一定保证会发生剩余的数据,造成了数据的不确定性,使用SO_LINGER可以阻塞close()的调用时间,直到数据完全发送。
TCP_NODELAY
该参数的使用与Nagle算法有关,Nagle算法是将小的数据包组装为更大的帧然后进行发送,而不是输入一次发送一次,因此在数据包不足的时候会等待其他数据的到了,组装成大的数据包进行发送,虽然该方式有效提高网络的有效负载,但是却造成了延时,而该参数的作用就是禁止使用Nagle算法,使用于小数据即时传输,于TCP_NODELAY相对应的是TCP_CORK,该选项是需要等到发送的数据量最大的时候,一次性发送数据,适用于文件传输。
IP_TOS
IP参数,设置IP头部的Type-of-Service字段,用于描述IP包的优先级和QoS选项。
ALLOW_HALF_CLOSURE
Netty参数,一个连接的远端关闭时本地端是否关闭,默认值为False。值为False时,连接自动关闭;为True时,触发ChannelInboundHandler的userEventTriggered()方法,事件为ChannelInputShutdownEvent。
ChannelFuture
Netty中所有的I/O操作都是异步的,这意味着任何的I/O调用都立即返回ChannelFuture,当一个I/O操作开始的时候,一个新的future对象机会创建
常用方法
await(),sync()
调用者线程会被阻塞,直到操作完成,sync()内部调用了await()
get()
获取线程处理结果
子主题
channel()
cannel()
addListener(GenericFutureListener)
get的替代方法
remove Listener(GennericFutuerListener)
ChannelPromise
Promise模式是一种回调模式,JDK非增强发Future并没有回调处理的模式,Netty通过Promise对Future进行扩展,用于设置I/O操作的异步回调处理
实现类:DefaultPromise
sync(),await()
setSuccess()、setFailure()
trySuccess()、tryFailure()
ByteBuf
用法和NIO的Buffer一样
相关辅助类
ByteBufHoler
ByteBufUtil
Buffer和ByteBuf对比
ByteBuffer长度固定,一旦分配完成,它的容量不能动态扩展和收缩,当我们的ByteBuffer进行put操作的时候,如果缓冲区剩余可写空间不够,就会发生BufferOverflowException异常;
ByteBuf进行put操作的时候会对剩余可用空间进行校验,如果剩余空间不足,需要新创建一个新的ByteBuffer,并将之前的ByteBuffer复制到新创建的ByteBuffer中,最后释放老的ByteBuffer;
ByteBuffer只有一个标识位控的指针position,读写的时候需要手工调用flip();
ByteBuf通过两个位置指针来协助缓冲区的读写操作,读操作使用readerIndex,写操作使用writerIndex。随者数据的写入writerIndex会增加,读取数据会被readerIndex增加,但它不会超过writeIndex。
ByteBuf进行put操作的时候会对剩余可用空间进行校验,如果剩余空间不足,需要新创建一个新的ByteBuffer,并将之前的ByteBuffer复制到新创建的ByteBuffer中,最后释放老的ByteBuffer;
ByteBuffer只有一个标识位控的指针position,读写的时候需要手工调用flip();
ByteBuf通过两个位置指针来协助缓冲区的读写操作,读操作使用readerIndex,写操作使用writerIndex。随者数据的写入writerIndex会增加,读取数据会被readerIndex增加,但它不会超过writeIndex。
私有协议
协议栈消息定义包括两部分
消息头(header)
crcCode
Netty消息的效验码
length
消息长度
sessionID
集群节点内全局唯一,由会话生成器ID生成
type
消息类型
业务请求消息、业务响应消息、业务ONE WAY消息、握手请求消息、握手应答消息、心跳请求消息、心跳应答消息
priority
消息优先级
attachment
可选字段,用于扩展消息头
消息体(body)
数据结构定义
NettyMessage
消息编解码
NettyMessageDecoder
NettyMessageEncoder
握手和安全认证Handler
LoginAuthReqHandler
LoginAuthRespHandler
心跳检测Handler
HeartBeatReqHandler
HeartBeatRespHandler
心跳超时Handler
ReadTimeoutHandler
网络与IO模型基础进阶
BIO、NIO及AIO线程模型详解
Netty线程模型及源码分析
高性能序列化协议protobuf及源码分析
粘包拆包现象及解决方案、编解码源码分析
Netty心跳机制
直接内存与Netty零拷贝详解
Netty之Http协议开发应用实战
Netty之WebSocket协议开发应用实战
计算机网络
网络编程
分类
按网络范围
局域网LAN
广域网WAN
计算机网络交换类型
电路交换
报文交换
分组交换
网络组件
终端设备
中间设备
网络介质
7层网络模型
1物理层
建立、维护、断开物理连接
2数据链路层
建立逻辑连接、进行硬件地址寻址、差错校验等功能
3网络层
进行逻辑地址寻址,实现不同网络之间的路径选择
协议有:ICMP、IGMP、IP
4传输层
定义了传输数据的协议端口号,以及流控和差错校验。
协议有:TCP、UDP
5会话层
建立、管理、终止会话
对应主机进程、指本地主机与远程主机正在进行的会话。
6表示层
数据的表示、安全、压缩。
格式有:JPEG、ASCII、EBCDIC、加密格式等
7应用层
网络服务与最终用户的一个接口。
协议有:HTTP、FTP、TFTP、SMTP、SNMP、DNS、TELNET、HTTPS、POP3、DHCP
TCP/IP概念层模型
TCP
进程
三次握手
三次握手:保证连接可以正常发送和接收
四次挥手
四次握手:确保双方都端开连接
TCP的滑动窗口
UDP
HTTP
http和tcp的区别
支持客户端/服务端模式
特点
状态码
get和post的区别
cookie和session的区别
http和Https的区别
socket
IP地址
子网划分
其它
0 条评论
下一页