Java NIO深入分析

以下我们系统通过原理,过程等方便给大家深入的简介了Java NIO的函数机制以及用法等,学习下吧。

前言

本篇主要讲解Java中的IO机制

分为两块:
第一块讲解多线程下的IO机制
第二块讲解如何在IO机制下优化CPU资源的浪费(New IO)

Echo服务器

单线程下的socket机制就不用我介绍了,不懂得可以去查阅下资料
那么多线程下,如果进行套接字的使用呢?
我们使用最简单的echo服务器来帮助大家理解

首先,来看下多线程下服务端和客户端的工作流程图:

可以看到,多个客户端同时向服务端发送请求

服务端做出的措施是开启多个线程来匹配相对应的客户端

并且每个线程去独自完成他们的客户端请求

原理讲完了我们来看下是如何实现的

在这里我写了一个简单的服务器

用到了线程池的技术来创建线程(具体代码作用我已经加了注释):

public class MyServer {
  private static ExecutorService executorService = Executors.newCachedThreadPool();  //创建一个线程池
  private static class HandleMsg implements Runnable{   //一旦有新的客户端请求,创建这个线程进行处理
  Socket client;   //创建一个客户端
  public HandleMsg(Socket client){  //构造传参绑定
   this.client = client;
  }
  @Override
  public void run() {
   BufferedReader bufferedReader = null;  //创建字符缓存输入流
   PrintWriter printWriter = null;   //创建字符写入流
   try {
    bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream()));  //获取客户端的输入流
    printWriter = new PrintWriter(client.getOutputStream(),true);   //获取客户端的输出流,true是随时刷新
    String inputLine = null;
    long a = System.currentTimeMillis();
    while ((inputLine = bufferedReader.readLine())!=null){
     printWriter.println(inputLine);
    }
    long b = System.currentTimeMillis();
    System.out.println("此线程花费了:"+(b-a)+"秒!");
   } catch (IOException e) {
    e.printStackTrace();
   }finally {
    try {
     bufferedReader.close();
     printWriter.close();
     client.close();
    } catch (IOException e) {
     e.printStackTrace();
    }
   }
  }
 }
 public static void main(String[] args) throws IOException {   //服务端的主线程是用来循环监听客户端请求
  ServerSocket server = new ServerSocket(8686);  //创建一个服务端且端口为8686
  Socket client = null;
  while (true){   //循环监听
   client = server.accept();  //服务端监听到一个客户端请求
   System.out.println(client.getRemoteSocketAddress()+"地址的客户端连接成功!");
   executorService.submit(new HandleMsg(client));  //将该客户端请求通过线程池放入HandlMsg线程中进行处理
  }
 }
}

上述代码中我们使用一个类编写了一个简单的echo服务器
在主线程中用死循环来开启端口监听

简单客户端

有了服务器,我们就可以对其进行访问,并且发送一些字符串数据
服务器的功能是返回这些字符串,并且打印出线程占用时间

下面来写个简单的客户端来响应服务端:

public class MyClient {
 public static void main(String[] args) throws IOException {
  Socket client = null;
  PrintWriter printWriter = null;
  BufferedReader bufferedReader = null;
  try {
   client = new Socket();
   client.connect(new InetSocketAddress("localhost",8686));
   printWriter = new PrintWriter(client.getOutputStream(),true);
   printWriter.println("hello");
   printWriter.flush();
   bufferedReader = new BufferedReader(new InputStreamReader(client.getInputStream()));   //读取服务器返回的信息并进行输出
   System.out.println("来自服务器的信息是:"+bufferedReader.readLine());
  } catch (IOException e) {
   e.printStackTrace();
  }finally {
   printWriter.close();
   bufferedReader.close();
   client.close();
  }
 }
}

代码中,我们用字符流发送了一个hello字符串过去,如果代码没问题
服务器会返回一个hello数据,并且打印出我们设置的日志信息

echo服务器结果展示

