优锐课学习笔记——Java IO 和 NIO

无意间看到“优锐课”一节讲NIO的课程,老师讲得特别好,认真听完后收获颇多,继续找资料学习并写下这篇文章分享给大家。
通过这篇文章,你可以了解有关Java的创建套接字的阻塞和非阻塞替代方法的更多信息。

介绍

套接字使用TCP/IP传输协议,是两个主机之间的最后网络通信。通常不需要处理它们,因为在它们之上构建了诸如HTTP或FTP的协议。但是,了解它们的工作原理很重要。

TCP:这是一种可靠的数据传输协议,可确保发送的数据完整且正确,并且需要建立连接。

Java提供了一种阻塞和非阻塞的替代方法来创建套接字,根据你的要求,你可以考虑使用其中一种。

Java阻塞IO

Java阻塞IO API包含在JDK中的java.net包下,通常最简单地使用。

该API基于字节流和可读取或写入的字符流的流。没有索引可用于来回移动,就像在数组中一样,它只是连续的数据流。

客户端每次请求与服务器的连接时,都会阻塞线程。因此,如果我们希望同时进行许多连接,则必须创建足够大的线程池。

ServerSocket serverSocket = new ServerSocket(PORT_NUMBER);
while (true) {
    Socket client = serverSocket.accept();
    try {
        BufferedReader in = new BufferedReader(
            new InputStreamReader(client.getInputStream())
        );
        OutputStream out = client.getOutputStream();
        in.lines().forEach(line -> {
            try {
                out.write(line.getBytes());
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
        client.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
  1. 使用给定的端口创建一个ServerSocket进行监听。
  2. 服务器将在调用accept( )时阻塞,并开始侦听客户端连接。
  3. 如果客户端请求连接,则accept( )返回一个Socket。
  4. 现在,我们可以从客户端(InputStream)读取数据并将数据发送回客户端(OutputStream)。

如果要允许多个连接,则必须创建一个线程池

ExecutorService threadPool = Executors.newFixedThreadPool(100);
 threadPool.execute(() -> {
     // SOCKET CREATION
 });

如你所见,此API有一些限制。我们将无法接受比计算机中可用线程更多的连接。因此,如果你希望有许多连接,则需要替代方法。

Java NIO

Java.nio 是用于套接字连接的非阻塞API,这意味着你对可用线程数的要求并不严格。使用此库,一个线程可以一次处理多个连接。

主要内容:

通道(Channel): 通道是输入和输出流的组合,因此它们允许你进行读取和写入,并且它们使用缓冲区来执行这些操作。

缓冲区(Buffer): 它是用于从通道读取并写入通道的内存块。 当你想从缓冲区读取数据时,需要调用flip(),以便将pos设置为0。

int read = socketChannel.read(buffer); // pos = n & lim = 1024
while (read != -1) {
  buffer.flip(); // set buffer in read mode - pos = 0 & lim = n
  while(buffer.hasRemaining()){
      System.out.print((char) buffer.get()); // read 1 byte at a time
  }
  buffer.clear(); // make buffer ready for writing - pos = 0 & lim = 1024
  read = socketChannel.read(buffer); // set to -1
}
  1. 在第1行,pos将等于写入缓冲区的字节数。
  2. 在第3行,调用flip( )将position设置为0,并限制为先前写入的字节数。
  3. 在第5行,它一次从Buffer读取一个字节,直到达到限制。
  4. 最后,在第7行,清除缓冲区。

选择器(Selector): 选择器可以注册多个通道,并将检查哪些通道已准备好接受新连接。与阻塞IO的accept()方法类似,当调用select()时,它将阻塞应用程序,直到Channel准备好进行操作为止。由于选择器可以注册多个通道,因此只需要一个线程即可处理多个连接。

选择键(Selection Key): 它包含特定频道的属性(兴趣集,就绪集,选择器/频道和可选的附加对象)。选择键主要用于了解通道的当前兴趣(isAcceptable(),isReadable(),isWritable()),获取通道并对该通道进行操作。

例如

我们将使用 Echo Socket Channel 服务器来显示NIO的工作方式。

var serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
var selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
    selector.select();
    var keys = selector.selectedKeys().iterator();
    while (keys.hasNext()) {
        var selectionKey = (SelectionKey) keys.next();
        if (selectionKey.isAcceptable()) {
            createChannel(serverSocketChannel, selectionKey);
        } else if (selectionKey.isReadable()) {
            doRead(selectionKey);
        } else if (selectionKey.isWritable()) {
            doWrite(selectionKey);
        }
        keys.remove();
    }
}
  1. 从第1行到第3行,创建一个ServerSocketChannel,你必须将其显式设置为非阻塞模式。套接字还配置为侦听端口8080。
  2. 在第5和第6行上,将创建一个Selector,并在Selector上注册ServerSocketChannel,其中的SelectionKey指向ACCEPT操作。
  3. 为了使应用程序始终保持侦听状态,阻塞方法select()位于无限while循环内,并且至少在选择了一个通道时,将调用select()调用wakeup()或中断线程。
  4. 然后,在第10行上,从选择器返回一组键,我们将迭代它们以执行就绪通道。
private static void createChannel(ServerSocketChannel serverSocketChannel, SelectionKey selectionKey) throws IOException {
    var socketChannel = serverSocketChannel.accept();
    LOGGER.info("Accepted connection from " + socketChannel);
    socketChannel.configureBlocking(false);
    socketChannel.write(ByteBuffer.wrap(("Welcome: " + socketChannel.getRemoteAddress() +
            "\nThe thread assigned to you is: " + Thread.currentThread().getId() + "\n").getBytes()));
    dataMap.put(socketChannel, new LinkedList<>()); // store socket connection
    LOGGER.info("Total clients connected: " + dataMap.size());
    socketChannel.register(selectionKey.selector(), SelectionKey.OP_READ); // selector pointing to READ operation
}
  1. 每次创建新连接时,isAcceptable()将为true,并且新通道将被注册到选择器中。
  2. 为了跟踪每个通道的数据,将其放入Map中,以套接字通道为键,并列出ByteBuffer。
  3. 然后,选择器将指向READ操作。
private static void doRead(SelectionKey selectionKey) throws IOException {
    LOGGER.info("Reading...");
    var socketChannel = (SocketChannel) selectionKey.channel();
    var byteBuffer = ByteBuffer.allocate(1024); // pos=0 & lim=1024
    int read = socketChannel.read(byteBuffer); // pos=numberOfBytes & lim=1024
    if (read == -1) { // if connection is closed by the client
        doClose(socketChannel);
    } else {
        byteBuffer.flip(); // put buffer in read mode by setting pos=0 and lim=numberOfBytes
        dataMap.get(socketChannel).add(byteBuffer); // find socket channel and add new byteBuffer queue
        selectionKey.interestOps(SelectionKey.OP_WRITE); // set mode to WRITE to send data
    }
}
  1. 在读取块中,将检索通道并将传入的数据写入ByteBuffer。
  2. 在第6行,我们检查连接是否已关闭。
  3. 在第9行和第10行,使用flip()将缓冲区设置为读取模式,并将其添加到Map中。
  4. 然后,调用interestOps()指向WRITE操作。
private static void doWrite(SelectionKey selectionKey) throws IOException {
    LOGGER.info("Writing...");
    var socketChannel = (SocketChannel) selectionKey.channel();
    var pendingData = dataMap.get(socketChannel); // find channel
    while (!pendingData.isEmpty()) { // start sending to client from queue
        var buf = pendingData.poll();
        socketChannel.write(buf);
    }
    selectionKey.interestOps(SelectionKey.OP_READ); // change the key to READ
}
  1. 再次检索通道,以便将保存在地图中的数据写入其中。
  2. 然后,将选择器设置为READ操作。
private static void doClose(SocketChannel socketChannel) throws IOException {
    dataMap.remove(socketChannel);
    var socket = socketChannel.socket();
    var remoteSocketAddress = socket.getRemoteSocketAddress();
    LOGGER.info("Connection closed by client: " + remoteSocketAddress);
    socketChannel.close(); // closes channel and cancels selection key
}
  1. 如果连接已关闭,则将通道从Map中移除,然后我们关闭该通道。

Java IO 与NIO

在IO和NIO之间进行选择将取决于用例。对于较少的连接和简单的解决方案,IO可能更适合你。而如果你想要更高效的东西同时处理数千个连接,则NIO可能是更好的选择,但请记住,这会带来很多代码复杂性。但是,有一些像Netty或Apache MINA这样的框架是在NIO之上构建的,隐藏了编程的复杂性。

感谢你的阅读!

感谢优锐课老师的讲解!

有什么问题可以加下Java学习资料交流qq群:907135806进群探讨。也可以添加vx:ddmsiqi,有更多JVM、Mysql、Tomcat、Spring Boot、Spring Cloud、Zookeeper、Kafka、RabbitMQ、RockerMQ、Redis、ELK、Git等Java学习资料和视频课程干货!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值