1、需求:

客户端向服务端发送文件,服务端接收并保存。

2、实现思路:

采用分段上传,即客户端将数据分成固定的段数,每次上传固定长度的数据(最后一次可能小于平均长度)。这样服务端客户端就会向打乒乓球一样来回交互,最终把文件传完。

3、要解决的问题:

  1. 怎么分段?
  2. 客户端怎么知道下一次从哪个位置开始传?
  3. 怎么知道有没有传完?

3.1 怎么分段?

获取文件的总的数据长度为115,比如我们固定分10段,则除以10,得11余5,即每次发送11个数据,发10次这样的,第11次发剩余的5个数据。

3.2 客户端怎么知道下一次从哪个位置开始传?

这就和刚刚说的乒乓交互联系起来了。服务端收到第一段数据后,计算下一段数据的起始位置并传给客户端,客户端收到后即可开始下一次的数据传输。

3.3 怎么知道有没有传完?

可以有多种实现方案,如下:
1)当读到最后的时候客户端是知道的,此时继续传客户端传完这一次就断开连接,因为传递的数据是空的,此时服务端判断接收到的数据是空的认为传完,不再向客户端返回响应,然后关闭文件,关闭连接。

2)客户端知道自己上传的文件大小是多少,将此文件大小的值一并传给服务端,服务端收到数据后,每次计算收到的数据的总大小等于文件的大小时表示客户端已传完。

4、交互图

java netty客户端接收数据 netty客户端发送数据_.net

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()
}