我们来运行:

1.打开server,开启循环监听:

2.打开一个客户端:

可以看到客户端打印出了返回结果

3.查看服务端日志:

很好,一个简单的多线程套接字编程就实现了

但是试想一下:

如果一个客户端请求中,在IO写入到服务端过程中加入Sleep,

使每个请求占用服务端线程10秒

然后有大量的客户端请求,每个请求都占用那么长时间

那么服务端的并能能力就会大幅度下降

这并不是因为服务端有多少繁重的任务,而仅仅是因为服务线程在等待IO(因为accept,read,write都是阻塞式的)

让高速运行的CPU去等待及其低效的网络IO是非常不合算的行为

这时候该怎么办?

NIO

New IO成功的解决了上述问题,它是怎样解决的呢?

IO处理客户端请求的最小单位是线程

而NIO使用了比线程还小一级的单位:通道(Channel)

可以说,NIO中只需要一个线程就能完成所有接收,读,写等操作

要学习NIO,首先要理解它的三大核心

Selector,选择器

Buffer,缓冲区

Channel,通道

博主不才,画了张丑图给大家加深下印象 ^ . ^

再给一张TCP下的NIO工作流程图(好难画的线条...)

大家大致看懂就行,我们一步步来

Buffer

首先要知道什么是Buffer

在NIO中数据交互不再像IO机制那样使用流

而是使用Buffer(缓冲区)

博主觉得图才是最容易理解的

所以...

可以看出Buffer在整个工作流程中的位置

来点实际点的,上面图中的具体代码如下:

1.首先给Buffer分配空间,以字节为单位

ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

创建一个ByteBuffer对象并且指定内存大小

2.向Buffer中写入数据:

1).数据从Channel到Buffer:channel.read(byteBuffer);
2).数据从Client到Buffer:byteBuffer.put(...);

3.从Buffer中读取数据:

1).数据从Buffer到Channel:channel.write(byteBuffer);
2).数据从Buffer到Server:byteBuffer.get(...);

Selector

选择器是NIO的核心,它是channel的管理者

通过执行select()阻塞方法,监听是否有channel准备好

一旦有数据可读,此方法的返回值是SelectionKey的数量

所以服务端通常会死循环执行select()方法,直到有channl准备就绪,然后开始工作

每个channel都会和Selector绑定一个事件,然后生成一个SelectionKey的对象

需要注意的是:

channel和Selector绑定时,channel必须是非阻塞模式

而FileChannel不能切换到非阻塞模式,因为它不是套接字通道,所以FileChannel不能和Selector绑定事件

在NIO中一共有四种事件:

1.SelectionKey.OP_CONNECT:连接事件

2.SelectionKey.OP_ACCEPT:接收事件

3.SelectionKey.OP_READ:读事件

4.SelectionKey.OP_WRITE:写事件

Channel

共有四种通道:

FileChannel:作用于IO文件流

DatagramChannel:作用于UDP协议

SocketChannel:作用于TCP协议

ServerSocketChannel:作用于TCP协议

本篇文章通过常用的TCP协议来讲解NIO

我们以ServerSocketChannel为例:

打开一个ServerSocketChannel通道

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

关闭ServerSocketChannel通道:

serverSocketChannel.close();

循环监听SocketChannel:

while(true){
 SocketChannel socketChannel = serverSocketChannel.accept();
 clientChannel.configureBlocking(false);
}

clientChannel.configureBlocking(false);语句是将此通道设置为非阻塞,也就是异步
自由控制阻塞或非阻塞便是NIO的特性之一

SelectionKey

SelectionKey是通道和选择器交互的核心组件

比如在SocketChannel上绑定一个Selector,并注册为连接事件:

SocketChannel clientChannel = SocketChannel.open();
clientChannel.configureBlocking(false);
clientChannel.connect(new InetSocketAddress(port));
clientChannel.register(selector, SelectionKey.OP_CONNECT);

