Как быстро загрузить список сообщений с почтового сервера?

190
15 июня 2018, 03:10

Занимаюсь разработкой почтового клиента под ОС Андроид с помощью C# и Xamarin Forms. Работа с сервером и протокол реализован, однако при создании GUI возникла проблема со скоростью подгрузки сообщений. Опыта разработки асинхронных и многопоточных приложений у меня нет, что как раз и мешает мне разобраться с этой проблемой. Листинг метода подргузки сообщений:

    private async Task<List<ImapMessageInfo>> LoadMessagesInfo()
    {
        foreach (var item in uidsCash)
        {
            await Task.Run(() =>
            {
                var header = streamOperator.ReceiveResponse($"$ UID FETCH {item} BODY[HEADER]", s => s.Contains("$ OK"))
                    .Where(n => !n.StartsWith("* ") && !n.StartsWith("$ ") && !(n == ")")).ToList();
                if (header.FirstOrDefault().StartsWith("$ OK"))
                    //throw new InvalidDataException($"Message #{item} was removed or not created");
                    return;
                var body = streamOperator.ReceiveResponse($"$ UID FETCH {item} BODY[TEXT]", s => s.Contains("$ OK"))
                    .Where(n => !n.StartsWith("* ") && !n.StartsWith("$ ") && !(n == ")")).ToList();
                var subject = streamOperator
                    .ReceiveResponse($"$ UID FETCH {item} BODY[HEADER.FIELDS (Subject)]", s => s.Contains("$ OK"))
                    .Where(n => !n.StartsWith("* ") && !n.StartsWith("$ ") && !(n == ")")).ToList();
                MessageModel msg = ParseBodyHeader(item, header, string.Join("\r\n", subject));
                ImapMessageInfo info = new ImapMessageInfo(msg)
                {
                    Flags = GetFlags(item)
                };
                info.Message.TextHtml = this.LoadText(header, body, "text/html");
                info.Message.TextPlain = LoadText(header, body, "text/plain");
                messagesCash.Add(info);
            });
        }
        return messagesCash.OrderByDescending(s => int.Parse(s.Message.Uid)).ToList();
    }

Данный код позволяет не зависать графическому интерфейсу, однако сам процесс загрузки никак не ускоряет - всего 10 сообщений загружается примерно за 10 секунд. Есть ли возможность ускорить этот процесс до приемлимого для приложения времени?

Обращение к серверу происходит в методе streamOperator.ReceiveResponse

UPD: В моем коде есть один стрим для чтения и записи файла с сервера. При многопоточном его использовании возникает исключение Mono.Security.Protocol.Tls.TlsException: Bad record MAC. Как сделать обращение к серверу многопоточным? Нужно ли для каждого потока создавать свой стрим, чтобы читать данные с сервера одновременно в нескольких потоках?

UPD2: Я обновил свой код в соответствии с предложениями в комментариях.

private async Task<List<ImapMessageInfo>> LoadMessagesInfo()
    {
        var tasks = uidsCash.Select(item => Task.Run (async () =>
        {
            TcpClient tcpClient = new TcpClient("imap.gmail.com", 993);
            SslStream ssl = new SslStream(tcpClient.GetStream());
            ssl.AuthenticateAsClient("imap.gmail.com");
            StreamOperator streamOperator1 = new StreamOperator(ssl);
            await streamOperator1.ReceiveResponse("", s => true);
            await streamOperator1.ReceiveResponse($"$ LOGIN {login} {password}", s => s.Contains("$ OK"));
            await streamOperator1.ReceiveResponse("$ SELECT INBOX", s => s.Contains("$ OK"));
            var headerAsync = await streamOperator1.ReceiveResponse($"$ UID FETCH {item} BODY[HEADER]", s => s.Contains("$ OK"));
            var header = headerAsync.Where(n => !n.StartsWith("* ") && !n.StartsWith("$ ") && !(n == ")")).ToList();
            if (header.FirstOrDefault().StartsWith("$ OK"))
                //throw new InvalidDataException($"Message #{item} was removed or not created");
                return;
            var bodyAsync = await streamOperator1.ReceiveResponse($"$ UID FETCH {item} BODY[TEXT]", s => s.Contains("$ OK"));
            var body = bodyAsync
                .Where(n => !n.StartsWith("* ") && !n.StartsWith("$ ") && !(n == ")")).ToList();
            var subjectAsync = await streamOperator1
                .ReceiveResponse($"$ UID FETCH {item} BODY[HEADER.FIELDS (Subject)]", s => s.Contains("$ OK"));
            var subject = subjectAsync
                .Where(n => !n.StartsWith("* ") && !n.StartsWith("$ ") && !(n == ")")).ToList();
            MessageModel msg = ParseBodyHeader(item, header, string.Join("\r\n", subject));
            ImapMessageInfo info = new ImapMessageInfo(msg)
            {
                Flags = GetFlags(item)
            };
            info.Message.TextHtml = this.LoadText(header, body, "text/html");
            info.Message.TextPlain = LoadText(header, body, "text/plain");
            lock (o)
                messagesCash.Add(info);
        }));
        await Task.WhenAll(tasks);
        //foreach (var item in uidsCash)
        //{
        //}
        return messagesCash.OrderByDescending(s => int.Parse(s.Message.Uid)).ToList();
    }

