WebSocket 分卷传输文件【Java to Java】

实现 Java程序文件分卷传输

用途:主要是避免两个程序使用WS方式互传时文件太大导致内存溢出的问题。

模块#服务端

配置
需要配置一个 Endpoint

@Configuration
public class WebConfig implements WebMvcConfigurer 
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

请求处理

@ServerEndpoint(value = "/ws/{key}", decoders = {ObjectDecoder.class}
        , encoders = {ObjectEncoder.class})
@Component
public class WebSocketServer {
	private static Logger log = LoggerFactory.getLogger(WebSocketServer.class);
	private String saveFolder = "D:/";//文件保存目录
	private List<byte[]> cache = new ArrayList<>();//数据缓存
	private File temp;//临时文件
    private int readPart = 0;//已读分卷数
	//不必要的省略....
	/**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     */
    @OnMessage(maxMessageSize = 20971520)
    public void onMessage(MultipartData message, Session session) throws Exception {
        log.debug("来自客户端的对象消息:" + message.getClass());
        //handlePartFileByCache(message);//内存缓存方式
        handlePartFileByTempFile(md);//临时文件方式
    }
    //方法具体定义在后面
}

代码中,最大消息字节必须大于分卷文件大小一些(因为还传了其他数据,根据需要提高)。

MultipartData 实现了 Multipart 接口, 具体实现参考 SpringMockMultipartFile.java
重点是Json转换时需要排除某些没有定义的属性(来源于get方法)

@JsonIgnoreProperties({"resource", "inputStream", "size", "empty"})
public class MultipartData implements MultipartFile, Serializable {
    private static final long serialVersionUID = 202106280002L;
    private Integer deviceId;//设备ID
    private int sumParts;//总共多少部分
    private int part;//当前第几部分
    private String name;//保存文件名
    private String originalFilename;//原始文件名
    private String contentType;//类型
    private byte[] bytes;//数据
}

对象处理方法,有两种根据需要替换

  • handlePartFileByTempFile() - 通过临时文件方式处理
/**
     * 处理文件分卷,通过临时文件方式
     * @param data 文件数据
     * @throws Exception 如果读写异常
     */
    private void handlePartFileByTempFile(MultipartData data) throws Exception{
        //如果缓存的数据与当前数据不匹配
        if(readPart != data.getPart()) {
            log.warn("分卷不匹配");
            readPart = 0;
            if(data.getPart() !=0) {
                log.warn("分卷丢失");
                return;//如果之前的数据丢失则终止
            }
        }
        if(data.getPart() == 0) {
            temp = File.createTempFile(data.getName(),"tmp");
        }
        try(FileOutputStream fos = new FileOutputStream(temp, true)){
            fos.write(data.getBytes());
        }
        readPart++;
        if(readPart != data.getSumParts()) {
            log.debug("文件: " + data.getName() + ", 总分卷: " + data.getSumParts() + ", 当前分卷: " + (data.getPart() + 1));
            return;
        }
        FileCopyUtils.copy(temp, new File(saveFolder, data.getName()));
        readPart = 0;//重置已读分卷数
        temp.delete();//删除临时文件
        log.debug("保存文件成功: " + saveFolder + data.getName());
    }
  • handlePartFileByCache() - 通过缓存方式处理
/**
     * 处理文件分卷,通过缓存方式
     * @param data 文件数据
     * @throws Exception 如果读写异常
     */
    private void handlePartFileByCache(MultipartData data) throws Exception{
        //如果缓存的数据与当前数据不匹配
        if(cache.size() != data.getPart()) {
            log.warn("分卷不匹配");
            cache.clear();
            if(data.getPart() !=0) {
                log.warn("分卷丢失");
                return;//如果之前的数据丢失则终止
            }
        }
        cache.add(data.getBytes());
        if(cache.size() != data.getSumParts()) {
            log.debug("文件: " + data.getName() + ", 总分卷: " + data.getSumParts() + ", 当前分卷: " + (data.getPart() + 1));
            return;
        }
        try(FileOutputStream fos = new FileOutputStream(new File(saveFolder, data.getName()))){
            for (byte[] bytes : cache) {
                fos.write(bytes);
            }
            log.debug("保存文件成功:" + saveFolder + data.getName());
            cache.clear();
        }
    }

收发数据不是字符串时,需要一个编码器和解码器,客户端与服务端都需要相同的

  • 编码器 Encoder (JSON的转换就不贴了,只需要注意不要出现null被“”替换的情况)
/**
 * description 对象编码器
 */
