2020-10-19

nio和netty详解

一、概述
Netty是一个Java的开源框架。提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。
Netty是一个NIO客户端,服务端框架。允许快速简单的开发网络应用程序。例如:服务端和客户端之间的协议,它简化了网络编程规范。

二、NIO开发的问题
1、NIO类库和API复杂,使用麻烦。
2、需要具备Java多线程编程能力(涉及到Reactor模式)。
3、客户端断线重连、网络不稳定、半包读写、失败缓存、网络阻塞和异常码流等问题处理难度非常大
4、存在部分BUG

NIO进行服务器开发的步骤:
1、创建ServerSocketChannel,配置为非阻塞模式;
2、绑定监听,配置TCP参数;
3、创建一个独立的IO线程,用于轮询多路复用器Selector;
4、创建Selector,将之前创建的ServerSocketChannel注册到Selector上,监听Accept事件;
5、启动IO线程,在循环中执行Select.select()方法,轮询就绪的Channel;
6、当轮询到处于就绪状态的Channel时,需要对其进行判断,如果是OP_ACCEPT状态,说明有新的客户端接入,则调用ServerSocketChannel.accept()方法接受新的客户端;
7、设置新接入的客户端链路SocketChannel为非阻塞模式,配置TCP参数;
8、将SocketChannel注册到Selector上,监听READ事件;
9、如果轮询的Channel为OP_READ,则说明SocketChannel中有新的准备就绪的数据包需要读取,则构造ByteBuffer对象,读取数据包;
10、如果轮询的Channel为OP_WRITE,则说明还有数据没有发送完成,需要继续发送。

三、Netty的优点
1、API使用简单,开发门槛低;
2、功能强大,预置了多种编解码功能,支持多种主流协议;
3、定制功能强,可以通过ChannelHandler对通信框架进行灵活的扩展;
4、性能高,通过与其他业界主流的NIO框架对比,Netty综合性能最优;
5、成熟、稳定,Netty修复了已经发现的NIO所有BUG;
6、社区活跃;
7、经历了很多商用项目的考验。

粘包/拆包问题
TCP是一个“流”协议,所谓流,就是没有界限的一串数据。可以想象为河流中的水,并没有分界线。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。
TCP粘包拆包问题示例图:

假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,可能存在以下4中情况。
    1、服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包;
    2、服务端一次接收到了两个数据包,D1和D2粘合在一起,被称为TCP粘包;
    3、服务端分两次读取到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余部分内容,这被称为TCP拆包;
    4、服务端分两次读取到了两个数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余内容D1_1和D2包的完整内容;
如果此时服务器TCP接收滑窗非常小,而数据包D1和D2比较大,很有可能发生第五种情况,既服务端分多次才能将D1和D2包接收完全,期间发生多次拆包;

问题的解决策略
由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案可归纳如下:
1、消息定长,例如每个报文的大小为固定长度200字节,如果不够,空位补空格;
2、在包尾增加回车换行符进行分割,例如FTP协议;
3、将消息分为消息头和消息体,消息头中包含消息总长度(或消息体总长度)的字段,通常设计思路为消息头的第一个字段使用int32来表示消息的总程度;
4、更复杂的应用层协议;

LineBasedFrameDecoder
为了解决TCP粘包/拆包导致的半包读写问题,Netty默认提供了多种编解码器用于处理半包。
LinkeBasedFrameDecoder的工作原理是它一次遍历ByteBuf中的可读字节,判断看是否有“\n”、“\r\n”,如果有,就一次位置为结束位置,从可读索引到结束位置区间的字节就组成一行。它是以换行符为结束标志的编解码,支持携带结束符或者不携带结束符两种解码方式,同事支持配置单行的最大长度。如果连续读取到最大长度后任然没有发现换行符,就会抛出异常,同时忽略掉之前读到的异常码流。

DelimiterBasedFrameDecoder
实现自定义分隔符作为消息的结束标志,完成解码。

FixedLengthFrameDecoder
是固定长度解码器,能够按照指定的长度对消息进行自动解码,开发者不需要考虑TCP的粘包/拆包问题。

