inputStream,outputStream
(2)面向字符操作的接口:Reader,Writer
File
(2)面向网络操作的I/O接口:Socket
以网络IO为例:
当客户端发送的网络包经过路由器和交换器的转发后到达对应服务端的网络适配器(网卡),并存储在对应网络I/O的套接字文件中,然后操作系统会将该文件中的数据一般通过DMA
复制到内存中供应用程序使用;
在Unix网络编程
这本书中概述了完成上述操作的几种模型:
(1)阻塞与非阻塞: 阻塞与非阻塞主要是从 CPU 的消耗上来说的,阻塞就是 CPU 停下来等待一个慢的操作完成后CPU 才接着完成其它的事。非阻塞就是在这个慢的操作在执行时 CPU 去干其它别的事,等这个慢的操作完成时,CPU 再接着完成后续的操作。
(2)同步与非同步: 同步与非同步主要是从程序方面来说的,同步指程序发出一个功能调用后,没有结果前不会返回。非同步是指程序发出一个功能调用后,程序便会返回,然后再通过回调机制通知程序进行处理。
recvfrom方法
等待客户端发送的数据发送到内存并返回;
这个模型最大的问题就是操作系统中最典型的CPU速度与外设速度不匹配的问题,网络适配器的速度相对于CPU的速度是极慢的,并且此时CPU却一直在阻塞。
recvfrom 方法
后,如果此时套接字文件还没有准备好,则直接返回一个错误信息,然后CPU就会去做其他事情,而该线程会不断获取CPU时间片进行轮询,所以该模式下虽然是非阻塞,但其线程切换确实很频繁的,所以通过该方式增加的CPU使用时间与线程切换的成本还是需要好好评估的;
并且当数据准备好后,并且线程获取到时间片再次调用recvfrom
时,线程还是需要等待数据拷贝至内存的。
多路复用IO:(Java NIO原理)
该模型通过一个方法select
,该方法一直会阻塞到IO事件的到来(即套接字文件准备好)再返回,这个时候我们再调用recvfrom方法
就只需要等待数据拷贝至内存即可;并且select方法
可以监听多个事件,所以联系到Java NIO中时,就是多个线程可以向同一个Selector
注册多个事件,从而达到了多路复用的效果。
异步IO(AIO):
aio_read
,应用程序调用后便直接返回,并且不需要像前几种模型一样需要等待数据拷贝至内存;
但其内在的实现还是很复杂的,底层还是使用BIO实现的,就不展开描述了,因为对编程人员好像并没有太大的作用。
网络适配器(网卡)的数据准备好的这个过程中
,而都是通发出种信号进行通知应用程序,虽然信号的实现方式或是用select
或是用更底层的方式,但本质上还是很相似的;但信号驱动IO也是需要线程等待数据拷贝至用户空间的。
Java中的Socket是对进行通信的两端的抽象,其封装了一系列TCP/IP层面的底层操作; 代码如下:
//通过一个IP:PORT套接字新建一个Socket对象,确定要连接的服务器的位置和端口 Socket socket = new Socket("127.0.0.1", 8089); //通过Socket对象拿到OutputStream,可以将其理解通过其向服务器端对应的套接字文件写入数据 OutputStream outputStream = socket.getOutputStream(); //使用默认的字符集去解析outputStream的字节流 PrintWriter printWriter = new PrintWriter(outputStream, true); /*向服务器发送一个HTTP1.1的请求*/ printWriter.println("GET /index.html HTTP/1.1"); printWriter.println("Host: localhost:8080"); printWriter.println("Connection Close"); printWriter.println(); 复制代码
//ServerSocket在该套接字上监听连接事件 ServerSocket serverSocket = new ServerSocket(8089, 1, InetAddress.getByName("127.0.0.1")); //服务端阻塞在accept()方法上,直到客户端的connect()请求,并返回一个Socket对象 socket = serverSocket.accept(); //从返回的Socket对象中获取该Socket对应的套接字文件的内容并进行读取 InputStream inputStream = socket.getInputStream(); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); int i = 0; while (i != -1) { i = bufferedReader.read(); System.out.println("拿到的数据为:"+(char)i); } socket.close(); 复制代码
其实Java BIO 即为对系统提供的网络I/O方法的封装;
我们一般都是适用Acceptor模型来进行BIO服务端的创建
,即通过一个ServerSocket()
监听来自客户端的连接,然后通过三次握手建立连接后便会创建一个子线程并通过线程池进行相应的逻辑处理;
backlog
这个参数来表明在服务端拒绝连接请求之前,可以排队的请求数量,所以这样的模型注定了BIO性能的局限性(排队的通信线程可能要阻塞一段时间),处理量的局限性;Java NIO
通过多路复用IO的模型实现了单个Selector线程
管理了多个连接,解决了BIO最致命的一个问题;
无论是In/OutputStream
还是Java NIO中的通道channel
本质上都是对网络I/O文件的抽象,与前者不同,channel
是双通道的,既可以读又可以写。
所以按照I/O多路复用 的模型,当channel
中的数据准备好了的时候会返回一个可读的事件,并且通过selector进行处理,安排相应的Socket进行相应数据的读取,这是一个数据可读的事件,而Selector可监听的事件有四种:
SelectionKey.OP_CONNECT // 连接事件 SelectionKey.OP_ACCEPT //接收事件 SelectionKey.OP_READ //数据可读事件 SelectionKey.OP_WRITE //可写事件 复制代码
socket.getInputStream.write()
方法来直接进行读写的,而NIO中向channel
中写入数据必须从buffer中获取,而channel
也只能向buffer写入数据,这样使得这样的操作更为接近操作系统执行I/O的方式;细一点讲,是因为在向OutputStream中write()
数据即为向接收方Socket对象中的InputStream
中的RecvQ队列中,而如果write()
的数据大于队列中每个数据对象限定的长度,就需要进行拆分,而这个过程,我们是不可以控制的,而且涉及到用户空间与内核空间地址的转换;但是当我们使用Buffer后,我们可以控制Buffer的长度,是否扩容以及如何扩容我们都可以掌握。
参考文章:www.ibm.com/developerwo…/** * @CreatedBy:CVNot * @Date:2020/2/21/15:30 * @Description: */ public class NIOServer { public static void main(String[] args) { try { //创建一个多路复用选择器 Selector selector = Selector.open(); //创建一个ServerSocket通道,并监听8080端口 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open().bind(new InetSocketAddress(8080)); //设置为非阻塞 serverSocketChannel.configureBlocking(false); //监听接收数据的事件 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while (true){ selector.select(); //拿到Selector关心的已经到达事件的SelectionKey集合 Set keys = selector.selectedKeys(); Iterator iterator = keys.iterator(); while (iterator.hasNext()){ SelectionKey selectionKey = (SelectionKey)iterator.next(); iterator.remove(); //因为我们只注册了ACCEPT事件,所以这里只写了当连接处于这个状态时的处理程序 if(selectionKey.isAcceptable()){ //拿到产生这个事件的通道 ServerSocketChannel serverChannel = (ServerSocketChannel)selectionKey.channel(); SocketChannel clientChannel = serverChannel.accept(); clientChannel.configureBlocking(false); //并为这个通道注册一个读事件 clientChannel.register(selectionKey.selector(), SelectionKey.OP_READ); } else if(selectionKey.isReadable()){ SocketChannel clientChannel = (SocketChannel)selectionKey.channel(); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); long bytesRead = clientChannel.read(byteBuffer); while(bytesRead > 0){ byteBuffer.flip(); System.out.printf("来自客户端的数据" + new String(byteBuffer.array())); byteBuffer.clear(); bytesRead = clientChannel.read(byteBuffer); } byteBuffer.clear(); byteBuffer.put("客户端你好".getBytes("UTF-8")); byteBuffer.flip(); clientChannel.write(byteBuffer); } } } } catch (IOException e) { e.printStackTrace(); } } } 复制代码
客户端:
/** * @CreatedBy:CVNot * @Date:2020/2/21/16:06 * @Description: */ public class NIOClient { public static void main(String[] args) { try { Selector selector = Selector.open(); SocketChannel clientChannel = SocketChannel.open(); clientChannel.configureBlocking(false); clientChannel.connect(new InetSocketAddress(8080)); 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()) { /** * 连接服务器端成功 * * 首先获取到clientChannel,然后通过Buffer写入数据,然后为clientChannel注册OP_READ事件 */ clientChannel = (SocketChannel) key.channel(); if (clientChannel.isConnectionPending()) { clientChannel.finishConnect(); } clientChannel.configureBlocking(false); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); byteBuffer.clear(); byteBuffer.put("服务端你好,我是客户端".getBytes("UTF-8")); byteBuffer.flip(); clientChannel.write(byteBuffer); clientChannel.register(key.selector(), SelectionKey.OP_READ); } else if (key.isReadable()) { //通道可以读数据 clientChannel = (SocketChannel) key.channel(); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); long bytesRead = clientChannel.read(byteBuffer); while (bytesRead > 0) { byteBuffer.flip(); System.out.println("server data :" + new String(byteBuffer.array())); byteBuffer.clear(); bytesRead = clientChannel.read(byteBuffer); } } else if (key.isWritable() && key.isValid()) { //通道可以写数据 } } } } catch (IOException e) { e.printStackTrace(); } } } 复制代码