重识NIO

#NIO实战


1. 重识socket编程

1.1 概念

传统的socket编程是bio的,是阻塞的,也就是说在单线程的情况下,serversocket只可以接受一个客户端连接,有个处理方法是,把serversocket设置为多线程的,即每次都创建一条线程来处理客户端的业务,这样serversocket可以连接多个客户端。但是线程数的暴增会增加cpu的负担,而且线程的来回切换也会降低性能。

1.2 代码片段
位置:/Users/xin/code/code/ideaproject/zexin-nio/src/main/java/zexin/con.demo/socket

2. 初识NIO

2.1 概念
  • NIO:New IO
  • NIO 1.0
  • Buffer
  • Channel
  • Selector

图片描述

2.2 Buffer
2.2.1 概念

一个Buffer本质上是内存的一块,可以将数据写入这块内存,或者从这块内存获取数据

图片描述

2.2.2 三大核心概念
  • capacity

buffer的大小,一旦设定就不可以更改,比如capacity为1024的IntBuffer,代表一次可以存放1024个int类型的值。一旦buffer的容量达到capacity,需要情况buffer,才能重新写入值

  • positon

下一个可读或可写的位置(下标)。写模式切换(flip)到读模式的时候,position会归0,这样就可以从头开始读写了

  • limit

写操作模式下,limit代表能写入的最大的数据,刚开始同capacity。写模式切换到读模式,此时limit等于buffer中的实际大小,因为buffer不一定被写满了。

图片描述

2.2.3 buffer实操
  • duplicate():共享底层数组数据,新缓冲区的内容将为此缓冲区的内容。此缓冲区内容的更改在新缓冲区中是可见的,反之亦然;这两个缓冲区的位置、界限和标记值是相互独立的。与当前的buffer的position,mark,limit,capacity相同
public CharBuffer duplicate() {
        return new HeapCharBuffer(hb,
                                     this.markValue(),
                                        this.position(),
                                        this.limit(),
                                        this.capacity(),
                                        offset);
    }
  • slice():创建新的字节缓冲区,其内容是此缓冲区内容的共享子序列。
    新缓冲区的内容将从此缓冲区的当前位置开始。此缓冲区内容的更改在新缓冲区中是可见的,反之亦然;这两个缓冲区的位置、界限和标记值是相互独立的。新缓冲区的位置将为零,其容量和界限将为此缓冲区中所剩余的字节数量,其标记是不确定的。
public CharBuffer slice() {
        return new HeapCharBuffer(hb,
                                        -1,
                                        0,
                                        this.remaining(),
                                        this.remaining(),
                                        this.position() + offset);
    }

新缓冲区的容量、界限、位置和标记值将与此缓冲区相同。当且仅当此缓冲区为直接时,新缓冲区才是直接的,当且仅当此缓冲区为只读时,新缓冲区才是只读的

  • clear():清除此缓冲区。将位置设置为 0,将限制设置为容量,并丢弃标记。此方法不能实际清除缓冲区中的数据,但从名称来看它似乎能够这样做,这样命名是因为它多数情况下确实是在清除数据时使用。
  • mark():标记位置,在使用resert的时候,可以恢复到这个位置。如果针对同个buffer多次mark,后面的mark会覆盖前面的mark
  • resert():恢复到mark的位置
  • get(): 顺序读取数据,postion会变化(必须在读模式下)
  • allocate: 创建一个堆内存的buffer
  • allocatDirect:创建一个直接内存的buffer
  • wrap:根据数组创建buffer,用于堆内存buffer,因为直接内存buffer的底层不是数组
  • put():往buffer中写入数据
  • compact():将缓冲区的位置设置为复制的字节数,而不是零,以便调用此方法后可以紧接着调用另一个相对 put 方法
## 其实就是把当前最后一个可写的位置设置为position,然后以容量大小为limit,把读模式转换成写模式了,并且去除mark标记
 public ByteBuffer compact() {

        System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
        position(remaining());
        limit(capacity());
        discardMark();
        return this;

    }
  • rewind():重绕此缓冲区。将位置设置为 0 并丢弃标记。
