java 修改最大nio连接数_NIO#深入认识NIO核心组件(一)

什么是IO?

IO指 Input(输入流)、OutPut(输出流) 以支持JAVA应用对外传输交换数据。传输入目标可以是文件、网络、内存等。

54c0795f833d54a4f2e3cca45729fc3e.png

IO模型的演变史

随着对传输效率的要求越来越高,java也逐步演变出了三代IO模型分别是 BIO、NIO、AIO。

8c260f26e9177a48e4e6f957bbcd9c99.png

BIO 同步阻塞式 (Blocking I/O)

在java1.4之前 这种传输的实现 只能通过inputStream和OutputStream 实现。这是一种阻塞式IO模型,应对连接数不多的文件系统还好,如果应对的是成千上万的网络服务,这种阻塞式模型就会造成大量的线程占用,造成服务器无法承载更高的并发。

NIO 同步非阻塞式(Non Blocking I/O)

为解决这个问题 java1.4之后引入了NIO ,它通过双向管道进行通信,并且支持以非阻塞式的方式进行,就解决了网络传输导致线程占用的问题。Netty其在底层就是采用这种通信模型。

AIO 同步非阻塞式(Asynchronous Blocking I/O)

NIO的非阻塞的实现是依赖选择器 对管道状态进行轮循实现,如果同时进行的管道较多,性能必会受影响,所以java1.7引入了 异步非阻塞式IO,通过异步回调的方式代替选择器。这种改变在windows下是很明显,在linux系统中不明显。现大部分JAVA系统都是通过linux部署,所以AIO直正被应用的并不广泛。所以我们接下学的学习重点更关注到BIO与NIO的对比。

BIO与NIO模型区别

两组模型最大的别区在于阻塞与非阻塞,而所谓的阻塞是什么呢?而非阻塞又是如何解决的呢?

在阻塞模型中客户端与服务端建立连接后,就会按顺序发生三个事件

  1. 客户端把数据写入流中。(阻塞)
  2. 通过网络通道(TPC/IP)传输到服务端。(阻塞)
  3. 服务端读取。

这个过程中服务端的线程会一直处阻塞等待,直到数据发送过来后执行第3步。

如果在第1步客户端迟迟不写入数据,或者第2步网络传输延迟太高,都会导致服务端线程阻塞时间更长。所以更多的并发,就意味着需要更多的线程来支撑。

a5ac65fb7202f38012810d9a3f09f744.png

为了解决BIO的线程等待问题,于是出现了NIO。BIO模型里是通1对1线程来等待 第1、2步的完成。而在NIO里是指派了选择器(Selector)来检查,是否满足执行第3步的条件,满足了就会通知线程来执行第3步,并处理业务。这样1、2步的延迟就与 用于处理业务线程无关。

NIO简易模型

6461292891e253298a7537883bcafdd9.png
多了一个IO线程,用于处理IO,所有的请求都需要通过IO进行管理,当请求发送过来的时候IO线程通知相应的工作线程开始工作,在此之前工作线程是可以干别的事情的,保证工作线程不用阻塞在网络连接过程中

IO线程只是检测连接的状态(Selector)判断连接是否发送请求过来

Tomcat BIO通信模型图

84e88b962dc7b0d9ecb08ff68da9d8d2.png
Acceptor:用来建立连接 ServerSocket.accept();
BIO中的阻塞指的是在Socket建立连接后,客户端和服务器如果不进行数据的传送的阻塞

Tomcat 非阻塞式IO通信模型图

3179b571d3767fcd6762b35bdf7a24c6.png
IO线程负责建立连接,当数据穿送过来后,将数据写入到缓冲区,再异步的通知线程池执行任务,这时数据已经在内存的缓冲区中,所以不会产生阻塞

NIO基础组件Channel和Buffer

在BIO API中是通过InputStream 与outPutStream 两个流进行输入输出。而NIO 使用一个双向通信的管道代替了它俩。管道(Channel)必须依赖缓冲区(Buffer)实现通信

94fc1dd4cc558d2e71c3ffd8023d724a.png
  1. 性能提高
  2. 增加功能
  3. 实现堆外内存映射(零拷贝)

缓冲区Buffer定义与结构

所有管道都依赖了缓冲区,必须先掌握它。所谓缓冲区就是一个数据容器内部维护了一个数组来存储。Buffer缓冲区并不支持存储任何数据,只能存储一些基本类型,就连字符串也是不能直接存储的。

0668b8b80b4cde8e0dc76ccb33c3f997.png
Buffer只能存储基本类型

Buffer 内部结构

在Buffer内部维护了一个数组,同时有三个属性我们需要关注即:capacity:容量, 即内部数组的大小,这个值一但声明就不允许改变position:位置,当前读写位置,默认是0每读或写个一个位就会加1limit:限制,即能够进行读写的最大值,它必须小于或等于capacity

