Mina NIO通信相关bug整理

1、java.lang.NegativeArraySizeException
发生在用负数长度创建数组时,原因是应用层报文设计数据最大长度为short,当数据超过short强制转换回丢失数据导致数值变负数。
所以在设计应用层报文时一定要注意数据的长度小于上限。本例中可以用int代替short让数据有2^32-1的长度。

2、滥用ExecutorFilter
Mina是基于REACTOR模型的,当连接建立对方发来消息首先由Processor线程池派出一个线程执行读取事件,经过粘包/缺包处理器处理,然后就完整的字节报文转换成上层应用报文。

假设解码器的代码为:
connector.getFilterChain().addLast("BaseFilter", new ProtocolCodecFilter(new BaseCodecFactory()));

如果在FilterChain之前加入代码:
connector.getFilterChain().addLast("threadpool",
new ExecutorFilter(Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() + 1)));

意思是解码器将不会在Processor线程池执行,而是由新创建的ExecutorFilter中的线程池执行。
这就导致了一个问题,假设数据长度8K,网络速度16K/S, 本地写入速度4K/S,明显网络速度大于本地IO速度,所以可能ExecutorFilter中的线程还没处理完一个完整应用层报文,Processor已经开始了下一个网络流的读取,从而导致数据错位。

本例中如果网络速度小于IO速度不会出现此问题,但是因为本地IO速度是动态的,当连接数增大IO遇到瓶颈很可能触发此问题。所以在FilterChain层加入线程池至少应该放在解码层后面,避免不同步处理数据的异常。

3、不同步发送文件分段导致文件写入不同步
虽然Reactor模型的SelectionKey.OP_READ 事件是按顺序读取SocketChannel的,但是如果在mina解码后配置:
connector.getFilterChain().addLast("threadpool",
new ExecutorFilter(Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() + 1)));
会导致处理上层报文的无序。

如果传输文件使用无序写入,就不能用FileOutputStream的追加模式:
FileOutputStream fos = new FileOutputStream(fileTask.zippedFilePath, true);
fos.write(filePart.data);
fos.flush();
fos.close();
因为写入是并发的,并不能保证其有序。

在并发环境下写入文件可以使用RandomAccessFile,支持在文件的指定位置写入:
RandomAccessFile randomAccessFile = new RandomAccessFile(fileTask.zippedFilePath, "rw");
long beginIndex = fileTask.fileSegmentSize*filePart.partId;
randomAccessFile.seek(beginIndex);
System.out.println("file length = "+randomAccessFile.length()+" , beginIndex = "+beginIndex);
randomAccessFile.write(filePart.data);
randomAccessFile.close();

因为文件处理是并行的,最后一段数据可能并不是最后处理完成的,所以怎么判断写入文件执行完毕?
发送文件分段时会传入当前文件分段的partId,在接收文件处理中使用AtomicInteger为partId计数,算出总计有多少个分段,再判断当前完成的分段数是否达到总数,就能够判断文件是否完全写入:

//每个线程执行完分段的写入将partId加1
int partId = fileTask.partId.incrementAndGet();

//算出总计有多少分段,用文件总大小除以分段的大小,如果有小数则加1取整
//最大传输4G的文件(2^32-1字节),超过则程序异常
int totalPart = (int) Math.ceil((double)fileTask.zippedFileSize/fileTask.fileSegmentSize);

if (partId==totalPart) {
//文件接收完毕
//md5验证
//通知对方
}

4、Processor线程池内的线程是固定的,要保证每个线程在运行期间不发生异常终止,否则Processor线程池不会重新创建线程。

你可能感兴趣的:(Mina NIO通信相关bug整理)