Java NIO通信基础

参考书籍:Netty、Redis、ZooKeeper高并发实战

1.  IO读写的基本原理

用户程序进行IO的读写,实际上是缓存区的复制。即read :从内核缓冲区复制到进程缓冲区;write:从进行缓冲区复制到内核缓冲区;上层程序的IO操作,实际上不是物理设备级别的读写,而是缓存的复制。内核缓冲区和物理设备(磁盘、网卡)之间的交换是由操作系统内核完成的。

  

2.  4种主要的IO模型

  •   同步阻塞IO

  进程进行读写,需要内核IO操作彻底完成后,才返回到进程空间执行用户的操作。伪代码:String res = read(); 开启read操作等到返回才能执行下一句。当读socket时,没有数据的话就一直等,等到有数据,内核也准备好了,再复制到进程空间返回。

  • 同步非阻塞IO

  不停轮询去读取,知道内容准备完成。

  • IO 多路复用模型

  使用一个选择器线程监控多个socket连接,当某个或某些socket网络连接有IO就绪状态,就返回进行相应的操作。一个线程处理成千上万个连接,相比于一个线程维护一个连接的阻塞IO模式相比,大大减少了系统的开销。

  • 异步IO模型(AIO)

  (1)当用户线程发起了read系统调用,立刻就可以开始去做其他的事,用户线程不阻塞。

  (2)内核就开始了IO的第一个阶段:准备数据。等到数据准备好了,内核就会将数据从内核缓冲区复制到用户缓冲区(用户空间的内存)。

  (3)内核会给用户线程发送一个信号(Signal),或者回调用户线程注册的回调接口,告诉用户线程read操作完成了。

  (4)用户线程读取用户缓冲区的数据,完成后续的业务操作。

  

  目前大多数的高并发网络服务使用的仍是IO多路复用模型。

3.  Linux文件句柄数

  Linux系统默认文件句柄数(即文件描述符,反应IO状态)的限制是1024个,也就是说一个进程最多可以有1024个Socket连接,这远远不够。

  临时修改: ulimit -n 1000000

  开机执行: etc/rc.local文件, ulimit -SHn 1000000

  永久:etc/security/limits.conf    soft nofile 1000000     hard nofile 1000000,-S soft表示软性极限,系统警告的极限值,-H hard表示硬性极限,实际的限制。

4.  Java NIO

  • 核心组件: Buffer(缓冲区)、Channel(通道)、Selector(选择器)
  • Buffer:本质上是一个内存块(数组),Buffer是个抽象类,实际有 ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer、 MappedBuffer

  属性:

capacity   容量,即可以容纳的最大数据量;在缓冲区创建时设置并且不能改变
  limit   读写的最大上限
  position   位置,缓冲区中下一个要被读或写的元素的位置
  mark   用来缓存position

方法

allocate()   创建缓冲区
put()   写入缓冲区
flip()   切换到读模式
get()   冲缓冲区中读取
rewind()   倒带,位置调整到0,即从头开始
mark()   position
reset()   position= mark
  • Channel(通道)

  四种类型

FileChannel   文件通道
new FileInputStream(srcFile).getChannel(); //获取,出入和输出流
newRandowAccessFile("filename.txt","rw").getChannel();//获取
read(Buffer); //读
write(Buffer);//写
close();//关闭 
force(true);//强制刷新到磁盘
SocketChannel   数据传输通道
//客户端:
SocketChannel socketChannel = SocketChannel.open();    //新建
socketChannel.configureBlocking(false);    //设置为非阻塞模式
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));//发起连接
while(! socketChannel.finishConnect()){    }//不断自旋,检查是否连接成功        
//服务器端

SeverSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = server.accept();    //当新连接事件到来时,通过监听通道的accept()方法获取
socketChannel.configureBlocking(false);    //设置为非阻塞模式

//读

ByteBuffer buf = ByteBuffer.allocate(1024);
int bytesRead = socketChannel.read(buf);    //返回值是读取的字节数

// 写
buffer.flip();    //重点!!!! 搞清楚Buffer和Channel的关系,读Channel的时候就是在写Buffer,当要写入通道时,自然要从Buffer读取,Buffer要切换read模式
socketChannel.write(buffer);
socketChannel.shutdownOutput();    //关闭之前发送结束标志(-1)
socketChannel.close();
ServerSocketChannel   监听通道

 

