实现 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 接口, 具体实现参考 Spring 的 MockMultipartFile.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());
});
}
}
只此可以发送文件到服务端了,如果需要可以移植保存文件到客户端,用于接收
以上代码未真正运行,根据源代码改编,欢迎指正