核心在register()方法,它返回一个SelectionKey对象

来检测channel事件是那种事件可以使用以下方法:

selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();

服务端便是通过这些方法 在轮询中执行相对应操作

当然通过Channel与Selector绑定的key也可以反过来拿到他们

Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();

在Channel上注册事件时,我们也可以顺带绑定一个Buffer:

clientChannel.register(key.selector(), SelectionKey.OP_READ,ByteBuffer.allocateDirect(1024));

或者绑定一个Object:

selectionKey.attach(Object);
Object anthorObj = selectionKey.attachment();

NIO的TCP服务端

讲了这么多,都是理论
我们来看下最简单也是最核心的代码(加那么多注释很不优雅,但方便大家看懂):

package cn.blog.test.NioTest;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;
public class MyNioServer {
 private Selector selector;   //创建一个选择器
 private final static int port = 8686;
 private final static int BUF_SIZE = 10240;
 private void initServer() throws IOException {
  //创建通道管理器对象selector
  this.selector=Selector.open();
  //创建一个通道对象channel
  ServerSocketChannel channel = ServerSocketChannel.open();
  channel.configureBlocking(false);  //将通道设置为非阻塞
  channel.socket().bind(new InetSocketAddress(port));  //将通道绑定在8686端口
  //将上述的通道管理器和通道绑定,并为该通道注册OP_ACCEPT事件
  //注册事件后,当该事件到达时,selector.select()会返回(一个key),如果该事件没到达selector.select()会一直阻塞
  SelectionKey selectionKey = channel.register(selector,SelectionKey.OP_ACCEPT);
  while (true){  //轮询
   selector.select();   //这是一个阻塞方法,一直等待直到有数据可读,返回值是key的数量(可以有多个)
   Set keys = selector.selectedKeys();   //如果channel有数据了,将生成的key访入keys集合中
   Iterator iterator = keys.iterator();  //得到这个keys集合的迭代器
   while (iterator.hasNext()){    //使用迭代器遍历集合
    SelectionKey key = (SelectionKey) iterator.next();  //得到集合中的一个key实例
    iterator.remove();   //拿到当前key实例之后记得在迭代器中将这个元素删除,非常重要,否则会出错
    if (key.isAcceptable()){   //判断当前key所代表的channel是否在Acceptable状态,如果是就进行接收
     doAccept(key);
    }else if (key.isReadable()){
     doRead(key);
    }else if (key.isWritable() && key.isValid()){
     doWrite(key);
    }else if (key.isConnectable()){
     System.out.println("连接成功!");
    }
   }
  }
 }
 public void doAccept(SelectionKey key) throws IOException {
  ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
  System.out.println("ServerSocketChannel正在循环监听");
  SocketChannel clientChannel = serverChannel.accept();
  clientChannel.configureBlocking(false);
  clientChannel.register(key.selector(),SelectionKey.OP_READ);
 }
 public void doRead(SelectionKey key) throws IOException {
  SocketChannel clientChannel = (SocketChannel) key.channel();
  ByteBuffer byteBuffer = ByteBuffer.allocate(BUF_SIZE);
  long bytesRead = clientChannel.read(byteBuffer);
  while (bytesRead>0){
   byteBuffer.flip();
   byte[] data = byteBuffer.array();
   String info = new String(data).trim();
   System.out.println("从客户端发送过来的消息是:"+info);
   byteBuffer.clear();
   bytesRead = clientChannel.read(byteBuffer);
  }
  if (bytesRead==-1){
   clientChannel.close();
  }
 }
 public void doWrite(SelectionKey key) throws IOException {
  ByteBuffer byteBuffer = ByteBuffer.allocate(BUF_SIZE);
  byteBuffer.flip();
  SocketChannel clientChannel = (SocketChannel) key.channel();
  while (byteBuffer.hasRemaining()){
   clientChannel.write(byteBuffer);
  }
  byteBuffer.compact();
 }
 public static void main(String[] args) throws IOException {
  MyNioServer myNioServer = new MyNioServer();
  myNioServer.initServer();
 }
}

