Netty学习要点

Netty学习要点

1、UNIX的5种I/O模型
2、epoll与select的对比
3、私有协议栈可靠性设计
4、ByteBuf和ByteBuffer对比
5、Netty的线程模型

UNIX的5种I/O模型:

  Linux的内核将所有外部设备都看做一个文件来操作,对一个文件的读写操作会调用内核提供的系统命令,返回一个file descriptor(fd,文件描述符)。而对一个socket的读写也会有相应的描述符,称为socketfd(socket描述符),描述符就是一个数字,它指向内核中的一个结构体(文件路径,数据区等一些属性)。

  阻塞I/O模型: 最常用的I/O模型就是阻塞I/O模型,缺省情形下,所有文件操作都是阻塞的。在进程空间中调用recvfrom,其系统调用直到数据包到达且被复制到应用进程的缓冲区或者发生错误时才返回,在此期间一直会等待,进程在从调用recvfrom开始到它返回到整段时间内都是被阻塞的。

在这里插入图片描述
  非阻塞I/O模型: recvfrom从应用层到内核的时候,如果该缓冲区没有数据的话,就直接返回一个EWOULDBLOCK错位,一般都会对非阻塞I/O模型进行轮询检查这个状态,看内核是不是有数据到来。
在这里插入图片描述
  I/O复用模型: Linux提供select/poll,进程通过将一个或多个fd传递给select或poll系统调用,阻塞在select操作上,这样select/poll可以帮我门侦测多个fd是否处于就绪状态。select/poll是顺序扫描fd是否就绪,而且支持的fd数量有限,因此它的使用受到来一些制约。Linux还提供了一个epoll系统调用,epoll使用基于事件驱动方式替代顺序扫描,性能更高。当有fd就绪时,立即回调函数rollback。
在这里插入图片描述
  信号驱动I/O模型: 首先开启套接口信号驱动I/O功能,并通过系统调用sigaction执行一个信号处理函数(此系统调用立即返回,进程继续工作,它是非阻塞的)。当数据准备就绪时,就为该进程生成一个SIGIO信号,通过信号回调通知应用程序调用recvfrom来读取数据,并通知主循环函数处理数据。
在这里插入图片描述
  异步I/O: 告知内核启动某个操作,并让内核在整个操作完成后(包括将数据从内核复制到用户自己的缓冲区)通知我门。这种模型与信号驱动模型的主要区别是:信号驱动I/O由内核通知我们何时可以开始一个I/O操作,异步I/O模型由内核通知我们I/O操作何时已经完成。
在这里插入图片描述

epoll与select的对比:

  (1)select最大的缺陷就是单个进程打开的FD是有一定的限制,由FD_SETSIZE设置,默认1024。epoll没有这个限制,所支持的FD上限是操作系统的最大文件句柄数,通过cat /proc/sys/fs/file -max查看,这个值跟系统的内存关系比较大。
  (2)select每次调用都会线性扫描全部大集合,导致效率呈现线性下降。epoll是根据fd上面的callback函数实现的,只有活跃的socket才会主动调用callback函数,其他idle状态的socket则不会。
  (3)内存复制这块,epoll是通过内核和用户空间mmap同一块内存来实现减少复制的。

私有协议栈可靠性设计:

(1)心跳机制设计:
  当网络处于空闲状态持续时间达到T(连续周期T没有读写消息)时,客户端主动发送Ping心跳消息给服务端。
  如果在下一个周期T到来时客户端没有收到对方发送到Pong心跳应答消息或者读取到服务端发送到其他业务消息,则心跳失败计数器加1.
  每当客户端接收到服务的业务消息或者Pong应答消息时,将心跳失败计数器清零,连续N此没有接收到服务端的Pong消息或者业务消息,则关闭链路,间隔INTERVAL时间后发起重连操作。
  服务端网络空闲状态持续时间达到T后,服务端将心跳失败计数器加1,只要接收到客户端发送到Ping消息或者其他业务消息,计数器请零。
  服务端连续N次没有接收到客户端的Ping消息或者其他业务消息,则关闭链路,释放自由,等待客户端重连。
(2)重连机制:
  如果链路中断,等待INTERVAL时间后,由客户端发起重连操作,如果重连失败,间隔周期INTERVAL后再次发起重连,直到连接成功。重连失败,客户端必须及时释放自身的资源并打印异常堆栈信息,方便后续的问题定位。
(3)重复登录保护
  当客户端握手成功之后,在链路处于正常状态下,不允许客户端重复登录,以防止客户端在异常状态下反复重连导致句柄资源被耗尽。服务端接收到客户端的握手请求消息之后,首先对IP地址进行合法性检验,如果校验成功,在缓存的地址表中查看客户端是否已经登录,如果已经登录,则拒绝重复登录,返回错误码-1,同时关闭TCP链路,并在服务端的日志中打印握手失败的原因。服务端主动关闭链路时,清空客户端的地址缓存信息。