public final Buffer rewind() {
        position = 0;
        mark = -1;
        return this;
    }
## 简单buffer的创建
public static void main(String[] args) {
        ByteBuffer buffer0 = ByteBuffer.allocate(10);
        if (buffer0.hasArray()) {
            System.out.println("buffer0 array:" + buffer0.array());
            System.out.println("buffer0 array offset:" + buffer0.arrayOffset());
        }

        System.out.println("buffer0 capacity:" + buffer0.capacity());
        System.out.println("buffer0 limit:" + buffer0.limit());
        System.out.println("buffer0 position:" + buffer0.position());
        System.out.println("buffer0 remaining:" + buffer0.remaining());


        System.out.println("--------------------------------------");

        //分配直接Buffer内存.底层非数组
        ByteBuffer buffer1 = ByteBuffer.allocateDirect(10);
        System.out.println("底层数组?" + buffer1.hasArray());

        System.out.println("buffer0 capacity:" + buffer1.capacity());
        System.out.println("buffer0 limit:" + buffer1.limit());
        System.out.println("buffer0 position:" + buffer1.position());
        System.out.println("buffer0 remaining:" + buffer1.remaining());

        System.out.println("--------------------------------------");

        //通过数组创建Buffer
        byte[] bytes = new byte[10];
        ByteBuffer buffer2 = ByteBuffer.wrap(bytes);
        if (buffer2.hasArray()) {
            System.out.println("buffer0 array:" + buffer2.array());
            System.out.println("buffer0 array offset:" + buffer2.arrayOffset());
        }

        System.out.println("buffer2 capacity:" + buffer2.capacity());
        System.out.println("buffer2 limit:" + buffer2.limit());
        System.out.println("buffer2 position:" + buffer2.position());
        System.out.println("buffer2 remaining:" + buffer2.remaining());

        System.out.println("--------------------------------------");

        //创建特定offset和length的buffer
        byte[] bytes2 = new byte[10];
        ByteBuffer buffer3 = ByteBuffer.wrap(bytes2, 2, 3);
        if (buffer3.hasArray()) {
            System.out.println("buffer3 array:" + buffer3.array());
            System.out.println("buffer3 array offset:" + buffer3.arrayOffset());
        }


        System.out.println("buffer3 capacity:" + buffer3.capacity());
        System.out.println("buffer3 limit:" + buffer3.limit());
        System.out.println("buffer3 position:" + buffer3.position());
        System.out.println("buffer3 remaining:" + buffer3.remaining());

        /***
         *   buffer3 array:[B@1d44bcfa
             buffer3 array offset:0
             buffer3 capacity:10
             buffer3 limit:5
             buffer3 position:2
             buffer3 remaining:3
         *   假设buffer为 0123456789
         *   那么positon为2,limit为5,中间可写的范围为234,并不包括5,在写模式下
         * 
         * 
         * 
         */

        buffer3.put((byte)'H').put((byte)'e').put((byte)'l');
        buffer3.flip();
        byte[] valueArray = buffer3.array();
        for (int index = 0; index < valueArray.length; index++) {
            System.out.println(index + ":" + valueArray[index]);
        }
    }
2.3 Channel
2.3.1 概念

所有的nio操作始于通道,通道是数据来源或数据写入的目的,主要地,java.nio包中主要实现一下几个channel

图片描述

  • FileChannel:文件通道,用于文件的读和写
  • DatagramChannel:用于UDP连接的接收和发送
  • SocketChannel:TCP连接通道,简单理解就是TCP客户端
  • ServerSocketChannel:TCP对应的服务端,用于监听某个端口进来的请求
2.3.2 NIO的读操作

NIO的读操作就是将数据从channel读到buffer中,进行后续处理,调用方法:channel.read(buffer)

在这里插入图片描述

2.3.3 NIO的写操作

NIO的写操作就是将数据从Buffer中写入到Channel中,调用方法:channel.write(buffer)