我打印了监听channel,告诉大家ServerSocketChannel是在什么时候开始运行的

如果配合NIO客户端的debug,就能很清楚的发现,进入select()轮询前

虽然已经有了ACCEPT事件的KEY,但select()默认并不会去调用

而是要等待有其它感兴趣事件被select()捕获之后,才会去调用ACCEPT的SelectionKey

这时候ServerSocketChannel才开始进行循环监听

也就是说一个Selector中,始终保持着ServerSocketChannel的运行

serverChannel.accept();真正做到了异步(在initServer方法中的channel.configureBlocking(false);)

如果没有接受到connect,会返回一个null

如果成功连接了一个SocketChannel,则此SocketChannel会注册写入(READ)事件

并且设置为异步

NIO的TCP客户端

有服务端必定有客户端

其实如果能完全理解了服务端

客户端的代码大同小异

package cn.blog.test.NioTest;
import java.io.IOException;
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;
public class MyNioClient {
 private Selector selector;   //创建一个选择器
 private final static int port = 8686;
 private final static int BUF_SIZE = 10240;
 private static ByteBuffer byteBuffer = ByteBuffer.allocate(BUF_SIZE);
 private void initClient() throws IOException {
  this.selector = Selector.open();
  SocketChannel clientChannel = SocketChannel.open();
  clientChannel.configureBlocking(false);
  clientChannel.connect(new InetSocketAddress(port));
  clientChannel.register(selector, SelectionKey.OP_CONNECT);
  while (true){
   selector.select();
   Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
   while (iterator.hasNext()){
    SelectionKey key = iterator.next();
    iterator.remove();
    if (key.isConnectable()){
     doConnect(key);
    }else if (key.isReadable()){
     doRead(key);
    }
   }
  }
 }
 public void doConnect(SelectionKey key) throws IOException {
  SocketChannel clientChannel = (SocketChannel) key.channel();
  if (clientChannel.isConnectionPending()){
   clientChannel.finishConnect();
  }
  clientChannel.configureBlocking(false);
  String info = "服务端你好!!";
  byteBuffer.clear();
  byteBuffer.put(info.getBytes("UTF-8"));
  byteBuffer.flip();
  clientChannel.write(byteBuffer);
  //clientChannel.register(key.selector(),SelectionKey.OP_READ);
  clientChannel.close();
 }
 public void doRead(SelectionKey key) throws IOException {
  SocketChannel clientChannel = (SocketChannel) key.channel();
  clientChannel.read(byteBuffer);
  byte[] data = byteBuffer.array();
  String msg = new String(data).trim();
  System.out.println("服务端发送消息:"+msg);
  clientChannel.close();
  key.selector().close();
 }
 public static void main(String[] args) throws IOException {
  MyNioClient myNioClient = new MyNioClient();
  myNioClient.initClient();
 }
}

输出结果

这里我打开一个服务端,两个客户端:

接下来,你可以试下同时打开一千个客户端,只要你的CPU够给力,服务端就不可能因为阻塞而降低性能

以上便是Java NIO的基础详解,如果大家还有什么不明白的地方可以在下方的留言区域讨论。

(0)

