Передача файла по сети. NET.Framework
6934
25
Вопрос к программистам C#. Хочу написать для себя, в качестве обучения, небольшую программку для передачи файлов по локальной сети (интернету). Простой принцип - на сервере запускается сервис и ждет входящее подключение от клиента. Клиент подключается и передает серверу полное имя файла. Сервер по заданному пути смотрит у себя наличие файла и если есть - начинает его передачу. Прием и передачу делаю с помощью вызовов BeginReceive и BeginSend соответсвенно, т.е. использую асинхронную передачу данных. Загвоздка в следующем: я полагаю, что файл нужно разбить на логические сегменты и пересылать их клиенту. Клиент принимает эти сегменты и... что дальше? Каким образом записать в файл? Т.е. я знаю как записывать данные в файл, но непонятен момент - что если запись в файл будет "отставать" от приема данных из сети? Тогда, если файл очень большой у меня будет слишком большое потребление памяти? Или можно принять сегмент и тут же переправить в поток на запись? Если не совсем понятно, что я хочу, то привожу кусок кода (он искусственный, так что не ругайтесь :))
private FileSteam fs=new FileStream("MyFile.dat",FileMode.Create);

private void OnReceiveFragment(IAsyncResult result)
{
Socket server = (Socket)result.AsyncState;
// Вот так можно? Буфер не переполнится?
// Вообще, как тут надо поступить?
fs.Write(m_bufferSegmFile,0,m_bufferSegmFile.Length);

server.BeginReceive(// Ждем от сервера фрагмент файла
m_bufferSegmFile,
0,
m_bufferSegmFile.Length,
SocketFlags.None,
new AsyncCallback(OnReceiveFragment),
server);


}
Viper
Да, забыл. Отправка файла в принципе имеет подобный вопрос. Как сдержать отправку? Или отказаться от асинхронного способа и отсылать полностью сегментами? Типа, со стороны сервера - отправил, дождался когда пришло подтверждение, еще отправил и так до конца. А со стороны клиента - принял, записал в файл, отправил подтверждение, снова ждешь прием...
Viper
Нет, буфер не переполнится
Немного кривовато, дело в том, что по сети может быть принято(в конце приема), меньше байт, чем m_bufferSegmFile.Length
Для того чтобы узнать сколько байт принято, обычно вызывают
int ReceivedLength = server.EndReceive(result);
соответственно далее будет
fs.Write(m_bufferSegmFile, 0, ReceivedLength);
Запись файла отставать не будет, так как она происходит синхронным вызовом, а не fs.BeginWrite - хотя не знаю, у вас не создается же новый поток там на каждый коллбэк OnReceiveFragment
Дима553
Спасибо.
>Немного кривовато, дело в том, что по сети может быть принято(в конце приема), меньше байт, чем m_bufferSegmFile.Length
Я знаю про это:улыб:просто писать сюда не стал, лениво:улыб:
А как поступить в этом случае (если это не конец приема)? Если принято меньше байт, чем ожидалось, что это может значить? Я думал, это ошибка передачи данных. Или такое невозможно? Т.е. если протокол следующий:
сервер - клиенту: держи 8 байт с размером сегмента.
клиент-серверу: принял, жду сегмент
сервер-клиенту: отпраляет сегмент.
Если клиену пришло меньше байт, чем сначала говорил сервер, тогда это ошибка? Связь там прервалась или еще что?
int ReceivedLength = server.EndReceive(result);
if(ReceivedLength!=m_bufferSegmFile.Length) ОШИБКА?
fs.Write(m_bufferSegmFile, 0, ReceivedLength);
т.е. запись уже производить не надо? Или как?
И еще вопрос. Каким должен быть размер сегмента оптимально?
Viper
EndReceive пытается вернуть все те байты которые скопились в сетевом буфере, а их может быть меньше чем запрашиваемая длина(хотя реализация EndReceive зависит от сетевого протокола, может быть и другая природа длины). Однако программист .Net не должен заморачиваться о сетевом протоколе, а просто должен предусмотреть что приемочный асинхронный метод может принять меньше чем он просит. Вызов же делегата обработчика с длиной = 0, возвращенной EndReceive означает конец асинхронной передачи. Собственно это все что должно заботить обработчика. Меньшее количество принятых байт чем просили ошибкой не считается.