Netty高性能之道
1、异步非阻塞通信
在IO编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者IO多路复用技术进行处理。IO多路复用技术通过把多个IO的阻塞复用到同一个Selector的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。与传统的多线程/多进程模型相比,IO多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降低了系统的维护工作量,节省了系统资源。
Netty的IO线程NioEventLoop由于聚合了多路复用器Selector,可以同时并发处理成百上千个客户端SocketChannel。由于读写操作都是非阻塞的,这就可以充分提升IO线程的运行效率,避免由频繁的IO阻塞导致的线程挂起。另外,由于Netty采用了异步通信模式,一个IO线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞IO中 一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。

2、高效的Reactor线程模型
常用的Reactor线程模型有三种,分别如下:
Reactor单线程模型;

Reactor多线程模型;
1、从主线程池中随机选择一个Reactor线程作为Acceptor线程,用于绑定监听端口,接收客户端连接;
2、Acceptor线程接收客户端连接请求之后,创建新的SocketChannel,将其注册到主线程池的其他Reactor线程上,由其负责接入认证、IP黑白名单过滤、握手等操作;
3、然后也业务层的链路正式建立成功,将SocketChannel从主线程池的Reactor线程的多路复用器上摘除,重新注册到Sub线程池的线程上,用于处理IO的读写操作。

3、无锁化的串行设计
在大多数场景下,并行多线程处理可以提升系统的并发性能。但是,如果对于共享资源的并发访问处理不当,会带来严重的锁竞争,这最终会导致性能的下降。为了尽可能地避免锁竞争带来的性能损耗,可以通过串行化设计,既消息的处理尽可能在同一个线程内完成,期间不进行线程切换,这样就避免了多线程竞争和同步锁。
为了尽可能提升性能,Netty采用了串行无锁化设计,在IO线程内部进行串行操作,避免多线程竞争导致的性能下降。表面上看,串行化设计似乎CPU利用率不高,并发程度不够。但是,通过调整NIO线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列——多个工作线程模型性能更优。
Netty串行化设计工作原理图如下:

    Netty的NioEventLoop读取到消息后,直接调用ChannelPipeline的fireChannelRead(Object msg),只要用户不主动切换线程,一直会由NioEventLoop调用到用户的Handler,期间不进行线程切换。这种串行化处理方式避免了多线程导致的锁竞争,从性能角度看是最优的。

4、高效的并发编程
Netty中高效并发编程主要体现:
1、volatile的大量、正确使用;
2、CAS和原子类的广泛使用;
3、线程安全容器的使用;
4、通过读写锁提升并发性能。

5、高性能的序列化框架
影响序列化性能的关键因素总结如下:
1、序列化后的码流大小(网络宽带的占用);
2、序列化与反序列化的性能(CPU资源占用);
3、是否支持跨语言(异构系统的对接和开发语言切换)。
Netty默认提供了对GoogleProtobuf的支持,通过扩展Netty的编解码接口,用户可以实现其他的高性能序列化框架

6、零拷贝
Netty的“零拷贝”主要体现在三个方面:
1)、Netty的接收和发送ByteBuffer采用DIRECT BUFFERS,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(HEAP BUFFERS)进行Socket读写,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。
2)、第二种“零拷贝 ”的实现CompositeByteBuf,它对外将多个ByteBuf封装成一个ByteBuf,对外提供统一封装后的ByteBuf接口。
3)、第三种“零拷贝”就是文件传输,Netty文件传输类DefaultFileRegion通过transferTo方法将文件发送到目标Channel中。很多操作系统直接将文件缓冲区的内容发送到目标Channel中,而不需要通过循环拷贝的方式,这是一种更加高效的传输方式,提升了传输性能,降低了CPU和内存占用,实现了文件传输的“零拷贝”。

7、内存池
随着JVM虚拟机和JIT即时编译技术的发展,对象的分配和回收是个非常轻量级的工作。但是对于缓冲区Buffer,情况却稍有不同,特别是对于堆外直接内存的分配和回收,是一件耗时的操作。为了尽量重用缓冲区,Netty提供了基于内存池的缓冲区重用机制。