在这里插入图片描述

2.4 Selector
2.4.1 概念

selector是java nio中的一个组件,用于检查一个或多个nio channel的状态十分处于可读,可写
如此可以实现单线程管理多个channels,也就是管理多个网络连接

在这里插入图片描述

2.4.2 相关API
  • selector

支持IO多路复用的抽象实体
注册selectable channel

  • SelectionKey

表示Selector和被注册的channel之间关系,一份凭证
selectionkey保存channel感兴趣的事件

  • Selector.select

代表有多少channel处于就绪了。也就是自上一次select后有多少channel进入就绪
更新所有就绪的SelectionKey的状态,并返回就绪的channel个数
在调用select之后,可以调用selectedKeys获取相应的key,但是该方法会把所有可用的key都查出来,如果某个key被用过了,记得要remove掉。
在调用select并返回了有channel就绪之后,可以通过选中的key集合来获取channel,这个操作通过调用selectedKeys()方法

在这里插入图片描述

2.4.3 Selector实操
  • Selector服务端
package zexin.com.demo.nio.selector;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

/**
 * selector的demo
 *
 * @author: xin
 * @create: 2018-12-20 上午11:10
 */
public class EchoHandler implements Runnable{
    private Selector selector;
    private ServerSocketChannel serverSocketChannel;
    private volatile boolean stop;
    private int num = 0;

    public EchoHandler(int port) {
        try {
            //创建一个selector,其在多线程中是安全的
            selector = Selector.open();
            System.out.println("selector=" + selector);
            //创建一个ServerSocketChannel,用于监听客户端连接
            serverSocketChannel = ServerSocketChannel.open();
            //设置阻塞模式为非阻塞
            serverSocketChannel.configureBlocking(false);
            //绑定IP和端口号
            /***
             * 参考网址:https://blog.csdn.net/aitangyong/article/details/49661907
             * 服务端socket处理客户端socket连接是需要一定时间的。ServerSocket有一个队列,存放还没有来得及处理的客户端Socket,
             * 这个队列的容量就是backlog的含义。如果队列已经被客户端socket占满了,如果还有新的连接过来,那么ServerSocket会拒绝新的连接。
             * 也就是说backlog提供了容量限制功能,避免太多的客户端socket占用太多服务器资源
             * 客户端每次创建一个Socket对象,服务端的队列长度就会增加1个
             * 服务端每次accept(),就会从队列中取出一个元素
             */
            serverSocketChannel.socket().bind(new InetSocketAddress(port), 1024);
            //把channel注册到selector中.且只对socket的accept事件感兴趣.也就说只有是accept事件,该channel才会准备就绪.
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("服务器在端口[" + port + "]等待客户请求......");
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(1);
        }
    }

