IO(三)

网络IO也是属于一整个IO的范畴的,只是一般说IO会先想到操作文件,对象序列化等。

IO最原始的是BIO也是阻塞IO,A向B发一个执行,必须等待B的响应。该模型的整体思路是有一个独立的Acceptor线程负责监听客户端的链接,它接收到客户端链接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这就是典型的一请求一应答的通讯模型。

 很完整的一条执行链路,但是缺陷很明显,就是必须发起一个新的线程,一对一,在业务量爆发的时间段会造成大量的线程,引起服务的可靠性下降。

后面就产生了新的伪异步IO

在BIO的基础上增加了线程池,这样就可以限制掉线程的创建,让它维持在一个可控的范围。同时也可以进行复用。

Acceptor接收到一个请求之后就会新增到队列里面,然后通过队列来进行一个个处理,但是在突发的高并发,如果线程池已经满了,依然会存在服务延迟的情况,但是至少服务端可以保证了安全。可用性提高了很多。但是依然没有解决掉通信线程阻塞的问题。

在JDK1.4之后引入了新的NIO模型。在概念中,NIO是这样说的,这是一个高速的,面向块的IO。通过定义包含数据的类以及块的形式处理这些数据。

那么什么是块,可以这样理解之前的IO都是实实在在的,一条一条来,面向块就是一堆一堆来。那么对应的如果对块进行处理的,块又是如何保证数据的时序性呢?

对应的看Java里面提供的新包。

缓冲区:在面向流的IO中,可以将数据直接写入或者将数据直接督导Stream对象中。在NIO库中,所有的数据都是用缓冲区进行处理的。在读数据时,它是直接读到缓冲区中的;在写数据时,写入到缓冲区中。任务和访问NIO中的数据,都是通过缓冲区进行操作的。Java NIO 有以下Buffer类型:

  • ByteBuffer
  • MappedByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

 这时候就要想了,如何实现缓冲区,假设我自己去实现,肯定是一个队列,然后进行同步的进出,只要新增的速度低于外出的数据就可以,那么缓冲数组就永远不会停息。

点进去看一看。

 应该思想能对上了,它会进行自己的check,然后使用数组作为内存连续地址,来进行缓存。

然后继续思考,如果有了一个内存连续地址,是不是应该需要一个通道来不停的传输,而且通道最好也有自己的缓冲。

就引入了Channel:

Channel是一个通道,网络数据通过Channel读取和写入。通道和流的不同之处在于通道是双向的,流只是在一个方向上移动(一个流必须是InputStream或者OutputStream的子类),而且通道可以用于读、写或者同时用于读写。

通道既可以是单向的也可以是双向的。只实现ReadableByteChannel接口中的read()方法或者只实现WriteableByteChannel接口中的write()方法的通道皆为单向通道,同时实现ReadableByteChannel和WriteableByteChannel为双向通道,比如ByteChannel。对于socket通道来说,它们一直是双向的,而对于FileChannel来说,它同样实现了ByteChannel,但是我们知道通过FileInputStream的getChannel()获取的FileChannel只具有文件的只读权限,那此时的在该通道调用write()会出现什么情况?不出意外的抛出了NonWriteChannelException异常。 

至于为什么有些可以是双向的,双向模式是否支持同步读写?这个问题,我其实也不太知道,所以一边看一边写了。

