Войти
ПрограммированиеФорумСеть

Собственный протокол передачи файлов на TCP

Страницы: 1 2 Следующая »
#0
18:33, 19 ноя. 2020

Решил опять заняться программерскими делишками.

Ну и разумеется замутить свой собственный протокол передачи файлов, че уж там. Зачем мне все эти HTTP, FTP и прочая ересь да?

Обычные примерчики из интернета типа, socket.send, socket.recv мне ну разумеется не подходят. Почему? Да потому что все они как один не учитывают Command Channel, а тупо шлют куски файла пока буффер не заполнится и не передадут весь файл. А мне нужно помимо скачивания/закачивания файлов еще иметь возможность в том же самом TCP соединении, параллельно - слать рандомные клиентские пакетики. Почему не замутить 2 разных соединения? да потому что я планирую сделать не просто скачивание обновлений, а целую систему синхронизации файлов, или даже собственный контроль версий. И пока синхронизируются файлы, пользователь может получать от сервера и другие пакеты, например об отвалившихся или подсоединившихся пользователях, да и мало ли чего еще.

Вся проблема только в буффере отправки, для буффера приема вообще нет разницы, файловые там пакеты или обычные.

К какому решению я пришел: делим мысленно буффер на 2 части: requiredSize и optionalSize (writerSize = requiredSize + optionalSize). Файловые пакеты мы будем записывать в Optional часть буффера, которая по идее должна быть значительно меньше, ибо рассчитана теоретически на данные одного кадра, а обязательный буффер может скапливать данные нескольких кадров если не успевает отправить. Дело в том, что у меня особая механика работы с пакетами - если пакет не влезает в буффер, то разъединяем соединение, отдельно очередь пакетов нигде не хранится, они сразу записываются. Это касается только обычных пакетов, но когда дело касается "необязательных" пакетов, к которым относятся наши файловые пакеты, то мы их записываем только в том случае, если есть свободное место (траффик) и не записываем пока оно не появится. Но в сумме, все данные находятся в одном буффере, никаких 2х отдельных буфферов нет, как и у сокета. Например мы записываем команду "привет", а затем пытаемся записать необязательные пакеты "кусок файла" пока необязательный размер не заполнится, сразу следом за командой "отправить файл".

В такой системе команды будут всегда отправляться в первую очередь, а optional пакеты следом.

Как мы узнаем что кусок файла отправлен? чтобы освободить вымышленное место. Есть 2 варианта для C# и для C++ (так как у меня библиотека работает между этими двумя языками, то мне нужны оба варианта), С++: на каждый пакет "кусок файла" должен приходить ответ, просто идентификатор пакета. Все такие ответы будут приходить в том же порядке что и отправки, поэтому мы просто берем из очереди сохраненные при отправке размеры пакетов и добавляем их в освобожденный размер.

C# может использовать вариант как у C++, но есть и проще вариант, ибо у C# сокетов есть асинхронные методы отправки пакетов, которые используют пользовательский буффер и которые завершаются только после того как пакет был отправлен по TCP со всеми Ack подтверждениями. В обычных C++ TCP беркли сокетах такого нет, там просто внутренний буффер. Поэтому если файл отправляется со стороны C#, то ему не обязательно потом получать ответы на каждый пакет. Ну а если это не сработает то всегда можно использовать вариант C++.

Размер Optional буффера можно скорее всего просто задать вручную, либо вычислить программно в зависимости от параметров системы, главное чтобы его хватало, чтобы реализовать потенциал пропускной способности сервера. Формально Optional буффер равноценен сокет буфферу, по своему смыслу. В то время как WriterBuffer это чисто моя фича, это не просто сокет буффер, но и сразу очередь пакетов, его переполнение равносильно переполнению очереди - разрыв соединения, и срочный патч с увеличенным буффером. За количеством пакетов следит уже разработчик сервера и логика сервера, ограничение Writer буффера это лишь крайняя мера, но вместо исключения просто разрыв соединения.

С буфферами разобрались, теперь че по протоколу? Я разделил передачу файлов на 2 задачи: передача манифеста, и передача файла(файлов). Всего 8 пакетов:

