Статьи на Delphi - Отправка файла через неблокирующий сокет от сервера к клиенту
Приветствую!
Я хотел поделиться своим опытом и написать об отправке файла через неблокирующий сокет от сервера к клиенту. "А что тут писать?" – спросите вы и будите совершенно правы и неправы одновременно. У людей, которые давно занимаются программированием, данная статья вызовет лишь легкую улыбку, но тем, кто впервые пытается разобраться в технологии сокетов она, возможно, поможет найти ответы на их вопросы.
В интернете есть множество статей описывающих технологию отправки и приемки файла, в том числе и на данном сайте. Но все эти статьи описывают прием лишь одного кусочка данных или сообщения, пришедшего к клиенту и последующую обработку этого блока в процедуре ClientSocket1Read / ServerSocket1ClientRead. Но мне же нужно было отправить файл, в идеале любого размера и все это сопроводить анимацией прогресс бара. Следовательно, методы sendtext и sendstream не годились, т.к. они не дают возможности визуализировать процесс отправки файла по кусочкам.
В первых статьях по сокетам, что мне попались в поисковиках, использовался для отправки и приема данных класс TmemoryStream. Со стороны сервера отправка проходила гладко и без заморочек, прогресс бар не зависал и выдавал равномерное приращение полосочки с процентами. На клиенте творилось нечто мистическое – при передаче тестового файла 104 Мб примерно с 20% полоса прогресса начинала замедляться и еще секунд через 5 выскакивала ошибка StackOverflow. Тут можно долго рассуждать где была ошибка: я виноват, либо класс кривой. Исходя из того, что почти все примеры в сети одинаковые и технология приема данных на клиентской стороне не отличается кардинально ни чем кроме имен переменных я решил поменять класс TmemoryStream на другой.
Другим классом оказался TfileStream и он уже заработал как часы. После использования этого класса все нестыковки в передаче данных ушли в небытие. Именно используя этот класс у меня получилось передать тестовый файл от сервера к клиенту. Ниже я рассмотрю лишь функцию отправки и приема файла. Остальное, довольно подробно прокомментировано в исходнике.
Сервер
//посылка файла через сокет procedure TForm1.SendFileSocket(fName: string); var nSend : Integer; sBuf : Pointer; begin try // открытие файла для чтения и последующей отправки fs := TFileStream.Create(edt1.Text, fmOpenRead); // курсор на начальную позицию, с которой нужно слать файл fs.Position := 0; repeat // выделение памяти под считываемые данные GetMem(sBuf, bSize + 1); // чтение куска данных (bSize) из файла nSend := fs.Read(sBuf^, bSize); // если что то прочиталось, то отправляем клиенту if nSend > 0 then begin ServerSocket1.Socket.Connections[0].SendBuf(sBuf^, nSend); // корректировка значений прогрес бара Progress(fs.Position, fs.Size); // задержка иначе будут потери пакетов Sleep(SleepTime); end; // освобождение участка памяти FreeMem(sBuf); Application.ProcessMessages; until nSend <= 0; // цикл выполняется пока хоть
// 1 байт будет прочитан из потока fs finally if Assigned(fs) then fs.Free; end; end;
Со стороны сервера отправку организовал в 1 процедуру SendFileSocket(fName:
string).
Первое что нужно сделать, это создать экземпляр класса TfileStream для чтения
файла и установить курсор в начало. Далее необходимо выделить память под данные,
которые будут отправляться кусками. В моем случае использовался блок bSize
размером в 4000 байт. Я, если честно, до сих пор не уловил для чего нужно
выделять память на 1 байт больше:
GetMem(sBuf, bSize + 1);
но думаю, что туда залетит символ окончания строки #0, чтобы при освобождении
памяти система знала, где заканчивается строка по адресу sBuf.
Следующее действие это чтение кусочка данных из тестового файла.
nSend := fs.Read(sBuf^, bSize);
Тут следует обратить внимание, что в функции GetMem используется указатель sBuf
без «птички» ^ , но в функции fs.Read указатель уже пишется как sBuf^. Почему
так? Описывать это я не буду, т. к. те, кто действительно хотят в этом
разобраться, следует почитать основы Delphi, а те, кто просто ищут кусок кода
для своей программы все равно не запомнят объяснения. Чтобы не путаться в том,
где ставить птичку, а где нет, могу сказать только следующее: если параметр
передается как указатель, то переменная-указатель пишется без птички. Но если
параметр функции есть сама область памяти, на которую ссылается указатель, то
тогда нужно ставить символ ^.
if nSend > 0 then begin ServerSocket1.Socket.Connections[0].SendBuf(sBuf^, nSend);
Если было считано хоть что то в буфер sBuf, то можно это «хоть что то» отправить клиенту. В моем случае тестирование проводилось с 1 клиентом, который селился в ServerSocket1.Socket.Connections[0] , но вы можете организовать, например, отправку файла всем клиентам в цикле. После того, как отправили данные клиенту, необходимо это дело визуализировать на экране:
Progress(fs.Position, fs.Size); // задержка иначе будут потери пакетов Sleep(SleepTime);
Тут я использую задержку, т.к. при отсутствии оной бывают ситуации потери
пакетов. Например на сервере ушло 100% данных, а на клиенте пришло только 99%. В
этих случаях необходимо дописывать контроль целостности передаваемых данных, но
это уже совсем другая история.
FreeMem(sBuf);
После отправки куска файла необходимо освободить память функцией FreeMem.
Возможно, что можно выделить память всего лишь раз до цикла отправки и
освободить ее уже после выполнения цикла, но это уже вам для раздумий.
Клиент
procedure TForm1.ClientSocket1Read(Sender: TObject;
// Socket: TCustomWinSocket); var nRead : Integer; rBuf : Pointer; begin ... else // режим получения файла begin repeat Socket.Lock; // выделение памяти под принятый кусок данных GetMem(rBuf, bSize + 1); // считывание данных nRead = количество считанных байт nRead := Socket.ReceiveBuf(rBuf^, bSize); // если что то считалось, то запись данных в файл if nRead > 0 then begin fs.WriteBuffer(rBuf^, nRead); Gauge1.Progress := fs.Size; end; FreeMem(rBuf); Socket.Unlock; Application.ProcessMessages; until (nRead <= 0); // если все данные считались, то переключение режима
// приема обратно и освобождение переменной потока if fs.Size = fSize then begin Receiving := False; fs.Free; Jornal('Файл принят!', Unassigned, clGreen); end; end; end;
На стороне клиента работа по приему данных происходит аналогично отправке на
стороне сервера с разницей лишь в том, что вместо чтения части файла происходит
запись полученного кусочка в файл.
nRead := Socket.ReceiveBuf(rBuf^, bSize); // если что то считалось, то запись данных в файл if nRead > 0 then begin fs.WriteBuffer(rBuf^, nRead);
Если считывается хотябы 1 байт, то пишем его в файловый поток (в файл).
Как сказано в руководствах – при передаче кусков данных через сокет возможны
ситуации, когда кусок данных придет в неизменном виде, а возможно и его
фрагментирование либо образование большего фрагмента данных. Т.е. если мы
ожидаем на входе блок размером bSize = 4000 байт, то на практике может прилететь
4500 байт. Я проводил опыты с выводом размера принятого фрагмента в журнал (RichEdit)
и оказывалось, что при жестко заданном размере в 8000 байт после нескольких
секунд отправки данных он (размер) мог поменять на 8189 байт. Поэтому, чтобы не
ориентироваться на фиксированный размер блока, считывание происходит в цикле.
Repeat ... until (nRead <= 0);
Пока хоть что то читается из буфера, программа пишет данные в файл.
Как только размер потока fs станет равным присланному до начала отправки размеру
файла fSize, то это означает, что мы получили весь файл и можно его сохранить,
освободив затем поток.
f fs.Size = fSize then begin Receiving := False; fs.Free; Jornal('Файл принят!', Unassigned, clGreen); end;
Как переслать размер файла до начала отправки всего файла? Можно это сделать обычной функцией sendtext – например SendText(ExtractFileName(YourFileName) + '#' + YourFuncFileSize(YourFileName)) либо посмотрите как это сделано в данной программе. Я использовал класс TstringList для передачи команд между клиентом и сервером. Да, это не очень красиво, но я ставил перед собой задачу максимально быстро разобраться в технологии приема/передачи файла.
На этом все.
Удачи в этом интересном и развивающем мозг деле!
Замечания и вопросы по статье отсылайте на
Исходный код и оригинал статьи: client-server-socket.zip (17 Кб).
Дата: 27.01.2014,
Автор: