Netty学习总结一(Netty优点场景、IO模型、JavaNIO)

Netty学习总结一(Netty优点场景、IO模型、JavaNIO)

Netty学习总结二(Reactor介绍和Netty线程模型)

Netty学习总结三(Netty启动流程和重要组件介绍)

Netty介绍

Netty优点

  • API使用简单,开发门槛低。
  • 功能强大,预置了多种编解码功能,支持多种主流协议。
  • 定制能力强,可以通过ChannelHandler对通信框架进行灵活扩展。
  • 性能高,与其他业界主流的NIO框架对比,Netty的综合性能最优。
  • 成熟、稳定,Netty修复了已经发现的所有JDK NIO中的BUG,业务开发人员不需要再为NIO的BUG而烦恼。
  • 社区活跃,版本迭代周期短,发现的BUG可以被及时修复

Netty应用场景

  • 互联网行业: 在分布式系统中, 各个节点之间需要远程服务调用, 高性能的 RPC 框架必不可少, Netty 作为
    异步高性能的通信框架, 往往作为基础通信组件被这些 RPC 框架使用。如Dubbo 协议默认使用 Netty 作为基础通信组件, 用于实现各进程节点之间的内部通信。
  • 游戏领域:无论是手游服务端还是大型的网络游戏,Java语言得到了越来越广泛的应用。Netty作为高性能的基础通信组件,它本身提供了TCP/UDP和HTTP协议栈。
    非常方便定制和开发私有协议栈,账号登录服务器,地图服务器之间可以方便的通过Netty进行高性能的通信
  • 大数据领域:经典的 Hadoop 的高性能通信和序列化组件 Avro 的 RPC 框架, 默认采用 Netty 进行跨界点通信

IO详解

IO读写的基础原理

想了解Netty需要先了解这些基础。
用户程序进行IO的读写,依赖于底层的IO读写,基本上会用到底层的read&write两大系统调用。在不同的操作系统中,IO读写的系统调用的名称可能不完全一样,但是基本功能是一样的。
调用操作系统的read,是把数据从内核缓冲区复制到进程缓冲区;而write系统调用,是把数据从进程缓冲区复制到内核缓冲区。
缓冲区的目的,是为了减少频繁地与设备之间的物理交换。
在Linux系统中,操作系统内核只有一个内核缓冲区。而每个用户程序(进程),有自己独立的缓冲区,叫作进程缓冲区。
用户程序的IO读写程序,在大多数情况下,并没有进行实际的IO操作,而是在进程缓冲区和内核缓冲区之间直接进行数据的交换

典型的系统调用流程

用户程序所使用的系统调用read&write,它们不等价于数据在内核缓冲区和磁盘之间的交换。read把数据从内核缓冲区复制到进程缓冲区,write把数据从进程缓冲区复制到内核缓冲区。
这里以read系统调用为例,先看下一个完整输入流程的两个阶段:

  1. 等待数据准备好。
  2. 从内核向进程复制数据。

如果是read一个socket(套接字),那么以上两个阶段的具体处理流程如下:

  1. 第一个阶段,等待数据从网络中到达网卡。当所等待的分组到达时,它被复制到内核中的某个缓冲区。这个工作由操作系统自动完成,用户程序无感知。
  2. 第二个阶段,就是把数据从内核缓冲区复制到应用进程缓冲区。

再具体一点,如果是在Java服务器端,完成一次socket请求和响应,完整的流程如下:

  1. 客户端请求:Linux通过网卡读取客户端的请求数据,将数据读取到内核缓冲区。
  2. 获取请求数据:Java服务器通过read系统调用,从Linux内核缓冲区读取数据,再送入Java进程缓冲区。
  3. 服务器端业务处理:Java服务器在自己的用户空间中处理客户端的请求。
  4. 服务器端返回数据:Java服务器完成处理后,构建好的响应数据,将这些数据从用户缓冲区写入内核缓冲区。这里用到的是write系统调用。
  5. 发送给客户端:Linux内核通过网络IO,将内核缓冲区中的数据写入网卡,网卡通过底层的通信协议,会将数据发送给目标客户端。

四种主要IO模型

同步阻塞IO(Blocking IO)

阻塞IO的特点是:在内核进行IO执行的两个阶段,用户线程都被阻塞了。
阻塞IO的优点是:应用的程序开发非常简单;在阻塞等待数据期间,用户线程挂起。在阻塞期间,用户线程基本不会占用CPU资源。
阻塞IO的缺点是:一般情况下,会为每个连接配备一个独立的线程;反过来说,就是一个线程维护一个连接的IO操作。在并发量小的情况下,这样做没有什么问题。但是,当在高并发的应用场景下,需要大量的线程来维护大量的网络连接,内存、线程切换开销会非常巨大。因此,基本上阻塞IO模型在高并发应用场景下是不可用的。

同步非阻塞IO(Non-blocking IO)

