IO请求底层流程
为了OS的安全性等的考虑,进程是无法直接操作I/O设备的,其必须通过系统调用请求内核来协助完成I/O动作,而内核会为每个I/O设备维护一个buffer。
如下图所示:
整个请求过程为: 用户进程发起请求,内核接受到请求后,从I/O设备中获取数据到buffer中,再将buffer中的数据copy到用户进程的地址空间,该用户进程获取到数据后再响应客户端。
在整个请求过程中,数据输入至buffer需要时间,而从buffer复制数据至进程也需要时间。
记住这两点很重要
1 等待数据准备 (Waiting for the data to be ready)
2 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
阻塞I/O (Blocking I/O)
在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:
当用户进程调用了某个系统调用,内核就开始了IO的第一个阶段:等待数据准备。对于network io来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候内核就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当内核一直等到数据准备好了,它就会将数据从内核中拷贝到用户内存,然后内核返回结果,用户进程才解除block的状态,重新运行起来。
所以,blocking IO的特点就是在IO执行的两个阶段都被block了。
非阻塞I/O (Non-Blocking I/O)
linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子:
当用户进程调用某个系统调用时,系统不会阻塞用户进程,而是立刻返回一个ewouldblock错误,从用户进程角度讲 ,并不需要等待,而是马上就得到了一个结果。用户进程判断标志是ewouldblock时,就知道数据还没准备好,于是它就可以去做其他的事了,于是它可以再次发送IO请求,一旦内核中的数据准备好了。并且又再次收到了用户进程的请求,那么它马上就将数据拷贝到了用户内存,然后返回。
IO概念
io流分为输入流及输出流,输入流是文件到内存,输出流是内存到文件。
输入流:把能够读取一个字节序列的对象称为输入流
输出流:把能够写一个字节序列的对象称为输出流
输入输出是相对于内存设备而言的,将外设(硬盘,键盘等)中的数据读取到内存设备
中叫输入;将内存设备中的数据写入到外设中叫输出,所以有读入,写出的称呼:读入到内
存,写出内存。
可以这样比喻:输入流和输出流中间连接着内存,输入流和输出流将读写分离开来进
行操作,先从外设读入内存,然后再写出内存转移到其他外设。
NIO
概念
NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector。传统IO基于字节流和字符流进行操作,而NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。
NIO和传统IO(一下简称IO)之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区。
IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变得可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
Channel
Channel和传统IO中的Stream很相似。虽然很相似,但是有很大的区别,主要区别为:通道是双向的,通过一个Channel既可以进行读,也可以进行写;而Stream只能进行单向操作,通过一个Stream只能进行读或者写。
以下是常用的几种通道:
- FileChannel 通过使用FileChannel可以从文件读或者向文件写入数据,只能使用阻塞模式运行;
- SocketChanel 通过SocketChannel,以TCP来向网络连接的两端读写数据;
- ServerSocketChannel 通过ServerSocketChanel能够监听客户端发起的TCP连接,并为每个TCP连接创建一个新的SocketChannel来进行数据读写;
- DatagramChannel 通过DatagramChannel,以UDP协议来向网络连接的两端读写数据。
FileChannel
Java NIO FileChannel是连接到文件的通道。使用FileChannel,可以从文件中读取数据,并将数据写入文件。 Java NIO FileChannel类是标准Java IO API读取文件的可替代方法。
FileChannel不能设置为非阻塞模式,它只能使用阻塞模式运行
读
//1.打开一个文件通道(FileChannel)需要通过InputStream,OutputStream或RandomAccessFile获取FileChannel。 以下是通过RandomAccessFile打开FileChannel的方法:
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
//2.从文件通道中读取数据
//首先分配缓冲区。从FileChannel读取的数据被读入缓冲区。
ByteBuffer buf = ByteBuffer.allocate(48);
//read()方法返回的int告诉缓冲区中有多少字节。 如果返回-1,则到达文件结尾。
int bytesRead = inChannel.read(buf);
while(bytesRead != -1)
{
buf.flip();
while(buf.hasRemaining())
{
System.out.print((char)buf.get());
}
buf.compact();
bytesRead = fileChannel.read(buf);
}
写
String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
channel.write(buf);
}
SocketChannel
ava NIO SocketChannel是连接到TCP网络套接字的通道。
可以通过两种方式创建SocketChannel:
- 在客户端打开SocketChannel并连接到Internet上的某个服务器。
- 在服务端当传入连接到达ServerSocketChannel时,可以创建SocketChannel。
客户端打开SocketChannel
SocketChannel socketChannel = SocketChannel.open();
//设置为非阻塞
socketChannel .configureBlocking(false);
socketChannel.connect(new InetSocketAddress("http://localhost", 80));
如果SocketChannel处于非阻塞模式,调用connect()方法,这个方法可能会在建立连接之前返回。 要确定是否建立了连接,可以调用finishConnect()方法,如下:
if(socketChannel.finishConnect() ){
//等待,或者做一些别的处理
}
非阻塞模式下,write()方法可能在没有写入任何内容的情况下返回。因此,需要在循环中调用write()方法。write()方法无法保证能写多少字节到SocketChannel。所以,我们重复调用write()直到Buffer没有要写的字节为止。
while(buffer.hasRemaining()){
socketChannel.write(buffer);
}
非阻塞模式下,read()方法在尚未读取到任何数据时可能就返回了。所以需要关注它的int返回值,它会告诉你读取了多少字节。
ServerSocketChannel
Java NIO ServerSocketChannel是一个可以监听传入TCP连接的通道,就像标准Java网络模块中的ServerSocket一样。
打开ServerSocketChannel:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
监听新进来的连接:当然,你会在while循环中使用一些其他的停止while循环的参数而不是true。
while(true){
SocketChannel socketChannel = serverSocketChannel.accept();
}
ServerSocketChannel可以设置成非阻塞模式。在非阻塞模式下,accept() 方法会立刻返回,如果还没有新进来的连接,返回的将是null。 因此,需要检查返回的SocketChannel是否是null.如:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
serverSocketChannel.configureBlocking(false);
while (true)
{
SocketChannel socketChannel = serverSocketChannel.accept();
if (socketChannel != null)
{
// do something with socketChannel...
}
}
DatagramChannel
1.打开DatagramChannel
DatagramChannel channel = DatagramChannel.open();
//绑定端口9999
channel.socket().bind(new InetSocketAddress(9999));
2.接受数据
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
channel.receive(buf);
receive方法将接收到的数据包的内容复制到给定的Buffer中。如果接收的数据包包含的数据多于缓冲区可以包含的数据,则会以静默方式丢弃剩余的数据。
3.发送数据
String newData = "New String to write to file..."
+ System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
int bytesSent = channel.send(buf, new InetSocketAddress("localhost", 80));
Selector
Selector类是NIO的核心类,Selector能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理。这样一来,只是用一个单线程就可以管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了多线程之间的上下文切换导致的开销。
与Selector有关的一个关键类是SelectionKey,一个SelectionKey表示一个到达的事件,这2个类构成了服务端处理业务的关键逻辑。
1、 Selector的创建
通过调用Selector.open()方法创建一个Selector对象,如下:
Selector selector = Selector.open();
2、注册Channel到Selector
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, Selectionkey.OP_READ);
Channel必须是非阻塞的。
所以FileChannel不适用Selector,因为FileChannel不能切换为非阻塞模式,更准确的来说是因为FileChannel没有继承SelectableChannel。Socket channel可以正常使用。
register() 方法的第二个参数。这是一个“ interest集合 ”,意思是在通过Selector监听Channel时对什么事件感兴趣。也就是声明这个通道是对什么感兴趣的通道,可以监听四种不同类型的事件:
- Connect 连接就绪
- Accept 接受就绪
- Read 读就绪
- Write 写就绪
这四种事件用SelectionKey的四个常量来表示:
SelectionKey.OP_CONNECT
SelectionKey.OP_ACCEPT
SelectionKey.OP_READ
SelectionKey.OP_WRITE
如果你对不止一种事件感兴趣,使用或运算符即可,如下:
SelectionKey.OP_READ | SelectionKey.OP_WRITE;
Selector维护的三种类型SelectionKey集合:
Selector维护的三种类型SelectionKey集合:
-
已注册的键的集合(Registered key set)
所有与选择器关联的通道所生成的键的集合称为已经注册的键的集合。并不是所有注册过的键都仍然有效。这个集合通过 keys() 方法返回,并且可能是空的。这个已注册的键的集合不是可以直接修改的;试图这么做的话将引发java.lang.UnsupportedOperationException。 -
已选择的键的集合(Selected key set)
所有与选择器关联的通道所生成的键的集合称为已经注册的键的集合。并不是所有注册过的键都仍然有效。这个集合通过 keys() 方法返回,并且可能是空的。这个已注册的键的集合不是可以直接修改的;试图这么做的话将引发java.lang.UnsupportedOperationException。 -
已取消的键的集合(Cancelled key set)
已注册的键的集合的子集,这个集合包含了 cancel() 方法被调用过的键(这个键已经被无效化),但它们还没有被注销。这个集合是选择器对象的私有成员,因而无法直接访问。
select()方法介绍:
在刚初始化的Selector对象中,这三个集合都是空的。 通过Selector的select()方法可以选择已经准备就绪的通道 (这些通道包含你感兴趣的的事件)。比如你对读就绪的通道感兴趣,那么select()方法就会返回读事件已经就绪的那些通道。下面是Selector几个重载的select()方法:
- int select():阻塞到至少有一个通道在你注册的事件上就绪了。
- int select(long timeout):和select()一样,但最长阻塞时间为timeout毫秒。
- int selectNow():非阻塞,只要有通道就绪就立刻返回。
select()方法返回的int值表示有多少通道已经就绪,是自上次调用select()方法后有多少通道变成就绪状态。之前在select()调用时进入就绪的通道不会在本次调用中被记入,而在前一次select()调用进入就绪但现在已经不在处于就绪的通道也不会被记入。例如:首次调用select()方法,如果有一个通道变成就绪状态,返回了1,若再次调用select()方法,如果另一个通道就绪了,它会再次返回1。如果对第一个就绪的channel没有做任何操作,现在就有两个就绪的通道,但在每次select()方法调用之间,只有一个通道就绪了。
一旦调用select()方法,并且返回值不为0时,则 可以通过调用Selector的selectedKeys()方法来访问已选择键集合 。如下:
Set selectedKeys=selector.selectedKeys();
进而可以放到和某SelectionKey关联的Selector和Channel。如下所示:
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) {
// a connection was established with a remote server.
} else if (key.isReadable()) {
// a channel is ready for reading
} else if (key.isWritable()) {
// a channel is ready for writing
}
keyIterator.remove();
}
SelectionKey
一个SelectionKey键表示了一个特定的通道对象和一个特定的选择器对象之间的注册关系。
key.attachment(); //返回SelectionKey的attachment,attachment可以在注册channel的时候指定。
key.channel(); // 返回该SelectionKey对应的channel。
key.selector(); // 返回该SelectionKey对应的Selector。
key.interestOps(); //返回代表需要Selector监控的IO操作的bit mask
key.readyOps(); // 返回一个bit mask,代表在相应channel上可以进行的IO操作。
ready 集合是通道已经准备就绪的操作的集合。JAVA中定义以下几个方法用来检查这些操作是否就绪.
//检查这些操作是否就绪的方法
key.isAcceptable();//是否可读,是返回 true
key.isWritable()://是否可写,是返回 true
key.isConnectable()://是否可连接,是返回 true
key.sAcceptable()://是否可接收,是返回 true
可以将一个对象或者更多信息附着到SelectionKey上,这样就能方便的识别某个给定的通道。例如,可以附加 与通道一起使用的Buffer,或是包含聚集数据的某个对象。使用方法如下:
//添加附加
key.attach(theObject);
//查询附加
Object attachedObj = key.attachment();
还可以在用register()方法向Selector注册Channel的时候附加对象。如:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
模板
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress("localhost", 8080));
ssc.configureBlocking(false);
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
while(true) {
int readyNum = selector.select();
if (readyNum == 0) {
continue;
}
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectedKeys.iterator();
while(it.hasNext()) {
SelectionKey key = it.next();
if(key.isAcceptable()) {
// 接受连接
} else if (key.isReadable()) {
// 通道可读
} else if (key.isWritable()) {
// 通道可写
}
it.remove();
}
}
客户端与服务端简单交互实例
服务端:
public class WebServer {
public static void main(String[] args) {
try {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress("127.0.0.1", 8000));
ssc.configureBlocking(false);
Selector selector = Selector.open();
// 注册 channel,并且指定感兴趣的事件是 Accept
ssc.register(selector, SelectionKey.OP_ACCEPT);
ByteBuffer readBuff = ByteBuffer.allocate(1024);
ByteBuffer writeBuff = ByteBuffer.allocate(128);
writeBuff.put("received".getBytes());
writeBuff.flip();
while (true) {
int nReady = selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove();
if (key.isAcceptable()) {
// 创建新的连接,并且把连接注册到selector上,而且,
// 声明这个channel只对读操作感兴趣。
SocketChannel socketChannel = ssc.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
}
else if (key.isReadable()) {
SocketChannel socketChannel = (SocketChannel) key.channel();
readBuff.clear();
socketChannel.read(readBuff);
readBuff.flip();
System.out.println("received : " + new String(readBuff.array()));
key.interestOps(SelectionKey.OP_WRITE);
}
else if (key.isWritable()) {
writeBuff.rewind();
SocketChannel socketChannel = (SocketChannel) key.channel();
socketChannel.write(writeBuff);
key.interestOps(SelectionKey.OP_READ);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
客户端:
public class WebClient {
public static void main(String[] args) throws IOException {
try {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8000));
ByteBuffer writeBuffer = ByteBuffer.allocate(32);
ByteBuffer readBuffer = ByteBuffer.allocate(32);
writeBuffer.put("hello".getBytes());
writeBuffer.flip();
while (true) {
writeBuffer.rewind();
socketChannel.write(writeBuffer);
readBuffer.clear();
socketChannel.read(readBuffer);
}
} catch (IOException e) {
}
}
}
运行结果:
先运行服务端,再运行客户端,服务端会不断收到客户端发送过来的消息。
Buffer
Buffer,故名思意,缓冲区,实际上是一个容器,是一个连续数组。Channel提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由Buffer。
在NIO中,Buffer是一个顶层父类,它是一个抽象类,常用的Buffer的子类有:
- ByteBuffer
- IntBuffer
- CharBuffer
- LongBuffer
- DoubleBuffer
- FloatBuffer
- ShortBuffer
如果是对于文件读写,上面几种Buffer都可能会用到。但是对于网络读写来说,用的最多的是ByteBuffer。
Buffer的三个属性:
- capacity
作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”.你只能往里写capacity个byte、long,char等类型。一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据。 - position
当你写数据到Buffer中时,position表示当前的位置。初始的position值为0.当一个byte、long等数据写到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1.
当读取数据时,也是从某个特定位置读。当将Buffer从写模式切换到读模式,position会被重置为0. 当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。 - limit
在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。
当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)
position和limit的含义取决于Buffer处在读模式还是写模式。不管Buffer处在什么模式,capacity的含义总是一样的。
Buffer的常用方法
- allocate(1024)
ByteBuffer buf = ByteBuffer.allocate(1024); 为buffer分配空间 - flip()
flip方法将Buffer从写模式切换到读模式。调用flip()方法会将position设回0,并将limit设置成之前position的值。 - clear()或者compact()
clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。 - rewind()
与flip()相似,但不影响limit属性。它只是将位置值设回0。您可以使用rewind()后退,重读已经被翻转的缓冲区中的数据。 - hasRemaining()
缓冲区中是否还有内容
Java中的所有的流可以分为几大类,它们的名字是什么,各代表什么
分为 字节输入流 InputStream
字节输出流 OutputStream
字符输入流 Reader
字符输出流 Writer
所有流都是这四个流的子类
字符流:用于处理文字和字符的 流。操作完成后不关闭流,数据无法传递。
字节流:文字以外的都可以用。操作完成后不需关闭流,也可以传递数据。
字符流有缓冲区,将数据接受于缓冲区,达到区的极限(通过手动 1.colse 2.flush)就会进入文件中,打开文件次数少。
字节流:每传一次数据就会打开一次文件,打开数据次数多。占用内存。
什么是java序列化,如何实现java序列化?
对象序列化,将对象以二进制的形式保存在硬盘上
反序列化;将二进制的文件转化为对象读取
实现serializable接口
java序列化中如果要标记为不可序列化的字段,可以使用关键字:tranisant修饰。
BufferedReader属于哪种流,它主要是用来做什么的,它里面有那些经典的方法
属于处理流中的缓冲流,可以将读取的内容存在内存里面,有readLine()方法
它有一个很特别的方法:readLine(),使用起来特别方便,每次读回来的都是一行,
省了很多手动拼接buffer的琐碎;
它比较高效,相对于一个字符/字节地读取、转换、返回来说,它有一个缓冲区,读
满缓冲区才返回;一般情况下,都建议使用它们把其它Reader/InputStream包起来,
使得读取数据更高效。
对于文件来说,经常遇到一行一行的,特别相符情景。