FileTransferBegin(required) // начало отправки, кто прислал этот пакет тот и отправляет.
  ManifestBegin(required) // этот пакет в required буффер, потому что манифест всегда один.
    byte[16] Hash // MD5 (или лучше SHA1?) хеш самого манифеста (всего что ниже этой переменной хеша).
    int Version // версия манифеста/обновления, для поддержки системы обновлений.
    int FilesCount
    long TotalFilesSize
    int EmptyFilesCount
    int EmptyDirectoriesCount
    int RecordsCount
  ManifestRecord(optional) // записей в манифесте может быть много, соответственно optional.
    string Name // (utf8, case-sensitive, null-terminated) // if ends with "/", then its empty directory.
    long FileSize // (only for files)
    byte[16] FileHash // MD5 (or better SHA1?) (only for files)
  ManifestEnd(required)
  
  // для каждого файла манифеста, для пустых файлов/папок пакеты не отправляются.
  FileBegin(optional) // этот пакет в optional буффере, потому что файлов может быть много.
  FileChunk(optional)
    byte[] Data
  FileEnd(optional)
FileTransferEnd(required)  // конец отправки или отмена
  byte Reason // причина окончания, 0 = все файлы переданы, 1 = прервано.

Отправка манифеста и файлов, это 2 отдельные задачи, но взаимосвязанные. Для приема/отправки файлов нам нужно установить манифест приема/отправки, не важно где мы его взяли, получили по сети или загрузили локально с файла. Сетевая библиотека автоматически записывает получаемые файлы строго в соответствии с манифестом, файловые пакеты не содержат никаких имён или размеров, всё находится в ManifestRecords.

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

Даже если нам нужно передать всего 1 файл, значит нужно указать манифест с 1м файлом. Который можно создать динамически или загрузить из файла.

Одновременно будет выполняться FileTransfer только в одну сторону, либо Send либо Recv, этого мне пока достаточно.

Итак вкрацце: вводим Optional буффер для файловых пакетов, которые в зависимости от перегруженности буффера либо записываются в буффер в этом кадре, либо в следующем, либо когда освободится место. В то время как обычные пакеты "всегда" записываются в Required буффер моментально, без всяких проверок есть место или нет.

Как это использовать? 2 варианта:
1) Система обновлений (Download/Upload).
  - Клиент соединяется к серверу
  - Если версия клиента не последняя, сервер посылает клиенту манифест с последней версией абсолютно всех файлов.
  - Клиент проверяет каждый файл на хешсумму и размер, генерирует свой Diff манифест с отличающимися файлами/папками, и посылает серверу.
  - Сервер начинает передачу всех файлов по клиентскому манифесту, если они были в серверном манифесте само собой. 

2) Самодельная простая система контроля версий или синхронизации (без чекинов).
  Скачивание/синхронизация последней версии:
    - Аналогично системе обновлений, за исключением того, что клиент получает только те файлы, к которым у него есть доступ на чтение или запись.
   
  Отправка коммита:
    - Клиент посылает пакет с инфой коммита.
    - Клиент посылает манифест с файлами коммита, которые разрешено изменять.
    - Клиент посылает файлы указанные в манифесте.

Если бы мне нужна была только система обновлений, возможно я бы ограничился HttpRequest, хотя тоже спорно, но я всё таки хочу собственную "простую и удобную" систему контроля версий, чисто под свой проект, ибо Perforce слишком сложный и кривой в настройке и установке, причем не только на стороне сервера, но и на стороне клиента, других вариантов кроме Perforce нет. А у меня будет работать по принципу "поставил и забыл", все настройки будут только на стороне сервера (включая разрешения и ignore list для проекта), а разработчику надо будет только указать путь к проекту. Функции контроля версий возможно буду добавлять при необходимости, если вообще буду, такие как бренчи например, а для начала достаточно просто одной рабочей ветки, системы коммитов и откатов.

А теперь нужно попробовать это реализовать.


#1
19:30, 19 ноя. 2020

P2P?

#2
0:37, 20 ноя. 2020

gamedeveloper01
> Почему не замутить 2 разных соединения?

Не понимаю, как нижеследующее может служить ответом на этот вопрос.

> да потому что я планирую сделать не просто скачивание обновлений, а целую систему синхронизации файлов,
> или даже собственный контроль версий. И пока синхронизируются файлы, пользователь может получать от сервера
> и другие пакеты, например об отвалившихся или подсоединившихся пользователях, да и мало ли чего еще.

#3
(Правка: 9:12) 9:06, 20 ноя. 2020

rcsim
> Не понимаю, как нижеследующее может служить ответом на этот вопрос.
Ну вообще да, никак. Но вся разница только в наличии виртуального буффера для файлов и в подтверждениях о полученных пакетах. В любом случае и там и там мне нужен какой то протокол для манифестов и файлов, а не просто слать файлы по одному, так что протокол и там и там получится тот же самый, его всё равно нужно делать. Проще явно не станет и соединение  прибавится.