同步非阻塞IO的特点:应用程序的线程需要不断地进行IO系统调用,轮询数据是否已经准备好,如果没有准备好,就继续轮询,直到完成IO系统调用为止。
同步非阻塞IO的优点:每次发起的IO系统调用,在内核等待数据过程中可以立即返回。用户线程不会阻塞,实时性较好。
同步非阻塞IO的缺点:不断地轮询内核,这将占用大量的CPU时间,效率低下。
总体来说,在高并发应用场景下,同步非阻塞IO也是不可用的。一般Web服务器不使用这种IO模型。这种IO模型一般很少直接使用,而是在其他IO模型中使用非阻塞IO这一特性。在Java的实际开发中,也不会涉及这种IO模型。
这里说明一下,同步非阻塞IO,可以简称为NIO,但是,它不是Java中的NIO,虽然它们的英文缩写一样,希望大家不要混淆。Java的NIO(New IO),对应的不是四种基础IO模型中的NIO(None Blocking IO)模型,而是另外的一种模型,叫作IO多路复用模型(IO Multiplexing)。

IO多路复用(IO Multiplexing)

IO多路复用模型的特点:IO多路复用模型的IO涉及两种系统调用(System Call),另一种是select/epoll(就绪查询),一种是IO操作。IO多路复用模型建立在操作系统的基础设施之上,即操作系统的内核必须能够提供多路分离的系统调用select/epoll。
和NIO模型相似,多路复用IO也需要轮询。负责select/epoll状态查询调用的线程,需要不断地进行select/epoll轮询,查找出达到IO操作就绪的socket连接。
IO多路复用模型与同步非阻塞IO模型是有密切关系的。对于注册在选择器上的每一个可以查询的socket连接,一般都设置成为同步非阻塞模型。仅是这一点,对于用户程序而言是无感知的。
IO多路复用模型的优点:与一个线程维护一个连接的阻塞IO模式相比,使用select/epoll的最大优势在于,一个选择器查询线程可以同时处理成千上万个连接(Connection)。系统不必创建大量的线程,也不必维护这些线程,从而大大减小了系统的开销。
Java语言的NIO(New IO)技术,使用的就是IO多路复用模型。在Linux系统上,使用的是epoll系统调用。
IO多路复用模型的缺点:本质上,select/epoll系统调用是阻塞式的,属于同步IO。都需要在读写事件就绪后,由系统调用本身负责进行读写,也就是说这个读写过程是阻塞的。

异步IO(Asynchronous IO)

异步IO模型的特点:在内核等待数据和复制数据的两个阶段,用户线程都不是阻塞的。用户线程需要接收内核的IO操作完成的事件,或者用户线程需要注册一个IO操作完成的回调函数。正因为如此,异步IO有的时候也被称为信号驱动IO。
异步IO异步模型的缺点:应用程序仅需要进行事件的注册与接收,其余的工作都留给了操作系统,也就是说,需要底层内核提供支持。
理论上来说,异步IO是真正的异步输入输出,它的吞吐量高于IO多路复用模型的吞吐量。
就目前而言,Windows系统下通过IOCP实现了真正的异步IO。而在Linux系统下,异步IO模型在2.6版本才引入,目前并不完善,其底层实现仍使用epoll,与IO多路复用相同,因此在性能上没有明显的优势。
大多数的高并发服务器端的程序,一般都是基于Linux系统的。因而,目前这类高并发网络应用程序的开发,大多采用IO多路复用模型。
大名鼎鼎的Netty框架,使用的就是IO多路复用模型,而不是异步IO模型。

Linux操作系统中文件句柄数的限制

在生产环境Linux系统中,基本上都需要解除文件句柄数的限制。原因是,Linux的系统默认值为1024,也就是说,一个进程最多可以接受1024个socket连接。这是远远不够的。

ulimit -n

Java NIO

在1.4版本之前,Java IO类库是阻塞IO;从1.4版本开始,引进了新的异步IO库,被称为Java New IO类库,简称为JAVA NIO。New IO类库的目标,就是要让Java支持非阻塞IO,基于这个原因,更多的人喜欢称Java NIO为非阻塞IO(Non-Block IO),称“老的”阻塞式Java IO为OIO(Old IO)。
Java NIO由以下三个核心组件组成:

  1. Channel(通道)
  2. Buffer(缓冲区)
  3. Selector(选择器)

NIO和OIO的对比

在Java中,NIO和OIO的区别,主要体现在三个方面:
(1)OIO是面向流(Stream Oriented)的,NIO是面向缓冲区(Buffer Oriented)的。
何谓面向流,何谓面向缓冲区呢?
OIO是面向字节流或字符流的,在一般的OIO操作中,我们以流式的方式顺序地从一个流(Stream)中读取一个或多个字节,因此,我们不能随意地改变读取指针的位置。而在NIO操作中则不同,NIO中引入了Channel(通道)和Buffer(缓冲区)的概念。读取和写入,只需要从通道中读取数据到缓冲区中,或将数据从缓冲区中写入到通道中。NIO不像OIO那样是顺序操作,可以随意地读取Buffer中任意位置的数据。
(2)OIO的操作是阻塞的,而NIO的操作是非阻塞的。
NIO如何做到非阻塞的呢?大家都知道,OIO操作都是阻塞的,例如,我们调用一个read方法读取一个文件的内容,那么调用read的线程会被阻塞住,直到read操作完成。
而在NIO的非阻塞模式中,当我们调用read方法时,如果此时有数据,则read读取数据并返回;如果此时没有数据,则read直接返回,而不会阻塞当前线程。
(3)OIO没有选择器(Selector)概念,而NIO有选择器的概念。
NIO的实现,是基于底层的选择器的系统调用。NIO的选择器,需要底层操作系统提供支持。而OIO不需要用到选择器。