Для обработки именно ошибок передачи данных в Net существует исключение SocketException и в этом объекте SocketException.ErrorCode(описание кодов ошибок находится в Windows Sockets version 2 API error code documentation ), это исключение как раз может возникнуть во время вызова EndReceive
Дима553
Про размер - в экзамплах часто ставят 1024. А вообще если это имеет какое о значение то можно вынести эту длину в настройку приложения и поэкспериментировать на конкретной платформе какое оптимальное соотношение память/скорость от размера этого буфера
Дима553
Спасибо за развернутый ответ. Т.о. получается, что, если сервером отправлять файл сегментами, например по 64 кб, а клиент их соответственно будет принимать, зная , что, размер сегмента равен 64 кб (т.е. некий протокол все же имеется:улыб:), то когда будет принято,например, 12 кб, это будет означать, что закачка файла завершена? А если потом узнать размер принятого файла и сравнить его с заявленным (сервер с самого начала пришлет размер), можно сделать вывод о корректности закачки? (контрольную сумму в расчет не берем)
И все-таки, какой размер сегмента будет оптимальным для передачи по сети?
Viper
Это будет означать какую то ошибку.
В сокет есть такое свойство ReceiveBufferSize по умолчанию оно 8192 - будем считать это оптимальным значением
Дима553
Дмитрий, вы меня запутали :безум:
В каком случае это будет означать ошибку?
Если файл размером 262 кб, будет отпраляться сегментами по 64 кб, будут отправлены 5 сегментов.
64+64+64+64+12. т.е. пятый сегмент должен быть равен 12 кб. Получив эти 12 кб, клиент сделает вывод, что закачка закончилась, запишет их к уже полученным 256 кб в файл и затем сравнит размер всего файла с заявленным значением. При совпадении можно сделать вывод, что файл скачался полностью. Если нет - тогда где-то ошибка. Верно? Или я что-то не понмаю? Просто если тут как-то по-другому, тогда как клиенту выделять место под буфер? От чего отталкиваться?
Дима553
Или нужно просто вести счетчик присланных байтов? И как только он сравнялся с заявленным размером файла, значит файл получен полностью?
Тогда это освобождает от мысли, что мы обязательно должны принять фиксированный пакет данных
Viper
В последнем посте я имел в виду что это означает какую то ошибку в программе, сделанную программистом. Если сервер послал 64к, а клиент принял не 64к и никакого исключения не возникло - то похоже где то ошибка программиста

Как я это представил:
Посылка одного сегмента
Сервер высылает 64к

