Widzę dziwne zachowanie podczas przesyłania dużych plików z pliku do gniazda przy użyciu zerowej kopii w Javie. Moje środowiska:FileChannel zero-copy transferTo nie może skopiować bajtów do SocketChannel
- Windows 7 64-bitowy JDK 1.6.0_45 i 1.7.0_79.
- Centos 6.6 64-bit JDK 1.6.0_35
Co robi program: klient kopie pliku wejściowego do gniazda, i kopie serwera gniazda do pliku wyjściowego z zastosowaniem metody zero-Copy: transferFrom i transferTo. Nie wszystkie bajty docierają do serwera, jeśli rozmiar pliku jest stosunkowo duży, 100 MB w przypadku systemu Windows i 2 GB + w przypadku CentOS. Klient i serwer znajdują się na tym samym komputerze, a do przesyłania danych używany jest adres localhost.
Zachowanie różni się w zależności od systemu operacyjnego. W systemie Windows klient pomyślnie wykonuje metodę transferTo. Liczba przesłanych bajtów jest równa wielkości pliku wejściowego.
long bytesTransferred = fileChannel.transferTo(0, inputFile.length(), socketChannel);
Serwer z drugiej strony, donosi mniejszą liczbę odebranych bajtów.
long transferFromByteCount = fileChannel.transferFrom(socketChannel, 0, inputFile.length());
Na Linuksie bytesTransferred na kliencie jest 2Gb nawet jeśli rozmiar pliku wejściowego jest 4Gb. Na obu konfiguracjach jest wystarczająco dużo miejsca.
W systemie Windows udało mi się przesłać plik 130 MB z jednym z następujących obejść: 1) zwiększanie rozmiaru bufora odbioru na serwerze i 2) dodawanie metody wątku wątku w kliencie. Prowadzi mnie to do wniosku, że metoda transferTo na kliencie kończy się, gdy wszystkie bajty są wysyłane do bufora gniazda, a nie do serwera. To, czy bajty te trafią na serwer, nie jest gwarantowane, co stwarza problemy w moim przypadku użycia.
Maksymalny rozmiar pliku linuksowego, który mogę przesłać za pomocą pojedynczego transferu. Inwokacja to 2 GB, jednak przynajmniej klient zgłasza poprawną liczbę bajtów wysłanych na serwer.
Moje pytania: jaki jest najlepszy sposób, aby klient zagwarantował dostarczenie pliku na serwer, między platformami? Jakie mechanizmy są używane do emulacji sendfile() w systemie Windows?
Oto kod:
Client - 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]));
}
}
}
Server - 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: Kod Linux stałe z pętli.
AKTUALIZACJA 2: Jedno możliwe obejście, które rozważam, wymaga współpracy klient-serwer. Po zakończeniu transmisji serwer zapisuje długość odebranych danych do klienta, który klient odczytuje w trybie blokowania.
Serwer odpowiada:
ByteBuffer response = ByteBuffer.allocate(8);
response.putLong(transferFromByteCount);
response.flip();
socketChannel.write(response);
serverSocketChannel.close();
Bloki klienta z Odczyt:
ByteBuffer response = ByteBuffer.allocate(8);
socketChannel.read(response);
response.flip();
long totalBytesReceived = response.getLong();
W efekcie klient czeka na bajty przejść przez wysyłanie i odbieranie bufory gniazd, aw rzeczywistości czeka bajtów, które zostaną zapisane w pliku wyjściowym. Nie ma potrzeby implementowania potwierdzeń pozapasmowych, a klient nie musi czekać zgodnie z sugestią zawartą w sekcji II.A https://linuxnetworkstack.files.wordpress.com/2013/03/paper.pdf w przypadku, gdy zawartość pliku jest zmienna.
"czekaj«odpowiedniej»czasu, po przepisaniu sam część pliku"
UPDATE 3:
przykładu zawierający poprawki modyfikowane przez @EJP i @ the8472 , z weryfikacją sumy i sumy kontrolnej pliku, bez śledzenia wyników. Zauważ, że obliczenie sumy kontrolnej CRC32 dla dużego pliku może potrwać kilka sekund.
Klient:
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));
}
}
Serwer:
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));
}
}
'transferTo()' jest nie podano do uzupełnij cały transfer w jednym wywołaniu. Musisz zapętlić. – EJP
@EJP bytesTransferred na kliencie zwraca tę samą liczbę bajtów jako długość pliku wejściowego w systemie Windows, więc nie ma pozostałych bajtów. Myślę, że widzę, że bytesTransferred zwraca, gdy bajty są umieszczane w buforze wysyłania, a nie kiedy są wysyłane na serwer. –
Dodano pętlę klienta zgodnie z sugestią @EJP –