Channel(通道)

在OIO中,同一个网络连接会关联到两个流:一个输入流(Input Stream),另一个输出流(Output Stream)。通过这两个流,不断地进行输入和输出的操作。
在NIO中,同一个网络连接使用一个通道表示,所有的NIO的IO操作都是从通道开始的。一个通道类似于OIO中的两个流的结合体,既可以从通道读取,也可以向通道写入。
介绍其中最为重要的四种Channel(通道)实现:FileChannel、SocketChannel、ServerSocketChannel、DatagramChannel。
对于以上四种通道,说明如下:
(1)FileChannel文件通道,用于文件的数据读写。
(2)SocketChannel套接字通道,用于Socket套接字TCP连接的数据读写。
(3)ServerSocketChannel服务器嵌套字通道(或服务器监听通道),允许我们监听TCP连接请求,为每个监听到的请求,创建一个SocketChannel套接字通道。
(4)DatagramChannel数据报通道,用于UDP协议的数据读写。

Buffer(缓冲区)

应用程序与通道(Channel)主要的交互操作,就是进行数据的read读取和write写入。为了完成如此大任,NIO为大家准备了第三个重要的组件——NIO Buffer(NIO缓冲区)。通道的读取,就是将数据从通道读取到缓冲区中;通道的写入,就是将数据从缓冲区中写入到通道中。
缓冲区的使用,是面向流的OIO所没有的,也是NIO非阻塞的重要前提和基础之一。
Buffer类是一个抽象类,对应于Java的主要数据类型,在NIO中有8种缓冲区类,分别如下:ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer、MappedByteBuffer。
前7种Buffer类型,覆盖了能在IO中传输的所有的Java基本数据类型。第8种类型MappedByteBuffer是专门用于内存映射的一种ByteBuffer类型。
实际上,使用最多的还是ByteBuffer二进制字节缓冲区类型。

Selector(选择器)

它一个IO事件的查询器。通过选择器,一个线程可以查询多个通道的IO事件的就绪状态。
选择器的使命是完成IO的多路复用。一个通道代表一条连接通路,通过选择器可以同时监控多个通道的IO(输入输出)状况。选择器和通道的关系,是监控和被监控的关系。
通道和选择器之间的关系,通过register(注册)的方式完成。调用通道的Channel.register(Selector sel, int ops)方法,可以将通道实例注册到一个选择器中。register方法有两个参数:第一个参数,指定通道注册到的选择器实例;第二个参数,指定选择器要监控的IO事件类型
可供选择器监控的通道IO事件类型,包括以下四种:
(1)可读:SelectionKey.OP_READ
(2)可写:SelectionKey.OP_WRITE
(3)连接:SelectionKey.OP_CONNECT
(4)接收:SelectionKey.OP_ACCEPT
判断一个通道能否被选择器监控或选择,有一个前提:判断它是否继承了抽象类SelectableChannel(可选择通道)。如果继承了SelectableChannel,则可以被选择,否则不能。
简单地说,一条通道若能被选择,必须继承SelectableChannel类。
使用选择器,主要有以下三步:

  1. 获取选择器实例;
	//获取选择器实例
    Selector selector = Selector.open();
  1. 将通道注册到选择器中;
		// 2.获取通道
        ServerSocketChannelserverSocketChannel = ServerSocketChannel.open();// 3.设置为非阻塞
        serverSocketChannel.configureBlocking(false);// 4.绑定连接
        serverSocketChannel.bind(newInetSocketAddress(SystemConfig.SOCKET_SERVER_PORT));// 5.将通道注册到选择器上,并制定监听事件为:“接收连接”事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
  1. 轮询感兴趣的IO就绪事件(选择键集合)。
		//轮询,选择感兴趣的IO就绪事件(选择键集合)
        while (selector.select() > 0) {
            Set selectedKeys = selector.selectedKeys();
            Iterator keyIterator = selectedKeys.iterator();while(keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();//根据具体的IO事件类型,执行对应的业务操作
                if(key.isAcceptable()) {// IO事件:ServerSocketChannel服务器监听通道有新连接
                } else if (key.isConnectable()) {// IO事件:传输通道连接成功
                } else if (key.isReadable()) {// IO事件:传输通道可读
                } else if (key.isWritable()) {// IO事件:传输通道可写
                }//处理完成后,移除选择键
                keyIterator.remove();}}

摘自《Netty、Redis、Zookeeper高并发实战》

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值