public class ObjectEncoder implements Encoder.Binary<MultipartData > {
    @Override
    public ByteBuffer encode(MultipartData object) throws EncodeException {
        try {
            return ByteBuffer.wrap(JsonUtil.toJson(object, JsonInclude.Include.NON_NULL).getBytes());
        } catch (Exception e) {
            throw new EncodeException(object, "编码失败: target class " + object.getClass(), e);
        }
    }

    @Override
    public void init(EndpointConfig endpointConfig) {
    }

    @Override
    public void destroy() {
    }
}
  • 解码器 Decoder (JSON 转对象也相对容易)
/**
 * description 对象解码器
 */
public class ObjectDecoder implements Decoder.Binary<MultipartData > {
    @Override
    public MultipartData decode(ByteBuffer bytes) throws DecodeException {
        try {
            return JsonUtil.parseJsonToObject(new String(bytes.array()), MultipartData.class);
        } catch (Exception e) {
            throw new DecodeException(bytes, "转换为类失败: target class " + MultipartData.class, e);
        }
    }

    @Override
    public boolean willDecode(ByteBuffer bytes) {
        //预处理,可拦截校验 true 放行
        return true;
    }

    @Override
    public void init(EndpointConfig endpointConfig) {
    }

    @Override
    public void destroy() {
    }
}

模块#客户端

配置
本功能无

请求处理

/**
 * WebSocket 通信
 */
@ClientEndpoint(decoders = {ObjectDecoder.class}
, encoders = {ObjectEncoder.class}, configurator = WebSocketConnector.SampleConfigurator.class)
//@Component //可选择注册组件方式运行
public class WebSocketConnector {
    private Session current;//当前session

    /**
     * 连接到服务器
     *
     * @param protocol 协议,如 "ws","wss"
     * @param host 主机,如"localhost", "127.0.0.1"
     * @param port 端口,如"80"
     * @param path 路径,如"ws/"
     * @param key 密钥,用于验证身份
     * @throws URISyntaxException  如果URL错误则抛出此异常
     * @throws IOException         连接链路不畅导致异常
     * @throws DeploymentException 协议等导致异常
     */
    public void connect(String protocol, String host, String port
            , String path, String key) throws URISyntaxException, IOException, DeploymentException {
        WebSocketContainer container = ContainerProvider.getWebSocketContainer();
        //session
        current = container.connectToServer(this
                , new URI(protocol + "://" + host + ":" + port + "/" + path + key));
    }
    
    /**
     * 客户端配置
     */
    public static class SampleConfigurator extends ClientEndpointConfig.Configurator {
        @Override
        public void beforeRequest(Map<String, List<String>> headers) {
			//前置处理
        }
        @Override
        public void afterResponse(HandshakeResponse handshakeResponse) {
            //后置处理
        }
    }

   /**
     * 异步发送文件数据
     * @param fileData 文件数据
     * @param handler 发送回调,如: (result) -> { if (!result.isOK()) log.error("异步发送消息失败", result.getException());}
     */
    public void asyncSendFileMessage(MultipartData fileData, SendHandler handler){
        current.getAsyncRemote().sendObject(fileData, handler);
    }
}

测试方法

    /**
     * 分割文件数据
     *
     * @param file      文件
     * @param maxPartKb 最大分块大小(Kb),指的是每一块最大的大小
     * @return 数据分块集合
     */
    public static List<byte[]> splitFileData(File file, int maxPartKb) {
        List<byte[]> list = new ArrayList<>();
        long length = file.length();
        int maxPartSize = maxPartKb * 1024;
        int parts = (int) Math.ceil(1.0 * length / maxPartSize);
        try (FileInputStream fis = new FileInputStream(file)) {
            int len;
            byte[] part = new byte[maxPartSize];
            while ((len = fis.read(part)) != -1) {
                byte[] item = new byte[len];
                System.arraycopy(part, 0, item, 0, len);
                list.add(item);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return list;
    }
    
    public static void main(String[] args) throws Exception{
        WebSocketConnector connector = new WebSocketConnector();
		connector.connect("ws","localhost", "80","ws/", "xxx");//打开连接
        log.info("WebSocket 连接打开");
        File f = new File("D:/file.sql");
        //分卷发送文件 最大支持字节 / 2 / 1024 = 分卷大小(KB) 可自定义
        List<byte[]> list = MultipartData.splitFileData(f, 20971520 / 2048);
        for (int i = 0; i < list.size(); i++) {
            byte[] bytes = list.get(i);
            MultipartData md = new MultipartData(1, "test_merge.sql", f.getName(),
                    i, list.size(), bytes);
            connector.asyncSendFileMessage(md, (result) ->{
                if(!result.isOK()) log.error("发送失败", result.getException());
            });
        }
    }

只此可以发送文件到服务端了,如果需要可以移植保存文件到客户端,用于接收

以上代码未真正运行,根据源代码改编,欢迎指正

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值