public class IoDemo implements Serializable{



public static void testFileChannelOnWrite() {
try {
RandomAccessFile accessFile = new RandomAccessFile("D://file.txt","rw");
FileChannel fc = accessFile.getChannel();
byte[] bytes = new String("hello every one").getBytes();
ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
fc.write(byteBuffer);
byteBuffer.clear();
byteBuffer.put(new String(",a good boy").getBytes());
byteBuffer.flip();
fc.write(byteBuffer);
fc.close();

} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
private static void testFileChannelOnRead() {
try {
FileChannel fileChannel = new FileInputStream(new File("D://file.txt")).getChannel();
ByteBuffer byteBuffer=ByteBuffer.allocate(10);
int n=0;
while (fileChannel.read(byteBuffer) != -1) {
byteBuffer.flip();//缓冲区写——> 读
while (byteBuffer.hasRemaining()) {
System.out.print((char) byteBuffer.get());
}
byteBuffer.clear();//缓冲区不会被自动覆盖,需要主动调用该方法
}
fileChannel.force(true);
fileChannel.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}

public static void main(String[] args) throws IOException {
testFileChannelOnRead();
testFileChannelOnWrite();
}

}

 例如这个通道读写的代码,其实没看出可以双向.....

但是一起执行的时候,第一遍输出是空的,第二遍执行的时候就是有控制台输出的!一下子有点奥妙的感觉了。百度了一下 找到一个名词:共享内存映射文件。很有可能就是因为这个。

然后还提供了一个多路复用器Selector

多路复用器提供选择已经就绪的任务的能力。简单来讲,Selector会不断地轮训注册在其上的Channel,如果某个Channel上面有新的TCP链接接入、读和写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的IO操作。先前的实现是基于select,有句柄数目限制1024,后面NIO2.0改成了epoll(AIO)就突破了最大链接句柄限制。异步通道提供两种方式获取结果:

1),通过java.util.concurrent.Future类来表示异步操作的结果;

2),在执行异步操作的时候传入一个java.nio.channels。由CompletionHandler接口的实现类作为操作完成的回调。

NIO2.0的异步套接字是真正的异步非阻塞IO,它对应UNIX网络编程中的事件驱动IO(AIO),它不需要通过多路复用器(selector)对注册的通道进行轮询操作即可实现异步读写,从而简化了NIO的编程模型。

通过轮训问答方式来进行异步。设计的很有美观。各司其职,互不侵犯。

Reactor具有单线程和多线程两种,多线程是为了应对高并发的情况。

单线程的模型:

多线程的模型:

A),有一个NIO线程-Acceptor线程用于监听服务端,接受客户端的TCP链接请求。

B),网络IO操作---读写等由一个NIO线程池负责,线程池可以采用标准的JDK线程池实现,它包含一个任务队列和N个可用的线程,由这些NIO线程负责消息的读取、解码、编码和发送。

C),一个NIO线程可以同时处理N条链路,但是一个链路只对应一个NIO线程,防止并发操作问题。

在绝大多数场景下,Reactor多线程模型可以满足性能需求。但是,在个别特殊场景中,一个NIO线程负责监听和处理所有客户端链接可能会存在性能问题。例如并发百万客户端链接,或者服务端需要多客户端握手进行安全认证,但是认证本身非常损耗性能。在这种场景下,单独一个Acceptor线程可能会存在性能不足的问题,为了解决性能问题,产生了第三种Reactor线程模型---主从Reactor多线程模型。

 

 主要特点是:服务端用于接收客户端链接不再是一个单独的NIO线程,而是一个独立的NIO线程池。Acceptor接收到客户端TCP链接请求并处理完成后(可能包含接入认证等),将新创建的SocketChannel注册到IO线程池(sub reactor线程池)的某个IO线程上,由它负责SocketChannel的读写和编码工作。Acceptor线程池仅仅用于客户端的登录、握手和安全认证,一旦链路建立成功,就将链路注册到后端subReactor线程池的IO线程上,由IO线程负责后续的IO操作。

我们发现NIO编程难度确实比同步阻塞BIO大很多,我们的NIO例程并没有考虑“半包读”和“半包写”,如果加上这些,代码将会更加复杂。NIO代码既然这么复杂,为什么它的应用却越来越广泛呢,使用NIO编程的优点总结如下。

(1)客户端发起的连接操作是异步的,可以通过在多路复用器注册OP_CONNECT等待后续结果,不需要像之前的客户端那样被同步阻塞。

(2)SocketChannel的读写操作都是异步的,如果没有可读写的数据它不会同步等待,直接返回,这样I/O通信线程就可以处理其他的链路,不需要同步等待这个链路可用。

(3)线程模型的优化:由于JDK的Selector在Linux等主流操作系统上通过epoll实现,它没有连接句柄数的限制(只受限于操作系统的最大句柄数或者对单个进程的句柄限制),这意味着一个Selector线程可以同时处理成千上万个客户端连接,而且性能不会随着客户端的增加而线性下降,因此,它非常适合做高性能、高负载的网络服务器。

JDK1.7升级了NIO类库,升级后的NIO类库被称为NIO2.0,引人注目的是,Java正式提供了异步文件I/O操作,同时提供了与UNIX网络编程事件驱动I/O对应的AIO。

 
 
smartcat.994
原文地址:https://www.cnblogs.com/SmartCat994/p/14020367.html