Многопоточное скачивание файла

Здравствуйте.

Есть такие сайты, с которых ну просто ооооооочень медленно качается (почему-то). Но при этом поддерживается многопоточное скачивание (почему-то).
Вот накидал код:

    public partial class Form1 : Form
    {
        public class ThreadDownload
        {
            public ThreadDownload()
            {

            }

            public void SetRange(Int64 byteFrom, Int64 byteTo)
            {

            }

            public void Run()
            {
                FileDownloader downloader = new FileDownloader();
                downloader.OnWorkProgress += EventWork;

                //тут скачиваем кусок файла и каждые N байт синхронизируемся
            }

            private void EventWork(object sender, Int64 bytesTransfered)
            {
                //тут надо как-то пройтись по всем потокам и узнать, сколько они скачали
                //и вывести это на форму
            }
        }

        public class ThreadFather
        {

            public string url;
            public string outputFileName;

            public int threadCount;
            private List<ThreadDownload> threads = new List<ThreadDownload>();

            public ThreadFather()
            {

            }

            public void Run()
            {
                //TODO: получить размер скачиваемого файла

                for (int i = 0; i < threadCount; i++)
                {
                    ThreadDownload thr = new ThreadDownload();
                    //TODO: вычислить позицию каждого куска
                    thr.SetRange(0, 1023);
                    threads.Add(thr);
                }

                for (int i = 0; i < threads.Count; i++)
                {
                    Thread thread = new Thread(threads[i].Run);
                    //TODO: создать обработчики событий
                    thread.Start();
                }

                //как подождать, пока все потоки докачаются?


                //TODO: склеить все куски в один файл
            }
        }

        private void btnDownload_Click(object sender, EventArgs e)
        {
            ThreadFather father = new ThreadFather();
            father.url = "some url";
            Thread thread = new Thread(father.Run);
            thread.Start();
        }
    }

Проблема здесь


            private void EventWork(object sender, Int64 bytesTransfered)
            {
                //тут надо как-то пройтись по всем потокам и узнать, сколько они скачали
                //и вывести это на форму
            }
        }

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

Это как? Что за сайт?

ютуб. С него некоторые видео качаются очень медленно и рывками. Качнёт 100кб и ждет 5 секунд, качнёт еще 100 и опять ждёт кого-то.

что как?
если качать тот же файл в многопотоке, то быстро получается.

А это надо?

Надо же просто взять размер из Content-Length, разделить на потоки, каждым отправить соотв. Range в заголовке, дождаться завершения всех.

а прогресс никуда не надо вывести? Как мы узнаем, сколько уже скачалось?

Для вывода прогресса можно не изобретать ничего, а взять готовый Progress<T>.

        public class ProgressItem
        {
            public int Id { get; }
            public int Processed { get; }
            public int Total { get; }

            public ProgressItem(int id, int processed, int total)
            {
                Id = id;
                Processed = processed;
                Total = total;
            }
        }
        public int DownloadHead()
        {
            Thread.Sleep(1000);

            int contentLength = 100542;
            return contentLength;
        }

        public void DownloadRange(int id, IProgress<ProgressItem> progressReporter,
                CancellationToken cancellationToken, int start, int end)
        {
            int current = start;
            int bufSize = 1000;

            while (current < end)
            {
                cancellationToken.ThrowIfCancellationRequested();

                Thread.Sleep(500);

                current += Math.Min(bufSize, end - current);

                progressReporter.Report(new ProgressItem(id, current - start, end - start));
            }
        }
        private CancellationTokenSource _cancellationTokenSource;

        private async void button1_Click(object sender, EventArgs e)
        {
            _cancellationTokenSource = new CancellationTokenSource();
            var cancellationToken = _cancellationTokenSource.Token;

            int contentLength;
            try
            {
                contentLength = await Task.Run(DownloadHead);
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
                return;
            }

            var progress = new Progress<ProgressItem>();
            var threadProgressDict = new Dictionary<int, ProgressItem>();
            progressBar1.Maximum = contentLength;
            progress.ProgressChanged += (s, p) =>
            {
                threadProgressDict[p.Id] = p;

                Debug.WriteLine($"Thread #{p.Id}: {p.Processed} / {p.Total}");

                progressBar1.Value = threadProgressDict.Values.Select(it => it.Processed).Sum();
            };

            int chunkSize = contentLength / 10;

            var tasks = Split(contentLength, chunkSize)
                .Select((range, id) => Task.Run(() => DownloadRange(id, progress, cancellationToken, range.Item1, range.Item2)));

            try
            {
                await Task.WhenAll(tasks);

                MessageBox.Show("Downloaded");
            }
            catch (OperationCanceledException)
            { }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }

        private void btnCancel_Click(object sender, EventArgs e)
        {
            _cancellationTokenSource.Cancel();
        }

        public IEnumerable<Tuple<int, int>> Split(int length, int chunkSize)
        {
            int current = 0;
            while (current < length)
            {
                int currentChunkSize = Math.Min(chunkSize, length - current);
                yield return new Tuple<int, int>(current, current + currentChunkSize);
                current += currentChunkSize;
            }
        }

не пойму, почему примерно в середине процесса выдаёт это