8、灵活的TCP参数配置能力
Netty在启动辅助类中可以灵活的配置TCP参数,满足不同的用户场景。合理设置TCP参数在某些场景下对于性能的提升可以起到的显著的效果,总结一下对性能影响比较大的几个配置项:
1)、SO_RCVBUF和SO_SNDBUF:通常建议值为128KB或者256KB;
2)、SO_TCPNODELAY:NAGLE算法通过将缓冲区内的小封包自动相连,组成较大的封包,阻止大量小封包的发送阻塞网络,从而提高网络应用效率。但是对于时延敏感的应用场景需要关闭该优化算法;
3)、软中断:如果Linux内核版本支持RPS(2.6.35以上版本),开启RPS后可以实现软中断,提升网络吞吐量。RPS根据数据包的源地址,目的地址以及目的和源端口,计算出一个hash值,然后根据这个hash值来选择软中断运行的CPU。从上层来看,也就是说将每个连接和CPU绑定,并通过这个hash值,来均衡软中断在多个CPU上,提升网络并行处理性能。

Nio中的重要概念
FileChannel:

从文件中读写数据,无法设置为非阻塞
public class Demo {
   public static void main(String[] args) {
       ByteBuffer buffer = ByteBuffer.allocate(2048);
       byte[] bb = new byte[2048];
       try {
           FileInputStream fis = new FileInputStream("D:\\vipkid\\bak\\pangolin-service\\src\\main\\resources\\data\\file.txt");
           FileChannel fc = fis.getChannel();
           long timeStar = System.currentTimeMillis();
           fc.read(buffer);
           buffer.flip();
           byte[] chars=buffer.array();
           String txt=new String(chars);
           System.out.println(txt);
           long endTime = System.currentTimeMillis();
           System.out.println("read time:" + (endTime - timeStar) + "ms");
/// 或者下面这种方法           
RandomAccessFile file = new RandomAccessFile("D:\\vipkid\\bak\\pangolin-service\\src\\main\\resources\\data\\file.txt", "rw");
           FileChannel channel = file.getChannel();
           ByteBuffer byteBuffer=ByteBuffer.allocate(2048);
           channel.read(buffer);
           String msg=new String(buffer.array());
           System.out.println(msg);
       } catch (FileNotFoundException e) {
           e.printStackTrace();
       } catch (IOException e) {
           e.printStackTrace();
       }
   }
}

DatagramChannel:
通过udp读写网络中的数据
SocketChannel:

通过tcp读取网络中的数据,可以设置为非阻塞
打开方式
SocketChannel socket=SocketChannel.open();
socket.connect(new InetSocketAddress("localhost",8080));
从SocketChannel上读取数据:
ByteBuffer buffer=ByteBuffer.allocate(2048);
buffer.clear();
int count=socket.read(buffer);//count标识读了多少字节
写数据:
String txt="vipkid has many bugs";
buffer.clear();
while(buffer.hasRemaining()){
  socket.write(buffer);
}

非阻塞模式+connect
socket.configureBlocking(false);
socket.connect(new InetSocketAddress("127.0.0.1",8080));
if(socket.isConnectable()){
  if(socket.isConnectionPending()){
  if(socket.finishConnect()){
    //do stm
    }
  }
}

ServerSocketChannel:

监听网络中的一个连接请求,为请求生成一个SocketChannel;
打开:ServerSocketChannel serverChannel=ServerSocketChannel.open();
serverChannel.socket().bind(new InetSocketAddress(9999));
serverChannel.configureBlocking(false);//非阻塞模式
while(true){
  serverChannel.accept();//阻塞直到一个连接请求到来
}

缓冲区类型:

