Powolny transfer pliku na Raspberry Pi

0

Posiadam prostą aplikację webową napisaną w technologii ASP.NET, która niegdyś działała na komputerze PC z Windows XP, później przeniosłem ją na Raspberry Pi. Swego czasu, zamiast tworzyć aplikacje od nowa, postanowiłem spróbować uruchomić ASP.NET i udało się wykorzystując serwer NginX i Mono. System operacyjny to Raspbian, przeprowadziłem aktualizację za pomocą apt-get update i apt-get upgrade.

Niedawno chciałem dorobić upload plików, tzn, że użytkownik wybiera pliki i są one wysyłane na serwer.

Wszystko działa poprawnie, ale transfer pliku jest niezmiernie wolny i w tym jest problem. Upload zrealizowałem w ten sposób, że plik oczytuje się w segmentach o ustalonej wielkości za pomocą Javascript FileApi i każdy segment wysyła się na serwer poprzez wywołanie AJAX, metoda POST.

Testowałem na pliku ze zdjęciem o wielkości niecałe 10MB i uzyskałem takie czasy:
Transfer przez FTP: 5 sekund
Wielkość segmentu 100k: 32 sekundy
Wielkość segmentu 1MB: 25 sekund
Wielkość segmentu 10MB (jedna iteracja przy wysyłaniu): 25 sekund

W przypadku wysyłania przez moją aplikację, na Raspberry Pi podczas transferu pliku było 100% obciążenie procesora.

Za pomocą obiektu System.Diagnos.Stopwatch stwierdziłem, że cały proces na serwerze trwa ok. 4 sekundy.

Konkretnie, test czasu zrobiłem dla wysyłania w jednej iteracji z tego powodu, że jeżeli JavaScript dostanie odpowiedź inną niż "OK", to wyświetla odpowiedź i przerywa proces wysyłania.

Na komputerze PC uzyskałem takie czasy:
1>0
2>0
2>173
3>173
4>181
5>184
6>184
7>184

Na Raspberry Pi czasy były takie:
1>0
2>0
2>3743
3>3748
4>3899
5>3899
6>3900
7>3900

Wygląda na to, że najwięcej czasu wchłania sam transfer zapytania z PC do Raspberry Pi. Oba komputery są wpięte bezpośrednio do tej samej sieci, więc mogą wykorzystać pełne możliwości Ethernetu.

Załączam kody źródłowe odpowiadające za wysyłanie plików.

Cały kod JavaScript (przycisk ładujący pliki wywołuje funkcję FileUploadStart):

