概述
前面几篇文章我们对Java的IO体系做了一个大致的介绍,从本文开始我们将对NIO、SELECTOR、EPOLL、Netty等携带例子做更深入的讲解。
如需持续了解请关注后随时查看。
解读:
非阻塞IO模型:当用户线程发起一个read操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦内核中的数据准备好了,并且又再次受到的用户线程的骑牛,那么它马上就将数据拷贝到了用户线程,然后返回。所以事实上,在非阻塞IO模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用CPU。典型的非阻塞IO模型一般如下:
While(true){
Data = socket.read();
If(data != error){
处理数据;
Break;
}
}
在非阻塞IO模型中有一个非常严重的问题,在while循环中需要不断的去询问内核数据是否准备就绪,这样会导致CPU占用率非常高,因此一般情况下很少使用while循环这种方式来读取数据。
全图(下面有分解图)
图解:
释义:
1.NIO模型解决了BIO模型的以下几个问题->
①.如果有一万个客户端连接服务端,那么服务端就需要创建一万个线程去处理请求。创建线程需要系统调用是很消耗资源的。消耗内存资源,而且cpu调度过程中线程的上下文切换也需要时间。以上问题的根本原因就是当客户端连接进入服务端后,服务端是阻塞状态的,干不了其他事情。
②.如果要解决以上问题则需要解决socket的阻塞问题,这样就形成了IO进化过程之NIO。
2.NIO有两层的理解,在JAVA(应用软件)使用是NEWIO(新IO体系)的意思,在系统调用层面是NonBlocking(非阻塞)的意思。
3.NIO解决了BIO模型的阻塞状态问题,但是为了技术的发展,我们需要找到当前NIO模型存在的几个问题->
①.c10K(一万个客户端)问题,高并发问题。如果有一万个客户端连入了当前ServerSocket服务端,由于所有客户端的数据处理都是在当前主线程中进行的,虽然现在新客户端的进入不再阻塞,但是当前线程每进行一次recvfrom操作都需要对一万个客户端进行循环问询,客户端是否有数据传入。有多少个客户端就需要循环进行多少次问询。每循环内会有O(N)复杂度的系统调用,但是在O(N)复杂度内可能只有两个客户端发来了数据。这样在问询过程当中会有9998次的问询时没有效果的,是无效调用。长时间下来就进行了无数次的无效的有系统调用成本的的系统调用轮询。
②.为了解决以上无效系统调用的问题,我们引入了
选择器(SELECT)
的概念。
也就是说有N个客户端连入了ServerSocket服务端,其中只有M个客户端发送了数据,那么我们就可以紧对M个客户端轮询即可。这样系统调用的时间复杂度就由O(N)降低到了O(M)。
M永远都是小于等于N的,只对有数据传入的客户端进行accept操作。
③.如果把每个客户端连接当成一条路,那么服务端的应用层每次循环都需要去1万条路看一次。在SELECT(多路复用器)中,把应用层的每次循环调用放到了内核层,让系统内核去帮忙循环1万条路,那条路有数据进入。这样就减少了应用层1万次调用系统层的消耗。因为调用系统层是非常消耗数据的。
现阶段已知的操作系统中的多路复用器有:
select(大部分系统都有)、
poll(大部分系统都有)、
epoll(linux系统)、
kqueue(unix系统)
代码示例
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;
import java.util.List;
/**
* Created by Bruce on 2020/9/16
* * 网络IO之NIO-服务端
* * NIO-写法
* ServerSocketChannel
**/
public class SocketServerNIO {
public static void main(String[] args) throws IOException, InterruptedException {
用于存放所有客户端连接
List<SocketChannel> channels = new LinkedList<SocketChannel>();
//JDK中区别于 Socket 的新IO体系。
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();//打开一个ServerSocket通道
/**
* //绑定端口
*/
serverSocketChannel.bind(new InetSocketAddress(8080));
System.out.println("step1 : new ServerSocket(8080) ");
/**
* 设置为非阻塞IO模型
*/
serverSocketChannel.configureBlocking(false);//设置为非阻塞IO模型
while (true){
Thread.currentThread().sleep(1000);//每次循环停顿一秒
/**
* //获取客户端连接请求------非阻塞状态
* 设置客户端为非阻塞状态
*/
SocketChannel socketChannel = serverSocketChannel.accept();
if(socketChannel == null){//没有客户端连接打印空信息
System.out.println("null client...");
}else{//有客户端连接
socketChannel.configureBlocking(false);//设置客户端连接为非阻塞状态
int clientPort = socketChannel.socket().getPort();//获取客户端的随机端口号
System.out.println("acceptSocketClient:" + clientPort);//打印客户端随机端口号
/**
* //把新进入的客户端放入队列中
* 这一步的目的是为了让客户端获取和从客户端获取数据区分开操作。
*/
channels.add(socketChannel);
}
/**
* //开启一个4096位的公用缓冲区,4KB
* 在实际使用的过程中一般一个SocketClannel通道对应一个缓冲区
* 且缓冲区大小为动态设置,因为4KB缓冲区存在不够用情况
* *****可以在堆里面----可以在堆外面
*
* 1.建议每个通道对应一个ByteBuffer进行数据处理;
* 2.数据是按照流的形式进入的通道,每个ByteBuffer中存在一组指针。指向用户数据流在通道中的位置;
* 3.当数据从客户端写入到ByteBuffer后,会有一个flip指针动态移动,始终指向数据结尾。
* 4.当处理客户端数据线程线程需要从ByteBuffer读取数据时,需要用一个limit指针指向flip指针当前的位置(数据结尾处),
* 同时反转flip指针至ByteBuffer数据开头(数据开始出)。limit指针和flip指针之间的距离就是本次数据传输的数据量大小。
* 5.ByteBuffer是一个缓冲区,可以减少碎片,重复利用也可以减少GC频繁分配的问题。
*/
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(4096);
/**
* 在这个阶段,循环每个连接进入的客户端,
* 判断每个客户端是否有数据传入
* 这个步骤是严格的串行化处理的。
*/
for(SocketChannel channel : channels){
/**
* //缓冲区中的数据个数,该个数存在几种情况
* <0 (-1)客户端错误,关闭了连接或者超时等
* =0 客户端无输入写入,跳出不做处理
* >0 客户端有数据写入
*/
int readNum = channel.read(byteBuffer);//把客户端传入的数据读取到缓冲区----不再阻塞
if(readNum < 0){
channel.close();
channels.remove(channel);
}else if(readNum == 0){
continue;
}else {
byteBuffer.flip();//缓冲区中指针反转,准备读取数据
byte[] bytes = new byte[readNum];//创建一个缓冲区中数据大小的字节数组
byteBuffer.get(bytes);//把缓冲区中的数据写入字节数组。
String str = new String(bytes);//字节数组中的数据转成可用于操作的String类型
System.out.println("acceptSocketClientData:" + channel.socket().getPort() + "---" + str);
/**
* //由于是众多的client公用一个缓冲区,
* 因此在每次循环完一个客户端数据后要把缓冲区清空
*/
byteBuffer.clear();
}
}
}
}
}
打印输出
在linux环境或者windows环境下使用nc命令链接服务端,查看服务端打印过程。
具体linux系统或者windows系统如何安装nc命令,请从网络搜索或查看目录文档 ‘网络IO涉及到的-linux指令.docx’。
五、Centos-Linux安装nc
六、windows环境下netcat的安装及使用
1.nc客户端2打印(Windows-nc命令打印)
C:\Users\Administrator>nc 127.0.0.1 8080
123456
789456
2.nc客户端1打印(Windows-nc命令打印)
C:\Users\Administrator>nc 127.0.0.1 8080
123456789456
ssdfsfsdf
3.服务端打印
step1 : new ServerSocket(8080) //服务端启动
null client...
null client...
acceptSocketClient:46769 //客户端1链接
null client...
acceptSocketClient:46771 //客户端2链接
null client...
null client...
null client...
acceptSocketClientData:46771---123456 客户端2发送了数据
null client...
null client...
null client...
acceptSocketClientData:46771---789456客户端2发送了数据
null client...
null client...
null client...
null client...
acceptSocketClientData:46769---123456789456 客户端1发送了数据
null client...
null client...
null client...
null client...
acceptSocketClientData:46769---ssdfsfsdf 客户端1发送了数据
null client...
注意:
在以上打印输出过程中,不管是否有客户端连接进入,线程都一直处于运行状态,而不是阻塞状态。
当有客户端链接后打印链接信息,继续循环运行。
当有客户端发送数据后,打印数据信息,继续循环运行。
往期JavaIO文章:
一、C10K问题经典问答
二、java.nio.ByteBuffer用法小结
三、Channel 通道
四、Selector选择器
五、Centos-Linux安装nc
六、windows环境下netcat的安装及使用
七、IDEA的maven项目的netty包的导入(其他jar同)
八、JAVA IO/NIO
九、网络IO原理-创建ServerSocket的过程
十、网络IO原理-彻底弄懂IO
十一、JAVA中ServerSocket调用Linux系统内核
十二、IO进化过程之BIO