(4)消息缓存重发:
  无论客户端还是服务端,当发生链路中断之后,在链路恢复之前,缓存在消息队列中待发送的消息不能丢失,等链路恢复之后,重新发送这些消息,保证链路中断期间消息不丢失。考虑到内存溢出的风险,消息缓存队列设置上限,当达到上限之后,应该拒绝继续向该队列添加新的消息。
(5)安全性设计:
  为了保证整个集群环境的安全,内部长连接采用基于IP地址的安全认证机制,服务端对握手请求消息的IP地址进行合法性校验,如果在白名单之内,则校验通过,否则,拒绝对方连接。还可以基于密钥和AES加密的用户名+密码认证机制,还可以采用SSL/TSL安全传输。
(6)可扩展性
  Netty协议栈具备一定的扩展能力,统一的消息拦截、接口日志、安全、加解密等可以被方便地添加和删除,不需要修改之前的逻辑代码,类型Servlet的FilterChain和AOP。

ByteBuf和ByteBuffer对比:

JDK中ByteBuffer的缺点:
  (1)ByteBuffer长度固定,一旦分配完成,它的容量不能动态扩展和收缩,当需要编码的POJO对象大于ByteBuffer的容量时,会发生索引越界异常。
  (2)ByteBuffer只有一个标识位置的指针position,读写的时候需要手工调用flip()和rewind()等,使用者必须小心谨慎地处理这些API,否则很容易导致呈现处理失败。
  (3)ByteBuffer的API功能有限,一些高级和实用的特性它不支持,需要使用者自己编程实现。
Netty中ByteBuf的优点:
  (1)支持容量动态扩展和收缩
  (2)API更丰富一些,使用起来更简单
  (3)缓冲区使用的是直接内存,非堆内存,它在堆外进行内存分配,相比于堆内存,它的分配和回收速度会慢一些,但是将它写入或者从Socket Channel中读取时,由于少了一次内存复制,速度比堆内存快。堆内存的分配和回收速度快,可以被JVM自动回收,缺点就是如果进行Socket的I/O读写,需要额外做一次内存复制,将堆内存对应的缓冲区复制到内核Channel中,性能会有一定程度的下降。
  (4)还可选择内存池来提供性能。从内存回收角度看,ByteBuf分为两类,基于对象池的ByteBuf和普通ByteBUf。两者的主要区别就是基于对象池的ByteBuf可以重用ByteBuf对象,它自己维护了一个内存池,可以循环利用创建的ByteBuf,提升内存的使用效率,降低由于高负载导致的频繁GC。

Netty的线程模型

  (1)Reactor单线程模型:一个NIO线程处理所有的工作,不支持高负载,高并发场景

EventLoopGroup reactorGroup = new NioEventLoopGroup(1);
	try {
		ServerBootstrap b =new ServerBootstrap();
		b.group(reactorGroup, reactorGroup)
			.channel(NioServerSocketChannel.class)

  (2)Reactor多线程模型:一个专门的NIO线程–Acceptor线程用于监听服务端,接收客户端的TCP连接请求。网络I/O操作–读、写等由一个NIO线程池负责,线程池可以采用标准等JDK线程池实现,它包含一个任务队列和N个可用线程,由这些NIO线程负责消息的读取、解码、编码和发送。1个NIO线程可以同时处理N个链路,但是1个链路只对应1个NIO线程,防止发生并发操作问题。可以满足性能需求,但是不稳定,因为一个NIO线程负责监听和处理所有的客户端连接可能会存在性能问题。比如服务端需要对客户端的握手消息进行安全认证,认证本身非常损耗性能。单独一个Acceptor线程可能会存在性能不足问题。

EventLoopGroup acceptorGroup = new NioEventLoopGroup(1);
EventLoopGroup IOGroup = new NioEventLoopGroup();
	try {
		ServerBootstrap b =new ServerBootstrap();
		b.group(acceptorGroup, IOGroup)
			.channel(NioServerSocketChannel.class)

  (3)主从Reactor线程模型:服务端用于接收客户端连接的不再是1个单独的NIO线程,而是一个独立的NIO线程池。Acceptor接收到客户端TCP连接请求处理完成后(可能包含接入认证等),将创建的SocketChannel注册到I/O线程池(sub reactor线程池)的某个I/O线程上,由它负责SocketChannel的读写和编解码工作。Acceptor线程池只用于客户端的登录、握手和安全认证,一旦链路建立成功,就将链路注册到后端subReactor线程池的I/O线程上,由I/O线程负责后续的I/O操作。推荐这种。

EventLoopGroup acceptorGroup = new NioEventLoopGroup();
EventLoopGroup IOGroup = new NioEventLoopGroup();
	try {
		ServerBootstrap b =new ServerBootstrap();
		b.group(acceptorGroup, IOGroup)
			.channel(NioServerSocketChannel.class)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值