Я хотел поделиться своим опытом и написать об отправке
файла через неблокирующий сокет от сервера к клиенту. "А что тут писать?" –
спросите вы и будите совершенно правы и неправы одновременно. У людей, которые
давно занимаются программированием, данная статья вызовет лишь легкую улыбку, но
тем, кто впервые пытается разобраться в технологии сокетов она, возможно,
поможет найти ответы на их вопросы.
В интернете есть множество статей описывающих технологию
отправки и приемки файла, в том числе и на данном сайте. Но все эти статьи
описывают прием лишь одного кусочка данных или сообщения, пришедшего к клиенту и
последующую обработку этого блока в процедуре
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.
Возможно, что можно выделить память всего лишь раз до цикла отправки и
освободить ее уже после выполнения цикла, но это уже вам для раздумий.
// 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 для передачи команд между клиентом и
сервером. Да, это не очень красиво, но я ставил перед собой задачу максимально
быстро разобраться в технологии приема/передачи файла.
На этом все.
Удачи в этом интересном и развивающем мозг деле!
Замечания и вопросы по статье отсылайте на
Crusl@mail.ru.