Search code examples
javafilesocketstcpjava-io

Trasmission of Files with a Java Socket


I've just developed an application which can send files throught the net using Java Sockets. The implementation work this way:

One peer sends the other a FileTransferMessage in which the essential info of the file are reported. Here's the code (without all the getters)

public class FileTransferMessage extends Message {

private final String fileName;
private final long fileSize;
private final int chunkSize;
private final PeerAddress transferAddress;

public FileTransferMessage(String senderID, PeerAddress transferAddress, String fileName, int chunkSize, long fileSize) {
    super(MessageType.FileTransferMessage, senderID);
    this.fileName = fileName;
    this.transferAddress = transferAddress;
    this.fileSize = fileSize;
    this.chunkSize = chunkSize;
}

}

Then the sending Peer starts a SendingStreamThread (created before sending the FileTransferMessage), shaped like this (note that PeerAddress is simply a record containing an IPaddress and a Port):

public class SendingStreamThread extends Thread {

private final String filePath;
private final ServerSocket serverSocket;
private final int batchSize;
private long remainingBatches;

public SendingStreamThread(Peer ownerPeer, String filePath, int batchSize, long fileSize) throws IOException {
    this.filePath = filePath;
    this.serverSocket = new ServerSocket(0, 1, InetAddress.getByName(ownerPeer.getThisPeerAddress().IPAddress()));
    this.remainingBatches = fileSize % batchSize == 0 ? fileSize / batchSize : fileSize / batchSize + 1;
    this.batchSize = batchSize;
}

@Override
public void run() {
    try (FileInputStream fileInputStream = new FileInputStream(filePath)) {
        Socket socket = serverSocket.accept();
        while (remainingBatches > 0) {
            byte[] bytes = fileInputStream.readNBytes(batchSize);
            socket.getOutputStream().write(bytes);
            remainingBatches--;
        }
        fileInputStream.close();
        socket.close();
        serverSocket.close();
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

public PeerAddress getServerSocketPort() {
    return new PeerAddress(serverSocket.getInetAddress().getHostAddress(), serverSocket.getLocalPort());
}

}

The receiving Peer on FileTransferMessage receive instatiates a ReceivingStreamThread, shaped like this:

public class ReceivingStreamThread extends Thread {

private final int chunkSize;
private final String finalPath;
private final PeerAddress source;
private final long fileSize;

public ReceivingStreamThread(FileTransferMessage fileTransferMessage) throws IOException {
    this.chunkSize = fileTransferMessage.getChunkSize();
    this.source = fileTransferMessage.getTransferAddress();
    this.fileSize = fileTransferMessage.getFileSize();
    String filePath = System.getProperty("user.home") + "\\Desktop\\";
    File file = new File(filePath + fileTransferMessage.getFileName());
    String extension = FilenameUtils.getExtension(fileTransferMessage.getFileName());
    String baseName = FilenameUtils.getBaseName(fileTransferMessage.getFileName());
    int i = 1;
    while (file.exists()) {
        baseName = FilenameUtils.getBaseName(fileTransferMessage.getFileName());
        baseName += " (" + i + ")";
        file = new File(filePath + baseName + "." + extension);
        i++;
    }
    finalPath = filePath + baseName + "." + extension + ".tempst";
    file = new File(finalPath);
    if (!file.createNewFile()) {
        if (!file.delete())
            throw new RuntimeException();
        else file.createNewFile();
    }
}

@Override
public void run() {
    try {
        Socket socket = new Socket(source.IPAddress(), source.port());
        FileOutputStream output = new FileOutputStream(finalPath);
        byte[] bytes = new byte[chunkSize];
        long readBytes = 0;
        while (readBytes != fileSize) {
            readBytes += socket.getInputStream().read(bytes);
            output.write(bytes);
        }
        System.out.println("Done reading");
        output.close();
        finalizeFile();
    } catch (IOException e) {
        File file = new File(finalPath);
        if (file.delete())
            throw new RuntimeException(e);
    }
}

private void finalizeFile() throws IOException {
    File file = new File(finalPath);
    File file1 = new File(FilenameUtils.getFullPath(finalPath) + FilenameUtils.getBaseName(finalPath));
    if (!file.renameTo(file1)) {
        throw new RuntimeException();
    }
}

This code seems to work fine when used with Peers on the same machine (using the loopback address 127.0.0.1) and different ports. The received file is complete and can be read succesfully (I've also tried with big videos and they can be played flawlessly). The thing changes when I try to run this code on two machines, and make them connect and transfer files. I've connected both the computers in the same local network, retrieved their ipv4 address using ipconfig on the cmd and they succesfully transmit each other Messages (actually using serialization of messages and another procedure). But when I try to transmit the file using this code the file on the receiving hand appear bigger (almost twice) and obviously corrupted. Is it possible that during the trasmission something adds up (like some overhead of the trasmission layer, even if it seems nosense to me) and since I'm reading directly the bytes I'm writing in the file also these accessory bytes? The thing the blows my mind is that in the receiving hand I'm checking if the quantity of bytes read is equal to the original fileSize to exit the reading loop. And my program succesfully exits the loop since it prints to me that it's done reading.


Solution

  • You read up to chunkSize bytes but always write exactly chunkSize bytes.

    This might work on your local network because read() can always get and return chunkSize bytes from the local network interface (it does not actually go over the network). But when traffic goes over the network, sometimes less bytes are available when you can call read() and read() will return less than chunkSize bytes.

    Change:

    readBytes += socket.getInputStream().read(bytes);
    output.write(bytes);
    

    to:

    int n = socket.getInputStream().read(bytes);
    output.write(bytes, 0, n);
    readBytes += n;