相关推荐

  • JAVA-4NIO之Channel之间的数据传输方法

    在Java NIO中,如果两个通道中有一个是FileChannel,那你可以直接将数据从一个channel(译者注:channel中文常译作通道)传输到另外一个channel. transferFrom():被动接收 FileChannel的transferFrom()方法可以将数据从源通道传输到FileChannel中(译者注:这个方法在JDK文档中的解释为将字节从给定的可读取字节通道传输到此通道的文件中). 方法的输入参数position表示从position处开始向目标文件写入数据,cou

  • Java使用NioSocket手动实现HTTP服务器

    NioSocket简单复习 重要概念 NioSocket里面的三个重要概念:Buffer.Channel.Selector Buffer为要传输的数据 Channel为传输数据的通道 Selector为通道的分配调度者 使用步骤 使用NioSocket实现通信大概如以下步骤: ServerSocketChannel可以通过configureBlocking方法来设置是否采用阻塞模式,设置为false后就可以调用register注册Selector,阻塞模式下不可以用Selector. 注册后,S

  • java的NIO管道用法代码分享

    Java的NIO中的管道,就类似于实际中的管道,有两端,一段作为输入,一段作为输出.也就是说,在创建了一个管道后,既可以对管道进行写,也可以对管道进行读,不过这两种操作要分别在两端进行.有点类似于队列的方式. 这里是Pipe原理的图示: 创建管道 通过Pipe.open()方法打开管道.例如: Pipe pipe = Pipe.open(); 向管道写数据 要向管道写数据,需要访问sink通道.像这样: Pipe.SinkChannel sinkChannel = pipe.sink(); 通过

  • Java NIO实例UDP发送接收数据代码分享

    Java的NIO包中,有一个专门用于发送UDP数据包的类:DatagramChannel,UDP是一种无连接的网络协议, 一般用于发送一些准确度要求不太高的数据等. 完整的服务端程序如下: public class StatisticsServer { //每次发送接收的数据包大小 private final int MAX_BUFF_SIZE = 1024 * 10; //服务端监听端口,客户端也通过该端口发送数据 private int port; private DatagramChann

  • JDK1.7 之java.nio.file.Files 读取文件仅需一行代码实现

    JDK1.7中引入了新的文件操作类java.nio.file这个包,其中有个Files类它包含了很多有用的方法来操作文件,比如检查文件是否为隐藏文件,或者是检查文件是否为只读文件.开发者还可以使用Files.readAllBytes(Path)方法把整个文件读入内存,此方法返回一个字节数组,还可以把结果传递给String的构造器,以便创建字符串输出.此方法确保了当读入文件的所有字节内容时,无论是否出现IO异常或其它的未检查异常,资源都会关闭.这意味着在读文件到最后的块内容后,无需关闭文件.要注意

  • Java NIO Path接口和Files类配合操作文件的实例

    Path接口 1.Path表示的是一个目录名序列,其后还可以跟着一个文件名,路径中第一个部件是根部件时就是绝对路径,例如 / 或 C:\ ,而允许访问的根部件取决于文件系统: 2.以根部件开始的路径是绝对路径,否则就是相对路径: 3.静态的Paths.get方法接受一个或多个字符串,字符串之间自动使用默认文件系统的路径分隔符连接起来(Unix是 /,Windows是 \ ),这就解决了跨平台的问题,接着解析连接起来的结果,如果不是合法路径就抛出InvalidPathException异常,否则就

  • Java NIO:浅析IO模型_动力节点Java学院整理

    也许很多朋友在学习NIO的时候都会感觉有点吃力,对里面的很多概念都感觉不是那么明朗.在进入Java NIO编程之前,我们今天先来讨论一些比较基础的知识:I/O模型.下面本文先从同步和异步的概念 说起,然后接着阐述了阻塞和非阻塞的区别,接着介绍了阻塞IO和非阻塞IO的区别,然后介绍了同步IO和异步IO的区别,接下来介绍了5种IO模型,最后介绍了两种和高性能IO设计相关的设计模式(Reactor和Proactor). 以下是本文的目录大纲: 一.什么是同步?什么是异步? 二.什么是阻塞?什么是非阻塞

  • Java中网络IO的实现方式(BIO、NIO、AIO)介绍

    在网络编程中,接触到最多的就是利用Socket进行网络通信开发.在Java中主要是以下三种实现方式BIO.NIO.AIO. 关于这三个概念的辨析以前一直都是好像懂,但是表达的不是很清楚,下面做个总结完全辨析清楚. 1. BIO方式 首先我用一个较为通俗的语言来说明: BIO 就是阻塞IO,每个TCP连接进来服务端都需要创建一个线程来建立连接并进行消息的处理.如果中间发生了阻塞(比如建立连接.读数据.写数据时发生阻碍),线程也会发生阻塞,并发情况下,N个连接需要N个线程来处理. 这种方式的缺点就是

  • JAVA-NIO之Socket/ServerSocket Channel(详解)

    一.ServerSocketChannel Java NIO中的 ServerSocketChannel 是一个可以监听新进来的TCP连接的通道, 就像标准IO中的ServerSocket一样.ServerSocketChannel类在 java.nio.channels包中. 打开 ServerSocketChannel 通过调用 ServerSocketChannel.open() 方法来打开ServerSocketChannel. 关闭 ServerSocketChannel 通过调用Se

  • Java NIO深入分析

    以下我们系统通过原理,过程等方便给大家深入的简介了Java NIO的函数机制以及用法等,学习下吧. 前言 本篇主要讲解Java中的IO机制 分为两块: 第一块讲解多线程下的IO机制 第二块讲解如何在IO机制下优化CPU资源的浪费(New IO) Echo服务器 单线程下的socket机制就不用我介绍了,不懂得可以去查阅下资料 那么多线程下,如果进行套接字的使用呢? 我们使用最简单的echo服务器来帮助大家理解 首先,来看下多线程下服务端和客户端的工作流程图: 可以看到,多个客户端同时向服务端发送

  • java NIO 详解

    Java NIO提供了与标准IO不同的IO工作方式: Channels and Buffers(通道和缓冲区):标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channel)和缓冲区(Buffer)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中. Asynchronous IO(异步IO):Java NIO可以让你异步的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还是可以进行其他事情.当数据被写入到缓冲区时,线程可以继续处理它.从缓冲区写入通道也类似. S

  • Java NIO和IO的区别

    下表总结了Java NIO和IO之间的主要差别,我会更详细地描述表中每部分的差异. 复制代码 代码如下: IO                NIO面向流            面向缓冲阻塞IO            非阻塞IO无                选择器 面向流与面向缓冲 Java NIO和IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的. Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方.此外,它不能前后移动流中的数

  • java nio基础使用示例

    在jdk1.4中提出的技术,非阻塞IO,采用的是基于事件处理方式.传统的io技术为阻塞的,比如读一个文件,惹read方法是阻塞的,直到有数据读入.归纳为:1.java io为阻塞,在打开一个io通道后,read将一直等待在端口一边读取字节内容,如果没有内容进来,read相当于阻塞掉了.2.在1的基础上改进为,开设线程,serversocker.accept()后让线程去等待,但是当并发量高的时候,相当耗费资源的.3.java nio为非阻塞,采用的是reactor反应堆模式,或者说observe

  • Java NIO原理图文分析及代码实现

    前言: 最近在分析hadoop的RPC(Remote Procedure Call Protocol ,远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议.可以参考:http://baike.baidu.com/view/32726.htm )机制时,发现hadoop的RPC机制的实现主要用到了两个技术:动态代理(动态代理可以参考博客:http://weixiaolu.iteye.com/blog/1477774 )和java NIO.为了能够正确地分析

  • 支撑Java NIO与NodeJS的底层技术

    支撑Java NIO 与 NodeJS的底层技术 众所周知在近几个版本的Java中增加了一些对Java NIO.NIO2的支持,与此同时NodeJS技术栈中最为人称道的优势之一就是其高性能IO,那么我们今天要讨论的话题就是支撑这些技术的底层技术. 开始之前先要提出的一个问题是: 为什么NodeJS和Java NIO2没有在更早的时间出现? 答案:个人认为是底层的支撑技术还不成熟. 那么,底层技术指的是什么呢?对的,我想很多人已经猜到,是操作系统技术.本文提出的两个概念Java NIO2和Node

随机推荐