Кстати я думаю сделать еще один отдельный пакет для таких подтверждений, чтобы не слать отдельный пакет для каждого подтверждения, а просто 1 пакет и в нем количество подтвержденных пакетов:

OptionalPacketReply(required)
  int Count // само поле можно сделать необязательным, если оно равно 1.

// А также пара изменений для возможности резюма:

FileTransferBegin(required)
  int ManifestRecordIndex // индекс файла в манифесте для продолжения загрузки, 0 по умолчанию.
  long FilePosition // позиция файла для продолжения загрузки, 0 по умолчанию.

Salamandr
> P2P?
Client-Server Only.

#4
11:01, 20 ноя. 2020

gamedeveloper01
> у C# сокетов есть асинхронные методы отправки пакетов, которые используют
> пользовательский буффер и которые завершаются только после того как пакет был
> отправлен по TCP со всеми Ack подтверждениями. В обычных C++ TCP беркли сокетах
> такого нет, там просто внутренний буффер.
Ты же понимаешь что и то и то через winapi работает? И там и там send и select, ну на стороне сервера ещё IOCP.

#5
12:19, 20 ноя. 2020

kipar
> Ты же понимаешь что и то и то через winapi работает? И там и там send и select,
> ну на стороне сервера ещё IOCP.
Понимаю, но факт остается фактом, в C# это как то реализовано. Хотя если честно я это не проверял, а просто принял на веру, и неизвестно будет ли это вообще работать на линуксе. Поэтому вариант с подтверждением пакетами может потребоваться и для C#. Это не особо важно.

А вообще я еще давно пытался искать инфу о том, можно ли в сокетах как то узнать сколько места осталось в буффере, сколько данных уже отправилось у TCP сокета, и везде видел только один ответ - это невозможно. Но когда изучал асинхронные сокеты в C#, то где то вычитал, что если установить сокет буффер = 0, и подавать в функцию SendAsync свой буффер, то этот буффер будет использоваться до полной отправки данных, включая TCP Ack. Опять же до реальных тестов еще дело не доходило. Пока это только теория. Как это всё реализовано в Линукс вообще загадка, а именно Линукс меня интересует в качестве сервера, даже если я буду использовать AWS, я всё равно выберу Линукс скорее всего.

#6
13:35, 20 ноя. 2020

gamedeveloper01
> А вообще я еще давно пытался искать инфу о том, можно ли в сокетах как то узнать сколько места осталось в буффере, сколько данных уже отправилось у TCP сокета, и везде видел только один ответ - это невозможно.
Если тебе нужно больше контроля за процессом отправки, надо использовать UDP, только всю логику подтверждений и перепосылок придется реализовывать самому.

#7
13:44, 20 ноя. 2020

}:+()___ [Smile]
> Если тебе нужно больше контроля за процессом отправки, надо использовать UDP,
> только всю логику подтверждений и перепосылок придется реализовывать самому.
Я решил что UDP если и буду использовать, то во первых только для игровых серверов, а во вторых, если решу переходить с TCP на UDP, то делать это буду только в самом конце, когда проект итак уже будет работать. То есть это дело не срочное и про него можно вообще забыть.

#8
(Правка: 14:41) 14:36, 20 ноя. 2020

gamedeveloper01
Используемые для отправки буфера в любом случае внутри винды, приложение ими не управляет. Никакое приложение не управляет, хоть на чем его пиши. Но цепочка буферов может быть длинной, по которой данные копируются. Если ты сам или твой инструмент завели дополнительного посредника, его может и получится исключить.

А узнать, сколько осталось, легко. В асинхронном режиме просто не отправится сверх того, для чего есть буфер. Командуешь "send", она же может не отправить сколько указано, сколько смогла отправить находится в возвращаемом значении. Остальное надо пытаться отправить еще раз чуть погодя. Чтобы не гадать когда это, чуть погодя, есть оповещение через "select".

Локальных неудач по TCP быть не может, она пока не доставит, не успокоится. Если совсем уж все плохо - порвется соединение. Но перед тем, как оно порвется, будут нарастать лаги до 30 сек.

А что такое пакеты по ТСР? Это вообще не пакетный протокол.

#9
(Правка: 16:28) 15:54, 20 ноя. 2020

Zab
> сколько смогла отправить находится в возвращаемом значении. Остальное надо
> пытаться отправить еще раз чуть погодя
это я итак знал, но твой коммент реально меня натолкнул на мысль, почему нельзя заполнять сокет буффер до отвязки каждый кадр, и при этом иметь внешний WriterBuffer. Короче говоря действительно можно обойтись без подтверждающих пакетов.