有了capacity做容量限制为什么还要有limit,原因往Buffer中写数据的时候 不一定会写满,而limit就是用来标记写到了哪个位置,读取的时候就不会超标。
如果读取超标就会报:BufferUnderflowException
同样写入超标也会报:BufferOverflowException

f9867b5ef5d2fa10239708f17133a39f.png
假设容器的容量为6,设置好后就不能再改变
public class IntBufferTest {

    @Test
    public void test1(){
        IntBuffer allocate = IntBuffer.allocate(6);// position=0、limit=6、capacity=6
        allocate.put(1);
        allocate.put(2); // position=2、limit=6、capacity=6
        allocate.flip(); // position=0、limit=2、capacity=6
        allocate.get();  // position=1、limit=2、capacity=6
        allocate.get();  //  position=2、limit=2、capacity=6
//        allocate.put(3); //  超出limit 限制 ==》 BufferOverflowException
        //allocate.get(); // 超出limit 限制 ==》 BufferUnderflowException

        // Buffer 没有删除数据这个说法
        // 循环利用空间
        // 回到缓冲的初始状态
        allocate.clear();  position=0、limit=6、capacity=6
        for (int i = 0; i < 6; i++) {
            allocate.put(i+1);
        }
        allocate.flip();
        allocate.get();
        allocate.get();
        allocate.mark();
        int i = allocate.get()+100;
        int i1 = allocate.get()+100;
        allocate.reset();
        allocate.put(i);
        allocate.put(i1);
        System.out.println(Arrays.toString(allocate.array()));
    }
}

b059529c139fe582bd3711ebd550029b.png

Buffer核心使用

  • allocate:声明一个指定大小的Buffer,position为0,limit为容量值
  • wrap:基于数组包装一个Buffer,position为0,limit为容量值

flip操作

为读取做好准备,将position 置为0,limit置为原position值

b44da4273f46c7cc1660a4f9ed1f8000.png

clear操作

为写入做好准备,将position 置为0,limit置为capacity值

注:clear不会实际清空数据

2fa44339e0284dbebdf679197295456e.png

mark操作

添加标记,以便后续调用 reset 将position回到标记,常用场景如:替换某一段内容

11485a6c2673cd821fc14e2447d38f95.png

目前我们知道,总共有4个值,分别是 mark、position、limit、capacity它们等于以下规则:

0 <= 标记 <= 位置 <= 限制 <= 容量

rewind操作

为重新读取做准备,position置位0,limit不变

remaining():表示limit和position之间的剩余空间


Channel定义

6d2f1a14396c01295d67c307d1a3c893.png

管道用于连接文件、网络Socket等。它可同时同时执行读取和写入两个I/O 操作,固称双向管道,它有连接和关闭两个状态,在创建管道时处于打开状态,一但关闭 在调用I/O操作就会报ClosedChannelException 。通过管道的isOpen 方法可判断其是否处于打开状态。

FileChannel 文件管道

9b6e138dd62dd6cdd8bbad70407dd1df.png

固名思议它就是用于操作文件的,除常规操作外它还支持以下特性:

  • 支持对文件的指定区域进行读写
  • 堆外内存映射,进行大文件读写时,可直接映射到JVM声明内存之外,从面提升读写效率。
  • 零拷贝技术,通过 transferFromtransferTo 直接将数据传输到某个通道,极大提高性能。
  • 锁定文件指定区域,以阻止其它程序员进行访问

打开FileChannel目前只能通过流进行间打开,如inputStream.getChannel() 和outputStream.getChannel() ,通过输入流打开的管道只能进行取,而outputStream打开的只能写。否则会分别抛出NonWritableChannelException与NonReadableChannelException异常。

如果想要管道同时支持读写,必须用RandomAccessFile 读写模式才可以。

FileChannel示例:

public class FileChannelTest  {

    String file_name="/Users/tommy/temp/coderead-netty/test.txt";
    @Test
    public void test1() throws IOException {
        //1. 打开文件管道
        /*FileInputStream inputStream=new FileInputStream(file_name);
        FileChannel channel = inputStream.getChannel();*/
        FileChannel channel = new RandomAccessFile(file_name,"rw").getChannel();

        ByteBuffer buffer=ByteBuffer.allocate(1024); // 声明1024个空间
        // 从文件中 读取数据并写入管道 再写入缓冲
        channel.read(buffer);
        buffer.flip();
        byte[] bytes= new byte[buffer.remaining()];
        int i =0;
        while (buffer.hasRemaining()){
            bytes[i++]= buffer.get();
        }
        // position=10 limit=10
        System.out.println(new String(bytes));

        // 把缓冲区数据写入到管道
        channel.write(ByteBuffer.wrap("hello 大叔".getBytes()));
        channel.close();
    }
}

