1、需求:
客户端向服务端发送文件,服务端接收并保存。
2、实现思路:
采用分段上传,即客户端将数据分成固定的段数,每次上传固定长度的数据(最后一次可能小于平均长度)。这样服务端客户端就会向打乒乓球一样来回交互,最终把文件传完。
3、要解决的问题:
- 怎么分段?
- 客户端怎么知道下一次从哪个位置开始传?
- 怎么知道有没有传完?
3.1 怎么分段?
获取文件的总的数据长度为115,比如我们固定分10段,则除以10,得11余5,即每次发送11个数据,发10次这样的,第11次发剩余的5个数据。
3.2 客户端怎么知道下一次从哪个位置开始传?
这就和刚刚说的乒乓交互联系起来了。服务端收到第一段数据后,计算下一段数据的起始位置并传给客户端,客户端收到后即可开始下一次的数据传输。
3.3 怎么知道有没有传完?
可以有多种实现方案,如下:
1)当读到最后的时候客户端是知道的,此时继续传客户端传完这一次就断开连接,因为传递的数据是空的,此时服务端判断接收到的数据是空的认为传完,不再向客户端返回响应,然后关闭文件,关闭连接。
2)客户端知道自己上传的文件大小是多少,将此文件大小的值一并传给服务端,服务端收到数据后,每次计算收到的数据的总大小等于文件的大小时表示客户端已传完。
4、交互图
5、代码实现
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.serialization.ClassResolvers;
import io.netty.handler.codec.serialization.ObjectDecoder;
import io.netty.handler.codec.serialization.ObjectEncoder;
/**
* @Description: 文件上传demo 服务端
* @Author: walking
* @Date: 2019年10月29日16:49:52
*/
public class FileUploadServer {
public void bind(int port) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(new ObjectEncoder());
ch.pipeline().addLast(new ObjectDecoder(Integer.MAX_VALUE, ClassResolvers.weakCachingConcurrentResolver(null))); // 最大长度
ch.pipeline().addLast(new FileUploadServerHandler());
}
});
ChannelFuture f = b.bind(port).sync();
System.out.println("服务端启动....");
f.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static void main(String[] args) {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
e.printStackTrace();
}
}
try {
new FileUploadServer().bind(port);
} catch (Exception e) {
e.printStackTrace();
}
}
}
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import java.io.File;
import java.io.RandomAccessFile;
/**
* @Description: 文件上传demo 服务端处理类
* @Author: walking
* @Date: $
*/
public class FileUploadServerHandler extends SimpleChannelInboundHandler<FileUploadEntity> {
private int byteRead;//读取到的数据的长度
private volatile int start = 0;//读取的起始位置
private String file_dir = "D:";//服务器保存文件的路径
private long startTime;//开始处理的时间
private RandomAccessFile randomAccessFile;
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
startTime = System.currentTimeMillis();
System.out.println("channelActive");
}
/**
* 获取客户端发送的数据
*
* @param ctx
* @param fileUploadEntity
* @throws Exception
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, FileUploadEntity fileUploadEntity) throws Exception {
FileUploadEntity ef = fileUploadEntity;
byte[] bytes = ef.getBytes();
byteRead = ef.getDataLength();//dataLength 每次接收到的数据长度
System.out.println("byteRead=>" + byteRead);
String md5 = ef.getFileName();//文件名
String path = file_dir + File.separator + md5;//文件路径
randomAccessFile = new RandomAccessFile(path, "rw");
randomAccessFile.seek(start);
randomAccessFile.write(bytes);//写入数据
randomAccessFile.close();
//修改初始值 记录下次要从哪个位置读数据,并返回给客户端 告诉客户端下一次从文件的哪个位置开始传
start = start + byteRead;
if (byteRead > 0) {
System.out.println("返回给客户端数据=》" + start);
ctx.writeAndFlush(start);//写回客户端
} else {
System.out.println("接收文件耗时:" + (System.currentTimeMillis() - startTime));
ctx.close();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
服务端处理程序中的这行代码 randomAccessFile.close();
为什么没有放到下面的else分支,在接收完毕时关闭文件呢?原因是,服务端每次接收到客户端的数据都会new一个RandomAccessFile
,所以每次接收完数据都要关闭,如果放到下面的else里,对于我们现在这个demo(分段上传)来说,new了多个RandomAccessFile
,放到else里造成只关了一次,一是资源的浪费,二是你会发现此时服务端保存的这个文件你是不能删除的,删除时会报正在被java占用的错。
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.serialization.ClassResolvers;
import io.netty.handler.codec.serialization.ObjectDecoder;
import io.netty.handler.codec.serialization.ObjectEncoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Date;
/**
* @Description: 文件上传demo 客户端
* @Author: walking
* @Date: 2019年10月29日16:50:25
*/
public class FileUploadClient {
private static String file_name="E:\\idea_work\\aa\\src\\Test1.java";
public void connect(int port, String host, final FileUploadEntity fileUploadEntity ) throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(new ObjectEncoder());
ch.pipeline().addLast(new ObjectDecoder(ClassResolvers.weakCachingConcurrentResolver(null)));
ch.pipeline().addLast(new FileUploadClientHandler(fileUploadEntity));
}
});
ChannelFuture f = b.connect(host, port).sync();
System.out.println("客户端启动...");
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
public static void main(String[] args) {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
e.printStackTrace();
}
}
try {
//构建上传文件对象
FileUploadEntity uploadFile = new FileUploadEntity();
File file = new File(file_name);
String fileName = file.getName();// 文件名
uploadFile.setFile(file);
uploadFile.setFileName(fileName);
//连接到服务器 并上传
new FileUploadClient().connect(port, "127.0.0.1", uploadFile);
} catch (Exception e) {
e.printStackTrace();
}
}
}
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.SimpleChannelInboundHandler;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
/**
* @Description: 客户端文件上传处理类
* 在这个例子中 实际上客户端的 channelRead 方法和服务端的 channelRead 方法在多次交互,
* 客户端的 channelActive 只执行了一次
* 在客户端的 channelRead 方法中实际上包含了 channelActive 的逻辑
* @Author: walking
* @Date: 2019年10月29日16:50:48
*/
public class FileUploadClientHandler extends SimpleChannelInboundHandler<FileUploadEntity> {
private int byteRead;
private volatile int start = 0;//数据读取的起始位置
private volatile int dataLength = 0;//需读取的数据长度
public RandomAccessFile randomAccessFile;
private FileUploadEntity fileUploadEntity;
private long startTime;//handler开始处理的起始时间
private int ping_pong_times = 0;//客户端与服务端交互次数记录
private final int dataGrameNum = 10;//数据段数
private int dataGrameLength = 0;//数据段数长度
public FileUploadClientHandler(FileUploadEntity ef) throws IOException {
if (ef.getFile().exists()) {
if (!ef.getFile().isFile()) {
System.out.println("Not a file :" + ef.getFile());
return;
}
}
this.fileUploadEntity = ef;
this.randomAccessFile = new RandomAccessFile(fileUploadEntity.getFile(), "r");
dataGrameLength = (int) (randomAccessFile.length() / dataGrameNum);
}
/**
* 向服务端发送数据
* 实际上这个方法只执行一次,相当于抛砖引玉,客户端通过这个方法像服务端发送一次数据后
* 客户端在channelRead方法受到服务端的响应 在这个方法里继续与服务端交互 接下来就向打乒乓球一样
* 开始了ping-pong通信知道满足退出条件结束通信
* 这里的结束条件就是文件上传完毕,程序读到文件末尾
* @param ctx
*/
@Override
public void channelActive(ChannelHandlerContext ctx) {
System.out.println("客户端发送消息=》channelActive");
try {
System.out.println("文件总大小=》" + randomAccessFile.length());
randomAccessFile.seek(0);//设置读取的起始位置
//lastLength = 11
dataLength = dataGrameLength;//分段发送,分11段,115/10=11余数5 10段11 最后一段5
startTime = System.currentTimeMillis();
if (upload(ctx)) {
System.out.println("channelActive=》第" + ping_pong_times + "次上传成功....");
System.out.println("channelActive=》等待服务端返回响应....");
System.out.println();
System.out.println();
} else {
System.out.println("channelActive=》文件已经读完");
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException i) {
i.printStackTrace();
}
}
/**
* 获取服务端返回的数据
*
* @param ctx
* @param msg
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof Integer) {
start = (Integer) msg;//读取到服务端修改后的初始值,第一次读取到start = 11
System.out.println("服务端传回的指针=》" + start);
if (start != -1) {
randomAccessFile.seek(start);//设置文件指针
int leaveDataLength = (int) (randomAccessFile.length() - start);//115 -11 =104
// a=33-0=33 b=33/10=3
if (leaveDataLength < dataGrameLength) {//104<11 not
dataLength = leaveDataLength;
// dataLength = 11 第11次 dataLength =5,
// 当服务端拿到最后一次的数据后返回的start=115即文件的原始长度
// 此时leaveDataLength剩余长度为0 lastLength=0 下面又调upload方法 只不过传的data是空
// 这样就造成多发一次空数据
}
if (upload(ctx)) {
System.out.println("channelRead=》第" + ping_pong_times + "次上传成功....");
} else {
System.out.println();
System.out.println();
System.out.println();
System.out.println("channelRead=》上传文件耗时:" + (System.currentTimeMillis() - startTime));
randomAccessFile.close();
ctx.close();
System.out.println("channelRead=》文件已经读完--------" + byteRead);
}
}
}
}
/**
* 这个方法是 SimpleChannelInboundHandler 抽象类中的抽象方法
* 它和上面的 channelRead 方法的区别是channelRead 是 ChannelInboundHandlerAdapter 的方法
* ChannelInboundHandlerAdapter 是 SimpleChannelInboundHandler 的父类
* 当我们的 handler 继承 SimpleChannelInboundHandler 时,如果同时重写了 channelRead 和 channelRead0 则默认执行前者
* @param channelHandlerContext
* @param fileUploadEntity
* @throws Exception
*/
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, FileUploadEntity fileUploadEntity) throws Exception {
System.out.println("channelRead0");
}
private boolean upload(ChannelHandlerContext ctx) throws IOException {
//当最后一次发送完毕后 lastLength为0 bytes为空数组 这时继续给服务端传递
// 服务端会判断拿到的数据长度为空时关闭连接和文件
// 而客户端会在下面判断randomAccessFile.length() - start > 0不成立时返回false 从而关闭连接关闭文件
byte[] bytes = new byte[dataLength];
if ((byteRead = randomAccessFile.read(bytes)) != -1) {
System.out.println();
ping_pong_times++;
fileUploadEntity.setDataLength(byteRead);
fileUploadEntity.setBytes(bytes);
ctx.writeAndFlush(fileUploadEntity);
System.out.println("数据起始位置start=>" + start
+ ",本次上传数据长度dataLength=>" + dataLength + ",本次读取长度=>" + byteRead);
System.out.println(fileUploadEntity);
if (randomAccessFile.length() - start > 0) {//判断是最后一次时返回false,以关闭客户端连接
return true;
} else {
return false;
}
}
return false;
}
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
/**
* @Description: 文件上传的文件信息实体类
* @Author: walking
* @Date: 2019年10月29日16:53:23
*/
public class FileUploadEntity implements Serializable {
private static final long serialVersionUID = 1L;
private File file;// 文件
private String fileName;// 文件名
private byte[] bytes;// 文件字节数组
private int dataLength;// 数据长度
//getter and setter
//overwrite toString()
}