+ бред

Можно попробовать и так, но с этим способом много вопросов и неудобств, касаемо куда мы будем записывать файловые пакеты до отправки в сокет, ибо для каждого пакета вызывать socket.send неприемлемо:
1) Команда записывается в WriterBuffer (без Required и Optional), а файловые пакеты сразу в сокет.
2) При отправке WriterBuffer пытается скопировать все данные в SocketBuffer.
3) Если осталось место в буффере, то пытаемся записать файловые пакеты до отвязки, сколько влезет. Последний пакет фрагментировать.
4) В следующем кадре пытаемся в первую очередь допослать фрагментированный пакет. А потом опять пытаемся повторить пункты 2-4.

Zab
> А что такое пакеты по ТСР?
Под пакетами по TCP я подразумеваю пользовательские пакеты пользовательского протокола. Packet (length, id, data).

PS: не обязательно понять все что я написал тут) это просто мысли вслух, но можно попробовать. Чуть позже буду реализовывать, там и выяснится, сработает всё что я тут написал или нет).

PS2: не, я уже вижу какой то бред в 1м способе, не принял во внимание что он итак заполнит сокет буффер в 1м кадре, так что Optional буффер там не имеет смысла. Остался 2й способ, но тогда получается придется ждать пока отправиться весь буффер прежде чем можно будет послать команду, может это и не проблема, буффер и не должен быть слишком большим, ждать придется не долго.

PS3: однако всё равно нужно доработать 1й способ, ибо сама идея Optional пакетов для TCP соединения мне нравится, а еще нравится что не нужно использовать 2 разных буффера.

#10
16:24, 20 ноя. 2020

Сколько осталось места в буфере - не выяснишь. Можно только попытаться послать много, а он пошлет сколько сможет.
Вероятно тебе все же нужен UDP. Там ты сможешь реализовывать всякие хитрые стратегии доставки. TCP на верхнем уровне туп. Ты скомандовал отсылку, а дальше не твое дело как доставить.
Надеюсь, ты осознаешь, что в процессе доставки может перенарезаться последовательность байт как угодно. Это поток байт, а не пакеты.

#11
(Правка: 16:58) 16:53, 20 ноя. 2020

Zab
Не, UDP не нужен. Всё отлично сработает и так.

Просто мне сейчас нужно понять как лучше буферизировать файл для отправки.

Zab
> Это поток байт, а не пакеты.
Мои пакеты в потоке байт, всё правильно. Можешь называть то что я называю пакетом - Message, если тебе так понятней. Но когда я говорю Пакет, я всегда имею ввиду прежде всего Message, если в контексте не указано обратное, потому что я не оперирую пакетами на уровне сети, они глубоко под капотом, пускай там и остаются.

#12
18:03, 20 ноя. 2020

Да я не против называть как угодно, лишь бы путаницы не возникло. И в tcp часто применяют термин "пакет", причем, как в твоем смысле, так и доставочные пакеты более низкого уровня иметь в виду могут. И чуть ли не каждый второй начинающий полагает, что как он нарезал при отправке, такими порциями и придет, тем более, что в локальной сети оно действительно так, тестируешь то обычно локально, иллюзии есть шанс сохранять долго.

Ключевой момент в использовании TCP - нельзя превышать производительность канала доставки. Эффект от перегрузки очень неприятный. Как организовать, чтобы не требовать невозможного - не знаю. Я такого не делал, но какие-то фокусы есть в арсенале.

#13
(Правка: 23:07) 18:08, 20 ноя. 2020

Zab
Не, размер приходящих порций не важен. Вообще тут никаким боком не связано. TCP сам следит за балансировкой и скоростью, мне этого достаточно. От меня требуется лишь вовремя пополнять сокет буфер.

Итак, есть идея как всё таки реализовать буферизацию файла. Разберу пример без командных пакетов, для понятности.

Допустим у нас изначально FreeOptionalSize = MaxOptionalSize = 32 байта. (буду тут использовать байты вместо килобайт для понятности). Для понятности также сделаем SocketBuffer тоже = 32. WriterBuffer = 64.

Значит первый кусок файла который мы загружаем сразу в WriterBuffer, асинхронно, будет = 32.
После того как кусок загружен, мы вызываем Flush. Тем самым помечаем все текущие данные которые есть в WriterBuffer на отправку.