52ed295edab99c3190229448ed83b11a.png

read方法会将数据写入到buffer 直到Buffer写满或者数据已经读取完毕。count 返回读取的数量,-1表示已读取完毕。

关于堆外内存映射与零拷贝等技术术,将会在后续章节中详细说明

DatagramChannel UDP套接字管道

udp 是一个无连接协议,DatagramChannel就是为这个协议提供服务,以接收客户端发来的消息。

udp实现步骤如下:

// 1.打开管道
DatagramChannel channel = DatagramChannel.open();
// 2.绑定端口
channel.bind(new InetSocketAddress(8080));// 绑定端口
ByteBuffer buffer = ByteBuffer.allocate(8192);
// 3.接收消息,如果客户端没有消息,则当前会阻塞等待
channel.receive(buffer); 



public class DatagramChannelTest {

    @Test
    public void test1() throws IOException {
        DatagramChannel channel=DatagramChannel.open();
        // 绑定端口
        channel.bind(new InetSocketAddress(8080));
        ByteBuffer buffer=ByteBuffer.allocate(8192);

        while (true){
            buffer.clear(); //  清空还原
            channel.receive(buffer); // 接收数据,如果没有数据则阻塞
            buffer.flip();
            byte[] bytes=new byte[buffer.remaining()];
            buffer.get(bytes);
            System.out.println(new String(bytes));
        }
    }
}
nc -vu 127.0.0.1 8080 这个命令可向udp发送消息

673c58f0f3adc672258af6eb8e56bfdf.png

TCP套接字管道

TCP是一个有连接协议,须建立连接后才能通信。这就需要下面两个管道:

  • ServerSocketChannel :用于与客户端建立连接
  • SocketChannel :用于和客户端进行消息读写

TCP管道实现步骤如下:

Plain  Text// 1.打开TCP服务管道
ServerSocketChannel channel = ServerSocketChannel.open();
// 2.绑定端口
channel.bind(new InetSocketAddress(8080));
// 3.接受客户端发送的连接请求,(如果没有则阻塞)
SocketChannel socketChannel = channel.accept();
ByteBuffer buffer=ByteBuffer.allocate(1024);
// 4.读取客户端发来的消息(如果没有则阻塞)
socketChannel.read(buffer);
// 5.回写消息
socketChannel.write(ByteBuffer.wrap("返回消息".getBytes()));
// 6.关闭管道
socketChannel.close();
可通过命令进行测试TCP服务 telnet 127.0.0.1 8080

上述例子接收一个消息,并返回客户端,然后关闭。它只能处理一个客户端的请求,然后整个服务就结束了。如果想要处理多个请求,我们可以加上一个循环来接收请求,然后在分配一个子线程去处理每个客户端请求。

ServerSocketChannel channel = ServerSocketChannel.open();
channel.bind(new InetSocketAddress(8080));
while(true){
  SocketChannel socketChannel = channel.accept();
 // 使用子线程处理请求
 new Thread(()->handle(socketChannel)).start();
}

处理客户端请求

private void handle(SocketChannel socketChannel){
      ByteBuffer buffer=ByteBuffer.allocate(1024);
 // 1.读取客户端发来的消息(如果没有则阻塞)  
      socketChannel.read(buffer);
 // 返回消息
      socketChannel.write(ByteBuffer.wrap("返回消息".getBytes()));
 // 关闭管道
      socketChannel.close();
 }

至此我们已完成了一个简单但完整的 请求响应模型。这个模型中每个客户端连接都会有一个子线程进行处理。在没有读到消息前这个线程会一直阻塞在read方法中。所以不难推断这就是一个典型的BIO阻塞模型。

那怎么在管道中实现非阻塞的NIO模型呢?下节课我们继续探讨。
public class ServerSocketChannelTest {
    @Test
    public void test1() throws IOException {
        ServerSocketChannel channel = ServerSocketChannel.open();
        channel.bind(new InetSocketAddress(8080));
        // 1.建立连接

        // 2.通信
        ByteBuffer buffer = ByteBuffer.allocate(8192);
        while (true) {
            handle(channel.accept());
        }
    }


    public void handle(final SocketChannel socketChannel) throws IOException {
        // 2.通信
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                ByteBuffer buffer = ByteBuffer.allocate(8192);
                while (true) {
                    try {
                        buffer.clear();
                        socketChannel.read(buffer);
                        // 从buffer 当中读出来
                        buffer.flip();
                        byte[] bytes = new byte[buffer.remaining()];
                        buffer.get(bytes);
                        String message = new String(bytes);
                        System.out.println(message);
                        // 写回去
                        buffer.rewind();
                        socketChannel.write(buffer);
                        if (message.trim().equals("exit")) {
                            break;
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
                try {
                    socketChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值