JAVA IO
IO 模型
同步和异步
同步异步 是指上图第一步 " 等待数据准备就绪 " 时的状态是否是同步的。
阻塞非阻塞
阻塞非阻塞 是指上图第二步 " 从内核空间拷贝到用户空间 " 如果是操作系统完成后通知用户线程则是非阻塞如果是用户线程等待操作系统完成就是阻塞。
NIO 同步非阻塞
0拷贝
0拷贝是指cpu不参与内存复制,0拷贝可以减少减少上下文切换,避免cpu数据拷贝带来的负载。可以通过DMA,mmap+write,sendfile,sendfile+DMA gather copy 实现
传统BIO
-
应用程序发起读数据操作,JVM会发起read()系统调用。
-
这时操作系统OS会进行一次上下文切换把用户空间切换到内核空间(第一次上线文切换)。
-
通过磁盘控制器把数据copy到内核缓冲区中,这里的就发生了一次DMA Copy(第一次拷贝)。
-
然后内核将数据copy到用户空间的应用缓冲区中,发生了一次CPU Copy(第二次拷贝)。
-
read调用返回后,会再进行一次上下文切换把内核空间切换到用户空间(第二次上线文切换)。
-
JVM 进程内发起 write() 系统调用
-
操作系统由用户态空间切换到内核态空间(第三次上下文切换),将用户态空间的缓存区数据原封不动的拷贝到内核态空间输出的 socket 缓存区中(第三次拷贝)
-
write() 系统调用返回,操作系统由内核态空间切换到用户态空间(第四次上下文切换),通过 DMA 引擎将数据从内核态空间的 socket 缓存区数据拷贝到协议引擎中(第四次拷贝)
可以看出传统bio 发了4次上下文切换,和4次数据copy,一次是DMA Copy( 内核从磁盘上面读取数据 是 不消耗CPU时间的,是通过磁盘控制器完成称之为DMA Copy),一次是CPU Copy。
Nio Zero Copy
从图中可以看出nio 少了从内核将数据copy到用户空间的应用缓冲区中的这一步 所以称之为0拷贝。 Linux中的sendfile()以及Java NIO中的FileChannel.transferTo()方法都实现了零拷贝的功能。
mmap 模式
mmap 通过内存映射, 将文件映射到内核缓冲区, 同时, 用户空间可以共享内核空间的数据。
- 程序启动,调用mmap,创建好虚拟映射区域,并且文件物理地址和进程虚拟地址的一一映射关系。
- 用户程序读取文件数据时,通过页表查询,发现物理内存上没有该数据,那么就
- 要系统调用,从用户态切换到内核态(第一次上下文切换)
- 通过DMA把磁盘中的数据读取到页缓存中(内核缓存区),然后切换到用户态(第二次上下文切换),因为用户空间已经有了虚拟映射地址,所以他是可以找到缓存在页缓存中的数据的,也就不需要再拷贝到用户空间去了。
- 接下来用户程序就要调用write方法,把数据写入网卡中。用户态再次切换到内核态(第三次上下文切换),然后使用CPU拷贝,把内核空间中的数据拷贝到Socket缓冲区。
- 再利用DMA技术把数据拷贝到网卡进行数据传输。
- 最后再切换回用户态(第四次上下文切换)
这样, 在进行网络传输时, 就可以减少内核空间到用户空间的拷贝次数。
总结: 3次拷贝, 3次状态切换, 不是真正意义上的零拷贝。
sendfile DMA gather copy
- 用户进程通过 sendfile() 函数向内核(kernel)发起系统调用上下文从用户态(user space)切换为内核态(kernel space)。
- DMA copy将磁盘数据copy到内核中
- 向socket buffer中追加当前要发送的数据在kernel buffer中的位置和偏移量
- DMA gather copy根据socket buffer中的位置和偏移量直接将kernel buffer中的数据copy到网卡
- 上下文从内核态(kernel space)切换回用户态(user space),sendfile 系统调用执行返回。
只有两次dma 拷贝和两次上下文切换没有cpu拷贝
Java NIO中的transferTo()
Java NIO中
FileChannel.transferTo(long position, long count,WriteableByteChannel target)
方法将当前通道中的数据传送到目标通道target中,在支持Zero-Copy的linux系统中,transferTo()的实现依赖于 sendfile()调用。
NIO JAVA demo
服务端
package io.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
public class Server implements Runnable{
//1 多路复用器(管理所有的通道)
private Selector seletor;
//2 建立缓冲区
private ByteBuffer readBuf = ByteBuffer.allocate(1024);
//3
private ByteBuffer writeBuf = ByteBuffer.allocate(1024);
public Server(int port){
try {
//1 打开路复用器
this.seletor = Selector.open();
//2 打开服务器通道
ServerSocketChannel ssc = ServerSocketChannel.open();
//3 设置服务器通道为非阻塞模式
ssc.configureBlocking(false);
//4 绑定地址
ssc.bind(new InetSocketAddress(port));
//5 把服务器通道注册到多路复用器上,并且监听阻塞事件
ssc.register(this.seletor, SelectionKey.OP_ACCEPT);
System.out.println("Server start, port :" + port);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while(true){
try {
//1 必须要让多路复用器开始监听
this.seletor.select();
//2 返回多路复用器已经选择的结果集
Iterator<SelectionKey> keys = this.seletor.selectedKeys().iterator();
//3 进行遍历
while(keys.hasNext()){
//4 获取一个选择的元素
SelectionKey key = keys.next();
//5 直接从容器中移除就可以了
keys.remove();
//6 如果是有效的
if(key.isValid()){
//7 如果为阻塞状态
if(key.isAcceptable()){
this.accept(key);
}
//8 如果为可读状态
if(key.isReadable()){
this.read(key);
}
//9 写数据
if(key.isWritable()){
//this.write(key); //ssc
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void write(SelectionKey key){
//ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//ssc.register(this.seletor, SelectionKey.OP_WRITE);
}
private void read(SelectionKey key) {
try {
//1 清空缓冲区旧的数据
this.readBuf.clear();
//2 获取之前注册的socket通道对象
SocketChannel sc = (SocketChannel) key.channel();
//3 读取数据
int count = sc.read(this.readBuf);
//4 如果没有数据
if(count == -1){
key.channel().close();
key.cancel();
return;
}
//5 有数据则进行读取 读取之前需要进行复位方法(把position 和limit进行复位)
this.readBuf.flip();
//6 根据缓冲区的数据长度创建相应大小的byte数组,接收缓冲区的数据
byte[] bytes = new byte[this.readBuf.remaining()];
//7 接收缓冲区数据
this.readBuf.get(bytes);
//8 打印结果
String body = new String(bytes).trim();
System.out.println("Server : " + body);
// 9..可以写回给客户端数据
sc.write(ByteBuffer.wrap("abcd".getBytes()));
} catch (IOException e) {
}
}
private void accept(SelectionKey key) {
try {
//1 获取服务通道
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//2 执行阻塞方法
SocketChannel sc = ssc.accept();
//3 设置阻塞模式
sc.configureBlocking(false);
//4 注册到多路复用器上,并设置读取标识
sc.register(this.seletor, SelectionKey.OP_READ);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new Thread(new Server(8765)).start();;
}
}
客户端
package io.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Scanner;
public class NClient {
//定义检测Sockethannel的Selector对象
private Selector selector=null;
static final int PORT=8765;
//定义处理编码的字符集
private Charset charset=Charset.forName("GBK");
//客户端SocketChannel
private SocketChannel sc=null;
public void init() throws IOException{
selector=Selector.open();
InetSocketAddress isa=new InetSocketAddress("127.0.0.1", PORT);
//调用open的静态方法创建连接指定的主机的SocketChannel
sc=SocketChannel.open(isa);
//设置该sc已非阻塞的方式工作
sc.configureBlocking(false);
//将SocketChannel对象注册到指定的Selector
sc.register(selector, SelectionKey.OP_READ);
//启动读取服务器数据端的线程
new ClientThread().start();
//创建键盘输入流
Scanner scan=new Scanner(System.in);
while(scan.hasNextLine()){
//读取键盘的输入
String line=scan.nextLine();
//将键盘的内容输出到SocketChanenel中
sc.write(charset.encode(line));
}
}
//定义读取服务器端的数据的线程
private class ClientThread extends Thread{
@Override
public void run() {
try{
while(selector.select()>0){
//遍历每个有可能的IO操作的Channel对银行的SelectionKey
for(SelectionKey sk:selector.selectedKeys()){
//删除正在处理的SelectionKey
selector.selectedKeys().remove(sk);
//如果该SelectionKey对应的Channel中有可读的数据
if(sk.isReadable()){
//使用NIO读取Channel中的数据
SocketChannel sc=(SocketChannel)sk.channel();
String content="";
ByteBuffer bff=ByteBuffer.allocate(1024);
while(sc.read(bff)>0){
sc.read(bff);
bff.flip();
content+=charset.decode(bff);
}
//打印读取的内容
System.out.println("聊天信息:"+content);
sk.interestOps(SelectionKey.OP_READ);
}
}
}
}catch(IOException io){
io.printStackTrace();
}
}
}
public static void main(String [] args) throws IOException{
new NClient().init();
}
}
AIO demo
服务端
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousChannelGroup;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Service {
public static void main(String[] args) throws Exception {
// 创建线程池
ExecutorService executor = Executors.newCachedThreadPool();
// 创建通道组(多个通道可以共用通道组)
AsynchronousChannelGroup asynchronousChannelGroup =
AsynchronousChannelGroup.withCachedThreadPool(executor, 1);
// 服务器通道 并把服务器通道注册到通道组
AsynchronousServerSocketChannel asynchronousChannel = AsynchronousServerSocketChannel.open(asynchronousChannelGroup);
// 绑定端口号
asynchronousChannel.bind(new InetSocketAddress(1234));
asynchronousChannel.accept(asynchronousChannel,new CompletionHandler<AsynchronousSocketChannel, AsynchronousServerSocketChannel>() {
@Override
public void completed(AsynchronousSocketChannel result, AsynchronousServerSocketChannel attachment) {
// 需要继续接收下一个连接
attachment.accept(attachment,this);
ByteBuffer buf = ByteBuffer.allocate(1024);
result.read(buf, buf, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer resultSize, ByteBuffer attachment) {
//进行读取之后,重置标识位
attachment.flip();
//获得读取的字节数
System.out.println("Server -> " + "收到客户端的数据长度为:" + resultSize);
//获取读取的数据
String resultData = new String(attachment.array()).trim();
System.out.println("Server -> " + "收到客户端的数据信息为:" + resultData);
// 可以主动关闭关闭客户端连接
// result.close();
String response = "服务器响应客户端:你好客户端 ";
try {
Thread.sleep(1000l);
ByteBuffer buf2 = ByteBuffer.allocate(1024);
buf2.put(response.getBytes());
buf2.flip();
result.write(buf2).get();
}catch (Exception e){
}
buf.clear();
// 多次对话的时候需要再次调用result.
result.read(buf, buf, this);
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
}
});
}
@Override
public void failed(Throwable exc, AsynchronousServerSocketChannel attachment) {
exc.printStackTrace();
}
});
// 因为处理是异步所以需要sleep
Thread.sleep(Integer.MAX_VALUE);
}
}
客户端
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.util.concurrent.ExecutionException;
public class Client implements Runnable{
private AsynchronousSocketChannel asc ;
public Client() throws Exception {
asc = AsynchronousSocketChannel.open();
}
public void connect(){
asc.connect(new InetSocketAddress("127.0.0.1", 1234));
}
public void write(String request){
try {
asc.write(ByteBuffer.wrap(request.getBytes())).get();
read();
} catch (Exception e) {
e.printStackTrace();
}
}
private void read() {
ByteBuffer buf = ByteBuffer.allocate(1024);
try {
asc.read(buf).get();
buf.flip();
byte[] respByte = new byte[buf.remaining()];
buf.get(respByte);
System.out.println(new String(respByte,"utf-8").trim());
write("服务端你好:"+System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while(true){
}
}
public static void main(String[] args) throws Exception {
Client c1 = new Client();
c1.connect();
new Thread(c1, "c1").start();
c1.write("服务端你好:"+System.currentTimeMillis());
Thread.sleep(Integer.MAX_VALUE);
}
}