Thread #4: 10000 / 10054
Thread #10: 2 / 2
Thread #9: 1000 / 10054

Что значит 2 / 2? По второму кругу заходит? Но я в коде не вижу, чтобы качалка два раза запускалась.

Так тут выводится сколько каждый поток скачал и сколько ему надо всего.

2/2 потому что

делится на 10 11 частей самым простым способом — 10 раз по 10054, и еще остаток 2. Последнему потоку досталось только 2 байта.

Эх, ну вроде что-то как-то работает. Еще не до конца разобрался, но тестовый файл качается.
И кстати, у вас в коде ошибка :grinning:

надо так:

yield return new Tuple<int, int>(current, current + currentChunkSize - 1);

Иначе, у всех чанков, кроме последнего, будет скачиваться один лишний байт.

А что если сервер не захочет вернуть размер файла? Тогда и размер чанка узнать не получится. В таком случае, что тут вернуть, чтобы он одним потоком качал? Или просто отдельное ветвление делать?
Да, знаю. Прогрессбар тогда никак не сделать, ну и ладно.

А так бывает с этим сервером?
Тогда ж все равно непонятно какие Range использовать.

А если теоретически, то он может вообще проигнорировать Range и весь файл отдать.
https://stackoverflow.com/a/27116999/964478

Бывает. И если качать не только с него - тоже может быть.

на Delphi можно поставить -1 и он до конца качает. Может оно просто на какой-нибудь MaxInt заменяется?

Тогда Range не нужен вообще в таких случаях.
Или вроде бы там можно указать 0-

Ну и по идее всегда надо проверять сколько на самом деле сервер отдал.

значит отдельное ветвление делать?

переписал ваш код вот так:


        public IEnumerable<Tuple<int, int>> Split2(int length, int chunkCount)
        {
            int chunkSize = length / chunkCount;
            for (int i = 0; i < chunkCount; i++)
            {
                int startPos = chunkSize * i;
                int endPos = i == chunkCount - 1 ? length : (startPos + chunkSize - 1);
                yield return new Tuple<int, int>(startPos, endPos);
            }
        }

Теперь файл делится на столько чанков, сколько укажешь. Без последнего ненужного маленького.

1 лайк

Вроде бы работает, только если длина == 0 или количество меньше длины, то получается фигня ) Можно добавить проверки перед циклом.

using System;
using System.Linq;
using System.Collections.Generic;

public class Program
{
	public static void Main()
	{
		Test(100, 2, new [] { Range(0, 49), Range(50, 100) });
		Test(100, 3, new [] { Range(0, 32), Range(33, 65), Range(66, 100) });
		Test(101, 3, new [] { Range(0, 32), Range(33, 65), Range(66, 101) });
		Test(2, 3, new [] { Range(0, 2) });
		Test(0, 3, new Tuple<int, int>[] { });
	}
	
	public static void Test(int length, int chunkCount, IEnumerable<Tuple<int, int>> expected)
	{
		var result = Split2(length, chunkCount);
		if (!expected.SequenceEqual(result))
		{
			Console.WriteLine($@"length={length}, chunkCount={chunkCount}
Expected: [{String.Join(", ", expected)}] 
Got: [{String.Join(", ", result)}]
");
		}
	}
	
	public static Tuple<int, int> Range(int it1, int it2)
	{
		return new Tuple<int, int>(it1, it2);
	}

	public static IEnumerable<Tuple<int, int>> Split2(int length, int chunkCount)
	{
		int chunkSize = length / chunkCount;
		for (int i = 0; i < chunkCount; i++)
		{
			int startPos = chunkSize * i;
			int endPos = i == chunkCount - 1 ? length : (startPos + chunkSize - 1);
			yield return new Tuple<int, int>(startPos, endPos);
		}
	}
}
length=2, chunkCount=3
Expected: [(0, 2)] 
Got: [(0, -1), (0, -1), (0, 2)]

length=0, chunkCount=3
Expected: [] 
Got: [(0, -1), (0, -1), (0, 0)]

https://dotnetfiddle.net/QHhIoh

Я знаю. Просто еще не дошёл до этого.
Еще будет ошибка, если размер файла будет меньше количества чанков или близким к нему.

почему? :thinking:
наоборот же - больше длинны.
Количество чанков/потоков должно быть, как минимум, в 2-3 тысячи раз меньше размера файла.
Если при этом получается 0, значит что файл слишком маленький и его надо качать одним потоком.
упс :flushed:

Здесь надо еще от размера файла отнять единицу


        public IEnumerable<Tuple<int, int>> Split(int length, int chunkCount)
        {
            if (chunkCount <= 1)
            {
                yield return new Tuple<int, int>(0, length - 1);
                yield break;
            }
            int chunkSize = length / chunkCount;
            for (int i = 0; i < chunkCount; i++)
            {
                int startPos = chunkSize * i;
                int endPos = i == chunkCount - 1 ? (length - 1) : (startPos + chunkSize - 1);
                yield return new Tuple<int, int>(startPos, endPos);
            }
        }

Заодно добавил проверку на 0. Теперь этот же код может в один поток качать.

Допилил код до презентабельного состояния

Пример использования есть в коде главной формы.