本文将通过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作了网络传输文件的最简单版本,实现了零拷贝发送文件,梳理了基本流程,深入挖掘下次在进行。。。