    private void stop() {
        this.stop = true;
    }
    @Override
    public void run() {
        while (!stop) {
            try {
                //选择有多少个channel是准备就绪的.在1000毫秒即1秒内无准备就绪的channel立马返回
                int readyNum = selector.select(1000);
                //在调用select并返回了有channel就绪之后,可以通过选中的key集合来获取channel,这个操作通过调用selectedKeys()方法.必须先select再调用下面的选择key
               if (readyNum > 0) {
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    Iterator<SelectionKey> iter = selectionKeys.iterator();
                    SelectionKey key = null;
                    while (iter.hasNext()) {
                        key = iter.next();
                        System.out.println("进入迭代key=" + key);
                        //如果不把迭代remove掉,那么key会重复被使用.是这样的,在不remove的情况下,在客户端发送消息时,虽然readyNum返回的数量是1(read事件),但是selectedKeys会把之前accept事件的key也查出来,所以就会出现重复的accept的key被使用
                        iter.remove();
                        try {
                            handleInput(key);
                        } catch (Exception e) {
                            e.printStackTrace();
                            if (key != null) {
                                //取消键,只是把它加入了取消键集,下次不会把该键所相关的channel选中
                                key.cancel();
                                //关闭该键集所在的channel
                                if (key.channel() != null) {
                                    key.channel().close();
                                }
                            }
                        }

                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        //多路复用器关闭后,所有注册在上面的channel等资源都会被自动关闭,所有不需要重复释放资源
        if (selector != null) {
            try {
                selector.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private void handleInput(SelectionKey key) throws Exception  {
        if (key.isValid()) {
            //处理接入的请求信息
            if (key.isAcceptable()) {
                ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                SocketChannel socketChannel = ssc.accept();
                socketChannel.configureBlocking(false);
                //把该socketChannel注册到selector通道中,并且感兴趣的时间是读事件
                SelectionKey sk = socketChannel.register(selector, SelectionKey.OP_READ);

                //原子加1(其内部维护了一个static final的原子数,无论创建多少SelectionKey,始终只有一个计数)
                sk.attach(num++);
            }

            if (key.isReadable()) {
                //处理读请求
                SocketChannel socketChannel = (SocketChannel)key.channel();
                ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                int readBytes = socketChannel.read(readBuffer);

                //有数据.这里应该写个循环重复读,否则在超过1024个字节后面的数据无法读取
                if (readBytes > 0) {
                    //切换到读模式
                    readBuffer.flip();
                    byte[] bytes = new byte[readBuffer.remaining()];
                    //把buffer的数据赋值到字节数组中
                    readBuffer.get(bytes);
                    String body = new String(bytes, "UTF-8");
                    System.out.println("来自客户端[" + key.attachment() + "]的输入:" + body.trim());

                    if (body.trim().equals("quit")) {
                        System.out.println("断开客户端[" + key.attachment() + "]的链接");
                        key.cancel();
                        socketChannel.close();
                    } else {
                        String response = "来自服务端的相应:" + body;
                        doWrite(socketChannel, response);
                    }
                } else if(readBytes < 0) {
                    //客户端链路关闭了
                    System.out.println("异常断开客户端[" + key.attachment() + "]的链接");
                    key.cancel();
                    socketChannel.close();
                }
            }
        }
    }

    private void doWrite(SocketChannel channel, String response) throws Exception {
        if (response != null && response.trim().length() > 0) {
            byte[] bytes = response.getBytes();
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            byteBuffer.put(bytes);
            byteBuffer.flip();
            channel.write(byteBuffer);
        }
    }
}

  • Selector客户端
package zexin.com.demo.nio.selector;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * selector的客户端demo
 *
 * @author: xin
 * @create: 2018-12-20 下午3:07
 */
public class EchoClienHandler implements Runnable {
    private String host;
    private int port;

    private Selector selector;
    private SocketChannel socketChannel2;

    private ExecutorService executorService;

    private volatile boolean stop;

    public EchoClienHandler(String host, int port) {
        this.host = host;
        this.port = port;
        this.executorService = Executors.newSingleThreadExecutor();

        try {
            selector = Selector.open();
            socketChannel2 = SocketChannel.open();
            //设置为非阻塞模式
            socketChannel2.configureBlocking(false);
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(1);
        }
    }


    @Override
    public void run() {
        try {
            //socketChannel注册到selector中,对连接事件感兴趣
            socketChannel2.register(selector, SelectionKey.OP_CONNECT);
            //socketChannel开始连接
            socketChannel2.connect(new InetSocketAddress(this.host, this.port));
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(1);
        }

        while (!stop) {
            try {
                int readyNum = selector.select(1000);
                //System.out.println("readyNum =" + readyNum);
                Set<SelectionKey> set = selector.selectedKeys();
                Iterator<SelectionKey> iter = set.iterator();
                SelectionKey key = null;
               if (readyNum > 0) {
                    while (iter.hasNext()) {
                        key = iter.next();
                        System.out.println("key=" +key);
                        System.out.println("进入迭代");
                        //iter.remove();
                        try {
                            hadleInput(key);
                        } catch (Exception e) {
                            e.printStackTrace();
                            if (key != null) {
                                key.cancel();
                                if (key.channel() != null) {
                                    key.channel().close();
                                }
                            }
                        }
                    }
               }

            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        if (selector != null) {
            try {
                selector.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        if (executorService != null) {
            executorService.shutdown();
        }
    }

    private void hadleInput(SelectionKey key) throws IOException {
        if (key.isValid()) {
            SocketChannel sc = (SocketChannel) key.channel();
            if (key.isConnectable()) {
                if (sc.finishConnect()) {
                    System.out.println("连接到服务器....");

                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    System.out.println("请输入消息,输…入`quit`退出");
                    executorService.submit(new Runnable() {
                        @Override
                        public void run() {
                            while (true) {
                                try {
                                    System.out.println("线程开始");
                                    buffer.clear();
                                    InputStreamReader ins = new InputStreamReader(System.in);
                                    BufferedReader br = new BufferedReader(ins);
                                    String msg = br.readLine();
                                    if ("msg".equals("quit")) {
                                        System.out.println("关闭客户端");
                                        key.cancel();
                                        key.channel().close();
                                        sc.close();
                                        stop = true;
                                        break;
                                    }

                                    buffer.put(msg.getBytes());
                                    buffer.flip();
                                    sc.write(buffer);
                                    System.out.println("请输入消息,输入`quit`退出1232");
                                } catch (IOException e) {
                                    e.printStackTrace();
                                }
                            }
                        }
                    });

                    //注册读事件
                    sc.register(selector, SelectionKey.OP_READ);
                }
            }

            if (key.isReadable()) {
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                int readBytes = sc.read(buffer);
                if (readBytes > 0) {
                    buffer.flip();
                    byte[] bytes = new byte[buffer.remaining()];
                    buffer.get(bytes);
                    String msg = new String(bytes, "UTF-8");
                    System.out.println(msg);
                    if ("quit".equals(msg)) {
                        this.stop = true;
                    }
                } else if (readBytes < 0) {
                    //服务端关闭
                    System.out.println("服务端异常关闭");
                    key.cancel();
                    key.channel().close();
                    sc.close();
                }
            }

            if(key.isWritable()){
                System.out.println("The key is writable");
            }


        }
    }
}

  • Selector服务端启动
public class NIOEchoServer {

 
    public static void main(String[] args) throws IOException {
	int port = 8080;
	if (args != null && args.length > 0) {
	    try {
		port = Integer.valueOf(args[0]);
	    } catch (NumberFormatException e) {
		// 采用默认值
	    }
	}
	EchoHandler timeServer = new EchoHandler(port);
	new Thread(timeServer, "NIO-MultiplexerTimeServer-001").start();
    }
}
  • Selector客户端启动
public class NIOEchoClient {

	public static void main(String[] args) {

		int port = 8080;
		if (args != null && args.length > 0) {
			try {
				port = Integer.valueOf(args[0]);
			} catch (NumberFormatException e) {
			}
		}
		new Thread(new EchoClienHandler("127.0.0.1", port), "NIOEchoClient-001").start();
	}
}

3. NIO带给我们的好处

  1. 事件驱动模型
    • 避免多线程
    • 单线程处理任务
  2. 非阻塞IO,IO读写不再阻塞,而是返回0
  3. 基于block的传输,通常比基于流的传输更高效
  4. 更高级的IO函数,zero-copy(只是在数据准备过程中不需要等待拷贝完成,在数据从内核态到用户态还是需要数据拷贝的)
  5. IO多路复用大大提高了java网络应用的可伸缩性和实用性

4. 使用NIO的注意事项

  1. 使用NIO不一定等于高性能
  2. NIO不一定更快的场景
    • 客户端应用
    • 连接数小于1000
    • 并发程度不高
    • 局域网环境下
  3. NIO完全屏蔽了平台差异(Linux poll/select/epoll,FreeBSD Kqueue)
    • NIO仍然是基于各个OS平台的IO系统实现,差异仍然存在
  4. 使用NIO做网络编程困难
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值