Я вижу странное поведение при передаче больших файлов из файла в сокет с использованием нулевой копии на Java. Мое окружение:Передача нулевой копии FileChannel Не удается скопировать байты в SocketChannel
- Windows 7 64-бит JDK 1.6.0_45 и 1.7.0_79.
- Centos 6,6 64-битный JDK 1.6.0_35
Что делает программа: клиент копирует входной файл в розетку, и сервер копирует сокет в выходной файл, используя методы нулевого копирования: transferFrom и о переходе. Не все байты достигают сервера, если размер файла относительно велик, 100 Мб + в случае Windows и 2 ГБ + в случае Centos. Клиент и сервер находятся на одном компьютере, а адрес локального адреса используется для передачи данных.
Поведение различно в зависимости от ОС. В Windows клиент успешно завершает метод transferTo. Количество переданных байтов равно размеру входного файла.
long bytesTransferred = fileChannel.transferTo(0, inputFile.length(), socketChannel);
Сервер с другой стороны, передает меньшее количество принятых байтов.
long transferFromByteCount = fileChannel.transferFrom(socketChannel, 0, inputFile.length());
В Linux bytesTransferred на клиенте 2Gb, даже если размер входного файла 4Gb. Для обеих конфигураций достаточно места.
В Windows я смог перенести файл 130 МБ с помощью одного из следующих способов: 1) увеличение размера буфера приема на сервере и 2) добавление метода слияния потока в клиенте. Это заставляет меня думать, что метод transferTo на клиенте завершается, когда все байты отправляются в буфер отправки сокетов, а не на сервер. Независимо от того, передаются ли эти байты на сервер, не гарантируется ли это, что создает проблемы для моего варианта использования.
Максимальный размер файла Linux, который я могу передать с помощью одной передачи для вызова, составляет 2 ГБ, однако, по крайней мере, клиент сообщает правильное количество байтов, отправленных на сервер.
Мои вопросы: что является лучшим способом для клиента обеспечить гарантированную доставку файла на сервер, кросс-платформенный? Какие механизмы используются для эмуляции sendfile() в Windows?
Вот код:
Клиент - ZeroCopyClient.java:
import org.apache.commons.io.FileUtils;
import java.io.*;
import java.net.*;
import java.nio.channels.*;
public class ZeroCopyClient {
public static void main(String[] args) throws IOException, InterruptedException {
final File inputFile = new File(args[0]);
FileInputStream fileInputStream = new FileInputStream(inputFile);
FileChannel fileChannel = fileInputStream.getChannel();
SocketAddress socketAddress = new InetSocketAddress("localhost", 8083);
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(socketAddress);
System.out.println("sending " + inputFile.length() + " bytes to " + socketChannel);
long startTime = System.currentTimeMillis();
long totalBytesTransferred = 0;
while (totalBytesTransferred < inputFile.length()) {
long st = System.currentTimeMillis();
long bytesTransferred = fileChannel.transferTo(totalBytesTransferred, inputFile.length()-totalBytesTransferred, socketChannel);
totalBytesTransferred += bytesTransferred;
long et = System.currentTimeMillis();
System.out.println("sent " + bytesTransferred + " out of " + inputFile.length() + " in " + (et-st) + " millis");
}
socketChannel.finishConnect();
long endTime = System.currentTimeMillis();
System.out.println("sent: totalBytesTransferred= " + totalBytesTransferred + "/" + inputFile.length() + " in " + (endTime-startTime) + " millis");
final File outputFile = new File(inputFile.getAbsolutePath() + ".out");
boolean copyEqual = FileUtils.contentEquals(inputFile, outputFile);
System.out.println("copyEqual= " + copyEqual);
if (args.length > 1) {
System.out.println("sleep: " + args[1] + " millis");
Thread.sleep(Long.parseLong(args[1]));
}
}
}
Сервер - ZeroCopyServer.java:
import java.io.*;
import java.net.*;
import java.nio.channels.*;
import org.apache.commons.io.FileUtils;
public class ZeroCopyServer {
public static void main(String[] args) throws IOException {
final File inputFile = new File(args[0]);
inputFile.delete();
final File outputFile = new File(inputFile.getAbsolutePath() + ".out");
outputFile.delete();
createTempFile(inputFile, Long.parseLong(args[1])*1024L*1024L);
System.out.println("input file length: " + inputFile.length() + " : output file.exists= " + outputFile.exists());
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().setReceiveBufferSize(8*1024*1024);
System.out.println("server receive buffer size: " + serverSocketChannel.socket().getReceiveBufferSize());
serverSocketChannel.socket().bind(new InetSocketAddress("localhost", 8083));
System.out.println("waiting for connection");
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("connected. client channel: " + socketChannel);
FileOutputStream fileOutputStream = new FileOutputStream(outputFile);
FileChannel fileChannel = fileOutputStream.getChannel();
long startTime = System.currentTimeMillis();
long transferFromByteCount = fileChannel.transferFrom(socketChannel, 0, inputFile.length());
long endTime = System.currentTimeMillis();
System.out.println("received: transferFromByteCount= " + transferFromByteCount + " : outputFile= " + outputFile.length() + " : inputFile= " + inputFile.length() + " bytes in " + (endTime-startTime) + " millis");
boolean copyEqual = FileUtils.contentEquals(inputFile, outputFile);
System.out.println("copyEqual= " + copyEqual);
serverSocketChannel.close();
}
private static void createTempFile(File file, long size) throws IOException{
RandomAccessFile f = new RandomAccessFile(file.getAbsolutePath(), "rw");
f.setLength(size);
f.writeDouble(Math.random());
f.close();
}
}
UPDATE 1: Linux код фиксируется с петлей.
ОБНОВЛЕНИЕ 2: Возможное обходное решение, которое я рассматриваю, требует взаимодействия клиент-сервер. По завершении передачи сервер записывает длину полученных данных обратно клиенту, который клиент считывает в режиме блокировки.
Сервер отвечает:
ByteBuffer response = ByteBuffer.allocate(8);
response.putLong(transferFromByteCount);
response.flip();
socketChannel.write(response);
serverSocketChannel.close();
Клиентские блоки чтения:
ByteBuffer response = ByteBuffer.allocate(8);
socketChannel.read(response);
response.flip();
long totalBytesReceived = response.getLong();
В результате, клиент ожидает байты, чтобы пройти через отправку и получение буферов сокета, а на самом деле жду для получения байтов в выходном файле. Нет необходимости реализовывать внеполосные подтверждения, и клиенту также не нужно ждать, как было предложено в разделе II.A. https://linuxnetworkstack.files.wordpress.com/2013/03/paper.pdf в случае, если содержимое файла изменено.
"ждать„соответствующий“количество времени, прежде чем переписывать же часть файла"
UPDATE 3:
Модифицированный пример, включающий исправления по @EJP и @ the8472 , с проверкой контрольной суммы длины и файла, без трассировки вывода. Обратите внимание, что для вычисления контрольной суммы CRC32 для большого файла может потребоваться несколько секунд.
Клиент:
import java.io.*;
import java.net.*;
import java.nio.*;
import java.nio.channels.*;
import org.apache.commons.io.FileUtils;
public class ZeroCopyClient {
public static void main(String[] args) throws IOException {
final File inputFile = new File(args[0]);
FileInputStream fileInputStream = new FileInputStream(inputFile);
FileChannel fileChannel = fileInputStream.getChannel();
SocketAddress socketAddress = new InetSocketAddress("localhost", 8083);
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(socketAddress);
//send input file length and CRC32 checksum to server
long checksumCRC32 = FileUtils.checksumCRC32(inputFile);
ByteBuffer request = ByteBuffer.allocate(16);
request.putLong(inputFile.length());
request.putLong(checksumCRC32);
request.flip();
socketChannel.write(request);
long totalBytesTransferred = 0;
while (totalBytesTransferred < inputFile.length()) {
long bytesTransferred = fileChannel.transferTo(totalBytesTransferred, inputFile.length()-totalBytesTransferred, socketChannel);
totalBytesTransferred += bytesTransferred;
}
//receive output file length and CRC32 checksum from server
ByteBuffer response = ByteBuffer.allocate(16);
socketChannel.read(response);
response.flip();
long totalBytesReceived = response.getLong();
long outChecksumCRC32 = response.getLong();
socketChannel.finishConnect();
System.out.println("CRC32 equal= " + (checksumCRC32 == outChecksumCRC32));
}
}
Сервер:
import java.io.*;
import java.net.*;
import java.nio.*;
import java.nio.channels.*;
import org.apache.commons.io.FileUtils;
public class ZeroCopyServer {
public static void main(String[] args) throws IOException {
final File outputFile = new File(args[0]);
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(8083));
SocketChannel socketChannel = serverSocketChannel.accept();
//read input file length and CRC32 checksum sent by client
ByteBuffer request = ByteBuffer.allocate(16);
socketChannel.read(request);
request.flip();
long length = request.getLong();
long checksumCRC32 = request.getLong();
FileOutputStream fileOutputStream = new FileOutputStream(outputFile);
FileChannel fileChannel = fileOutputStream.getChannel();
long totalBytesTransferFrom = 0;
while (totalBytesTransferFrom < length) {
long transferFromByteCount = fileChannel.transferFrom(socketChannel, totalBytesTransferFrom, length-totalBytesTransferFrom);
if (transferFromByteCount <= 0){
break;
}
totalBytesTransferFrom += transferFromByteCount;
}
long outChecksumCRC32 = FileUtils.checksumCRC32(outputFile);
//write output file length and CRC32 checksum back to client
ByteBuffer response = ByteBuffer.allocate(16);
response.putLong(totalBytesTransferFrom);
response.putLong(outChecksumCRC32);
response.flip();
socketChannel.write(response);
serverSocketChannel.close();
System.out.println("CRC32 equal= " + (checksumCRC32 == outChecksumCRC32));
}
}
'о переходе()' не указано в завершить весь перенос за один раз. Вы должны зациклиться. – EJP
@EJP bytesTransferred on client возвращает тот же самый байт, что и длина входного файла в Windows, поэтому оставшихся байтов нет. Я думаю, что я вижу, что bytesTransferred возвращается, когда байты помещаются в буфер отправки, а не когда они отправляются на сервер. –
Добавлен клиентский цикл, предложенный @EJP –