Клиент начинает асинхронное чтение. При размере буфера 8к он совершит 8-10(непредсказуемо) асинхронных чтений, где размер прочитанной информации будет 8к или меньше(что никакой ошибки не означает), записывает эта инфа в файловый поток. Последнее чтение показывает, что считано 0 - это означает, что логическое чтение завершено - и именно это является главным признаком того что закачка закончилась, а не то что получился блок менее запланированных 8к. Если за эти 8-10 асинхронных акций, всего было считано менее 64к (или 12 к если это в конце), то опять же - где то возникло исключение необработанное, но это не обязательно ошибка сети, а скорее ошибка программиста и приложению не надо пенять что "связь прервалась", а обработать возможные исключения
Дима553
Вот оно что. А я и не заметил даже такого свойства у типа Socket.:хммм:
Получается, если я отправляю сегмент файла, используя метод BeginSend, исполняющая среда .NET отправляет его "кусками", которые равны свойству ReceiveBufferSize серверного сокета?
А прием клиентом файла соответственно ведется тоже такими "кусками", размер которого задан в ReceiveBufferSize сокета клиента? Следовательно, должно выполниться несколько колбэков, прежде чем скачается сегмент файла?
Если это так, тогда зачем мы указываем в аргументах метода BeginReceive, такой параметр как size? С отправкой все понятно. Но с приемом... Зачем тогда его указывать, если мы все равно не получим больше байт, чем указано в свойстве ReceiveBufferSize сокета у клиента? Что-то я уже совсем запутался:хммм:
Viper
Основные принципы работы с сокетами, в дотнете не силен, но в остальных языках так:
принимающий сокет не знает общее количество байт вашего файла, он читает их из сокет-буфера и отдает вам. Вы вправе решать сами - закончилась ли передача или нет, вы вправе их интерпретировать как хотите. Лишь в одном вы можете быть уверены - в том что данные поступают последовательно и без потерь (провалов) - особенности реализации именно протокола TCP, в случае UDP-сокетов прилодение должно само контролировать целостность передаваемых данных и отсутствие потерь. Так же вы должны определится с тем, кто будет закрывать сокет, если сокет закрывает клиент, сервер вправе решить что передача завершена. Илио сервер ждет от клиента ключевую последовательность байтов, определяющую конец передачи и закрывает сокет сам.
В общем случае, если вы передаете файл таким способом, принимая первую последовательность, определяющую имя файла и его размер, вы должны действовать следующим образом:
читать из сокета все полученные байты, одновременно ведя счетчик принятых байтов и контролируя - не произошло ли закрытие сокета вследствие таймаута. Если сокет закрыт по таймауту, вы должны сбросить буфер (файл принят не целиком), а в случае достижения счетчиком контрольного числа байтов, переданных вам в заголовке, закрыть сами сокет и записать файл на диск. В клиенте при передаче заголовка и в момент засовывания в сокет файла, вы должны после операции отправки данных снимать состояние сокета - нет ли ошибки таймаута после записи в сокет, и если есть, начинать передачу сначала, а если передан весь файл, проверить состояние сокета и если он не закрыт то закрыть его. В общем как-то так, я не знаю какие вы объекты и функции используете в дотнете для доступа к сокету, а именно к созданию, привязке, открытию, передаче/приеме данных и контролю состоянию сокета (активен/неактивен), но думаю подставите ваши знания в мое объяснение.
Viper
size служит дополнительным ограничением сверху по количеству принятых байт. Используется для того, что если идет запись полученной информации в какой то созданный программистом буфер(массив например), не выйти за пределы этого массива.
BeginReceive выгребает выгребает то что лежит в буфере, если там лежит меньше size то он весь буфер сокета вам отдаст, если больше то он выгребет ровно size информации, все остальное оставит там в буфере.
Дима553
Все, разобрался.:улыб:Спасибо вам большое.
to Mad_Dollar: и вам спасибо:улыб:
Дима553
Наткнулся на странное поведение программы. Если я передаю сервером файл, то все нормально. Клиент его правильно принимает. Т.е. если принято 0 байт, значит закачка завершена, все работает. Но наоборот - не работает. Запускаем сервер, он начинает слушать порт. Если после перехвата входящего подключения я сервером начинаю ждать файл, т.е. приблизительно так.
Прошу прощения за псевдокод, но так быстрее.
BeginAccept ( имя коллбэка)
коллбэк:
{
BeginReceive( имя коллбэка-приемщика)
}
коллбэк-приемщик:
{
делаем проверку на количество принятых байтов, если равно нулю, выходим из потоков
иначе
делаем еще что-то
BeginReceive(имя коллбэка-приемщика)
}

Так вот, если файл принимает сервер, а передает клиент - то 0 байт почему-то не принимаются. Следовательно, я не могу узнать, принят ли файл целиком или нет. Я решил вводить количество принятых байт в лог-файл. Затем просуммировал все байты в логе и получил размер файла. Однако в конце лога не было нуля. Таким образом, клиент закончил передачу, а сервер еще непонятно чего ждет. Код для приема сервером файла использую тот же. Как быть? Где я ошибся?
Понятно, что чисто технически это можно обойти. Например, сначала передать размер файла, а потом сам файл и в процессе приема вести счетчик принятых байт. По получении - закрыть сокет. Но правильно ли это?
Viper
если принято 0 байт
УПД - понял откуда ноль. возможно я не прав, но в данном контексте ноль может означать, что при попытке буфера сокета там не оказалось байтов. То есть вы вычерпали весь буфер и ваш процесс должен завершить попытки чтения сокета до следующего вызова коллбэка, однако это не означает конец передачи клиентом файла.

у вас условие завершения закачки а) ошибка сокета (порвалась связь) б) количество принятых после заголовка байтов совпало с количеством байтов, заявленных при начале передачи в заголовке. примерно так

