本篇博客主要讲述BIO、NIO的网络模型以及零拷贝
public class BIOServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket=new ServerSocket(8088);
while(true){
//阻塞,放弃CPU资源
Socket clientSocket = serverSocket.accept();
System.out.println("连接成功IP:"+clientSocket.getInetAddress());
byte[] bs=new byte[1024];
InputStream is = clientSocket.getInputStream();
//阻塞,放弃CPU资源
is.read(bs);
System.out.println(new String(bs));
}
}
}
client端:
public class BIOClient {
public static void main(String[] args) throws IOException {
Socket socket=new Socket("127.0.0.1",8088);
OutputStream outputStream = socket.getOutputStream();
Scanner scanner=new Scanner(System.in);
String str = scanner.next();
outputStream.write(str.getBytes());
outputStream.close();
}
}
以上是BIO的网络模型,存在的问题:
serverSocket.accept()
方法发生阻塞。is.read(bs)
方法发生阻塞。测试结果:
2.BIO网络模型代码(多线程模型)
为了解决连接操作、流读取操作都在主线程中阻塞导致的服务端同时只能处理一个客户端连接进行优化修改,优化思路:让accept()连接在主线程中阻塞,每当一个客户端连接成功,则启动一个子线程让每个连接的read()操作在子线程中阻塞。
服务器端代码:
public class BIOServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket=new ServerSocket(8088);
while(true){
//阻塞,放弃CPU资源
Socket clientSocket = serverSocket.accept();
System.out.println("连接成功IP:"+clientSocket.getInetAddress());
//创建子线程处理新来的客户端连接
new Thread(new Runnable() {
@Override
public void run() {
byte[] bs=new byte[1024];
InputStream is = null;
try {
is = clientSocket.getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
try {
//阻塞,放弃CPU资源
is.read(bs);
} catch (IOException e) {
e.printStackTrace();
}
System.out.println(new String(bs));
}
}).start();
}
}
}
测试结果:
以上结果是使用多线程处理客户端请求,成功的解决了服务器端同时只能处理一个客户端的连接,分析以上模型存在的问题:
为了解决以上所有的问题,在JDK1.4版本中诞生了NIO。
NIO又可以成为Non-blocking IO
1.NIO的网络模型代码
Server端代码(客户端还可以用之前的):
public static void main(String[] args) throws IOException {
ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
//把服务器端通道设置为非阻塞,这样在accept()操作就不会进行阻塞了
serverSocketChannel.configureBlocking(false);
while(true){
for(SocketChannel client:socketList){
if((readSign=client.read(byteBuffer))>0){
//切换到写模式
byteBuffer.flip();
byte[] bs=new byte[readSign];
byteBuffer.get(bs);
System.out.println(new String(bs));
byteBuffer.clear();
}else if(readSign<0){
//如果读到的长度为-1证明断开连接了
System.out.println(client.getRemoteAddress()+"断开连接");
socketList.remove(client);
}
}
if((acceptSign=serverSocketChannel.accept())!=null){
//如果接收到客户端连接请求,则把客户端连接通道设置为非阻塞,
// 这样在read()操作时就不会阻塞
acceptSign.configureBlocking(false);
System.out.println("连接成功IP:"+acceptSign.getRemoteAddress());
socketList.add(acceptSign);
}
}
}
}
服务器端把ServerSocketChannel
通道设置为非阻塞模式,在进行accept()接收客户端连接的时候就不会进行阻塞了,再把接收到的客户端连接的SocketChannel
设置为非阻塞并放到一个队列中保存,在进行read()读取客户端消息的时候就不会进行阻塞了。再不断循环该队列判断是否有客户端发送消息,若读到的消息长度为-1则说明该客户端断开连接了,从队列中删除。这样就可以实现服务端一个线程同时处理多个客户端连接。
结果如下:
以上服务端模型虽然解决了使用单线程来同时处理所有的客户端连接,但是仔细分析会发现其中存在一个问题:不断循环已经连接的客户端列表,挨个查看是否有读请求,假如List
中存在1W个客户端连接,每次循环只有一个客户端发了消息,会有9999个无用循环,针对这个问题NIO还提供了一个核心组件Selector
选择器,使用如下:
public class NIOServer2 {
static ByteBuffer bs=ByteBuffer.allocate(1024);
static int len=-1;
public static void main(String[] args) throws IOException {
ServerSocketChannel server=ServerSocketChannel.open();
server.bind(new InetSocketAddress(8080));
//设置为非阻塞模式
server.configureBlocking(false);
//创建一个选择器
Selector selector=Selector.open();
//将接收客户端连接的通道注册到选择其中
server.register(selector, SelectionKey.OP_ACCEPT);
//轮询访问监听器上是否有已经就绪时间,该操作会阻塞
while(selector.select()>0){
//给所有就绪时间创建一个迭代器,迭代这些事件
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while(iterator.hasNext()){
SelectionKey next = iterator.next();
//判断就绪事件,是什么事件
if (next.isAcceptable()){
//如果是连接就绪,获取连接的客户端
SocketChannel socketChannel = server.accept();
//将获取到连接的客户端设置为非阻塞状态
//并且注册到选择器中,监听该连接的读请求
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("连接成功IP:"+socketChannel.getRemoteAddress());
}else if(next.isReadable()){
//如果是读就绪,就处理该客户端的消息
SocketChannel socketChannel = (SocketChannel) next.channel();
if ((len = socketChannel.read(bs))>0){
//把缓冲区设置为读模式
bs.flip();
byte[] b=new byte[len];
bs.get(b);
System.out.println("接收到IP:"+socketChannel.getRemoteAddress()+"发送的:"+new String(b));
bs.clear();
}else{
//如果收到的消息长度为-1,则客户端断开连接,
//服务器端需要关闭该通道,否则该通道会一直发送-1进入,一直处理该事件
socketChannel.close();
}
}
//删除处理完的事件
iterator.remove();
}
}
}
}
使用Selector
选择器,把所有的Channel
以及对这些Channel
感兴趣的事件注册到选择器中,Selector.select()
是一个阻塞方法,用该方法判断注册的通道中是否有感兴趣的事件需要处理,,这就称作I/O多路复用。
注意:根据测试当客户端断开连接后,服务器端对应的SocketChannel
会不断发送读就绪事件,而且读到的长度为-1,所以当客户端断开连接后服务器端需要把对应该客户端的Channel
进行关闭。关闭后也就从Selector
中注销了。
结果:
以上就是NIO的网络模型,不过没有使用到零拷贝
对于Java来说万物皆对象,对于Linux来说万物皆文件,使用一个叫做file descriptor
后面简称fd,比如下面几种文件文件描述符:
Linux系统对自身进行了划分,一部分核心软件独立于普通应用程序,运行在较高的特权级别上,它们驻留在被保护的内存空间上,拥有访问硬件设备的所有权限,Linux将此称为内核空间。
相对地,应用程序则是在“用户空间”中运行。运行在用户空间的应用程序只能看到允许它们使用的部分系统资源,并且不能使用某些特定的系统功能,也不能直接访问内核空间和硬件设备,以及其他一些具体的使用限制。
将用户空间和内核空间置于这种非对称访问机制下有很好的安全性,能有效抵御恶意用户的窥探,也能防止质量低劣的用户程序的侵害,从而使系统运行得更稳定可靠。 --百度百科
可以简单理解为:
Selector.select()
方法就会阻塞,发生系统调用Kernel会去查看所有注册到Selector
选择器中的并感兴趣的事件,当有事件发生处理该事件。Linux云服务器端代码:
通过使用strace -ff -o ./log/NIO java BIOServer
命令来跟踪这段代码的系统调用,将会阻塞,如图:
/proc/1164/
目录下的fd
目录中维基上是这么描述零拷贝的:零拷贝描述的是CPU不执行拷贝数据从一个存储区域到另一个存储区域的任务,这通常用于通过网络传输一个文件时以减少CPU周期和内存带宽。
零拷贝的优点:
一次读写请求进行4次用户空间和内核空间上下文的切换,以及2次DMA拷贝和2次CPU拷贝,很容易发现这两次CPU拷贝不是必要的。
可以看到一次sendFile
系统调用需要2次用户空间和内核空间状态转换,2次DMA拷贝和1次CPU拷贝,这样做有一个限制==因为数据没有拷贝到用户空间,所以用户无法对数据进行修改,==可以看一下Linux内核提供的sendFile
提供的方法:
从Linux 2.4版本开始,操作系统底层提供了带有scatter/gather的DMA来从内核空间缓冲区中将数据读取到协议引擎中。这样一来待传输的数据可以分散在存储的不同位置上,而不需要在连续存储中存放。那么从文件中读出的数据就根本不需要被拷贝到socket缓冲区中去,只是需要将缓冲区描述符添加到socket缓冲区中去,DMA收集操作会根据缓冲区描述符中的信息将内核空间中的数据直接拷贝到协议引擎中。
总的来说,带有DMA收集拷贝功能的sendfile实现的I/O只使用了2次用户空间与内核空间的上下文切换,以及2次数据的拷贝,而且这2次的数据拷贝都是非CPU拷贝。这样一来我们就实现了最理想的零拷贝I/O传输了,不需要任何一次的CPU拷贝,以及最少的上下文切换。
mmap(内存映射)是一个比sendfile昂贵但优于传统I/O的方法。
总的来说,通过mmap实现的零拷贝I/O进行了4次用户空间与内核空间的上下文切换,以及3次数据拷贝。其中3次数据拷贝中包括了2次DMA拷贝和1次CPU拷贝。
零拷贝参考地址:https://www.jianshu.com/p/e76e3580e356