本文将通过JavaNIO实现文件的局域网发送功能。
目的:
1.为了体验零拷贝
2.为了体验NIO的Channel,Buffer,Selector
3.为了体验NIO方式网络传输文件和传统网络方式传输文件的差异(性能差异)。
什么是零拷贝:
那先来说说传统的文件传输:在本地读取文件(FileInputStream)通过网络传输(socket.getInputStream,socket.getOutputstream)发送到另一台设备进行存储(FileOutPutStream)过程,以及在操作这些流所用到的缓存(自定义byte数组或者各种BufferdInputStream/BufferdOutputStream)中,这些操作都在我们浅显来看,是这样的:磁盘——网络——磁盘的过程;稍微深一层次的,应该是这样的:磁盘—内存—网络—内存—磁盘的过程;再深一层次的,应该是这样:磁盘—操作系统内存(内核态-从磁盘读取过来)—应用程序内存(用户态)—操作系统内存(内核态-网络准备发送)—网络中传输—操作系统内存(内核态-网络接收)—应用程序内存(用户态)—操作系统内存(内核态-准备存磁盘)—磁盘。
重点来了,零拷贝(需要操作系统支持)就是用来减少甚至杜绝操作系统内存(内核态)到应用程序内存(用户态)的拷贝过程的。让cpu不要浪费在这种内存间拷贝的操作上,而是用在其他高效的计算上,零拷贝通过内存地址映射的方式,让网络/磁盘直接到系统内存中(内核态)中读取缓存的数据进行网络传输/本地存储,而不用再让数据到应用程序内存中来”转一圈“。
java中NIO中的的FileChannel类的transferTo、transferFrom就能够实现这个操作,直接操作内核态,不经过用户态。
接下来就是NIO了:
1.Channel:
我一直把Channel看成是双向流,这一点在其实现类的API中可以很明显的看出来,在使用过程中,通过对API的调用,更能感同身受。
最常用的实现类:
1.FileChannel,常用API如下(先列在这,后期再来逐一梳理):
1 public abstract class FileChannel extends AbstractInterruptibleChannel 2 implements SeekableByteChannel, GatheringByteChannel, ScatteringByteChannel 3 { 4 //打开或者创建(无则创建)一个文件,成功后返回该文件的Channel对象 5 public static FileChannel open(Path path, OpenOption... options) 6 7 public static FileChannel open(Path path,Set<? extends OpenOption> options, FileAttribute<?>... attrs) 8 //从文件读 9 public abstract int read(ByteBuffer dst) throws IOException; 10 public abstract long read(ByteBuffer[] dsts, int offset, int length)throws IOException; 11 public final long read(ByteBuffer[] dsts) throws IOException 12 //写文件 13 public abstract int write(ByteBuffer src) throws IOException; 14 public abstract long write(ByteBuffer[] srcs, int offset, int length) 15 public final long write(ByteBuffer[] srcs) throws IOException 16 //截取指定大小的文件 17 public abstract FileChannel truncate(long size) throws IOException; 18 //强制将所有对此通道的文件更新写入包含该文件的存储设备中 19 public abstract void force(boolean metaData) throws IOException; 20 //将字节从此通道的文件传输到给定的可写入字节通道(支持零拷贝) 21 public abstract long transferTo(long position, long count,WritableByteChannel target) throws IOException 22 //将字节从给定的可读取字节通道传输到此通道的文件中(支持零拷贝) 23 public abstract long transferFrom(ReadableByteChannel src, long position,long count) throws IOException 24 //将指定的文件映射到内存中 25 public abstract MappedByteBuffer map(MapMode mode, long position, long size) throws IOException;
2.SocketChannel
2.1 SocketChannel,常用API如下:
1 public abstract class SocketChannel 2 extends AbstractSelectableChannel 3 implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel 4 { 5 //打开套接字通道并将其连接到远程地址。 6 public static SocketChannel open(SocketAddress remote) throws IOException 7 //开启连接,可以使阻塞和非阻塞,在普通socket中,此处是阻塞的 8 public abstract boolean connect(SocketAddress remote) throws IOException
9 //完成套接字通道的连接过程。 10 public abstract boolean finishConnect() throws IOException 11 //将字节序列从此通道中读入给定的缓冲区。 12 public abstract int read(ByteBuffer dst)throws IOException 13 public abstract long read(ByteBuffer[] dsts,int offset,int length) throws IOException 14 public final long read(ByteBuffer[] dsts) throws IOException 15 //将字节序列从给定的缓冲区中写入此通道。 16 public abstract int write(ByteBuffer src) throws IOException 17 public abstract long write(ByteBuffer[] srcs,int offset, int length) throws IOException 18 public final long write(ByteBuffer[] srcs) throws IOException 19 }
2.2SocketServerChannel,常用API如下:
1 public abstract class ServerSocketChannel extends AbstractSelectableChannel implements NetworkChannel 2 { 3 //打开服务器套接字通道,开启网络服务器第一步 4 public static ServerSocketChannel open() throws IOException 5 //接受到此通道套接字的连接 6 public abstract SocketChannel accept() throws IOException 7 //继承自java.nio.channels.spi.AbstractSelectableChannel,设置阻塞模式(阻塞或者非阻塞) 8 public final SelectableChannel configureBlocking(boolean block) throws IOException 9 //继承自java.nio.channels.SelectableChannel,实现选择网络模型 10 register(Selector sel, int ops) throws ClosedChannelException 11 }
2.Buffer
Buffer是一个虚类,他的子类有ByteBuffer,ShortBuffer,IntBuffer,LongBuffer,FloatBuffer,DoubleBuffer,CharBuffer基本类型都有对应的Buffer,但是bollean没有,这些子类也是虚类。
在Buffer类中这,需要注意几个成员变量,及几个实现方法:
1 public abstract class Buffer { 2 //标志位,可以通过设置此变量的值,reset()方法可以将操作指针下标回滚到该位置 3 private int mark = -1; 4 //当前操作指针的下标 5 private int position = 0; 6 //当前有效数据的最大范围,也可以说是当前操作指针能操作到的最大范围,类似集合的size()方法的返回值。 7 private int limit; 8 //当前buffer的最大值(容量),类似于集合的capacity 9 private int capacity; 10 11 12 //重绕缓冲区-读取后操作指针回到0位置—针对多线程读取的时候,position位置改变。 13 public final Buffer rewind() 14 //重置缓冲区——position回到上一个mark点,mark回到-1 15 //类似于回滚到指定位置 16 public final Buffer reset() 17 //清除缓冲区——position回到0,mark回到-1,limit回到capacity 18 //一般用在读取后,清空缓冲区(缓冲区数据还在,只是操作指针归零了) 19 public final Buffer clear() 20 //翻转缓冲区——limit来到position,positio回到0,mark回到-1 21 //一般用在read()方法前 22 public final Buffer flip()
接下来上代码—客户端:
1 public class Client { 2 public static void main(String[] args) throws IOException, InterruptedException { 3 //创建客户端的通道 4 SocketChannel sc = SocketChannel.open(); 5 //设置为非阻塞 6 sc.configureBlocking(false); 7 Socket s; 8 //发起连接 9 //连接过程在BIO中会产生阻塞 10 sc.connect(new InetSocketAddress("localhost", 60253)); 11 //判断连接是否建立 12 while (!sc.isConnected()) { 13 //如果没连上,试图再次连接 14 //如果连接多次依然失败,则意味着这个连接失败 15 //sc.finishConnect底层会自动计数,计数多次依然失败则抛出异常 16 sc.finishConnect(); 17 } 18 19 //发送数据 20 sc.write(ByteBuffer.wrap("hello server".getBytes())); 21 22 //读数据 23 Thread.sleep(50); 24 FileChannel fc = new FileOutputStream(new File("D:\\FeiQ.exe")).getChannel(); 25 ByteBuffer buffer = ByteBuffer.allocate(500); 26 int len = 0; 27 while ((len = sc.read(buffer)) > 0) { 28 System.out.println(len); 29 // 30 buffer.flip(); 31 fc.write(buffer); 32 buffer.clear(); 33 34 } 35 //关流 36 sc.close(); 37 } 38 }
服务器端代码
1 public class Server { 2 public static void main(String[] args) throws IOException { 3 4 /**选择器要求通道必须是非阻塞的*/ 5 6 //开启服务器端的通道 7 ServerSocketChannel ssc = ServerSocketChannel.open(); 8 //绑定 9 ssc.bind(new InetSocketAddress(60253)); 10 //开启选择器 11 Selector selc = Selector.open(); 12 //将服务器注册到选择器上 13 ssc.configureBlocking(false); 14 ssc.register(selc, SelectionKey.OP_ACCEPT); 15 // 16 while(true){ 17 //进行选择 18 selc.select(); 19 //获取选择出来的事件 20 Set<SelectionKey> set = selc.selectedKeys(); 21 //遍历集合,根据事件类型不同,进行处理 22 Iterator< SelectionKey> it = set.iterator(); 23 while (it.hasNext()) { 24 SelectionKey key = it.next(); 25 26 //可接受 27 if (key.isAcceptable()) { 28 //真正需要处理的事件,需要通道来完成 29 //从这个事件中需要获取到需要进行accept的通道 30 ServerSocketChannel sscx = (ServerSocketChannel) key.channel(); 31 //接受连接 32 SocketChannel sc = sscx.accept(); 33 //需要给这个通道注册可读或者可写事件 34 //如果需要注册可读,也主要注册可写——在注册的时候,后注册的事件会覆盖之前的事件 -或者| 35 //将sc设置为非阻塞 36 sc.configureBlocking(false); 37 sc.register(selc, SelectionKey.OP_WRITE + SelectionKey.OP_READ); 38 } 39 //可读 40 if (key.isReadable()) { 41 //先从事件中获取通道 42 SocketChannel sc = (SocketChannel) key.channel(); 43 System.out.println("11111"); 44 //读取数据 45 ByteBuffer dst = ByteBuffer.allocate(1024); 46 sc.read(dst); 47 dst.flip(); 48 System.out.println(new String(dst.array(), 0, dst.limit())); 49 50 //注销read事件 +或者^ 51 sc.register(selc, key.interestOps() ^ SelectionKey.OP_READ); 52 } 53 //可写 54 if (key.isWritable()) { 55 //先从事件中获取通道 56 SocketChannel sc = (SocketChannel) key.channel(); 57 58 //写入事件 59 //sc.write(ByteBuffer.wrap("hello client".getBytes())); 60 FileChannel fc = FileTrans.getFileChannel(); 61 62 long len = fc.size(); 63 int postion = 0; 64 while (postion < len) { 65 long readlen = fc.transferTo(postion, fc.size(), sc); 66 postion += readlen; 67 68 } 69 System.out.println(fc.size()); 70 71 //注销掉writer 72 sc.register(selc, key.interestOps() ^ SelectionKey.OP_WRITE); 73 } 74 it.remove(); 75 } 76 } 77 } 78 } 79 80 class FileTrans{ 81 public static FileChannel getFileChannel() throws FileNotFoundException{ 82 FileInputStream fos = new FileInputStream(new File("E:\\FeiQ.exe")); 83 return fos.getChannel(); 84 } 85 }
代码功能很简单,1.服务器采用选择网络模型,开启后一直while循环,监听客户端连接,每有一个客户端连接过来,就向他发送一个文件;2.客户端连接上客户端后,先向客户端发送一个Hello Server,然后就开始接收服务器端发送过来的文件,边接收,边存储到本地。
在服务器端,核心代码如下:transferTo是零拷贝的,效率极高。
在客户端,核心代码如下:transFrom在知道文件大小的情况下,可以使用,这样可以实现零拷贝,效率高。ByteBuffer应该不是零拷贝,需要核实下。
以上,本文内容结束了。
今天用NIO作了网络传输文件的最简单版本,实现了零拷贝发送文件,梳理了基本流程,深入挖掘下次在进行。。。