优秀的编程知识分享平台

网站首页 > 技术文章 正文

一篇文章帮你搞定Java NIO原理及其应用后续

nanyue 2024-10-04 18:27:15 技术文章 11 ℃

关于Buffer和Channel的注意事项和细节

1、ByteBuffer支持类型化的put和get,put放入的是什么数据类型,get就应该使用相应的数据类型来取出,否则可能有BufferUnderflowException异常。

public class NIOByteBufferPutGet {
    public static void main(String[] args) {
        //创建一个Buffer
        ByteBuffer buffer = ByteBuffer.allocate(64);
        //类型化方式放入数据
        buffer.putInt(10);
        buffer.putLong(8);
        buffer.putChar('我');
        buffer.putShort((short) 4);
        //复位
        buffer.flip();
        //取出
        System.out.println(buffer.getInt());
        System.out.println(buffer.getLong());
        System.out.println(buffer.getChar());
        System.out.println(buffer.getShort());
    }
}

2、可以将一个普通Buffer转成只读Buffer。

public class ReadOnlyBuffer {
    public static void main(String[] args) {
        //创建一个buffer
        ByteBuffer buffer = ByteBuffer.allocate(64);
        for(int i=0;i<64;i++){
            buffer.putShort((byte)i);
        }
        buffer.flip();
        //得到一个只读的Buffer
        ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
        System.out.println(readOnlyBuffer);
        //读取
        while (readOnlyBuffer.hasRemaining()){
            System.out.println(readOnlyBuffer.get());
        }
        //因为这个buffer已经是只读的,下面的操作会抛出异常ReadOnlyBufferException
        readOnlyBuffer.put((byte)100);
    }
}

3、NIO还提供了MappedByteBuffer,可以让文件直接在内存(堆外的内存)中进行修改,而如何同步到文件由NIO来完成。


/**
 * 说明
 * 1、MappedByteBuffer 可让文件直接在内存(堆外内存)修改,操作系统不需要拷贝一次
 */
public class MappedByteBufferTest {
    public static void main(String[] args) throws Exception{
        RandomAccessFile randomAccessFile = new RandomAccessFile("1.txt","rw");
        //获取对应的通道
        FileChannel channel = randomAccessFile.getChannel();
        /**参数1:FileChannel.MapMode.READ_WRITE 使用读写模式
         * 参数2:0:可以直接修改的起始位置
         * 参数3: 5:是映射到内存的大小(不是索引位置),即将1.txt的多少个字节映射到内存,可以直接修改的范围就是0~5
         * 实际类型DirectByteBuffer
         */
        MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
        //修改第一个字节为H
        mappedByteBuffer.put(0,(byte)'H');
        //修改第四个字节为9
        mappedByteBuffer.put(3,(byte)'9');
        randomAccessFile.close();
        System.out.println("修改成");
    }
}

4、前面我们讲的读写操作,都是通过一个Buffer来完成的,NIO还支持通过多个Buffer(即Buffer数组)完成读写操作,即Scattering和Gatering

/**
 * Scattering:将数据写入到buffer时,可以采用buffer数组,依次写入【分散】
 * Gatering:从buffer读取数据时,可以采用buffer数组,依次读出
 */
public class ScatteringAndGatheringTest {
    public static void main(String[] args) throws Exception {
        //使用 ServerSocketChannel 和 SocketChannel 网络
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        InetSocketAddress inetSocketAddress = new InetSocketAddress(7000);
        //绑定端口到socket,并启动
        serverSocketChannel.socket().bind(inetSocketAddress);
        //创建buffer数组
        ByteBuffer[] byteBuffers = new ByteBuffer[2];
        byteBuffers[0] = ByteBuffer.allocate(5);
        byteBuffers[1] = ByteBuffer.allocate(3);
        //等待客户端连接(telnet)
        SocketChannel socketChannel = serverSocketChannel.accept();
        //假定从客户端接收8个字节
        int messageLenght = 8;
        //循环读取
        while (true){
            int byteRead = 0;
            while (byteRead<messageLenght){
                long read = socketChannel.read(byteBuffers);
                //累计读取的字节数
                byteRead +=read;
                System.out.println("byteRead="+byteRead);
                Arrays.asList(byteBuffers).stream().map(buffer -> "postion="+buffer.position()+",limit="+buffer.limit()).forEach(System.out::println);
            }
            //将所有的buffer进行flip
            Arrays.asList(byteBuffers).forEach(Buffer::flip);
            //将数据读出显示到客户端
            long byteWrite = 0;
            while (byteWrite<messageLenght){
                long write = socketChannel.write(byteBuffers);
                byteWrite += write;
            }
            //将所有的buffer进行clear
            Arrays.asList(byteBuffers).forEach(Buffer::clear);
            System.out.println("byteRead:="+byteRead+";byteWrite:="+byteWrite+";messageLength:="+messageLenght);
        }
    }
}

Selector(选择器)

1、Java的NIO,用非阻塞的IO方式,可以用一个线程处理多个客户端的连接,就会使用到Selector(选择器);

2、Selector能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理,这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求;

3、只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大的减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程;

4、避免了多个线程之间的上下文切换导致的开销;

Selector类相关方法

注意事项

