NIO
非阻塞io模型。
很多人会先谈谈阻塞io的坏处, 阻塞io会单线程接受然后多线程处理的结构。而他的读写io其实会很花费时间,而创建线程后,会有很多时间浪费与此处。
不过,这个模型最本质的问题在于,严重依赖于线程。但线程是很”贵”的资源,主要表现在:
- 线程的创建和销毁成本很高,在Linux这样的操作系统中,线程本质上就是一个进程。创建和销毁都是重量级的系统函数。
- 线程本身占用较大内存,像Java的线程栈,一般至少分配512K~1M的空间,如果系统中的线程数过千,恐怕整个JVM的内存都会被吃掉一半。
- 线程的切换成本是很高的。操作系统发生线程切换的时候,需要保留线程的上下文,然后执行系统调用。如果线程数过高,可能执行线程切换的时间甚至会大于线程执行的时间,这时候带来的表现往往是系统load偏高、CPU sy使用率特别高(超过20%以上),导致系统几乎陷入不可用的状态。
- 容易造成锯齿状的系统负载。因为系统负载是用活动线程数或CPU核心数,一旦线程数量高但外部网络环境不是很稳定,就很容易造成大量请求的结果同时返回,激活大量阻塞线程从而使系统负载压力过大
NIO重要部分
在我理解里NIO重要概念有3个,Select,Channel,buff。还有一个逻辑概念是handler。
select从Channel拿数据,从buff中储存。buff是内存的部分,相当于从内核区域拷贝到用户区域的媒介,提高效率。select和channel不直接接触,而是通过buff来接触。
channel
channel,很抽象的概念,一个传输过程理解。把他类比为进程通信过程使用的管道会好理解点。作用是一个媒介吧,现代设备都很多管线,其实很多设计都来源生活。你去出行,火车站都会有登陆口,每个人通过车票通过登陆口引导,可以直接到达指定的站台。channel作用类似于这个,但channel是双工,每一端可进可出,和tcp类似。
4种类型
FileChannel: 从文件中读写数据;
DatagramChannel: 通过 UDP 读写网络中数据;
SocketChannel: 通过 TCP 读写网络中数据;
ServerSocketChannel: 可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。
buff
发送给一个通道的所有数据都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要先读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区。
缓冲区实质上是一个数组,但它不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。
缓冲区包括以下类型:
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffe
缓冲区变量
因为读写每个channel都是相同的缓冲区。所以读写相当于在一个数组在操作。靠着2个变量limit和position操作和一个函数flip操作。通过通过clear进行置位操作。
selector
选择器,核心部分,作用:轮询channel。对感兴趣的事件进行注册,使用一个单独的channel也就是ServerSocketChannel来接受连接时间,然后根据这个再去处理其他的时间。
public class NIOServer {
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
ServerSocket serverSocket = ssChannel.socket();
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
serverSocket.bind(address);
while (true) {
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();
// 服务器会为每个新连接创建一个 SocketChannel
SocketChannel sChannel = ssChannel1.accept();
sChannel.configureBlocking(false);
// 这个新连接主要用于从客户端读取数据
sChannel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
SocketChannel sChannel = (SocketChannel) key.channel();
System.out.println(readDataFromSocketChannel(sChannel));
sChannel.close();
}
keyIterator.remove();
}
}
}
private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024);
StringBuilder data = new StringBuilder();
while (true) {
buffer.clear();
int n = sChannel.read(buffer);
if (n == -1) {
break;
}
buffer.flip();
int limit = buffer.limit();
char[] dst = new char[limit];
for (int i = 0; i < limit; i++) {
dst[i] = (char) buffer.get(i);
}
data.append(dst);
buffer.clear();
}
return data.toString();
}
}
整体代码如上。其中主要channel是serverSocketchannel,可以用来接受新的连接。然后对于新连接,在创建一个新的channel,在对该channel进行绑定感兴趣事件。而select具体对于channel绑定,需要根据底层算法决定(select,poll,epoll)也就是数组还是红黑树还是链表。
IO过程
IO过程是区分了各个不同IO方式。IO过程宏观上分为
- 数据等待过程
- 内核区域复制数据到用户区
过程1每次查询数据是否准备好,都会发起系统调用
(操作系统高消耗操作),过程1进行阻塞等待结果就是阻塞io,过程1进行轮询查询结果就是非阻塞io。对于io方式的选择并不唯一,都有各自的好处,主要部分是cpu与线程切换消耗的平衡。要根据实际任务场景决定方式,从阻塞时间出发,短期阻塞使用非阻塞方式较好,长期使用非阻塞会造成cpu消耗多。
阻塞io模型
非阻塞io模型
selector.select();
而采用IO多路复用,其实本质和阻塞io很像,但区别是多路复用是一次性查询多个IO事件,使用的是同一个线程,一旦有数据准备好了,就会通知返回进行处理。下面简化了这个过程。
Set<SelectionKey> keys = selector.selectedKeys();
然后获得完成的文件描述符,然后得到对应的channel,这个时候数据拷贝到内核区域,然后在使用handler过程处理。
NIO的accpet
public SocketChannel accept() throws IOException {
synchronized(this.lock) {
if (!this.isOpen()) {
throw new ClosedChannelException();
} else if (!this.isBound()) {
throw new NotYetBoundException();
} else {
SocketChannelImpl var2 = null;
int var3 = 0;
FileDescriptor var4 = new FileDescriptor();
InetSocketAddress[] var5 = new InetSocketAddress[1];
InetSocketAddress var6;
try {
this.begin();
if (!this.isOpen()) {
var6 = null;
return var6;
}
this.thread = NativeThread.current();
do {
var3 = this.accept(this.fd, var4, var5);
} while(var3 == -3 && this.isOpen());
} finally {
this.thread = 0L;
this.end(var3 > 0);
assert IOStatus.check(var3);
}
if (var3 < 1) {
return null;
} else {
IOUtil.configureBlocking(var4, true);
var6 = var5[0];
var2 = new SocketChannelImpl(this.provider(), var4, var6);
SecurityManager var7 = System.getSecurityManager();
if (var7 != null) {
try {
var7.checkAccept(var6.getAddress().getHostAddress(), var6.getPort());
} catch (SecurityException var13) {
var2.close();
throw var13;
}
}
return var2;
}
}
}
}
应该是从fd读入,然后又有一个文件描述,然后他设置得InetSocketAddress是一个大小,所以我猜测他是一次只能督导一个。然后通过v6创建v2然后返回。再往里面就是native方法,还不知道怎么看native方法.总的来说accept只能接受一个调用accept.然后accept之后,会将SocketChannelimpl返回,然后就是新的channel了.这个channel就是新channel.
handler
相当于IO完成之后的处理,handler在NIO中是个很弱的概念吧,我看了许多文章都写的很潦草,应该出于NIO本身没对handler这个概念封装,所以NIO直接使用上往往都没逻辑解耦,而在Netty中handler会好使用很多,使用封装后对这个概念进行强化。
来说accept只能接受一个调用accept.然后accept之后,会将SocketChannelimpl返回,然后就是新的channel了.这个channel就是新channel.
handler
相当于IO完成之后的处理,handler在NIO中是个很弱的概念吧,我看了许多文章都写的很潦草,应该出于NIO本身没对handler这个概念封装,所以NIO直接使用上往往都没逻辑解耦,而在Netty中handler会好使用很多,使用封装后对这个概念进行强化。