сервер:
сбросить_буфер_приема_всего_файла;
установить_счетчик_принятого_в_ноль;
делать_пока (состояние_сокета!=ошибка)и(принято_байт
Mad_Dollar
Тут дело немного в другом. Весь файл за раз принять не удается. Поэтому коллбэк выполняется много раз, при чем каждый раз число принятых байтов колеблется. Бывает 15к, бывает 120к, но не больше size, что я выставил. Так вот во время очередного выполнения коллбэка число принятых байт равно нулю. Это значит, что передача закончена. Во всех примерах кода MSDN происходит подобная проверка. Т.е. получается, что если нет exception'ов, то передача корректно выполнилась. Так вот, если я принимаю файл клиентом - коллбэк запускается, когда принято 0 байт. А если принимаю сервером - коллбэк не запускается и сервер ждет еще байты. Но они естественно не приходят. И следовательно я не могу закрыть файловый поток, куда пересылал полученные байты. Повторюсь, технически это можно обойти - вести счетчик принятых байт.
Но когда сервер передает файл, а клиент принимает - счетчик вести не обязательно. Если запущенный коллбэк видит, что принято 0 байт, то он вызывает закрытие файлового потока (я же в файл передаю данные) а затем закрывает сокет и не ждет дальше данные. Т.е. мне вести счетчик необязательно. Но когда я клиентом передаю файл на сервер - коллбэк не вызывается. Такое ощущение, что сервер делает передачу 0 байт, а клиент нет. Как-то так:улыб:
Viper
Теперь понял о чем вы. Откуда серверу знать что передача закончилась? Пусть клиент после завершения передачи закроет сокет, и вы получите вызов коллбэка с нулем.
Viper
А вы не забыли что в случае сервера колл,эк установленный в BeginAccept, рабочий сокет получается так:
public static void Listen_Callback(IAsyncResult ar){
Socket s = (Socket) ar.AsyncState;
Socket s2 = s.EndAccept(ar);
....
}
s2 это сокет с которым потом работаем, а не просто
public static void Listen_Callback(IAsyncResult ar){
Socket s = (Socket) ar.AsyncState;
....
}
как в случае клиента
Mad_Dollar
Хм... Но ведь клиент как-то узнает, что сервер завершил передачу. И сокет не закрывается. И я сам его принудительно не закрываю. Почему обратное не верно? А если мне пока не нужно разрывать связь?
Дима553
Нет, не забыл. Я получаю сокет для работы с конкретным клиентом именно с помощью EndAccept. И потом запускаю BeginReceive именно от этого сокета, а не от того, который слушает входящие подключения. Просто коллбэк, который должен запустить корректное завершение принятия файла не выполняется в случае, если это сервер. А если это клиент (тот же самый код) - то этот коллбэк выполняется (где, как всегда идет проверка на количество полученных байт и принимается соответствующее решение)
Mad_Dollar
А ведь действительно, закрыл сокет на клиенте - коллбэк на сервере выполнился. Но как быть, если связь разрывать не надо? Например, при шифрованой передаче. Придумать простейший протокол и вести счетчик принятых байт?
Viper
Ох боже ты мой... :шок: Я ведь на сервере тоже сокет закрывал... Просто не заметил этого. Простите великодушно. Компостирую вам мозги, а сам ни черта не вижу:хммм:
Viper
Но как быть, если связь разрывать не надо? Например, при шифрованой передаче. Придумать простейший протокол и вести счетчик принятых байт?
Именно.
Дима553
Опять проблема :dnknow:
Нашел в MSDN как можно отслеживать состояние подсоединенного сокета. Там рекомендовалось отсылать 0 байт по этому сокету. Если вылез Exception (кроме errorcode=10035), тогда сокет отсоединен. Я организовал в клиенте таймер, который каждые полсекунды делает отправку 0 байт серверу. И теперь - странность:
а) Если я просто выключаю сервер (просто закрываю окно сервера), тогда клиент отлавливает разрыв соединения.
б) Если я вызываю в сервере методы (Disconnect, Close, Shutdown - уже всяко их перепробовал) сокета, который работает с клиентом - клиент этого не видит и по-прежнему отсылает 0 байт. Даже если я после этого программного разрыва закрою сервер(!) - клиент все равно их шлет. Т.е. сервер уже давно выключен - клиент думает, что сервер на связи.
в) Если после всего этого, клиентом отправить >0 байт - тогда наконец-то он понимает, что связи уже никого нет.
Вопрос. MSDN не прав, или я что-то опять не так делаю? :dnknow:
Можно, конечно, выкрутиться и отслылать один байт. Тогда все прекрасно отследится, но меня смущает MSDN.