DatagramChannel   数据报通道(UDP)

UDP不是面向连接的协议。

//发送
dChannel.send(buffer, new InetSocketAddress(NioDemoConfig.SOCKET_SERVER_IP, NioDemoConfig.SOCKET_SERVER_PORT));


//接收

DatagramChannel datagramChannel = DatagramChannel.open();
        datagramChannel.configureBlocking(false);
        datagramChannel.bind(new InetSocketAddress(
                NioDemoConfig.SOCKET_SERVER_IP
                , NioDemoConfig.SOCKET_SERVER_PORT));
        Print.tcfo("UDP 服务器启动成功!");
        Selector selector = Selector.open();
        datagramChannel.register(selector, SelectionKey.OP_READ);
        while (selector.select() > 0) {
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            ByteBuffer buffer = ByteBuffer.allocate(NioDemoConfig.SEND_BUFFER_SIZE);
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                if (selectionKey.isReadable()) {
                    //操作二:读取DatagramChannel数据报通道数据
                    SocketAddress client = datagramChannel.receive(buffer);
                    buffer.flip();
                    Print.tcfo(new String(buffer.array(), 0, buffer.limit()));
                    buffer.clear();
                }
            }
            iterator.remove();
        }

        selector.close();
        datagramChannel.close();

 

  • Selector选择器类

  可供选择器监控的通道IO事件:

    可读: SelectionKey.OP_READ  

    可写: SelectionKey.OP_WRITE

    连接: SelectionKey.OP_CONNECT  完成握手连接,处于“连接就绪状态”

    接收:  SelectionKey.OP_APPEPT    服务器通道接收到新的连接请求时,处于“接收就绪状态”

  (1)通道和选择器之间通过,channel.register(Selector sel, int ops)方法进行注册。 ops为通道IO事件,多个用位或符号 '|' 连接.

  (2)并不是所有的通道都可以被监控,FileChannel就不可以。只有继承了SelectableChannel的通道才可以被选择器监控,Java NIO所有的Socket套接字通道都继承了SelectableChannel。

  (3)SelectionKey类。一个IO事件发生后,如果之前在选择器中注册过,就会被选择器选中,并放入SelectionKey的集合中。SelectionKey中包含发生IO事件的通道、IO事件类型、选择器对象等很多属性

  使用方式:

  

// 1、获取Selector选择器
        Selector selector = Selector.open();

        // 2、获取通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 3.设置为非阻塞
        serverSocketChannel.configureBlocking(false);
        // 4、绑定连接
        serverSocketChannel.bind(new InetSocketAddress(NioDemoConfig.SOCKET_SERVER_PORT));
        Logger.info("服务器启动成功");

        // 5、将通道注册到选择器上,并注册的IO事件为:“接收新连接”
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        // 6、轮询感兴趣的I/O就绪事件(选择键集合)
        while (selector.select() > 0) {
            // 7、获取选择键集合
            Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator();
            while (selectedKeys.hasNext()) {
                // 8、获取单个的选择键,并处理
                SelectionKey selectedKey = selectedKeys.next();

                // 9、判断key是具体的什么事件
                if (selectedKey.isAcceptable()) {
                    // 10、若选择键的IO事件是“连接就绪”事件,就获取客户端连接
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    // 11、切换为非阻塞模式
                    socketChannel.configureBlocking(false);
                    // 12、将该通道注册到selector选择器上
                    socketChannel.register(selector, SelectionKey.OP_READ);
                } else if (selectedKey.isReadable()) {
                    // 13、若选择键的IO事件是“可读”事件,读取数据
                    SocketChannel socketChannel = (SocketChannel) selectedKey.channel();

                    // 14、读取数据
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    int length = 0;
                    while ((length = socketChannel.read(byteBuffer)) >0) {
                        byteBuffer.flip();
                        Logger.info(new String(byteBuffer.array(), 0, length));
                        byteBuffer.clear();
                    }
                    socketChannel.close();
                }
                // 15、移除选择键
                selectedKeys.remove();
            }
        }

        // 7、关闭连接
        serverSocketChannel.close();

 

posted @ 2022-10-06 19:14  迷路的圆  阅读(52)  评论(0编辑  收藏  举报