// Kodowanie tekstu do przeslania
function HttpQueryStringEncode(s)
{
    return encodeURIComponent(s).replace(/\-/g, "%2D").replace(/\_/g, "%5F").replace(/\./g, "%2E").replace(/\!/g, "%21").replace(/\~/g, "%7E").replace(/\*/g, "%2A").replace(/\'/g, "%27").replace(/\(/g, "%28").replace(/\)/g, "%29");
}

// Wywolanie AJAX na podstawie http://kursjs.pl/kurs/ajax/ajax.html
function ajax(options)
{
    options = {
        type: options.type || "POST",
        url: options.url || "",
        onComplete: options.onComplete || function () { },
        onError: options.onError || function () { },
        onSuccess: options.onSuccess || function () { },
        dataType: options.dataType || "text",
        sendData: options.sendData || ""
    };

    var xml = new XMLHttpRequest();
    xml.open(options.type, options.url, true);

    xml.onreadystatechange = function ()
    {
        if (xml.readyState == 4)
        {
            if (httpSuccess(xml))
            {
                var returnData = (options.dataType == "xml") ? xml.responseXML : xml.responseText
                options.onSuccess(returnData);
            }
            else
            {
                options.onError(xml.status + " - " + xml.statusText);
            }
            options.onComplete();
            xml = null;
        }
    };

    if (options.sendData)
    {
        xml.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
        xml.send(options.sendData);
    }
    else
    {
        xml.send();
    }

    function httpSuccess(r)
    {
        try
        {
            return (r.status >= 200 && r.status < 300 || r.status == 304 || navigator.userAgent.indexOf("Safari") >= 0 && typeof r.status == "undefined")
        }
        catch (e)
        {
            return false;
        }
    }
}


// Lista obiektow plikow
var FileList = [];

// Wielkosc jednego segmentu
var FileChunkSize = 1000000;

// Iterator listy plikow
var FileListCurrent = 0;

// Liczba plikow
var FileListCount = 0;

// Identyfikator pliku
var FileListName = [];

// Wielkosc pliku
var FileListSize = [];

// Polozenie poczatku segmentu
var FileListOffset = [];

// Okresla, czy trwa ladowanie danych
// 0 - Brak ladowania
// 1 - Trwa ladowanie
// 2 - Ladowanie zatrzymane przez uzytkownika
var FileWorking = 0;

// Rozpoczecie ladowania plikow
function FileUploadStart(evt)
{
    FileChunkSize = parseInt(document.getElementById("ctl00_ContentPlaceHolder1_ChunkSize").value);
    FileWorking = 1;

    // Inicjalizacja wysylania
    ajax({
        type: "POST",
        url: "AjaxCore.aspx",
        sendData: "command=uploadinit&name=" + HttpQueryStringEncode(FileListName[FileListCurrent]),
        onSuccess: function (msg)
        {
            if (msg == "OK")
            {
                FileUploadResult(msg, 0);
            }
            else
            {
                FileUploadResult(msg, 1);
            }
        },
        onError: function (msg)
        {
            FileUploadResult("AJAX ERROR: " + msg, 1);
        }
    });

    // Po kazdym odebraniu segmentu nastepuje przesuniecie wskaznika o dlugosc segmentu,
    // wiec po wyslaniu pustego segmentu nalezy przesunac wskaznik do tylu,
    // zeby przed wyslaniem pierwszego segmentu wskaznik byl przesuniety na poczatek pliku
    FileListOffset[FileListCurrent] = 0 - FileChunkSize;
}

// Przerwanie ladowania plikow
function FileUploadStop(evt)
{
    FileWorking = 2;
    FileUploadStatus();
}

// Wysylanie jednego segmentu - odczyt pliku na podstawie http://www.html5rocks.com/en/tutorials/file/dndfiles/
function FileUploadChunk()
{
    // Aktualizacja statusu wysylania
    FileUploadStatus();

    // Sprawdzanie, czy ladowanie segmentow nie zostalo zakonczone lub przerwane
    // Sprawdzenie poprawnosci wielkosci segmentu - poprawny to od 1B do 256MB
    if ((FileWorking != 1) || (FileChunkSize <= 0) || (FileChunkSize > 268435456))
    {
        FileWorking = 0;
        return;
    }

    // Obliczanie wielkosci segmentu
    var CurrentChunkSize = FileChunkSize;
    if ((FileListOffset[FileListCurrent] + CurrentChunkSize) > FileListSize[FileListCurrent])
    {
        CurrentChunkSize = FileListSize[FileListCurrent] - FileListOffset[FileListCurrent];
    }

    // Odczyt segmentu z pliku w sposob asynchroniczny
    var reader = new FileReader();
    reader.onerror = function (evt)
    {
        switch (evt.target.error.code)
        {
            case evt.target.error.NOT_FOUND_ERR: Print("File Not Found!"); break;
            case evt.target.error.NOT_READABLE_ERR: Print("File is not readable"); break;
            case evt.target.error.ABORT_ERR: Print("Aborted"); break; // noop
            default: Print("An error occurred reading this file."); break;
        }
    };
    reader.onloadend = function (evt)
    {
        if (evt.target.readyState == FileReader.DONE)
        {
            // Obciecie poczatku segmentu - ciag "data:application/octet-stream;base64," lub podobny
            var RAW = evt.target.result;
            var RAWX = RAW.indexOf(',');
            if (RAWX > 0)
            {
                RAW = RAW.substring(RAWX + 1);
            }

            // Wywolanie zaladowania segmentu na serwer - segment jest wysylany w BASE64
            ajax({
                type: "POST",
                url: "AjaxCore.aspx",
                sendData: "command=upload&name=" + HttpQueryStringEncode(FileListName[FileListCurrent]) + "&data=" + RAW.replace(/\=/g, "%3D").replace(/\+/g, "%2B").replace(/\//g, "%2F"),
                onSuccess: function (msg)
                {
                    if (msg == "OK")
                    {
                        FileUploadResult(msg, 0);
                    }
                    else
                    {
                        FileUploadResult(msg, 1);
                    }
                },
                onError: function (msg)
                {
                    FileUploadResult("AJAX ERROR: " + msg, 1);
                }
            });
        }
    };
    var blob = FileList[FileListCurrent].slice(FileListOffset[FileListCurrent], FileListOffset[FileListCurrent] + CurrentChunkSize);
    reader.readAsDataURL(blob);
}

// Zdarzenie wybrania plikow - przygotowanie listy plikow i zerowanie statusu wyslania
function FileUploadSelect(evt)
{
    FileList = evt.files;
    FileListCount = FileList.length;
    FileListCurrent = 0;
    for (var i = 0; i < FileListCount; i++)
    {
        FileListOffset[i] = 0;
        FileListSize[i] = FileList[i].size;
        FileListName[i] = FileList[i].name;
    }
    FileUploadStatus();
}

// Aktualizacja listy plikow ze statusami zaladowania
function FileUploadStatus()
{
    document.getElementById("ctl00_ContentPlaceHolder1_FileUploaderStatus").innerHTML = "";
    var DispPercent = 0;
    for (var i = 0; i < FileListCount; i++)
    {
        if (FileListSize[i] > 0)
        {
            DispPercent = Math.floor(FileListOffset[i] * 100 / FileListSize[i]);
        }
        document.getElementById("ctl00_ContentPlaceHolder1_FileUploaderStatus").innerHTML += FileListName[i] + ": " + FileListOffset[i] + "/" + FileListSize[i] + " (" + DispPercent + "%)" + "<br />";
    }
}

function FileUploadResult(Result, Err)
{
    // Jezeli nastapil blad, to nalezy zatrzymac proces, wyswietlic komunikat i przeladowac strone
    if (Err)
    {
        FileWorking = 0;
        alert(Result);
        location.reload(true);
    }

    // Przesuwanie wskaznika pliku, sprawdzanie, czy caly plik zostal zaladowany
    FileListOffset[FileListCurrent] += FileChunkSize;
    if (FileListOffset[FileListCurrent] > FileListSize[FileListCurrent])
    {
        FileListOffset[FileListCurrent] = FileListSize[FileListCurrent];
        FileListCurrent++;
        if (FileListCurrent >= FileListCount)
        {
            // Jezeli przeslano wszystkie pliki, to nalezy zatrzymac prace i przeladowac strone
            FileWorking = 0;
            location.reload(true);
        }
    }

    // Odczyt i wyslanie nastepnego segmentu pliku
    FileUploadChunk();
}

Plik AjaxCore.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="AjaxCore.aspx.cs" Inherits="Web.AjaxCore" %>

Plik AjaxCore.aspx.cs

using System;
using System.Collections.Generic;
using System.IO;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace Web
{
    public partial class AjaxCore : System.Web.UI.Page
    {
        protected override void Render(HtmlTextWriter writer)
        {
            System.Diagnostics.Stopwatch SW = new System.Diagnostics.Stopwatch();
            string Info = "";
            SW.Start();

            
            // Odczyt sesji
            AppEnvironment Env = AppEnvironment.GetEnv(Session);

            // Czyszczenie odpowiedzi serwera
            Response.Clear();

            // Katalog na serwerze, w ktorym dokonuje sie operacji
            string UploadPath = Env.CurrentDir.Peek() + Env.DirSeparator;

            // Inicjalizacja wysylania - usuwanie ewentualnego pliku o tej samej nazwie, co wysylany
            if (Request.Form["command"] == "uploadinit")
            {
                // Sprawdzanie, czy mozna modyfikowac biezacy katalog
                if (Env.Root_[Env.CurrentRoot].Access)
                {
                    try
                    {
                        File.Delete(UploadPath + Request.Form["name"]);
                        Response.Write("OK");
                    }
                    catch (Exception E)
                    {
                        Response.Write("ERROR: " + E.Message);
                    }
                }
                else
                {
                    Response.Write("ERROR: No permissions");
                }
            }

            // Wysylanie segmentu pliku
            if (Request.Form["command"] == "upload")
            {
                Info = "1>" + SW.ElapsedMilliseconds.ToString() + "\n";

                // Sprawdzanie, czy mozna modyfikowac biezacy katalog
                if (Env.Root_[Env.CurrentRoot].Access)
                {
                    try
                    {
                        Info += "2>" + SW.ElapsedMilliseconds.ToString() + "\n";

                        // Odczyt segmentu na podstawie kodu BASE64
                        byte[] Chunk = Convert.FromBase64String(Request.Form["data"]);

                        Info += "2>" + SW.ElapsedMilliseconds.ToString() + "\n";

                        // Zapisanie segmentu do pliku
                        FileStream FS = new FileStream(UploadPath + Request.Form["name"], FileMode.Append, FileAccess.Write);

                        Info += "3>" + SW.ElapsedMilliseconds.ToString() + "\n";
                        
                        FS.Write(Chunk, 0, Chunk.Length);

                        Info += "4>" + SW.ElapsedMilliseconds.ToString() + "\n";

                        FS.Close();

                        Info += "5>" + SW.ElapsedMilliseconds.ToString() + "\n";

                        Response.Write("OK");
                    }
                    catch (Exception E)
                    {
                        Response.Write("ERROR: " + E.Message);
                    }
                }
                else
                {
                    Response.Write("ERROR: No permissions");
                }
            }
            Info += "6>" + SW.ElapsedMilliseconds.ToString() + "\n";


            // Zakladanie katalogu
            if (Request.Form["command"] == "dir")
            {
                // Kod niezwiazany z wysylaniem plikow
            }

            Info += "7>" + SW.ElapsedMilliseconds.ToString() + "\n";
            //if (Request.Form["command"] == "upload")
            //{
            //    Response.Write("\n" + Info);
            //}

            // Stale elementy odpowiedzi
            Response.AddHeader("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate");
            Response.AddHeader("Pragma", "no-cache");
            Response.AddHeader("Expires", "Fri, 01 Jan 1990 00:00:00 GMT");
            Response.End();
        }
    }
}

Aby serwer mógł działać, musi być proces uruchamiany tym poleceniem:
sudo fastcgi-mono-server4 /applications=/:. /socket=tcp:127.0.0.1:9000 /root=/usr/share/nginx/www

Co jest wąskim gardłem i jak to rozwiązać? Sposób obsługi AJAX wynalazłem metodą prób i błędów. Czy jest on poprawny? Może do obsługi AJAX jest lepszy inny sposób, niż plik obiekt WebForm z podmienioną metodą Render?

Czy napisanie całej aplikacji w PHP lub JSP (Java) rozwiąże problem i wydajność będzie większa?

0

Zamiast kombinować z Render, sensowniej byłoby użyć WebAPI albo chociaż web handlera (ashx), jeśli koniecznie chcesz mieć WebForms.

0
somekind napisał(a):

Zamiast kombinować z Render, sensowniej byłoby użyć WebAPI albo chociaż web handlera (ashx), jeśli koniecznie chcesz mieć WebForms.

Zamieniłem ASPX na ASHX we wszystkich miejscach, w których odpowiedź jest generowana w kodzie. Nic się nie zmieniło.

Mam jeszcze pytanie odnośnie uruchamiania serwera ASPX. Z grubsza, serwer mam skonfigurowany w sposób opisany na tej stronie: http://www.pihomeserver.fr/en/2013/04/07/raspberry-pi-home-server-installer-un-serveur-asp-net/
Na końcu jest napisane, że należy uruchomić fastcgi-mono-server4 z odpowiednimi parametrami.

Napisałem skrypt, który uruchamia ten serwer i utrzymuje go uruchomiony na wypadek, gdyby się zatrzymał z jakiegoś powodu:

#!/bin/bash
while true
do
 sudo fastcgi-mono-server4 /applications=/:. /socket=tcp:127.0.0.1:9000 /root=/usr/share/nginx/www
done

W jaki sposób sprawić, że jak się podłącza RPI do prądu, to ten plik się uruchomi zaraz po wystartowaniu systemu Raspbian, coś jak Autostart w Windows? W chwili obecnej, ja się loguję poprzez XRDP, otwieram konsolę, uruchamiam ten plik i zostawiam otwartą sesję, co jest skuteczne, ale mało rozsądne.

0

Zacząłem eksperymentować z PHP, korzystam z faktu, że w przypadku NginX w jednej aplikacji można łączyć ASP i PHP. Zrealizowałem eksperymentalny upload pliku w PHP i czas trwania ładowania to ok. 5 sekund, podobnie, jak w FTP. Czyli uploadowanie pliku mam w PHP, a całą resztę w ASPX. Możliwe, że z czasem całą aplikację przepiszę do PHP. Czy w serwerze IIS (Windows) takie połączenie też jest możliwe?

Okazuje się, że ASP.NET nie bardzo się nadaje do obsługi gigantycznych wywołań.

0
andrzejlisek napisał(a):

Zamieniłem ASPX na ASHX we wszystkich miejscach, w których odpowiedź jest generowana w kodzie. Nic się nie zmieniło.

Tu nie chodzi o zmianę rozszerzenia tylko o napisanie web handlera, czyli zaimplementowanie IHttpHandler, a nie dziedziczenie po Page.

andrzejlisek napisał(a):

Okazuje się, że ASP.NET nie bardzo się nadaje do obsługi gigantycznych wywołań.

Bardzo mocne stwierdzenie, jak na kogoś, kto ASP.NET nie używa.

Jak widać, najwięcej czasu w Twoim kodzie schodzi na Convert.FromBase64String. Dlaczego w ogóle to robisz?

0
somekind napisał(a):
andrzejlisek napisał(a):

Zamieniłem ASPX na ASHX we wszystkich miejscach, w których odpowiedź jest generowana w kodzie. Nic się nie zmieniło.

Tu nie chodzi o zmianę rozszerzenia tylko o napisanie web handlera, czyli zaimplementowanie IHttpHandler, a nie dziedziczenie po Page.

Oczywiście, że nie zmieniłem rozszerzenia, bo to nic nie zmieni. Zachowałem kod, skasowałem plik aspx, utworzyłem nowy ashx, dziedziczący po IHttpHandler i IRequiresSessionState, wkleiłem kod i go dostosowałem.

somekind napisał(a):
andrzejlisek napisał(a):

Okazuje się, że ASP.NET nie bardzo się nadaje do obsługi gigantycznych wywołań.

Bardzo mocne stwierdzenie, jak na kogoś, kto ASP.NET nie używa.

Ja myślę, że czym innym jest i zupełnie inaczej działa ASP.NET na IIS na Windows Server (komputer PC z procesorem wielordzeniowym), a czym innym jest ASP.NET na nginx zapewniany przez Mono na Raspbian (Raspberry Pi). Ja mam do czynienia z tym drugim przypadkiem. Zauważyłem, że RPi działa tak, że serwerem jest nginx, tylko jak przyjmuje zapytanie o aspx lub ashx, to za pomocą gniazd sieciowych wysyła zapytanie dalej do pracującego procesu FastCGI, który obsługuje ASP.NET, być może to wciąga te 15-20 sekund.

somekind napisał(a):

Jak widać, najwięcej czasu w Twoim kodzie schodzi na Convert.FromBase64String. Dlaczego w ogóle to robisz?

Fragment pliku wysyłam wykorzystując AJAX na podstawie http://kursjs.pl/kurs/ajax/ajax.html, a HTTP może przyjmować parametry wyłącznie w formie tekstu, co sprawia, że nie da się wysłać bezpośrednio danych binarnych. Przeglądarka w Javascript, odczytuje plik, zamienia na BASE64, wysyła na serwer, a serwer dokonuje konwersji odwrotnej za pomocą Convert.FromBase64String. Akurat wysyłanie pliku zrobiłem w PHP, konwersja również jest zachowana, jednak transfer jest dużo szybszy. Jeżeli da się przesłać dane binarne w postaci niezakodowanej, czyli bez konwersji do i z BASE64, to proszę wskazać, jak zmienić wywołanie AJAX bazując na kodzie JavaScript w pierwszym poście? Wtedy wystarczy zawartość Request.Form["data"] lub $_POST['data'] zapisać do pliku otwieranego w trybie "append", bez dodatkowych operacji.

0
andrzejlisek napisał(a):

Przeglądarka w Javascript, odczytuje plik, zamienia na BASE64, wysyła na serwer, a serwer dokonuje konwersji odwrotnej za pomocą Convert.FromBase64String.

W
T
F?!

Zaprzęgasz FileReadera do tego żeby odczytać plik tylko po to żeby go przesłać?
Zużywasz niepotrzebnie moc procesora u klienta, na serwerze i w dodatku zwiększasz o 1/3 ilość danych przesyłanych przez sieć (czyli niepotrzebnie jeszcze zapychasz łącze)
Jest dziesiątki sposobów żeby to zrobić normalnie, choćby użycie przeznaczonego do tego input type file do ramki albo ewentualnie https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Sending_and_Receiving_Binary_Data#section_3

0
Pijany Terrorysta napisał(a):
andrzejlisek napisał(a):

Przeglądarka w Javascript, odczytuje plik, zamienia na BASE64, wysyła na serwer, a serwer dokonuje konwersji odwrotnej za pomocą Convert.FromBase64String.

W
T
F?!

Zaprzęgasz FileReadera do tego żeby odczytać plik tylko po to żeby go przesłać?
Zużywasz niepotrzebnie moc procesora u klienta, na serwerze i w dodatku zwiększasz o 1/3 ilość danych przesyłanych przez sieć (czyli niepotrzebnie jeszcze zapychasz łącze)
Jest dziesiątki sposobów żeby to zrobić normalnie, choćby użycie przeznaczonego do tego input type file do ramki albo ewentualnie https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Sending_and_Receiving_Binary_Data#section_3

W takim razie, jak z pliku podanego w input type file odczytać nie całą treść, tylko jej wycinek? Wtedy bym próbował wysłać w sposób opisany w linku, który podałeś. Docelowo, ma być możliwe wysyłanie plików o kilku MB do ok. 3-4GB na plik. W małym pliku, wysyłanym w jednej iteracji, to input type file wystarczy, a tu chodzi o to, żeby wyświetlać postęp wysyłania.

0

robisz to tylko po to żeby pokazać postęp? W takim razie też jest już mnóstwo gotowych rozwiązań

https://www.google.pl/search?q=ajax+file+upload+with+progress

0
Pijany Terrorysta napisał(a):

robisz to tylko po to żeby pokazać postęp? W takim razie też jest już mnóstwo gotowych rozwiązań

https://www.google.pl/search?q=ajax+file+upload+with+progress

Zacząłem próbować na podstawie takiego przykładu:
http://jsfiddle.net/rudiedirkx/jzxmro8r/

Nawet już wiem, jak śledzić postęp i jak przerwać rozpoczęte ładowanie (metoda abort() w XMLHttpRequest), stwierdziłem, że tak naprawdę dane są wysyłane jeden raz jakby w całości, tylko XMLHttpRequest udostępnia zdarzenia umożliwiające śledzenie postępu i momentu zakończenia lub przerwania. Czy dobrze się domyślam, że w takim razie, w konfiguracji serwera HTTP muszę ustawić maksymalną wielkość zapytania na nieco większą niż maksymalna wielkość pliku, którą przewiduję wysyłać? Teoretycznie mógłbym ustawić 100GB i się nie przejmować, ale ograniczanie wielkości żądania chyba po coś jest. Po drugie, Raspberry Pi ma "zaledwie" 512MB pamięci RAM, więc nie utrzyma w sobie całego zapytania, jak wchodzi plik powiedzmy 2GB (a takie muszą dać się wysłać), zanim rozpocznie się proces obsługi żądania w PHP lub ASPX. Możnaby połączyć oba podejścia, czyli wysyłać plik w kawałkach po 100MB.

2
andrzejlisek napisał(a):

Ja myślę, że czym innym jest i zupełnie inaczej działa ASP.NET na IIS na Windows Server (komputer PC z procesorem wielordzeniowym), a czym innym jest ASP.NET na nginx zapewniany przez Mono na Raspbian (Raspberry Pi). Ja mam do czynienia z tym drugim przypadkiem.

No właśnie, więc to żadne ASP.NET tylko jego alternatywna implementacja.

Zauważyłem, że RPi działa tak, że serwerem jest nginx, tylko jak przyjmuje zapytanie o aspx lub ashx, to za pomocą gniazd sieciowych wysyła zapytanie dalej do pracującego procesu FastCGI, który obsługuje ASP.NET, być może to wciąga te 15-20 sekund.

Przecież Twoje własne logi mówią o tym, co tak długo trwa - dekodowanie stringa z BASE64, które jest zupełnie niepotrzebne.

Trochę się tym pobawiłem i nie rozumiem, czemu tak skomplikowałeś to wszystko...

HTML:

<input type="file" id="uploadFile" name="file" />
<a class="btn btn-primary" href="#" id="btnUpload">Upload file</a>

JS:

<script type="text/javascript">
    $(function () {
        $('#btnUpload').click(function () {
            UploadFile($('#fileUpload')[0].files);
        });
    });

    function UploadFile(selectedFile) {
        var file = selectedFile[0];

        var bufferSize = 1 * 1024 * 1024;
        var filePos = 0;
        var endPos = bufferSize;
        var parts = [];

        while (filePos < file.size) {
            parts.push(file.slice(filePos, endPos));
            filePos = endPos;
            endPos = filePos + bufferSize;
        }

        for (var i = 0; i < parts.length; i++) {
            var filePartName = file.name + ".part_" + i + "." + parts.length;
            UploadFilePart(parts[i], filePartName);
        }
    }

    function UploadFilePart(chunk, fileName) {
        var data = new FormData();
        data.append('file', chunk, fileName);
        $.ajax({
            type: "POST",
            url: 'http://localhost:59155/UploadFile/',
            contentType: false,
            processData: false,
            data: data
        });
    }
</script>

No i C#:

public class UploadFileHandler : IHttpHandler
{
    const string partDelimiter = ".part_";

    public bool IsReusable
    {
        get { return true; }
    }

    public void ProcessRequest(HttpContext context)
    {
        string uploadRootPath = context.Server.MapPath("~/App_Data/files");
        Directory.CreateDirectory(uploadRootPath);

        var request = context.Request;

        string originalFileName = GetOriginalFileName(request.Files[0].FileName);
        int allPartsCount = GetAllPartsCount(request.Files[0].FileName);

        foreach (string file in request.Files)
        {
            var filePart = request.Files[file];
            if (filePart != null && filePart.ContentLength > 0)
            {
                SavePart(filePart, uploadRootPath);

                var filePartsNames = GetAllPartNames(originalFileName, uploadRootPath);
                if (filePartsNames.Length == allPartsCount)
                {
                    MergeParts(originalFileName, uploadRootPath, filePartsNames);
                }
            }
        }
    }

    private static string GetOriginalFileName(string firstPartName)
    {
        string originalFileName = firstPartName.Substring(0, firstPartName.IndexOf(partDelimiter));
        return originalFileName;
    }

    private static int GetAllPartsCount(string firstPartName)
    {
        int allPartsCount = int.Parse(firstPartName.Substring(firstPartName.LastIndexOf(".") + 1));
        return allPartsCount;
    }

    private static void SavePart(HttpPostedFile filePart, string uploadRootPath)
    {
        string filePartPath = Path.Combine(uploadRootPath, filePart.FileName);

        if (File.Exists(filePartPath))
        {
            File.Delete(filePartPath);
        }

        using (var fileStream = File.Create(filePartPath))
        {
            filePart.InputStream.CopyTo(fileStream);
        }
    }

    private static string[] GetAllPartNames(string originalFileName, string uploadRootPath)
    {
        string searchPattern = Path.GetFileName(originalFileName) + "*";
        var filePartsNames = Directory.GetFiles(uploadRootPath, searchPattern);
        return filePartsNames;
    }

    static readonly Regex PartNumberRegex = new Regex(@"\.part_(?<number>\d+)\.\d+", RegexOptions.Compiled);

    private static void MergeParts(string originalFileName, string uploadRootPath, string[] filePartsNames)
    {
        string outputFilePath = Path.Combine(uploadRootPath, originalFileName);
        using (var outputStream = File.Create(outputFilePath))
        {
            var sortedNames = filePartsNames.OrderBy(GetNumberFromFilePartName).ToArray();

            foreach (var filePartName in sortedNames)
            {
                using (var inputStream = File.OpenRead(filePartName))
                {
                    inputStream.CopyTo(outputStream);
                }

                File.Delete(filePartName);
            }
        }
    }

    private static int GetNumberFromFilePartName(string name)
    {
        var digits = PartNumberRegex.Match(name).Groups["number"].Value;
        return int.Parse(digits);
    }
}

Żadnego kodowania i dekodowania, pliki są dzielone po stronie JS na paczki po 1MB, przez HTTP idą normalnie, na serwerze części są odbierane i zapisywane jako oddzielne pliki, a gdy przyjdą już wszystkie, to na koniec są łączone i pliki częściowe są usuwane. I nie musisz manipulować maksymalną wielkością requestu na serwerze. Takie coś rozwiązuje Twój problem?

1 użytkowników online, w tym zalogowanych: 0, gości: 1