Решил оформить предыдущие труды по отправке файла на HTTP сервер чанками в виде компонент ( и клиентская и серверная части созданы от TComponent) и немного упорядочить код и сделать его более прозрачным и понятным. Не знаю, насколько это у меня получилось, но попробовал. Также добавил юнит-тестов. Даже пробовал делать все в стиле TDD, но не крут я пока для этого подхода. Что же, будем учиться.
Зачем отправлять файл чанками?
Насколько я понял из общения со своим другом программистом, это нужно для того, чтобы повысить устойчивость передачи файла. Скажем, если произошел обрыв или произошло что-то ещё, чтобы не начинать отправку большого файла сначала.
Вообще тут что то вроде баланса между скоростью и устойчивостью. Можно отправлять файл целиком, но устойчивость тут никакая, а можно отправлять очень маленькими кусочками, но тогда скорость никакая. Это тема отдельных тестов.
В данной версии компонент нет функции отправки файла с места обрыва, я только начал ее писать, пока не хватило моральных сил, так как нет супер необходимости для того проекта, который я делаю. Но сделать эту функцию не так сложно, как я вижу. По своим силам вечер-два.
Текущая версия просто делит файл на чанки заданного размера, отправляет их один за одним на сервер. Я всё же придерживаюсь последовательной передачи, потому как если лить в несколько потоков, да ещё несколько юзеров – может случиться out of memory или другого ресурса.
К текущему моменту я понял как важна атомарность в программировании, чтобы каждый отдельный компонент, модуль делал только одну свою маленькую функцию. Раньше лепил все подряд и код получался кашевидным) Поэтому в TPS_ChunkUploader только отправка на сервер. И несколько основных событий, то есть вот так…
Лучше всего тестировать в отдельном потоке – все преимущества многопоточного программирования так сказать – ничего не висит, файл, даже если большой – просто передается. А первые 2 кнопки отправляют файл в основном потоке.
Справа как вы видите обработка событий, не всех, но об этом позже.
Основная идея алгоритма
Если файл меньше размера чанка – отправить его полностью, если больше – делить на чанки, собрать их в отдельной директории на диске, собрать по ним статистику в файл в JSON формате – отправить этот файл в той же временной папке , отправлять по кусочками сами чанки, запросить сервер – все ли кусочки дошли, собрать файл и в любом случае – уничтожить временные директории на клиенте и сервере
На сервере все загружается в “папку текущего дня”, примерно так public/files/1/2017/2/9, 1 в данном случае означает ID пользователя. То есть под каждого пользователя своя папка на сервере.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
function TSendFileInChunks_TPS_ChunkUploader. SendFileInChunks(AFilePath: String; AChunkSize:Int64 ): Boolean; var FileSize:Int64; ChunksInfoFileName:string; idUserString:String; begin Result:=false; //Checks Assert(AFilePath<>'','Please fill AFilePath'); Assert(AChunkSize<>0,'Please fill AChunkSize'); //GetFileSize FileSize:=GetFileSize(AFilePath); // Create output dir on server FRelativeOutputDirOnServer:=VisualFrame_TPS_ChunkUploader.Directories.CreateOutputDirOnServer(); if FileSize<AChunkSize then begin if Assigned(FOnUploadInChunksBegin) then FOnUploadInChunksBegin(Self,FileSize); if SendOneWholeFile(AFilePath,FRelativeOutputDirOnServer) then Result:=true; if Assigned(FOnUploadInChunksEnd) then FOnUploadInChunksEnd(Self,FileSize); end else if FileSize>=AChunkSize then begin //CreateAllDirs with VisualFrame_TPS_ChunkUploader.Directories do begin OriginFileName:=ExtractFileName(AFilePath); CreateUniqueNameFromOriginName(); FFileName:=OriginFileName; // for local using FRelativeTempDirOnServer:=CreateTempDirOnServer(); FAbsoluteTempDirOnClient:=CreateTempDirOnClient(); end; try //MainWork ChunksInfoFileName:='ChunksInfo.txt'; FDivideAndGatherFile.DivideFileInChunks(AFilePath,AChunkSize,FAbsoluteTempDirOnClient); GatherInfoAboutChunksToFile(FAbsoluteTempDirOnClient,ChunksInfoFileName,FileSize); //SendChunksInfoFile(FRelativeTempDirOnServer,FAbsoluteTempDirOnClient+ChunksInfoFileName); // <<Deprecated // Send Chunks To Server and Gather Them with VisualFrame_TPS_ChunkUploader.HTTPRequests do begin if Assigned(FOnUploadInChunksBegin) then FOnUploadInChunksBegin(Self,FileSize); SendManyFiles(FAbsoluteTempDirOnClient,FRelativeTempDirOnServer); // check if all chunks in proper directory on server if IsCheckChunksIntegrityOk(FRelativeTempDirOnServer,ChunksInfoFileName) then begin //Preparing params idUserString:=VisualFrame_TPS_ChunkUploader.HTTP_ConnectionParams.IDUser; if Assigned(FOnGatherChunksOnServerBegin) then FOnGatherChunksOnServerBegin(Self,FileSize); //Gather Chunks OnServer In One File if GatherFileFromChunks( FRelativeTempDirOnServer, FRelativeOutputDirOnServer, ExtractFileName(AFilePath), idUserString ) then Result:=true; if Assigned(FOnUploadInChunksEnd) then FOnUploadInChunksEnd(Self,FileSize); end; end; finally //Delete All Temporary Dirs if TDirectory.Exists(FAbsoluteTempDirOnClient) then TDirectory.Delete(FAbsoluteTempDirOnClient,true); VisualFrame_TPS_ChunkUploader.HTTPRequests. DeleteDirOnServer(FRelativeTempDirOnServer); end; end; |
Как настроить и использовать компонент TPS_ChunkUploader?
Я делаю это как-то вот так… После прописывания всех необходимых путей, инсталляции bpl, бросаем компонент на форму
Настраиваем его таким образом, прописываем HTTP параметры, ReatTimeOut и события, более подробно смотрите в исходниках
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
procedure TfExampleMain.FormCreate(Sender: TObject); begin FCS:=TCriticalSection.Create; with PS_ChunkUploader1.VisualFrame_TPS_ChunkUploader do begin with HTTP_ConnectionParams do begin Protocol:='http'; Host:='localhost';//'40.85.142.196';//'192.168.1.102';//'localhost'; Port:='40000'; IdUser:='1'; // << Critical important to use ID that are in DataBase coffeetest.users User:='SomeUser'; DefineServerToRequest(); end; // works much faster without setting HTTPRequests.IdHTTP.ConnectTimeout // HTTPRequests.IdHTTP.ConnectTimeout:=5*60000; // 5 minutes for big files HTTPRequests.IdHTTP.ReadTimeout:=5*60000; // 5 minutes for big files end; // Events settings with PS_ChunkUploader1.VisualFrame_TPS_ChunkUploader.SendFileInChunks do begin OnDivideInChunksOnClientBegin:=Self.OnDivideInChunksOnClientBegin; OnDivideInChunksOnClient:=Self.OnDivideInChunksOnClient; OnUploadInChunksBegin:=Self.OnUploadInChunksBegin; OnUploadInChunks:=Self.OnUploadInChunks; OnGatherChunksOnServer:=Self.OnGatherChunksOnServer; end; end; |
Я здесь разберу пример использования только через отправку в отдельном потоке, выглядит это таким образом
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
procedure TfExampleMain.bTest_in_ThreadClick(Sender: TObject); var ChunkUploaderThread:TChunkUploaderThread; begin //ShowMessage('Use any files you want. It is Separate thread here'); if OpenDialog.Execute then begin ChunkUploaderThread:=TChunkUploaderThread.Create(true); ChunkUploaderThread.ExampleMain:=Self; ChunkUploaderThread.FileName:=OpenDialog.FileName; ChunkUploaderThread.ChunkSize:=1024*1024*4; // 4 Mb ChunkUploaderThread.Start end; end; |
Собственно ничего сложного нет. Вся основная кухня в модуле потока. Смотрите исходники.
Почему серверная часть в виде какого-то аддона?
Можно было сделать ее как отдельный независимый компонент, но мне надо было какой-то промежуточный компонент, нечто вроде добавки к серверу, так как сам сервер у меня уже есть. Это UniGUI сервер, собственно.
Как настроить и использовать компонент TPS_HTTPFileServerAddon ?
По аналогии – бросаем его на форму
Настраиваем его примерно таким образом
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
procedure TfExampleHTTPFileServer.FormCreate(Sender: TObject); begin idHTTPServer.DefaultPort := 40000; idHTTPServer.Active := false; // Activating Server Here bToggleStartStopServer.Click; //IdHTTPServer.Bindings.Add.Port := 80; //IdHTTPServer.Bindings.Add.Port := 443; IdHTTPServer.Bindings.Add.Port := 40000; PS_HTTPFileServerAddon1.HTTPServerToRequest:= //'http:// << no need here 'localhost:'+idHTTPServer.DefaultPort.ToString(); end; |
Остальное смотрите в исходниках.
Джентельменский набор запросов сервера для данной задачи я сформулировал таким образом
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
procedure TfExampleHTTPFileServer.IdHTTPServerCommandGet(AContext: TIdContext; ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo); var ChunksInfo:string; begin with PS_HTTPFileServerAddon1.HTTPCommandGet_TPS_HTTPFileServerAddon do begin if ARequestInfo.URI='/testConnection' then begin AResponseInfo.ContentText := 'ok'; AResponseInfo.ResponseNo := 200; AResponseInfo.WriteContent; end else if ARequestInfo.URI=('/CreateDirOnServer') then CreateDirOnServer(AContext,ARequestInfo,AResponseInfo) else if ARequestInfo.URI=('/DeleteDirOnServer') then DeleteDirOnServer(AContext,ARequestInfo,AResponseInfo) else if ARequestInfo.URI=('/SendOneWholeFile') then ReceiveOneWholeFile(AContext,ARequestInfo,AResponseInfo) else if ARequestInfo.URI=('/JustGatherFileFromChunks') then JustGatherFileFromChunks(AContext,ARequestInfo,AResponseInfo) else if ARequestInfo.URI=('/CheckChunksIntegrity') then CheckChunksIntegrity(AContext,ARequestInfo,AResponseInfo) else if ARequestInfo.URI=('/SendChunksInfoFile') then ReceiveChunksInfoFile(AContext,ARequestInfo,AResponseInfo,ChunksInfo); end; end; |
Как видите, этот Addon я использую, чтобы обработать запросы сервера. В дальнейшем много-много всего можно будет усовершенствовать, но пока что так.
Юнит-Тесты
К клиентской части написал 16 юнит тестов по всем основным функциям. Поскольку это клиент-серверная технология, то прохождение тестов на клиенте косвенно говорит о нормальной функциональности на сервере, для сервера отдельные тесты писать не стал.
Над тестами еще надо поработать – кое какие из них оставляют лишние файлы и папки на клиенте, но это потом.
События
Я создал следующие события
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
FOnDivideInChunksOnClientBegin:TUploadInChunksEvent; {< field for Event FOnDivideInChunksOnClient Begin} FOnDivideInChunksOnClient:TUploadInChunksEvent; {< field for Event FOnDivideInChunksOnClient} FOnDivideInChunksOnClientEnd:TUploadInChunksEvent; {< field for Event FOnDivideInChunksOnClient Begin} FOnUploadInChunksBegin:TUploadInChunksEvent; {< field for Event OnUploadInChunksBegin} FOnUploadInChunks:TUploadInChunksEvent; {< field for Event FOnUploadInChunks} FOnGatherChunksOnServerBegin:TUploadInChunksEvent; {< field for Event FOnSendingChunksToServer} FOnUploadInChunksEnd:TUploadInChunksEvent; {< field for Events FOnUploadInChunksEnd} |
Можно отслеживать прогресс деления на чанки и отправки чанков на сервер, что я собственно и сделал, также есть событие о начале склеивания чанков и самое последнее событие OnUploadInChunksEnd можно использовать, например, для добавления записи в базу данных о том, что файл на сервере.
Иcходники
05_ChunkUploader
08_HTTPFileServerAddon