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?