1、NIO中的ServerSocketChannel功能类似ServerSocket、SocketChannel功能类似Socket;

2、selector相关方法说明

① selector.select();//阻塞

② selector.select(1000);//阻塞1000毫秒,在1000毫秒后返回

③ selector.wakeup();//唤醒selector

④ selector.selectNow();//不阻塞,立马返还

NIO非阻塞网络编程原理分析图

NIO非阻塞网络编程相关的(Selector,SelectionKey,ServerSocketChannel和SocketChannel)关系梳理图

对上图说明

1、当客户端连接时,会通过ServerSocketChannel得到SocketChannel;

2、Selector进行监听select方法,返回有事件发生的通道的个数

3、将socketChannel注册到selector上,register(Selector sel,int ops),一个selector上可以注册多个SocketChannel;

4、注册后返回一个SelectionKey,会和该Selector关联(集合);

5、进一步得到各个SelectionKey(有事件发生时);

6、再通过SelectionKey反向获取SocketChannel,方法channel();

7、可以通过得到的channel,完成业务处理;

NIO非阻塞网络编程快速入门

案例要求

1、编写一个NIO入门案例,实现服务器端和客户端之间的数据简单通讯(非阻塞);

2、目的:理解NIO非阻塞网络编程机制

代码示例如下:

服务端

/**
 * 编写一个NIO入门案例,实现服务器端和客户端之间的数据简单通讯(非阻塞)
 */
public class NIOServer {
    public static void main(String[] args) throws Exception {
        //创建ServerSocketChannel -> ServerSocket
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //得到一个Selector对象
        Selector selector = Selector.open();
        //绑定一个端口6666,在服务器端监听
        serverSocketChannel.socket().bind(new InetSocketAddress(6666));
        //设置为非阻塞
        serverSocketChannel.configureBlocking(false);
        //把serverSocketChannel注册到selector,关心的事件是OP_ACCEPT
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        //这里打印的是1
        System.out.println("注册后的selectionkey 数量="+selector.keys().size());

        //循环等待客户端连接
        while (true){
            //这里我们等待3秒,如果没有事件发生,返回
            if(selector.select(3000)==0){
                System.out.println("服务器等待了3秒,无连接");
                continue;
            }
            //如果返回的>0,就获取到相关的selectionKey集合
            //1、如果返回的>0,表示已经获取到了关注的事件
            //2、selector.selectedkeys() 返回关注事件的集合
            //通过selectionKeys方向获取通道
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            //遍历Set<SelectionKey>,使用迭代器遍历
            Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
            while (keyIterator.hasNext()){
                //获取到Selectionkey
                SelectionKey key = keyIterator.next();
                //根据key对应的通道发生的事件做相应处理
                //如果是OP_ACCEPT,代表有新的客户端连接进来了
                if(key.isAcceptable()){
                    //该客户端生成一个SocketChannel
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    System.out.println("客户端连接成功,生成了一个socketChannel:"+socketChannel.hashCode());
                    //将socketChannel设置为非阻塞
                    socketChannel.configureBlocking(false);
                    //将socketChannel注册到selector,关注事件为OP_READ,同时给socketChannel关联一个Buffer
                    socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                    // 如果不断的有客户端连接进来,在这里可以看到不断的打印出2,3,4...
                    System.out.println("客户端连接后,注册的selectionkey 数量="+selector.keys().size());
                }
                //如果发生了OP_READ读事件
                if(key.isReadable()){
                    //通过key反向获取到对应的channel
                    SocketChannel channel = (SocketChannel)key.channel();
                    //获取到该channel关联的buffer
                    ByteBuffer buffer = (ByteBuffer) key.attachment();
                    channel.read(buffer);
                    System.out.println("from 客户端 "+new String(buffer.array()));
                }
                //手动从集合中移除当前的selectionkey,防止重复操作
                keyIterator.remove();
            }

        }
    }
}

客户端

public class NIOClient {
    public static void main(String[] args) throws Exception{
        //得到一个网络通道
        SocketChannel socketChannel = SocketChannel.open();
        //设置非阻塞
        socketChannel.configureBlocking(false);
        //提供服务端的ip和端口
        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
        //连接服务器
        if(!socketChannel.connect(inetSocketAddress)){
            while (!socketChannel.finishConnect()){
                System.out.println("因为连接需要时间,客户端不会阻塞,可以做其他工作...");
            }
        }
        //如果连接成功,就发送数据
        String str ="hello,NIO";
        ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
        //发送数据,将buffer数据写入channel
        socketChannel.write(buffer);
        System.in.read();
    }
}

SelectionKey

1、SelectionKey,表示Selector和网络通道的注册关系,共四种

① int OP_ACCEPT:有新的网络连接可以accept,值为16

② int OP_CONNECT:代表连接已经建立,值为8

③ int OP_READ:代表读操作,值为1

④ int OP_WRITE:代表写操作,值为4

源码中:

public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;

2、SelectionKey相关方法

ServerSocketChannel

1、serverSocketChannel在服务器端监听新的客户端Socket连接;

2、相关方法如下

SocketChannel

1、SocketChannel,网络IO通道,具体负责进行读写操作。NIO把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区;

2、相关方法如下

最近发表
标签列表