ByteBuffer
CharBuffer、IntBuffer、LongBuffer、ShortBuffer
常用方法:
get() 从缓冲区中获取数据
put() 往缓冲区中添加数据
read() 从channel中读取数据到buffer
write() 往channel中写入数据
capacity - 缓冲区大小,无论是读模式还是写模式,此属性值不会变;
position - 写数据时,position表示当前写的位置,每写一个数据,会向下移动一个数据单元,初始为0;最大为capacity - 1
切换到读模式时,position会被置为0,表示当前读的位置
limit - 写模式下,limit 相当于capacity 表示最多可以写多少数据,切换到读模式时,limit 等于原先的position,表示最多可以读多少数据。
MappedByteBuffer
内存映射文件和之前说的 标准IO操作最大的不同之处就在于它虽然最终也是要从磁盘读取数据,
但是它并不需要将数据读取到OS内核缓冲区,而是直接将进程的用户私有地址空间中的一部分区
域与文件对象建立起映射关系,就好像直接从内存中读、写文件一样,速度当然快了(省去了把数据拷贝到OS内核缓冲区)
MappedByteBuffer 将文件直接映射到内存(这里的内存指的是虚拟内存,并不是物理内存)
代码示例:
public class MappedByteBufferDemo {
    public static void main(String[] args) {
        try {
            RandomAccessFile file=new RandomAccessFile("/Users/penny/code/vip/from.txt","rw");
            FileChannel channel=file.getChannel();
            MappedByteBuffer buffer=channel.map(FileChannel.MapMode.READ_WRITE,0,file.length());
            byte[] bytes=new byte[(int)file.length()];
            buffer.get(bytes);
            System.out.println(new String(bytes));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

下面为基于NIO实现的客户端代码:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;

public class Client implements Runnable{
    private BlockingQueue<String> words;
    private Random random;
       
    public static void main(String[] args) {        
        //种多个线程发起Socket客户端连接请求  
        for(int i=0; i<1; i++){
            Client c = new Client();  
            c.init();  
            new Thread(c).start();  
        }        
    }  
   
    @Override  
    public void run() {       
        SocketChannel channel = null;
        Selector selector = null;
        try {  
            channel = SocketChannel.open();  
            channel.configureBlocking(false);  
            //请求连接  
            channel.connect(new InetSocketAddress("localhost", 8383));
            selector = Selector.open();  
            channel.register(selector, SelectionKey.OP_CONNECT);
            boolean isOver = false;  
               
            while(! isOver){  
                selector.select();  
                Iterator ite = selector.selectedKeys().iterator();
                while(ite.hasNext()){  
                    SelectionKey key = (SelectionKey) ite.next();  
                    ite.remove();  
                       
                    if(key.isConnectable()){  
                        //连接操作是否在该通道上执行
                        if(channel.isConnectionPending()){  
                            if(channel.finishConnect()){  
                                //只有当连接成功后才能注册OP_READ事件  
                                key.interestOps(SelectionKey.OP_READ);  
                                   
                                channel.write(CharsetHelper.encode(CharBuffer.wrap(getWord())));
                                sleep();  
                            }  
                            else{  
                                key.cancel();  
                            }  
                        }                                                
                    }  
                    else if(key.isReadable()){  
                        ByteBuffer byteBuffer = ByteBuffer.allocate(128);
                        channel.read(byteBuffer);  
                        byteBuffer.flip();  
                        CharBuffer charBuffer = CharsetHelper.decode(byteBuffer);  
                        String answer = charBuffer.toString();   
                        System.out.println(Thread.currentThread().getId() + "---" + answer);  
                           
                        String word = getWord();  
                        if(word != null){  
                            channel.write(CharsetHelper.encode(CharBuffer.wrap(word)));  
                        }  
                        else{  
                            isOver = true;  
                        }  
                        sleep();                         
                    }  
                }  
            }                            
        } catch (IOException e) {
            e.printStackTrace();  
        }  
        finally{  
            if(channel != null){  
                try {  
                    channel.close();  
                } catch (IOException e) {                        
                    e.printStackTrace();  
                }                    
            }  
               
            if(selector != null){  
                try {  
                    selector.close();  
                } catch (IOException e) {  
                    e.printStackTrace();  
                }  
            }  
        }  
    }  
   
    private void init() {  
        words = new ArrayBlockingQueue<String>(5);
        try {  
            words.put("hi");  
            words.put("who");  
            words.put("what");  
            words.put("where");  
            words.put("bye");  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }    
           
        random = new Random();  
    }  
       
    private String getWord(){  
        return words.poll();  
    }  
   
    private void sleep() {  
        try {  
            TimeUnit.SECONDS.sleep(random.nextInt(3));
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }    
       
    private void sleep(long l) {  
        try {  
            TimeUnit.SECONDS.sleep(l);  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
    }  
}

服务端代码:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

public class XiaoNa {
    private ByteBuffer readBuffer;
    private Selector selector;
     
    public static void main(String[] args){
        XiaoNa xiaona = new XiaoNa();
        xiaona.init();
        xiaona.listen();
    }
     
    private void init(){
        readBuffer = ByteBuffer.allocate(1024);
        ServerSocketChannel servSocketChannel;
         
        try {
            servSocketChannel = ServerSocketChannel.open();
            servSocketChannel.configureBlocking(false);
            //绑定端口
            servSocketChannel.socket().bind(new InetSocketAddress(8383));
             
            selector = Selector.open();
            servSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }      
    }
 
    private void listen() {
        while(true){
            try{
                selector.select();             
                Iterator ite = selector.selectedKeys().iterator();
                 
                while(ite.hasNext()){
                    SelectionKey key = (SelectionKey) ite.next();                  
                    ite.remove();//确保不重复处理
                     
                    handleKey(key);
                }
            }
            catch(Throwable t){
                t.printStackTrace();
            }                          
        }              
    }
 
    private void handleKey(SelectionKey key)
            throws IOException, ClosedChannelException {
        SocketChannel channel = null;
         
        try{
            if(key.isAcceptable()){
                ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
                channel = serverChannel.accept();//接受连接请求
                channel.configureBlocking(false);
                channel.register(selector, SelectionKey.OP_READ);
            }
            else if(key.isReadable()){
                channel = (SocketChannel) key.channel();
                readBuffer.clear();
                /*当客户端channel关闭后,会不断收到read事件,但没有消息,即read方法返回-1
                 * 所以这时服务器端也需要关闭channel,避免无限无效的处理*/              
                int count = channel.read(readBuffer);
                 
                if(count > 0){
                    //一定需要调用flip函数,否则读取错误数据
                    readBuffer.flip();
                    /*使用CharBuffer配合取出正确的数据
                    String question = new String(readBuffer.array());  
                    可能会出错,因为前面readBuffer.clear();并未真正清理数据
                    只是重置缓冲区的position, limit, mark,
                    而readBuffer.array()会返回整个缓冲区的内容。
                    decode方法只取readBuffer的position到limit数据。
                    例如,上一次读取到缓冲区的是"where", clear后position为0,limit为 1024,
                    再次读取“bye"到缓冲区后,position为3,limit不变,
                    flip后position为0,limit为3,前三个字符被覆盖了,但"re"还存在缓冲区中,
                    所以 new String(readBuffer.array()) 返回 "byere",
                    而decode(readBuffer)返回"bye"。            
                    */
                    CharBuffer charBuffer = CharsetHelper.decode(readBuffer);
                    String question = charBuffer.toString();
                    System.out.println("question:"+question);
                    String answer = getAnswer(question);
                    channel.write(CharsetHelper.encode(CharBuffer.wrap(answer)));
                }
                else{
                    //这里关闭channel,因为客户端已经关闭channel或者异常了
                    channel.close();               
                }                      
            }
        }
        catch(Throwable t){
            t.printStackTrace();
            if(channel != null){
                channel.close();
            }
        }      
    }
     
    private String getAnswer(String question){
        String answer = null;
         
        switch(question){
        case "who":
            answer = "我是小娜\n";
            break;
        case "what":
            answer = "我是来帮你解闷的\n";
            break;
        case "where":
            answer = "我来自外太空\n";
            break;
        case "hi":
            answer = "hello\n";
            break;
        case "bye":
            answer = "88\n";
            break;
        default:
                answer = "请输入 who, 或者what, 或者where";
        }
        return answer;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值