Вопросы, связанные с написанием собственного загрузчика файлов с использованием нескольких потоков Java - PullRequest
0 голосов
/ 29 апреля 2010

В моей нынешней компании я делаю PoC о том, как мы можем написать утилиту для загрузки файлов. Мы должны использовать сокет программирования (TCP / IP) для загрузки файлов. Одним из требований клиента является то, что файл (который будет большого размера) должен передаваться порциями, например, если у нас есть файл размером 5 МБ, то у нас может быть 5 потоков, которые передают 1 МБ каждый. Я написал небольшое приложение, которое загружает файл. Вы можете скачать проект eclipe

от http://www.fileflyer.com/view/QM1JSC0

Краткое объяснение моих занятий

  • FileSender.java: Этот класс предоставляет байты файла. У него есть метод sendBytesOfFile (long start, long end, long sequenceNo), который дает количество байтов.

    import java.io.File;
    
    import java.io.IOException;
    
    import java.util.zip.CRC32;
    
    import org.apache.commons.io.FileUtils;
    
    
    public class FileSender {
    
        private static final String FILE_NAME = "C:\\shared\\test.pdf";
    
        public ByteArrayWrapper sendBytesOfFile(long start,long end, long sequenceNo){
            try {
                File file = new File(FILE_NAME);
                byte[] fileBytes = FileUtils.readFileToByteArray(file);
                System.out.println("Size of file is " +fileBytes.length);
                System.out.println();
                System.out.println("Start "+start +" end "+end);
                byte[] bytes = getByteArray(fileBytes, start, end);
                ByteArrayWrapper wrapper = new ByteArrayWrapper(bytes, sequenceNo);
                return wrapper;
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    
        private byte[] getByteArray(byte[] bytes, long start, long end){
            long arrayLength = end-start;
            System.out.println("Start : "+start +" end : "+end + " Arraylength : "+arrayLength +" length of source array : "+bytes.length);
            byte[] arr = new byte[(int)arrayLength];
    
            for(int i = (int)start, j =0; i < end;i++,j++){
                arr[j] = bytes[i];
            }
            return arr;
        }
    
        public static long fileSize(){
            File file = new File(FILE_NAME);
            return file.length();
        }
    }
    
  • FileReceiver.java - Этот класс получает файл.

Небольшое объяснение, что делает этот файл

  1. Этот класс находит размер файла, который нужно извлечь из Отправителя
  2. В зависимости от размера файла он находит начальную и конечную позиции до тех пор, пока не будут прочитаны байты.
  3. Он запускает n потоков, дающих начало, конец, порядковый номер каждого потока и список, который разделяют все потоки.
  4. Каждый поток читает количество байтов и создает ByteArrayWrapper.
  5. Объекты ByteArrayWrapper добавляются в список
  6. Тогда у меня есть цикл while, который в основном гарантирует, что все потоки сделали свою работу
  7. наконец, он сортирует список по порядковому номеру.
  8. затем байты объединяются, и формируется полный байтовый массив, который преобразуется в файл.

Код получателя файла

package com.filedownloader;

import java.io.File;

import java.io.IOException;

import java.util.ArrayList;

import java.util.Collections;

import java.util.Comparator;

import java.util.List;

import java.util.zip.CRC32;

import org.apache.commons.io.FileUtils;


public class FileReceiver {

    public static void main(String[] args) {
        FileReceiver receiver = new FileReceiver();
        receiver.receiveFile();

    }
    public void receiveFile(){
        long startTime = System.currentTimeMillis();
        long numberOfThreads = 10;
        long filesize = FileSender.fileSize();

        System.out.println("File size received "+filesize);
        long start = filesize/numberOfThreads;
        List<ByteArrayWrapper> list = new ArrayList<ByteArrayWrapper>();

        for(long threadCount =0; threadCount<numberOfThreads ;threadCount++){
            FileDownloaderTask task = new FileDownloaderTask(threadCount*start,(threadCount+1)*start,threadCount,list);
            new Thread(task).start();
        }

        while(list.size() != numberOfThreads){
            // this is done so that all the threads should complete their work before processing further.
            //System.out.println("Waiting for threads to complete. List size "+list.size());
        }

        if(list.size() == numberOfThreads){
            System.out.println("All bytes received "+list);
            Collections.sort(list, new Comparator<ByteArrayWrapper>() {
                @Override
                public int compare(ByteArrayWrapper o1, ByteArrayWrapper o2) {

                    long sequence1 = o1.getSequence();
                    long sequence2 = o2.getSequence();
                    if(sequence1 < sequence2){
                        return -1;
                    }else if(sequence1 > sequence2){
                        return 1;
                    }
                    else{
                        return 0;
                    }
                }
            });


            byte[] totalBytes = list.get(0).getBytes();
            byte[] firstArr = null;
            byte[] secondArr = null;
            for(int i = 1;i<list.size();i++){
                firstArr = totalBytes;
                secondArr = list.get(i).getBytes();
                totalBytes = concat(firstArr, secondArr);

            }

            System.out.println(totalBytes.length);
            convertToFile(totalBytes,"c:\\tmp\\test.pdf");

            long endTime = System.currentTimeMillis();
            System.out.println("Total time taken with "+numberOfThreads +" threads is "+(endTime-startTime)+" ms" );

        }
    }

    private byte[] concat(byte[] A, byte[] B) {
       byte[] C= new byte[A.length+B.length];
       System.arraycopy(A, 0, C, 0, A.length);
       System.arraycopy(B, 0, C, A.length, B.length);
       return C;
    }

    private void convertToFile(byte[] totalBytes,String name) {
        try {
            FileUtils.writeByteArrayToFile(new File(name), totalBytes);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

Код ByteArrayWrapper

package com.filedownloader;

import java.io.Serializable;

public class ByteArrayWrapper  implements Serializable{

    private static final long serialVersionUID = 3499562855188457886L;

    private byte[] bytes;
    private long sequence;

    public ByteArrayWrapper(byte[] bytes, long sequenceNo) {
        this.bytes = bytes;
        this.sequence = sequenceNo;
    }

    public byte[] getBytes() {
        return bytes;
    }

    public long getSequence() {
        return sequence;
    }
}

Код FileDownloaderTask

import java.util.List;


public class FileDownloaderTask implements Runnable {

    private List<ByteArrayWrapper> list;
    private long start;
    private long end;
    private long sequenceNo;

    public FileDownloaderTask(long start,long end,long sequenceNo,List<ByteArrayWrapper> list) {
        this.list = list;
        this.start = start;
        this.end = end;
        this.sequenceNo = sequenceNo;
    }

    @Override
    public void run() {
        ByteArrayWrapper wrapper = new FileSender().sendBytesOfFile(start, end, sequenceNo);
        list.add(wrapper);
    }
}

Вопросы, связанные с этим кодом

  1. Быстро ли загружается файл при использовании нескольких потоков? В этом коде я не вижу преимущества.

  2. Как мне решить, сколько потоков я должен создать?

  3. Являются ли они любыми библиотеками с открытым исходным кодом, которые делают это

  4. Файл, который получает получатель файла, действителен и не поврежден, но контрольная сумма (я использовал FileUtils из common-io) не соответствует. В чем проблема?

  5. Этот код освобождает память при использовании с большим файлом (более 100 МБ), т. Е. Потому что создается байтовый массив. Как я могу избежать?

Я знаю, что это очень плохой код, но я должен написать это за один день - :). Пожалуйста, предложите любой другой хороший способ сделать это?

Ответы [ 4 ]

1 голос
/ 29 апреля 2010

Здесь есть куча вопросов, на которые нужно ответить. Я не собираюсь проходить весь код, но могу дать вам несколько советов.

Прежде всего, некоторые ускорители загрузки действительно используют заголовок HTTP Range для параллельной загрузки частей файла. Почему это работает? TCP пытается выделить полосу пропускания справедливо на соединение . Таким образом, если вы скачиваете файл с сервера, пропускная способность которого завышена, вы можете получить большую долю пропускной способности, добавив больше соединений. Тот же принцип применяется к серверам, которые ограничивают исходящую пропускную способность, которая обычно также применяется к каждому соединению (иногда принимая во внимание IP).

Очевидно, что если бы все это делали, у нас оставалось бы много TCP-соединений и их накладных расходов, а не большая пропускная способность для фактической загрузки, поэтому даже эти ускорители загрузки будут использовать только 2- 4 соединения. Более того, если вы пишете сервер, вам не нужно об этом беспокоиться, так как вы будете только замедлять себя (добавляя дополнительные издержки).

Выход из памяти: не используйте байтовый массив, используйте (буферизованный) InputStream (или, если у вас есть время, научитесь использовать java.nio и байтовые буферы) и читайте куски во время отправки файл. Java-учебники охватывают все основы .

1 голос
/ 29 апреля 2010

1) Другая причина, по которой несколько соединений могут быть быстрее, связана с размером окна TCP.

throughput <= window size / roundtrip time

Подробнее см. http://en.wikipedia.org/wiki/TCP_tuning#Window_size.

Вы не увидите такой большой разницы, если будете запускать тесты в локальной сети, потому что время приема-передачи достаточно мало.

2) Единственный способ узнать наверняка - попробовать. И правильное количество потоков будет зависеть от окружающей среды. Если вам нужно загрузить действительно большие файлы, возможно, стоит сначала запустить небольшую программу калибровки, которая попытается загрузить с другим количеством потоков.

3) Я давно не заглядывал туда, но у Azureus (теперь он называется Vuze) есть довольно полный API для загрузки чего-либо из торрент-файлов на FTP ... И они, вероятно, имеют довольно эффективную реализацию ...

Удачи!

Редактировать (уточнение размера окна):

То, что вы пытаетесь сделать, это максимизировать пропускную способность (загрузка файлов быстрее). Вы не можете ничего сделать с поездкой в ​​оба конца, это зависит от сети. Что вы можете сделать, это увеличить размер окна. Размер окна настраивается автоматически (имеется множество документов по этому вопросу, но мне лень его гуглить), чтобы он наилучшим образом соответствовал текущему состоянию сети. Как правило, большее окно означает лучшую пропускную способность, если нет перегрузки или потери пакетов.

В лучшем случае вы получите размер окна 64 Кбит, на данный момент, если вы не используете некоторые приемы (Jumbo frame / window scaling), которые не поддерживаются всеми маршрутизаторами в Интернете, вы застрянете по максимуму пропускная способность:

throughput >= 64Kbit / roundtrip time

Поскольку вы не можете получить большее окно, вы должны открыть несколько окон, чтобы обойти это ограничение.

Примечания:

  • Как сказал aioobe, UDP не подвержен тем же ограничениям, это одна из причин, по которой он более эффективен.
  • Очень эффективным и масштабируемым протоколом для распространения больших файлов является Bittorrent. Пока вам не нужна аутентификация / авторизация загрузок, это может работать для вас. И если вам нужна авторизация, вы всегда можете зашифровать файлы ...
0 голосов
/ 30 апреля 2010

Не считывайте огромные куски файлов в память. Не удивительно, что у тебя кончается. Просто найдите нужную позицию в файле и начните копирование через буфер разумного размера:

int count;
byte[] buffer = new byte[8192];
// or whatever takes your fancy, but sizes > the socket send buffer size are pointless
while ((count = in.read(buffer)) > 0)
  out.write(buffer, 0, count);
out.close();
in.close();

Одну и ту же логику можно использовать на обоих концах - при записи файла в приемник используйте RandomAccessFile и ищите соответствующее смещение перед запуском этого цикла.

Однако, как отметили другие респонденты, требование клиента действительно довольно бессмысленно. Он ничего не покупает, кроме расходов и рисков. Я бы просто передавал файл через одно соединение.

Что вы должны сделать, это установить большие сокеты для буфера отправки и получения на обоих концах, например 60k. По умолчанию 8k в Windows, что бесполезно мало.

0 голосов
/ 29 апреля 2010

1 Быстро ли загружается файл при использовании нескольких потоков?В этом коде я не вижу преимущества.

Нет.Я был бы очень удивлен, если бы это было так.У ЦП никогда не будет проблем с запитыванием сетевого буфера.

2 Как мне решить, сколько потоков я должен создать?

В моеммнение, 0 лишних тем.

4 Файл, который получает получатель файла, действителен и не поврежден, но контрольная сумма (я использовал FileUtils из common-io) не совпадает.В чем проблема?

Убедитесь, что вы случайно не полагаетесь на строки и определенные кодировки.

5 Этот код выделяется из памяти при использовании с большим файлом (см. Выше100 Мб) т.е. потому что байтовый массив который создан.Как я могу избежать?

Очевидным решением будет чтение небольших кусков файла.Взгляните на метод чтения DataInputStream

http://java.sun.com/j2se/1.4.2/docs/api/java/io/DataInputStream.html#read%28byte[],%20int,%20int%29

И, наконец, несколько общих указаний по этому вопросу: вместо использования нескольких потоков для такого рода вещей я настоятельно рекомендую вамвзглянуть на пакет java.nio, в частности java.nio.channels и класс Selector.

РЕДАКТИРОВАТЬ: если вы действительно хотите получить его супер-эффективный, и иметь очень большие файлы, вы могли бы извлечь выгоду из использования UDP, и обрабатывать порядок пакетов и подтверждения самостоятельно.TCP, например, гарантирует, что полученные пакеты приходят в том же порядке, что и отправленные пакеты.Это не то, на что вы сильно полагаетесь (поскольку вы можете легко кодировать «смещение в байтах» для каждой дейтаграммы самостоятельно) и, следовательно, вам не нужно «платить» за него.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...