Далее в кадре 1 мы пытаемся отправить все эти данные, так как SocketBuffer еще пустой, то отправятся сразу все 32 байта. И мы сразу укажем что FreeOptionalSize снова = 32. И сразу в этом кадре начнем асинхронную загрузку следующего куска файла = 32. Как только кусок будет загружен вызовем Flush, FreeOptionalSize = 0.

В кадре 2, допустим что 2й кусок файла уже загрузился, мы его сразу посылаем. Но теперь в сокет влезло только 10 байт. Таким образом FreeOptionalSize = 10. В этом кадре больше ничего не делаем.

В кадре 3 допустим в сокет влезло еще 10 байт. Таким образом FreeOptionalSize = 20. Так как 20 это уже больше половины (16), то мы начинаем загрузку куска файла размером = 16. И опять после окончания вызываем Flush, FreeOptionalSize = 4.

Таким образом первые 1-2 куска мы загружаем в WriterBuffer полностью, а затем по идее они должны загружаться по половинке. Куски меньше мы не грузим, только либо половину, либо целый. И постепенно передадим таким образом весь файл. Плюс в еще в том, что мы подгружаем асинхронно куски сразу в WriterBuffer, а не в отдельный буффер и потом синхронно в WriterBuffer. Минус лишнее копирование.

Без учёта командных пакетов всё вроде просто, просто надо будет для командных пакетов просчитывать освободившийся FreeOptionalSize особым образом, так как отправляться в сокет будут не только Optional данные, но и накопившиеся Required. Хотя конечно теоретически во время передачи файла там может вообще не быть никаких командных пакетов, либо очень редко, но учитывать всё равно как то надо.

Например можно при записи Optional данных добавлять в очередь OptionalInfo(positionStart, positionEnd) и когда WriterBuffer будет отправлять именно этот регион в SocketBuffer, то добавлять его к FreeOptionalSize, а когда регион будет записан в сокет полностью, то удалять OptionalInfo из очереди. Допустим мы будем использовать Optional данные только для файлов, тогда очередь будет всего из 2х кусков файла (половинка + половинка), так что переполнение или разбухание подобной очереди не грозит.

Ладно, теперь вроде теоретически должно сработать.

Еще добавлю, что пакеты FileBegin и FileEnd больше не нужны. Так как загружаться будет не просто кусок файла. А "сырой кусок из файлов", то есть мы асинхронно наполняем буффер либо куском файла, либо несколькими файлами пока не заполним этот буффер нужным размером. Загрузка будет происходить используя манифест, размер, имена и порядок файлов в нём. Также на обратной стороне потом эти файлы "из сырого куска" будут расфасовываться в оболочки согласно манифесту. Короче говоря, мы посылаем не файлы по одному, а куски данных.

Также WriterSize теперь != MaxRequiredSize + MaxOptionalSize, вместо этого достаточно WriterSize = MaxRequiredSize >= MaxOptionalSize, то есть командные пакеты могут записываться полностью в весь WriterBuffer, а FreeOptionalSize будет зависеть от оставшегося места и от MaxOptionalSize.

#14
(Правка: 11:40) 8:53, 21 ноя. 2020

Я тут думаю по поводу манифеста.

Что если манифест воспринимать точно также как файл. Манифесты итак хранятся в файле. Но я думал сделать, что при загрузке манифеста из файла создаём объект с записями, тоже объектами. Но теперь я думаю, что можно загружать манифест в виде потока байт и потом использовать ManifestReader для его последовательного парсинга. В общем то мне не нужно иметь доступ к записям манифеста в случайном порядке, мне просто нужно получать эти записи слева направо.

Таким образом, можно не создавать List<ManifestRecord> ManifestRecords. А просто считывать их по порядку: ManifestReader.ReadHeader(), ManifestReader.ReadRecord().

Тогда весь манифест можно хранить в виде буфера байт. И тем самым упростить протокол передачи файловых данных. Теперь не нужны пакеты: ManifestBegin, ManifestRecord, ManifestEnd. Вместо этого мы просто используем пакет FileChunk, как и для файлов. И также посылаем его по кускам из 32/16.

Получится такой протокол:

FileTransferBegin
  byte Type // 0 = manifest, 1 = files.
  long Position // >= 0 = resume
FileChunk // формируем только если FreeOptionalSize >= MaxOptionalSize / 2.
  byte[] Data
FileTransferEnd
  byte Reason // 0 = finished, 1 = cancelled

Формат манифеста уже парсится после того, как данные манифеста были полностью получены.

PS: красота.

PS2: но потом еще нужно добавить возможность Pause/Resume/Cancel. Так что протокол немножко вырастет.

Страницы: 1 2 Следующая »
ПрограммированиеФорумСеть