文件传输
文件(File)是最常见的数据源之一,在程序中经常需要将数据存储到文件中,例如图片文件、声音文件等数据文件。在实际使用时,文件都包含一个特定的格式,这个格式需要程序员根据需求进行设计。读取已有的文件时也需要熟悉对应的文件格式,才能把数据从文件中正确地读取出来。
在NIO类库提供之前,Java所有的文件操作分为两大类:
基于字节流的InputStream和OutputStream;
基于字符流的Reader和Writer。
通过NIO新提供的FileChannel类库可以方便地以“管道”方式对文件进行各种I/O操作,相比于传统以流的方式进行的I/O操作有了很大的变化和改进。
FileChannel简介
Java NIO中的FileChannel是一个连接到文件的通道,可以通过这个文件通道读写文件。JDK1.7之前NIO1.0的FileChannel是同步阻塞的,JDK1.7版本对NIO类库进行了升级,升级后的NIO2.0提供了异步文件通道AsynchronousFileChannel,它支持异步非阻塞文件操作(AIO)。
在使用FileChannel之前必须先打开它,FileChannel无法直接被打开,需要通过使用InputStream、OutputStream或RandomAccessFile来获取一个FileChannel实例。下面示范如何通过RandomAccessFile打开FileChannel。
RandomAccessFile billFile = new RandomAccessFile("home/lilinfeng/sms.bill", "rw"); FileChannel channel = billFile.getChannel();
如果需要从FileChannel中读取数据,要申请一个ByteBuffer,将数据从FileChannel中读取到字节缓冲区中。read()方法返回的int值表示有多少字节被读到了字节缓冲区中,如果返回-1,表示读到了文件末尾。如果需要通过FileChannel向文件中写入数据,需要将数据复制或者直接存放到Byte Buffer中,然后调用FileChannel.write()方法进行写操作。
示例代码如下:
String content = "13888888888|北京市海淀区|全球通|VIP用户|"; ByteBuffer writeBuffer = ByteBuffer.allocate(128); writeBuffer.put(content.getBytes()); writeBuffer.flip(); channel.write(buf);
使用完FileChannel之后,需要通过close()方法关闭文件句柄,防止出现句柄泄漏。
可以通过FileChannel的position(long pos)方法设置文件的位置指针,利用该特性可以实现文件的随机读写。
Netty文件传输开发
在实际项目中,文件传输通常采用FTP或者HTTP附件的方式。事实上通过TCP Socket+File的方式进行文件传输也有一定的应用场景,尽管不是主流,但是掌握这种文件传输方式还是比较重要的,特别是针对两个跨主机的JVM进程之间进行持久化数据的相互交换。
具体场景如下。
(1)Netty文件服务器启动,绑定8080作为内部监听端口;
(2)在CMD控制台,通过telnet和文件服务器建立TCP连接;
(3)控制台输入需要下载的文件绝对路径;
(4)文件服务器接收到请求消息后进行合法性判断,如果文件存在,则将文件发送给CMD控制台;
(5)CMD控制台打印文件名和文件内容。
服务端代码示例:
import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.LineBasedFrameDecoder; import io.netty.handler.codec.string.StringDecoder; import io.netty.handler.codec.string.StringEncoder; import io.netty.util.CharsetUtil; public class FileServer { public void run(int port) throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .option(ChannelOption.SO_BACKLOG, 100) .childHandler(new ChannelInitializer() { public void initChannel(Channel ch)throws Exception { ch.pipeline().addLast( //它的作用是将文件内容编码为字符串 new StringEncoder(CharsetUtil.UTF_8), //在ChannelPipeline中添加了LineBasedFrameDecoder,它能够按照回车换行符对数据报进行解码。 new LineBasedFrameDecoder(1024), //新增StringDecoder,它的作用是将数据报解码成为字符串,两个解码器组合起来就是文本换行解码器。 new StringDecoder(CharsetUtil.UTF_8), new FileServerHandler()); } }); ChannelFuture f = b.bind(port).sync(); System.out.println("Start file server at port : " + port); f.channel().closeFuture().sync(); } finally { // 优雅停机 bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } public static void main(String[] args) throws Exception { int port = 8080; if (args.length > 0) { try { port = Integer.parseInt(args[0]); } catch (NumberFormatException e) { e.printStackTrace(); } } new FileServer().run(port); } } import io.netty.channel.ChannelHandlerContext; import io.netty.channel.DefaultFileRegion; import io.netty.channel.FileRegion; import io.netty.channel.SimpleChannelInboundHandler; import java.io.File; import java.io.RandomAccessFile; public class FileServerHandler extends SimpleChannelInboundHandler { private static final String CR = System.getProperty("line.separator"); public void messageReceived(ChannelHandlerContext ctx, Object o) throws Exception { String msg = (String) o; File file = new File(msg); if (file.exists()) { //首先对文件的合法性进行校验,如果不存在,构造异常消息返回。 if (!file.isFile()) { ctx.writeAndFlush("Not a file : " + file + CR); return; } ctx.write(file + " " + file.length() + CR); //如果文件存在,使用RandomAccessFile以只读的方式打开文件, RandomAccessFile randomAccessFile = new RandomAccessFile(msg, "r"); //通过Netty提供的DefaultFileRegion进行文件传输, //它有如下三个参数。 //FileChannel:文件通道,用于对文件进行读写操作; //Position:文件操作的指针位置,读取或者写入的起始点; //Count:操作的总字节数。 FileRegion region = new DefaultFileRegion( randomAccessFile.getChannel(), 0, randomAccessFile.length()); //直接调用ChannelHandlerContext的write方法实现文件的发送。Netty底层对文件写入进行了封装,上层应用不需要关心发送的细节。 ctx.write(region); // 最后写入回车换行符告知CMD控制台:文件传输结束。 ctx.writeAndFlush(CR); randomAccessFile.close(); } else { ctx.writeAndFlush("File not found: " + file + CR); } } public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } }
测试结果:
➜ doc-dir git:(develop) ✗ telnet localhost 8080 Trying ::1... Connected to localhost. Escape character is '^]'. /Users/***/info.txt File not found: /Users/***/info.txt /Users/***/Documents/info.txt /Users/***/Documents/info.txt 4355 RandomStringUtils.random(5, new char[]{'a','b','c','d','e','f'}); /finance/exportofflinerecord?shopSettleBillId=2314&shopId=184 bills?limit=50&offset=0&lastId=undefined&queryMonth=11&queryYear=2015&shopSettleTime=1&shopType=2&shopValue=184&t=1446705363502 createOfflineRefundRecordName(shopName, shopSettleBillDTO);