Однако при обращению с сервера даже с разных стримов возникает ошибка NotSupportedException: Вызов метода BeginWrite невозможен, если другая операция write находится в режиме ожидания. Метод работы с сервером:

 public async Task<List<string>> ReceiveResponse(string query, Func<string, bool> isLastLine)
    {
        try
        {
            if(query != string.Empty)
            {
                byte[] buffer = Encoding.ASCII.GetBytes(query + "\r\n");
                await stream.WriteAsync(buffer, 0, buffer.Length);
            }
        }
        catch
        {
            throw;
        }
        try
        {
            StringBuilder sb = new StringBuilder();
            string @string;
            do
            {
                byte[] buffer = new byte[2048];
                int bytesGot = await stream.ReadAsync(buffer, 0, buffer.Length);
                if (bytesGot == 0)
                {
                    throw new EndOfStreamException("Error while reading");
                }
                @string = Encoding.UTF8.GetString(buffer).Replace("\0", string.Empty);
                sb.Append(@string);
            } while (!isLastLine(@string));
            return sb.ToString().Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries).ToList();
        }
        catch
        {
            throw;
        }
    }

Вот так пробую вызывать всю эту систему:

var resp = Task.Run(() => imap.LoadMessages());
        Task.WaitAny(resp);
        var from = resp.Result;

Метод LoadMessages вызывает LoadMessageInfo:

public async Task<List<ImapMessageInfo>> LoadMessages(/*DateTime since*/)
    {
        if (this.messagesCash != null)
            return this.messagesCash;
        messagesCash = new List<ImapMessageInfo>();
        if (uidsCash == null)
            uidsCash = await this.GetExistingUids();
        return await LoadMessagesInfo();
    }
Answer 1

Вы запускаете таски одну за другой, то есть пока текущая задача не кончилась, новая не начнется.

Попробуйте запустить их параллельно, например:

private async Task<List<ImapMessageInfo>> LoadMessagesInfo()
{
    var tasks = uidsCash.Select(item => Task.Run(() => {/*..Ваш код..*/})).ToArray();
    await Task.WhenAll(tasks);  
    return messagesCash.OrderByDescending(s => int.Parse(s.Message.Uid)).ToList();
}
READ ALSO
Как использовать Instagram API в ASP.NET MVC?

Как использовать Instagram API в ASP.NET MVC?

При использовании Facebook API проблем не возникло - я просто передавал необходимые запросы в готовые методы, и метод возвращал jsonobject который я конвертил...

218
Выбор if else внутри конструктора

Выбор if else внутри конструктора

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

226
Запрос ввода из нового окна

Запрос ввода из нового окна

Как в Windows Forms организовать ввод данных из нового окна? При этом, нужно, чтобы введенные данные после нажатия кнопки, допустим "ОК", выводились...

182
Хранение бд в папке с программой

Хранение бд в папке с программой

Появилась необходимость переносной проги, без использования Managment StudioВроде как создал локальную бд с типом

170