MCP
Показаны сообщения с ярлыком .net. Показать все сообщения
Показаны сообщения с ярлыком .net. Показать все сообщения

вторник, 27 февраля 2018 г.

Хранение данных в памяти

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

Для начала скажу, что сам я, пока так и не реализовал эту идею в полной мере, были продукты, где это активно использовалось, но в основном, продукты, за которые я ответственен работают достаточно быстро и так, так что как-то и не требуется усложнение и смена парадигмы. Но, когда-нибудь, обязательно попробую!

Почему не стоит полагаться на кеш SQL-сервера?

Потому что он построен именно на модели кеша для данных, и в него можно попадать или не попадать. Конечно, в MS SQL есть OLTP таблицы, которые хранятся в памяти, но они больше для очень активных данных, и вообще это на уровне SQL, причём MS SQL. А без них — используются стандартные алгоритмы для поиска, оптимизированные для данных, находящихся на диске, как результат, необходимый кеш для очень 100% попадания в память многократно превышает размер реальных данных.

Я же предлагаю забить на это, и максимально хранить все данные из базы в памяти (загружать их из базы на старте, или ленивым образом).

Что это даёт?


  • Резко упрощается внешняя логика пользователя. Во многих случаях можно отказаться от джойнов, если нет дополнительной фильтрации. Например, классический джойн с данных с пользователем (например, вывести автора данных) можно заменить на выборку данных и доставание всех пользователей по ID. Это очёнь дешёво
  • Можно использовать очень тупые алгоритмы, и это всё равно будет очень быстро. Фуллскан 10000 записей почти незаметная вещь, а если надо будет ещё быстрее — всегда можно будет прикрутить "индекс" в виде словарика
  • Можно избавиться от отдельного кеша для тяжёлых данных, ибо и так всё в памяти
  • Тупо быстрее из-за отсутствия запросов к внешнему SQL-серверу
  • Легко хранить данные, которые тяжело забирать из SQL (объект с зависимыми детишками, слабо-структурированные документы, JSON/XML поля)
  • При этом, если приложение написано с использованием LINQ, то местами можно вообще не заметить разницы между базой и памятью (если грамотно спланировать архитектуру приложения)

 А в память влезет?

А вот это как раз главный вопрос, который всех останавливает и которого все боятся. И из-за этого, всё так и останавливается на уровне идей. Но давайте прикинем необходимое количество памяти.
  • База данных в 20ГБ содержит где-то 20ГБ данных (логично, да?), в памяти это будет занимать примерно столько же. Найти подходящий сервер — не так уж и сложно. Естественно, базы в 100МБ вообще влезут в память без проблем
  • Очень часто в "больших" базах большой объём занимают всякие полумёртвые данные — журналы, результаты импорта, файлы, подписанные данные, акксесс логи... Эти данные нужны очень редко, их можно не хранить в памяти, тем самым кардинально снизив объём "реальных" данных
  • Многие данные нужны только в небольшом количестве. Например, у вас есть 10000 пользователей в системе, но активны только 1000, тут можно использовать какой-нить LRU кеш и не держать в памяти все объекты, а только активные. Опять же, очень сокращает необходимый объём памяти
  • Ну и для реально огромных баз данных можно уж держать в памяти только специально выделенные объекты (например, справочники). Хотя, с такими объёмами у вас будет проблем побольше чем просто держать в памяти

Как реализовать?

Поскольку я ещё не делал это в полном виде, то могу только предположить следующие варианты:
  • Собственный кеш класса, ратающего с сущностью (e.g. UserManager), он сам решает, что и как кешировать. Проблемы в куче аналогичного кода в разных классах и сложность с инвалидацией. Плюсы: в каждом конкретном случае можно использовать самые эффективные варианты
  • Мемоизация и автомемоизация методов. Плюсы: очень упрощается код, минусы: сложно инвалидировать и оптимально использовать данные. 
  • Обёртка над ORM (или использование встроенных средств типа Second Level Cache), которая сделает всё сама. Проблемы: сложно в реализации и конфигурировании. Плюсы: полная прозрачность в использовании со стороны кода

Краткий итог

Данное агрессивное кеширование и разработка с учётом того, что оно есть, может кардинально поменять работу с данными в вашем приложении. Код будет проще, тупее и понятнее, но при этом весьма быстро работать, даже если выполняет сложные операции с данными.При этом сама база данных останется и в ней всегда можно покопаться, поделать аналитику, но при этом не сильно думать об её оптимизации (индексах, статистике и прочем тюнинге), это всё будет на уровне приложения, которое знает, что оно хочет от данных и как оно будет их использовать.

понедельник, 27 ноября 2017 г.

Ненавистный .NET

Последнее время совсем не пишу в блог, как-то нет подходящих тем, могу только сообщить, что такой ненависти к Microsoft я давно не испытывал. Попытка поработать с .NET Core 2.0 сразу же привела к идиотским ошибкам, типа 2 entry point у приложения. При этом второй генерируется самостоятельно (!), другими словами, у Microsoft новые отличнейшие идеи, как всё должно работать по их мнению, вместо того, чтобы просто сделать рабочий продукт.

Если кончится мат и появятся слова, постараюсь написать что-то более членораздельное. Но, блин, у Microsoft был отличнейший .NET, приложения на котором просто работали... Теперь не так, они могут падать по совершенно различным причинам, а Dll Hell уже перешёл все границы. Ну, как, как можно так портить жизнь разработчикам за их же деньги...

понедельник, 2 октября 2017 г.

Версионирование .NET Core

Когда-то писал про .NET Core и обещал написать больше, но было лениво, так что руки не дошли.

Сейчас просто для понимания бреда, который творится с версионированием краткое описание разных версий. Это может быть полезно, т.к. вышел .NET Core 2.0 и .NET Standard 2.0, и версии слились в экстазе. Но на самом деле они разные, и скоро разъедутся и будут опять портить всем жизнь. Итак, временно забудем про существование 2.0, и вспомним, что есть:


  • Вы собрались писать под .NET Core, соответственно выбираете, какую версию хотите, вы можете выбрать версию 1.0 (на момент написания 1.0.7) или 1.1 (на момент написания 1.1.4), при этом, особой разницы в этом нет. 
  • На самом деле, вы можете выбрать рантайм или сдк Логично, что для разработки нужен SDK, для версии рантайма 1.0.7, сдк имеет версию 1.1.4 (т.е. рантайм разрабатывается и старый и новый одновременно, сдк только новый)
  • После этого, вы можете решить, что использовать, .NET Standard или .NET Core. Для библиотек лучше использовать Standard, у него версии: 1.0,1.1,1.2,1.3,1.4,1.5,1.6, для запускаемых файлов лучше Core, у него версии 1.0 и 1.1
  • Впрочем, вы можете писать библиотеки на Core, а экзешники на Standard, в этом не очень много смысла, но в целом он есть
  • Версии Standard для удобства используют стандартную библиотеку NETStandard.Library, она бывает версий 1.6.0 и 1.6.1
  • В этой библиотеке есть стандартные библиотеки, которые любят называться как большие и иметь версию 4.3.0 (большие имеют версию 4.0.0). Впрочем, иногда бывают и 4.2.0 и 4.1.0, и всякие разные
Т.е. приложение мод .NET Core 1.0 может запускаться в рантайме 1.1.4, иметь зависимость на библиотеку .NET Standard 1.3, которая использует библиотеку NETStandard.Library 1.6.1 и это всё будет замечательно работать! Главное надо понять, что это просто разные версии разных библиотек. 

Сейчас вышел .NET Standard 2.0, и всё стало совсем просто: приложение под .NET Core 2.0 запускается в рантайме 2.0, имеет зависимость на библиотеку .NET Standard 2.0, которая использует библиотеку NETStandard.Library 2.0.0. К сожалению, скоро все эти версии опять разъедутся в разные стороны, и опять будет путаница. Но. надеюсь, вы теперь будете во все оружии.

PS: Сейчас слушаю про version hell в .NET Core 2.0, и становится страшно, там добавили совместимости из-за которой много всего развалилось, несмотря на обещанную совместимость.


среда, 8 марта 2017 г.

Visual Studio 2017 и очередная упоротость от Microsoft

Данный пост навеян статьёй о выходе VS2017, в которой есть такая шикарнейшая фраза:

We’re now encouraging Visual Studio 2015 users to migrate to MSBuild and csproj from project.json. As I stated above, we will not be supporting any of the new .NET Core tools in Visual Studio 2015. We also won’t be updating the Visual Studio 2015 project.json-based tools.
И вот эта фраза меня убило, хотя новости ходили давно, но тут они всё-таки сделали всё "в самом лучшем виде". И я не могу не высказаться по этому поводу.

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

В результате, часть времени в проекте уходит на миграцию на новую версию, часть на обучение разработчиков как сегодня правильно писать и часть на исправление незамеченных регрессов. В общем, это хорошо так перекликается с одним из моих ранних постов про велосипеды. Я, собственно, до сих пор предпочитаю использовать .NET 4.0: он работает начиная с XP и VS2010, он не мёртвый (апдейты выходят и для него, и свежие .NET улучшают его код, ибо в реальности одно и тоже), да и в новых не так уж и много полезных фич, чтобы переходить на него. Кроме того, никаких проблем с версионированием, которые поехали дальше: 4.5, 4.5.1, 4.5.2, 4.6, 4.6.1, 4.6.2 — без гугла различия и не вспомнишь.

Но тут решили делать всё по-новому и перешли на .NET4.6 и .NET Core, всё-таки у Core есть хорошие плюшки в виде нативной компиляции и гарантированной работы под Linux, да и разрабатывается она давно.

Про сам .NET Core писать не буду, ибо мелочь уже написана, на на ещё один пост не набирается, но вот что понравилось безоговорочно, это файлы project.json, которые очень похожи на package.json, но для Core. Собственно, наконец-то у Microsoft получился вменяемый формат проекта, который можно редактировать ручками не в плане хотфиксов, а прямо-таки изменять поведение проекта. Ну и работать с проектом не только в студии, но хоть в саблайме.

И тут приходит Microsoft и говорит, извините, факир был пьян, мы возвращаем всё назад. При этом новые фичи будут работать только в новой студии, а старые поддерживать мы больше не будем. Т.е. фактически кинули всех владельцев 2015-ой студии, которые работали с проектами на Core. При этом Microsoft всю жизнь тащила обратную совместимость, ибо так правильно и ынтырпрайзно.  Но, судя по всему, в команде .NET царит атмосфера вида: как хочется и как им удобнее. В результате, мало того, что ты летишь в самолёте, у которого по ходу дела меняют крылья на другие, ещё и забирают двигатели с фразой — они отстой, в новых всё лучше, покупайте наших слонов!

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

Эээх... как же хорошо было в .NET4.0...

четверг, 15 декабря 2016 г.

Краткий анализ .NET Core/Standard/Framework

Тут в очередной раз разбирался с тем, что в Microsoft нагородили с .NET, пока окончательно не понял, но в целом, чтобы не пересказывать основные статьи про всё это дело, просто сообщу, то что я понял:

  • Есть .NET Core, это специальный кроссплатформенный фреймворк
  • Есть .NET Framework, это классический .NET, работающий под винду
  • Есть .NET Standard, это набор API, который гарантированно будет работать в .NET Core и .NET Framework (ну и всяких ксамаринах)
  • Код, изначально написанный на .NET Framework, не будет работать под .NET Core
  • Если есть желание писать под .NET Standard, чтобы работало под большим .NET Framework, то лучше не делать так. Ибо работать будет, но плохо.
  • Потому что вся магия в том, что API похожее, но разное. 
  • Другими словами, версия под .NET Framework будет использовать очень похожий, но отдельный набор API. Если он весь скрыт внутри. то ничего страшного, никто не увидит. Если же вылезает наружу, могут быть неприятные последствия.
  • Ещё раз уточню, что если написано, что Standard 1.2=Framework 4.5.1, то это не значит, что код, написанный под 4.5.1 будет работать на уровне 1.2. Это просто значит, что если написать код, под 1.2, то его с помощью бубна можно заставить работать на 4.5.1. При этом набор API в 1.2 особо нигде не расписан, например, там нет криптографии. Странно, но вот так.
  • Судя по всему, самая популярная версия .NET Standard — 1.3, что в ней такого особого, пока не разобрался
Т.е. в целом ситуация складывается странная. Если хочется разрабатывать универсальные вещи, то лучше тупо не думать, а разрабатывать под .NET Core, забив на .NET Framework полностью. Потому что поставить .NET Core не сложно, а существущий код под .NET Framework всё равно придётся переделывать, так что, смысла во взрослом фреймворке на мой взгляд немного.

PS: Возможно, после дальнейшего знакомства со всем этим делом, моё мнение изменится, тогда обновлю пост. Но пока есть ощущение ужасной неразберихи и путаницы в API, чего стоит только набор версий фреймворка 4.5, 4.5.1, 4.6, 4.6.1, 4.6.2 — надо долго гуглить, чтобы найти отличия, но при этом для каждого из них, есть своя версия .NET Standard, но, поскольку, скоро будет .NET Standard 2.0, использовать .NET Standard 1.5 и 1.6, не рекомендуется из-за проблем с совместимостью. Ещё раз, стандартная версия фреймворка, сделанная для совместимости, будет несовместима сама с собой. А нам с этим жить...

воскресенье, 7 августа 2016 г.

Мегафичи моего архиватора

Я тут всё хвастаюсь, что делаю свой архиватор (и уже конкретно задолбал всех окружающих), а он всё делается, правда как-то весьма не спеша. В общем, решил рассказать про пару фич, которые на мой взгляд относительно уникальны для подобной задачи.

Полное шифрование без раскрытия информации

Т.е. вообще ничего не говорим. Даже что это архив нужного типа. Безопасность и красота. Есть рандомный набор байт, а является ли он архивом, или ещё чем-то — науке неизвестно. При этом оставлено обычное шифрование, которое можно оптимизировать в плане скорости, но оно раскрывает базовую информацию об архиве, что для обычных ситуаций не проблема, а наоборот удобство, но ведь надо периодически делать фичи и для параноиков.  

Управляющие блоки 

Вот с этим у других совсем грустно. Смысл в том, что у нас есть поток, который посылает много данных, мы их сжимаем, передаём дальше. Всё хорошо, но тут нам потребовалось послать дополнительную информацию, которая хоть и относится к этим данным, но по факту является метой. Что с этим можно сделать? Можно открыть другой поток, что решает все проблемы, но усложняет логику. Можно данные бить на блоке и в начале каждого блока ставить флажок о типе данных. Неплохое решение, но опять же, всё усложняет, плюс портит сжатие данных, т.к. данные разнородные.
В моём формате архиватора дофига места для подобных флагов, поэтому вы можете вместе с основными данными послать управляющие. Они не будут сжиматься, а пойдут сбоку, позволяя контролировать поток. Может быть (да и скорее всего), название выбрано неудачно, но смысл в том, что можно в один поток отправить два независимых набора данных, которые не очень сильно будут друг-другу мешать. При этом данные концептуально разные, так что у них есть логический смысл.

Восстановление данных (не сделано)

Мой архиватор по умолчанию использует CRC32C для проверки целостности, но хочется ещё и восстанавливать слегка повреждённые байты. И у меня есть прототип кода. Мало у кого есть подобное. У меня будет. Но пока не сделано, и это печально. Как будет сделано, буду хвалиться.


К чему я всё это написал? (Конечно же похвастаться!) К тому, что несовершенство мира раскрывается с каждой маленькой задачей. Вначале оказалась проблема с потоками и флашем, что привело к написанию моего архиватора, потом отсутствие управляющих последовательностей, восстановления данных... Что же будет дальше? Что ещё отсутствует из забавных фич в текущем наборе библиотек? Что ещё волшебного стоит мне добавить в архиватор?

воскресенье, 29 мая 2016 г.

Проблемы архиваторов, о которых никто не говорит

Я тут давно начал "писать" архиватор. Ну, т.е. как начал, сделал самое интересное, а допинать до конца терпения не хватило. Точнее почти хватило, но меня не устроили результаты, а, как известно, первая заповедь перфекциониста-прокрастинатора: лучше сделать хорошо, но никогда, чем плохо, но сейчас!

В общем, я полтора года не трогал свой архиватор, а тут решил, что всё-таки стоит пожертвовать 0.2% степени сжатия и 30МБ/c скорости, и выбрать простую и дубовую реализацию, так что. архиватор почти дописан, осталось сделать последний рывок и вычистить все баги, и добавить лоска. Впрочем, про сам мой алгоритм расскажу как-нибудь в другой раз.
Для начала расскажу про одну небольшую проблему, из-за которой, я начал писать архиватор, а не тупо взял готовый. Проблема весьма специфическая, но если вы в неё встрянете, то будет плохо, очень плохо.

В общем, вкратце, проблема называется Flush. Т.е. нормальная поддержка архиватором данной команды. Что это значит? А то, что большинство реализаций архиваторов внутри работают с блоками определённой длины (самой любимой) независимо! Т.е. команда Flush приводит к тому, что внутренний буфер отправляется в нижележащий стрим. И если в буфере много данных, то и проблем нет, а если мало, то результат становится очень неприятным с точки зрения качества сжатия.

Но ситуация может быть ещё хуже, архиваторы могут тупо проглатывать команду Flush (передаю привает реализации GZip в .NET, да она проглатывает Flush)! Что это значит? Это значит, что в некоторых задачах вы в принципе не можете использовать данную реализацию.

Собственно, про задачи-то я и забыл рассказать. Представьте, у вас есть TCP канал, в котором вы обмениваетесь сообщениями, например JSON'ом (так сейчас модно). Сообщения вам надо проталкивать на другую сторону и очень хочется их сжимать. А поскольку сообщения зависимые и похожие, то и сжимать зависимо. Проталкивать их надо командой Flush, что очевидно, а сообщения у вас небольшие. Что получается? Смотрите картинку:

В качестве тестовых данных, дамп википедии в 100 Мб (один из стандартных шаблонов для сжатия). По оси X - размер блока, который флушится, по оси Y - степень сжатия.
Что видно? ЧТо на блоке в 16 байт накладные расходны на заголовок превысили все ожидания (только GZip рулит за счёт мелкого заголовка). В дальнейшем, всё становится лучше, но до блока в 16Кб счастья особого нет. Т.е. сжимается, что уже неплохо, но могло быть и лучше.

Вот, если для примера взять бекап какой-нить базы (какой уже не помню, но это настоящая продуктовая база была). Особенность бекапа базы — очень выражена периодичность на 64Кб, да и вообще много похожести. Хотя флушить бекап базы не очень логично, но для примера пойдёт:

Результаты те же, только более явно выражены. И эта явность как раз и намекет, что 64Кб — это таки нашё всё. А лучше больше. Тот же график, но у него подрезано начала, чтобы был лучше виден масштаб:

Тут видно, что LZ4 за счёт максимального блока в 262Кб (по дефолту у данной реализации), вполне неплохо идёт дальше, но такой размер блока уже как-то лучше подходит для статичных данных, а там можно и более серьёзные аргументы, начинающиеся на цифру 7 применить.

В общем, цель моего архиватора: сделать так, чтобы Flush'и не оказывали серьёзного влияния, чтобы вы могли использовать архиватор не особо парясь. И как видите, всё вроде бы получается (мой - красненький). 1024 байта уже позволяют его весьма эффективно использовать.

Правда на блок уходит 8 лишних байт... и это меня бесит... Ждите следующий пост через пару лет...

воскресенье, 17 апреля 2016 г.

ZeroMQ vs NetMQ

Последнее время не спеша изучаю ZeroMQ, а так как пишу в основном на .NET, то решил сравнить производительность двух реализаций, "официальной" clrzmq4 (в nuget'е пакет называется ZeroMQ, и все примеры с сайта на нём), и альтернативный вариант NetMQ, написанный полностью на .NET и совместимый с ZeroMQ.

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

Для самого простого варианта я взял Publisher/Subscribe модель и начал закидывать сообщения по-кругу, и смотреть на результат. Результат оказался слегка неожиданным, но красноречивым. Не буду делать графики, ибо и без них всё понятно. Просто некоторые пункты:

  • clrzmq4/NetMQ одинаково быстры (около миллиона сообщений в секунду) при работе с массивами байт
  • Два паблишера на одного сабскрайбера (странный вариант, ну да ладно), гораздо быстрее работают у NetMQ 
  • При попытке работы со строками clrzmq4 начинает резко сливать производительность (в 3 раза). Судя по-всему, не очень удачная работа с маршаллингом
  • С clrzmq4 надо быть очень аккуратным, пропущенный Dispose даст прирост в скорости, но может вызвать неприятные ошибки
В общем, тестировать производительность дальше неинтересно, не думаю, что там есть какая-то волшебная разница на разных паттернах, с учётом того, что у NetMQ гораздо приятнее API, рекомендую рассматривать в качестве реализации API сразу же его и не париться с выбором.
NetMQ — быстрее и удобнее.

воскресенье, 15 ноября 2015 г.

Чем мне не нравятся async/await

Тут на днях у нас вышел спор с коллегами по поводу async/await в Microsoft .NET. Мне эта технология не очень нравится, несмотря на её круть, но сформулировать причины сходу весьма непросто. В этом посте попробую объяснить, в чём же проблемы (естественно, это моё мнение, и оно может не совпадать с генеральной линией партии).

1. Это не про потоки, это про асинхронность в одном

Т.е. основной смысл этой конструкции в том, чтобы занять основной поток, пока его часть ждёт какой-то асинхронной операции. Т.е. пока мы скачиываем файл, мы можем поделать ещё что-то, а не тупо ждать завершения. Идея здравая, но есть нюанс: должно существовать то, чем вам заняться. Например, если у нас клиентское приложение, мы в этом же потоке обработаем всякие события перерисовки, если однопоточное серверное, займёмся другим клиентом. Но если нам нечем заняться, то смысла в конструкции нет. Т.е. если мы сделали отдельный поток для пользователя и ждём когда скачается файл, чтобы что-то сделать. Мы не выиграем ничего от использования async/await. Просто будем использовать как небольшой синтаксический сахар с гигантcкой работой под капотом не по делу.

2. Неявная точка асинхронности

Когда мы вызываем асинхронный метод, мы не знаем, когда он реально перейдёт в асинхронность. Другими словами, возьмём стандартный подход: 
Task.Factory.StartNew(DoSomething);

Сразу после вызова данного метода, мы можем продолжать работу. А теперь глянем на async/await:
var t = DoSomething();
..........
await t;

В данном коде мы не знаем, когда реально дойдёт до нас управление, и будет ли реальная асинхронность. Фактически, мы вернёмся в метод, когда в DoSomething() будет встречен await, а когда это будет — зависит от реализации. Фактически, точка асинхронности слово await, но она находится внутри метода который мы вызываем, как результат, мы нам надо знать реализацию метода DoSomething, чтобы было всё прозрачно и понятно.
В большинстве случаев, это не проблема, но когда начинаются проблемы, вот такие мелкие нюансы очень портят жизнь.
Например, указано, что эксепшены пробрасываются на await, в результате можем получить код вида:
У которого вначале уничтожится мир, потом будет всё хорошо, а потом вылетит эксепшен. На мой взгляд не очень очевидно.

Вся эта неявность приводит к следующей проблеме:

3. Легко завалить производительность, при этом всё будет работать

Как я уже указал, данная технология про асинхронность. Соответственно всё должно быть асинхронно, всегда и везде. А что будет, если мы где-то забудем асинхронность? Или не мы, а тот кто написал библиотеку для нас. Может быть он не очень хороший программист и допустил маааленькую ошибку и сделал операцию синхронной. Как вы думаете, что будет? Да ничего фатального! Просто пользователь будет ждать. Например, один await не в том месте, привёл к тому, что HTTP-proxy на async/await стала обрабатывать все запросы от пользователей по очереди. И это работало год. Заметили случайно, при генерации отчётов сайт переставал открываться. А всему виной — ошибка в коде, про который все забыли и который с точки зрения логики, работал замечательно.

Другие примеры, чтобы понять масштаб проблемы: делаем запрос по HTTP, и перед запросом решили порезолвить DNS, синхронно (ну не нашли асинхронный метод). Можем получить 2 секунды тормозов на ровном месте. Использовали бы явную асинхронность вызовом метода в отдельном потоке — проблем не было бы. Ещё вариант — логгер, давно написанный, отлаженный, пишет в файлик, асинхронной версии не имеет, никому не нужна. Всё работает годами. Потом кто-то на том же интерфейсе решил запилить логгирование в базу, результат — пока один пишет, все ждут (у нас ведь синхронная версия). Провал производительности.

Хотя про то, что всё будет работать, я погорячился. Когда писал этот пост выяснил, что легко получить Deadlock из-за повторного входа и контекстов синхронизации. Не буду пересказывать статью, там на третьем примере всё достаточно просто и понятно рассказывается. 

В результате, чтобы справиться с проблемой мы должны использовать везде асинхронные версии, и это нас приводит к следующей проблеме

4. async/await заразные


Чтобы заиспользовать его, методу надо указать что он async. Если метод async, то вызывающий его метод должен использовать await и самому стать тоже async.
Т.е. весь код резко начинает обрастать async'ами, как снежный ком. Вырваться из данной западни можно двумя способами: async void (не используйте никогда!) или явным хаком с вызовом Wait у таска, возвращаемого async методом (что может привести к некоторым проблемам с контекстом синхронизации).
В общем, для нормального использования async'ов, у нас должна быть вся библиотека, написанная на них, которая использует другие библиотеки, написанные тоже на них. Надо писать сразу весь код в этой концепции, добавление небольшого куска не даёт никакого профита. А раз это концепция, то хотелось бы явно выделять её. Например, я вызываю метод DoSomething(), в зависимости от того, async он или нет, при вызове будут абсолютно разные вещи. Но без поддержки IDE я никогда не у знаю об этом. Если я добавлю к сигнатуре async, позднее, код скомпилируется и будет работать как-то, но о проблемах я узнаю по факту, а не во время компиляции.

И это косвенно подводит нас к очередной проблеме

5. Reflection и кодогенерация

Reflection должен знать про то, что методы бывают async, т.к. от этого зависит логика их работы (банально проброс эксепшена о котором я уже упомянул вначале). Оверрайдить данные методы кодогенрацией (моё любимое занятие) тоже надо аккуратно, ибо просто для понимания, вызов метода:
public static async Task DoSomething() { Console.WriteLine("A"); await Task.Delay(1000); }

По факту превратится в что-то подобное:
 А реальная логика будет запрятана далеко, и будет выглядеть как-то так:
Согласитесь, не самый простой для понимания пример.
А так как все эти async/await сделаны на уровне языка, а не MSIL, то среда ничего не знает про это, и тем же рефлекшеном достать это сложно. Что в связи с неявной точкой асинхронности может резко менять логику.

Например, мы хотим добавить валидацию параметров метода через AOP, простой пример, они не должны быть null. Так вот, если мы это делаем кодогенрацией, мы пропустим слово async и добавим провеку  в метод. Вызывающий код упадёт на вызове метода. Если же мы добавим проверку вручную, то на слове await (да, это мой любимый пример с эксепшенами, раз я его разобрал). При этом не обязательно использовать кодогенерацию, достаточно просто наследования.

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

Выводы

На мой взгляд, данная технология похожа на внутренности Perl'а. Для тех, кто не знает этот чудесный язык, поясню: смотришь примеры, видишь как всё просто и круто. Начинаешь углубляться в язык, чтобы понять как это работает и с какого-то момента становится очень страшно. Ты понимаешь огромную магию внутри всего этого. Ты понимаешь, что неверное движение может всё поломать, а работает только благодаря умным людям, которые написали кишки и программистам, которые не суют нос, куда не надо. И лишь спустя длительное время ты становишься таким умным человеком, и тебе становится всё понятно. Тебе спокойно. Так вот, я с async/await не стал таким умным человеком, но мне страшно, я уже могу всё сломать и я боюсь что кто-то из моей команды тоже может всё сломать, и не поймёт этого. И я не пойму. В результате всё будет работать не так как надо, а мы не будет знать, в чём проблема. А чтобы узнать, не достаточно статей "Async и await для чайников". Надо действительно погружаться в технологию. А оно мне не надо. Я не пишу приложения под Windows 10, а больше смысла-то и  нет, хотя Microsoft толкает её в новые библиотеки, вполне можно прожить и без неё, без этого странного волшебства и магии.

четверг, 23 апреля 2015 г.

Ещё одна проблема HttpWebReqest

Я уже писал про грабли HttpWebRequest, но тут нашёл ещё одну забавную, и местами неприятную. Правда наполовину она относится к HttpWebResponse, но классы связаны метровым канатом, так что не принципиально.
Вкратце: работа с куками организована очень оригинально, и не очень логично.

Для удобства распишу по условным пунктам. Для начала диспозиция:
  • Сервер возвращает куки в Http-заголовке Set-Cookie
  • Если надо установить две куки, сервер передаёт два заголовка (не очень логично, но ок)
  • HttpWebResponse имеет пропертю Headers, и по имени заголовка можно получить значение
Тут начинаются проблемы, ибо куки две, а заголовок один. Как вы думаете что вернётся? Не буду томить, скажу, что вернётся в этом методе содержимое двух кук через запятую. Очень, блин удобно. Считаем, что куки у нас разломаны в этом виде.

Но! У Headers можно взять значения через .GetValues(), вернётся честный массив из двух элементов. И вроде бы всё хорошо, и пост можно жакончить, но тут приходит сервер, и выдаёт нам:

Set-Cookie: ABC=123; expires=Fri, 31 Dec 2010 23:59:59 GMT; path=/;


Вы заметили, что между пятницей и 31-ым числом есть запятая? HttpWebResponse тоже заметил, и вместо этой куки честно вернул нам две, обе разломанные. Всё, приехали.

Но обойти это надо, поэтому можно сделать следующие вещи:
  • Вручную распарсить значения кук, зная, что запятая, по стандарту, запрещённый символ. Т.е. встретится она может только в expires
  • Взять у HttpWebResponse пропертю Cookies, с уже обработанными куками
Вроде второй вариант самый правильный и логичный, за исключением случаев, когда вам хочется посмотреть на изначальные данные от сервера, а не обработанные странным кодом. Но чтобы он работал, надо обязательно, у HttpWebReqest установить пропертю CookieContainer, иначе вам вернётся пустой массив в респонзе.

request.CookieContainer = new CookieContainer()

Немного нелогично (нам ведь только ответные нужны), но в принципе допустимо. И всё из-за весьма странной реализации работы с заголовками.

На этом всё, буду ловить очередные грабли данного класса. 

воскресенье, 15 июня 2014 г.

Использование быстрых алгоритмов сжатия в потоковом режиме

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

Начнём по-порядку.
  • zlibnet — считает метод Flush сигналом для записи финального блока, что приводит к некорректному поведению. Надо чинить (а лучше переписать самому, где там мой блокнотик?)
  • SharpCompress — любитель вызывать метод записи с длиной в 0 байт, что, имхо, весьма неаккуратно. Может на некоторые потоки создавать бессмысленную нагрузку
  • SharpZipLib — никаких нареканий
  • Встроенный GZip — буфер в 8Кб, полностью игнорируется Flush, оказывается для потоков его использовать нельзя.
  • Snappy — блочный алгоритм, блоки с префиксом и контрольной суммой, что накладывает отпечаток на возможностях
  • Snappy for .NET — нет реализации стримов, пропускаем
  • Snappy.NET — блок прибит гвоздями в 64Кб, Flush вызывает запись независимого блока. Т.е. частый флуш приведёт к потере сжатия и увеличению объёма
  • LZ4 — тоже блочный, при уменьшении блока резко падает степень сжатия, поведение аналогично Snappy. В реализациях (lz4-net и LZ4.NET) проблем не обнаружил (кроме самого факта с проблемами потока). Из хороших новостей — потоковое API для LZ4 разрабатывается (прямо в тот момент, как я пишу этот текст). Оно ещё не стандартизировано, но шансы есть, что будет всё круто.
  • LZF — я его достал с дальнего ящика и смотрю на него. Его особенность, что сжатие позволяет писать данные хоть по байту (и читать сжатые по байту). Правда исходные данные пока выглядят блочными, но думаю это можно будет поправить, если поисследовать алгоритм. 

суббота, 14 июня 2014 г.

Выбор быстрых алгоритмов сжатия под .NET

Для начала, пара таблиц для привлечения внимания:

Быстрый компьютер:
MemCopy:         1561.050       1709.7218        100.000%
GZipStd:           66.736        221.6318          6.335%
#ZipLib.Gzip:      52.800        136.0018          6.358%
zlibnet:          100.572        466.2888          6.500%
SharpComp.GZip:    52.568        154.7598          6.501%
Snappy.Net:       970.382        944.8468         13.312%
SnappyforNet:     997.337       1795.2078         14.499%
lz4net/CC:        485.191       1122.0048         10.740%
lz4net/MM:        997.337       1158.1988         10.740%
lz4net/N:         535.883       1122.0048         10.740%
lz4net/S:         386.066        690.4648         10.740%
lz4net/HC:         42.794       1282.2918          7.751%
LZ4.Net:          997.337       1158.1988         10.896%
QuickLZ:          460.310        528.0028          8.032%
LZO_1X   :       1683.082       1561.0508         11.824%
LZF     :         272.001        398.9358         13.882%
Медленный компьютер:
MemCopy:          394,551        394,5518        100,000%
GZipStd:           18,006         50,4278          8,738%
#ZipLib.Gzip:      16,137         45,2198          6,358%
zlibnet:           31,086        105,6008          6,500%
SharpComp.GZip:    18,356         46,6898          6,501%
Fail Snappy.Net: Инициализатор типа "Crc32C.NativeProxy" выдал исключение.
SnappyforNet:     260,175        432,5808         14,499%
Fail lz4net/CC: Ссылка на объект не указывает на экземпляр объекта.
Fail lz4net/MM: Ссылка на объект не указывает на экземпляр объекта.
lz4net/N:         218,928        228,6898         10,740%
lz4net/S:         120,484        141,9148         10,740%
Fail lz4net/HC: Ссылка на объект не указывает на экземпляр объекта.
LZ4.Net:          234,668        274,0778         10,896%
QuickLZ:           60,445         65,0448          8,032%
LZO_1X   :        374,001        505,6928         11,827%
LZF     :          44,880         60,3438         13,882%

Это я тестировал различные реализации алгоритмов сжатия и их скорости. Столбцы: скорость сжатия в MB/s, скорость декомпрессии, процент сжатого текста, относительно исходного материала.

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

Собственно, поэтому сразу же выкину из дальнейшего рассмотрения следующие алгоритмы:

  • QuickLZ — проблемы с лицензией,
  • LZO — работает через P/Invoke, мутный враппер, какие-то косяки с дебагом, проблемы с 64 битами не ясно дальнейшее развитие, собственно, его высокие показатели в тестах отчасти связаны с ограниченностью функционала, из-за которого, тест оказался в более выгодном положении относительно некоторых других (я даже не уверен, что он стабильно работает, хотя то что работает хотя бы один раз, это точно, я проверил)
  • LZF — хорош как вариант микро-компрессора (собственно, весь код можно зафигачить в 200 строчек не сильно экономя, при этом результат вполне сносный. Но, если вы не специалист по алгоритмам, не очень рекомендую заниматься этим делом. Хотя, возможно идея довести код до ума, вполне неплохая (надо записать себе в блокнот "обязательно сделать в следующей жизни").
Также в алгоритме не приняли участие: BZip2, LZMA и PPMd (степень сжатия отличная, скорость настолько низкая, что даже ради научного интереса их тут не стоит использовать.

Некоторые алгоритмы вида классического LZ77, не были найдены под .NET, поэтому тоже их пропускаем.

Теперь детально разберу оставшиеся GZip, LZ4, Snappy.

Gzip

Собственно, самый известный алгоритм сжатия, использующийся поголовно везде (хотя правильнее сказать, что алгоритм — Deflate, а GZip — поток с дополнительной метаинформацией). Если вы будете использовать его — у вас не будет никаких проблем с совместимостью, так что он очень хорош в плане требования по памяти и работы в потоковом режиме.
Но с выбором реализации есть некоторые проблемы — если вы сравните две верхних таблицы, то увидите что GZipStd (я так обозвал встроенный в .NET) даёт абсолютно разные варианты. Хитрость в том, что до .NET4.5, реализация GZip в .NET была ужасная, и её использовать стоило только в одном случае — никогда. Сейчас всё изменилось, и если вы пишите под 4.5, то вполне стоит использовать этот вариант, если нет критичного требования по скорости.

Если нужна максимальная скорость, то используйте zlibnet, это P/Invoke wrapper, поэтому он работает весьма шустро. Если у вас нет желания использовать P/Invoke (чуть сложнее деплой и требуется больше прав приложению), используйте SharpCompress, он мне показался чуть более удобным, быстрым и функциональным относительно классического SharpZipLib. Есть ещё библиотека SevenZipLib — P/Invoke wrapper к 7zip, но по внешниему интерфейсу я не очень понял, как работать с GZip, хотя в описании указано.

Snappy

Алгоритм от Гугла, ориентированный на максимальную скорость. Для .NET есть 2 реализации P/Invoke с оригинальными названиями: Snappy for .NET и Snappy.Net. Есть Safe-реализация Snappy.Sharp, но я её даже не пробовал, т.к. судя по всему работы ещё дофига, она полузаброшена, ничего особо не протестировано. Опять же, если есть желание — берите сами и дописывайте, иначе не советую использовать (записал второй пункт в блокнот).

Сам алгоритм очень шустрый (судя по всему, разработка велась с учётом особенностей процессоров и их кеширвоания), но сжатие у него так себе. Также у обоих реализаций есть проблемы. Snappy.Net не работает в 32х битах из-за какой-то ошибки в реализации библиотеки, вычисляющей CRC32 (третий пункт в блокнот — написать автору, что он лох и надо поправить). Snappy for .NET — требует VS2010 runtime, о чём надо помнить (я для тестов подложил нужные dll'ки на тестовый компьютер).
В общем, пока следует использовать с осторожностью, это не Production-решение

LZ4

Один из моих фаворитов, благо скорость отличная, но надо выбрать реализацию. Их две и обе хорошие. lz4-net — P/Invoke wrapper и LZ4.NET, флакон от автора с четырьмя разными реализациями, которые выбираются по приоритету и доступности: Mixed Mode, C++/CLI (требуется установленный VS2010 runtime, проверка идёт по наличию пакета в реестре, а не по DLL), Unsafe, Safe. Также, автор, возможно будет пилить дальше и улучшать свой код.

Также у алгоритма есть HC версия, которая даёт лучшее сжатие (но скорость сильно проседает), зато декомпрессия просто безумная. По идее, можно использовать это сжатие для данных, которые редко пишутся, но активно читаются.

Качество сжатия алгоритма зависит от дополнительного буфера на словарь, который в разных реализациях по дефолту 1Мб и 256Кб, в реальности, 64Кб дают пристойный результат, но и 1Мб не очень жалко для объёмных данных. Имейте в виду.

Заключение

Я, пока в раздумьях по поводу алгоритма и реализации, склоняюсь к GZip в P/Invoke исполнении и LZ4 в комплектном. Надо заранее определиться, какая скорость вам требуется: если вы передаёте огромные данные по сети со скоростью 1МБ/c, то GZip'а вам хватит за глаза, а сжатие будет активно помогать уменьшить объёмы. Если же сеть в гигабит, а данных немного, то со сжатием связываться вообще не стоит. LZ4 сидит где-то посередине и при своей скорости подходит для всего мало-мальски сжимаемого.

Решайте сами, я пока думаю, решение напишу позднее, когда потестирую всё это в продакшене (т.е. возможно, спустя длительное время).

вторник, 24 декабря 2013 г.

Забавности с async/await

Тут решил в одном проекте таки заюзать это чудо, вещь, конечно, забавная, но мозг выворачивает наизнанку. Обычное асинхронное программирование после этого кажется абсолютно простым и очевидным.
В качестве выворота мозга приведу простой пример (он упрощён до предела, в реальности, конечно, всё было сложнее):

public static void Process()
{
try
{
DoSomething();
}
catch (Exception)
{
}
}

public static async void DoSomething()
{
throw new Exception();
}

Итак, есть метод Process, который ловит все исключения и обрабатывает их, и метод DoSomething, который в процессе своей работы может кинуть эксепшен.
Как думаете, что будет в результате выполнения данного кода (вызова метода Process)?

Подумали? Теперь правильный ответ. Будет необработанный эксепшен и аварийное завершение приложения. Бздынь! Размещаем метод DoSomething где-нибудь подальше и наслаждаемся неожиданными крешами приложения в самый неожиданный момент.

Вот это один из примеров разрыва шаблонов. Чтобы такого не повторилось DoSomething должен возвращать не void а человеческий Task, после этого, чтобы избавиться от предупреждения о некорректном поведении в методе Process надо написать await DoSomething, а чтобы скомпилировалось к Process добавить async. И вот только после этого всё будет работать как и ожидалось.

Приятного кодинга 

суббота, 17 августа 2013 г.

Методы и производительность динамического создания конструкторов

В .NET достаточно развитые средства рефлексии, и как результат этим активно пользуются. При этом вариантов и способов использования достаточно много. Эти способы отличаются в зависимости от того что надо: по типу действий (создать объект, вызвать его метод, получить/изменить значение поля), по информации об объекте (мы заранее знаем сигнатуру или не знаем), по типу доступа (публичные/приватные методы), по наличию параметров.

Я как-то делал доклад по этим технологиям на YaNA, но с тех пор прошло много времени, обнился Framework, и в моей жизни количество кода с рефлексией значительно возросло, поэтому я решил обновить знания, провести тесты и поделиться результатами.

Сегодня я рассмотрю достаточно простой способ: вызов конкструктора без параметров и с известными параметрами. Это достаточно часто встречающаяся задача, например, она может использоваться для создания плагинов и модулей. При этом мы знаем, что объект имеет определённый тип (отнаследован от интерфейса, например), так что в дальнейшем мы можем вызывать методы напрямую. Но тем не менее, мы не можем напрямую создать объект, т.к. у нас есть только информация о типе (Type), но изначально мы просто не можем написать new MyObject() — данные придут позднее.

Соответственно есть несколько путей решений:

Activator.CreateInstance

Самый простой и удобный метод создания объекта:
Activator.CreateInstance(myObjectType)

Также можно передать параметры для конструктора. Всё просто и очевидно.

ConstructorInfo.Invoke

"Классический рефлекшен". 
var constructorInfo = myObjectType.GetConstructor(new Type[0]);
constructorInfo.Invoke(new object[0]);

Т.е. мы получаем объект ConstructorInfo и через рефлексию создаём объект. В данном примере нет параметров, но их легко можно передать массивом объектов

Compiled Expression

Ещё один сравнительно новый вариант создания объекта, который заключается в построении Expression'а и дальнейшей компиляции его в делегат (анонимную функцию). Способ создания без параметров выглядит так:
var d = Expression.Lambda<Func<object>>(Expression.New(constructorInfo)).Compile();

В результате у нас будет делегат, возвращающий object (можно специфицировать точнее), который мы можем вызывать как обычный метод.

Если конструктор имеет параметры и нам точно известны, какие они должны быть, мы можем использовать подобный код:
var p1 = Expression.Parameter(typeof(int));
var p2 = Expression.Parameter(typeof(string));
var newExpression = Expression.New(constructorInfo, p1, p2);
var d = Expression.Lambda<Func<int, string, object>>(newExpression, true, p1, p2).Compile();

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

Compiled Expression with Any Parameters

Чтобы избавиться от этой форы и рассмотреть случаи, когда мы заранее не можем знать параметры, надо сгенерировать что-то общее. Логично будет рассмотреть случай, когда у нас есть делегат, принимающий массив параметров типа object, при этом данный делегат внутри сам проводит преобразования для корректного вызова. Код будет выглядеть примерно так:

var argsExpr = Expression.Parameter(typeof(object[]));
var constructorParams = constructorInfo.GetParameters()
.Select((p, i) => Expression.Convert(Expression.ArrayAccess(argsExpr, Expression.Constant(i)), p.ParameterType));
var newExpr = Expression.New(constructorInfo, constructorParams);
var d = Expression.Lambda<Func<object[], object>>(newExpr, true, argsExpr).Compile();

Т.е. мы передаём массив, в котором находятся аргументы, потом из этого массива достаём параметры для конструктора, конвертируем к нужному типу и вызываем конструктор.

Dynamic Method

Самый сложный метод, который заключается в использовании DynamicMethod и ручной генерации MSIL'а. Поскольку решение в отличие от остальных не совсем тривиальное, детальное описание реализации оставлю для следующей статьи (будет ссылка, когда статья будет написана). Мы его также как и Expression можем сделать специфичным, или обобщённым. У обобщённого основная идея та же, что и в предыдущем методе: мы генерируем делегат, принимающий массив object'ов, внутри приводим аргументы к нужному типу и вызываем конструктор. Только делаем всё это нативно, генерируя MSIL и компилируя его.

Результаты производительности

Мерять тут можно много чего. Но я остановился на следующих условиях:
  • Только публичные конкструкторы (приватные значительно дольше, а необходимость в их использовании весьма специфичная)
  • Время подготовки нужного объекта не замеряется. Подразумевается, что если уж есть цель оптимизации, то этим временем можно пренебречь. Хотя оно может и вносить ощутимые задержки на первый вызов. Если для вас это будет существенно, наверное стоит провести замеры на ваших условиях и железе.
  • Объекты очень простые но не совершенно пустые (сложность этих объектов может сильно повлиять на расстановку сил, нивелировав или максимально усилив различия между методами
  • Проверяется Debug и Release режим, чтобы выяснить ускорение, получаемое кодом от оптимизации
  • Конкретные цифры я не покажу, ибо это сильно зависит от железа. Только отношение
  • Также для сравнения добавлен прямой вызов конструктора 

Вызов конструктора без параметров

reflection constructor performance

По графику можно сделать простой вывод: если скорость не очень критична, используйте Activator, а если объектов нужно создавать очень много, то Expression. И да, Release режим в некоторых случаях рулит. Рефлекшен через ConstructorInfo самый медленный. Если у вас есть желание, можете использовать DynamicMethod, он самый быстрый.

Вызов конструктора с параметрами

Я взял конструктор с двумя параметрами int и string, чтобы было заметно влияние боксинга. Результаты получились следующие:
reflection constructor performance

Те же графики, но без активатора, чтобы был лучше виден масштаб:
reflection constructor performance


В данном случае Activator показал дичайшие тормоза, что очень удивительно, относительно предыдущего варианта. Обобщённый экспрешшен в 2 раза медленнее специализированного (хоть на графике это плохо видно) и в 4-6 раз медленнее прямого вызова (для сравнения скорости). Т.е. вывод тут такой — используйте скомпилированные экспрешшены, по-возможности с явными типами, если нет, то с обобщёнными, потери не очень сильные. При этом обобщённый DynamicMethod быстрее обобщённого Expresssion'а, но медленнее типизированного. Т.е. если хочется максимальной скорости, придётся относительно много писать, чтобы сделать максимально типизированный и производительный DynamicMethod.

понедельник, 27 мая 2013 г.

Цифровая подпись больших файлов на .NET в потоковом режиме

Стандартный механизм в .NET по подписи через SignedCms, весьма симпатичный, но есть одна проблема: для подписи необходимо передать массив байт, т.е. если надо подписать большой файл, то вначале прочитать в память в .NET потом засунуть этот же объём системному API, что весьма затратно по памяти.

Конечно, в CryptoPro есть замечательный метод SignHash, который позволяет заранее посчитать хеш и его подписать (что идентично подписи самого файла), но это только у CryptoPro, стандартное API даже на низком уровне не позволяет провести такое.

Гугление выдало код, который может шифровать в потоковом режиме, но у него обнаружилось несколько проблем:

  • Почему-то используется FileStream, хотя достаточно просто Stream
  • Реально всё равно читается всё содержимое (!) а потом подписывается в потоковом режиме
  • Нет поддержки Detached подписи, хотя добавить её несложно
  • Внаглую из сертификата берётся приватный ключ, который опять же считается RSA (наивные! совершенно не знают что в России творится)
В общем, я поправил этот код, и выкладываю его здесь, ибо взял из блога исходники, надо их таким же образом вернуть, чтобы людям было приятно.

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

StreamCms.cs
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Runtime.InteropServices;
using System.ComponentModel;

namespace NS1
{
 [ComVisible(false)]
 public class StreamCms
 {
  // File stream to use in callback function
  private Stream _callbackFile;

  // Streaming callback function for encoding
  private bool StreamOutputCallback(IntPtr pvArg, IntPtr pbData, int cbData, bool fFinal)
  {
   if (cbData == 0) return true;
   // Write all bytes to encoded file
   var bytes = new byte[cbData];
   Marshal.Copy(pbData, bytes, 0, cbData);
   _callbackFile.Write(bytes, 0, cbData);

   if (fFinal)
   {
    // This is the last piece. Close the file
    _callbackFile.Flush();
    _callbackFile.Close();
    _callbackFile = null;
   }

   return true;
  }

  // Encode StreamCms with streaming to support large data
  public void Encode(X509Certificate2 cert, Stream inFile, Stream outFile, bool isDetached)
  {
   // Variables

   IntPtr hProv = IntPtr.Zero;
   IntPtr SignerInfoPtr = IntPtr.Zero;
   IntPtr CertBlobsPtr = IntPtr.Zero;
   IntPtr hMsg = IntPtr.Zero;

   try
   {
    // Prepare stream for encoded info
    _callbackFile = outFile;

    // Get cert chain
    var chain = new X509Chain();
    chain.Build(cert);
    var chainElements = new X509ChainElement[chain.ChainElements.Count];
    chain.ChainElements.CopyTo(chainElements, 0);

    // Get certs in chain
    var certs = new X509Certificate2[chainElements.Length];
    for (int i = 0; i < chainElements.Length; i++)
    {
     certs[i] = chainElements[i].Certificate;
    }

    // Get context of all certs in chain
    var CertContexts = new Win32.CERT_CONTEXT[certs.Length];
    for (int i = 0; i < certs.Length; i++)
    {
     CertContexts[i] = (Win32.CERT_CONTEXT)Marshal.PtrToStructure(certs[i].Handle, typeof(Win32.CERT_CONTEXT));
    }

    // Get cert blob of all certs
    var CertBlobs = new Win32.BLOB[CertContexts.Length];
    for (int i = 0; i < CertContexts.Length; i++)
    {
     CertBlobs[i].cbData = CertContexts[i].cbCertEncoded;
     CertBlobs[i].pbData = CertContexts[i].pbCertEncoded;
    }

    // Get CSP of client certificate

    Win32.CRYPT_KEY_PROV_INFO csp;
    GetPrivateKeyInfo(GetCertContext(cert), out csp);

    bool bResult = Win32.CryptAcquireContext(
     ref hProv,
     csp.pwszContainerName,
     csp.pwszProvName,
     (int)csp.dwProvType,
     0);
    if (!bResult)
    {
     throw new Exception("CryptAcquireContext error #" + Marshal.GetLastWin32Error().ToString(), new Win32Exception(Marshal.GetLastWin32Error()));
    }

    // Populate Signer Info struct
    var SignerInfo = new Win32.CMSG_SIGNER_ENCODE_INFO();
    SignerInfo.cbSize = Marshal.SizeOf(SignerInfo);
    SignerInfo.pCertInfo = CertContexts[0].pCertInfo;
    SignerInfo.hCryptProvOrhNCryptKey = hProv;
    SignerInfo.dwKeySpec = (int)csp.dwKeySpec;
    SignerInfo.HashAlgorithm.pszObjId = cert.SignatureAlgorithm.Value; // Win32.szOID_OIWSEC_sha1;

    // Populate Signed Info struct
    var SignedInfo = new Win32.CMSG_SIGNED_ENCODE_INFO();
    SignedInfo.cbSize = Marshal.SizeOf(SignedInfo);

    SignedInfo.cSigners = 1;
    SignerInfoPtr = Marshal.AllocHGlobal(Marshal.SizeOf(SignerInfo));
    Marshal.StructureToPtr(SignerInfo, SignerInfoPtr, false);
    SignedInfo.rgSigners = SignerInfoPtr;

    SignedInfo.cCertEncoded = CertBlobs.Length;
    CertBlobsPtr = Marshal.AllocHGlobal(Marshal.SizeOf(CertBlobs[0]) * CertBlobs.Length);
    for (int i = 0; i < CertBlobs.Length; i++)
    {
     Marshal.StructureToPtr(CertBlobs[i], new IntPtr(CertBlobsPtr.ToInt64() + (Marshal.SizeOf(CertBlobs[i]) * i)), false);
    }

    SignedInfo.rgCertEncoded = CertBlobsPtr;

    // Populate Stream Info struct
    var StreamInfo = new Win32.CMSG_STREAM_INFO
                      {
                       cbContent = (int)inFile.Length,
                       pfnStreamOutput = StreamOutputCallback
                      };

    // Open message to encode
    hMsg = Win32.CryptMsgOpenToEncode(
     Win32.X509_ASN_ENCODING | Win32.PKCS_7_ASN_ENCODING,
     isDetached ? Win32.CMSG_DETACHED_FLAG : 0,
     Win32.CMSG_SIGNED,
     ref SignedInfo,
     null,
     ref StreamInfo);

    if (hMsg.Equals(IntPtr.Zero))
    {
     throw new Exception("CryptMsgOpenToEncode error #" + Marshal.GetLastWin32Error().ToString(), new Win32Exception(Marshal.GetLastWin32Error()));
    }

    // Process the whole message
    ProcessMessage(hMsg, inFile);
   }
   finally
   {
    // Clean up

    if (inFile != null)
    {
     inFile.Close();
    }

    if (_callbackFile != null)
    {
     _callbackFile.Close();
    }

    if (!CertBlobsPtr.Equals(IntPtr.Zero))
    {
     Marshal.FreeHGlobal(CertBlobsPtr);
    }

    if (!SignerInfoPtr.Equals(IntPtr.Zero))
    {
     Marshal.FreeHGlobal(SignerInfoPtr);
    }

    if (!hProv.Equals(IntPtr.Zero))
    {
     Win32.CryptReleaseContext(hProv, 0);
    }

    if (!hMsg.Equals(IntPtr.Zero))
    {
     Win32.CryptMsgClose(hMsg);
    }
   }
  }

  // Decode StreamCms with streaming to support large data
  public void Decode(Stream dataFile, Stream signFile, Stream outFile, bool isDetached)
  {
   // Variables

   IntPtr hMsg = IntPtr.Zero;
   IntPtr pSignerCertInfo = IntPtr.Zero;
   IntPtr pSignerCertContext = IntPtr.Zero;
   IntPtr hStore = IntPtr.Zero;

   try
   {
    // Get data to decode

    // Prepare stream for decoded info
    _callbackFile = outFile;

    // Populate Stream Info struct
    var StreamInfo = new Win32.CMSG_STREAM_INFO
                      {
                       cbContent = (int)signFile.Length,
                       pfnStreamOutput = StreamOutputCallback
                      };

    // Open message to decode
    hMsg = Win32.CryptMsgOpenToDecode(
     Win32.X509_ASN_ENCODING | Win32.PKCS_7_ASN_ENCODING,
     isDetached ? Win32.CMSG_DETACHED_FLAG : 0,
     0,
     IntPtr.Zero,
     IntPtr.Zero,
     ref StreamInfo);
    if (hMsg.Equals(IntPtr.Zero))
    {
     throw new Exception("CryptMsgOpenToDecode error #" + Marshal.GetLastWin32Error().ToString(), new Win32Exception(Marshal.GetLastWin32Error()));
    }

    // Process the whole message
    if (isDetached)
    {
     ProcessMessage(hMsg, signFile);
     ProcessMessage(hMsg, dataFile);
    }
    else
    {
     ProcessMessage(hMsg, signFile);
    }

    // Get signer certificate info
    int cbSignerCertInfo = 0;
    bool bResult = Win32.CryptMsgGetParam(
     hMsg,
     Win32.CMSG_SIGNER_CERT_INFO_PARAM,
     0,
     IntPtr.Zero,
     ref cbSignerCertInfo);
    if (!bResult)
    {
     throw new Exception("CryptMsgGetParam error #" + Marshal.GetLastWin32Error().ToString(), new Win32Exception(Marshal.GetLastWin32Error()));
    }

    pSignerCertInfo = Marshal.AllocHGlobal(cbSignerCertInfo);

    bResult = Win32.CryptMsgGetParam(
     hMsg,
     Win32.CMSG_SIGNER_CERT_INFO_PARAM,
     0,
     pSignerCertInfo,
     ref cbSignerCertInfo);
    if (!bResult)
    {
     throw new Exception("CryptMsgGetParam error #" + Marshal.GetLastWin32Error().ToString(), new Win32Exception(Marshal.GetLastWin32Error()));
    }

    // Open a cert store in memory with the certs from the message
    hStore = Win32.CertOpenStore(
     Win32.CERT_STORE_PROV_MSG,
     Win32.X509_ASN_ENCODING | Win32.PKCS_7_ASN_ENCODING,
     IntPtr.Zero,
     0,
     hMsg);
    if (hStore.Equals(IntPtr.Zero))
    {
     throw new Exception("CertOpenStore error #" + Marshal.GetLastWin32Error().ToString(), new Win32Exception(Marshal.GetLastWin32Error()));
    }

    // Find the signer's cert in the store
    pSignerCertContext = Win32.CertGetSubjectCertificateFromStore(
     hStore,
     Win32.X509_ASN_ENCODING | Win32.PKCS_7_ASN_ENCODING,
     pSignerCertInfo);
    if (pSignerCertContext.Equals(IntPtr.Zero))
    {
     throw new Exception("CertGetSubjectCertificateFromStore error #" + Marshal.GetLastWin32Error().ToString(), new Win32Exception(Marshal.GetLastWin32Error()));
    }

    // Set message for verifying
    var SignerCertContext = (Win32.CERT_CONTEXT)Marshal.PtrToStructure(pSignerCertContext, typeof(Win32.CERT_CONTEXT));
    bResult = Win32.CryptMsgControl(
     hMsg,
     0,
     Win32.CMSG_CTRL_VERIFY_SIGNATURE,
     SignerCertContext.pCertInfo);
    if (!bResult)
    {
     throw new Exception("CryptMsgControl error #" + Marshal.GetLastWin32Error().ToString(), new Win32Exception(Marshal.GetLastWin32Error()));
    }
   }
   finally
   {
    // Clean up
    if (!pSignerCertContext.Equals(IntPtr.Zero))
    {
     Win32.CertFreeCertificateContext(pSignerCertContext);
    }

    if (!pSignerCertInfo.Equals(IntPtr.Zero))
    {
     Marshal.FreeHGlobal(pSignerCertInfo);
    }

    if (!hStore.Equals(IntPtr.Zero))
    {
     Win32.CertCloseStore(hStore, Win32.CERT_CLOSE_STORE_FORCE_FLAG);
    }

    if (dataFile != null)
    {
     dataFile.Close();
    }

    if (signFile != null)
    {
     signFile.Close();
    }

    if (_callbackFile != null)
    {
     _callbackFile.Close();
    }

    if (!hMsg.Equals(IntPtr.Zero))
    {
     Win32.CryptMsgClose(hMsg);
    }
   }
  }

  private void ProcessMessage(IntPtr hMsg, Stream dataStream)
  {
   long streamSize = dataStream.Length;
   if (streamSize == 0)
    throw new CryptographicException("Cannot encode zero length data");
   var gchandle = new GCHandle();
   const int ChunkSize = 1024 * 1024;
   var dwSize = (int)((streamSize < ChunkSize) ? streamSize : ChunkSize);
   var pbData = new byte[dwSize];

   try
   {
    var dwRemaining = streamSize;
    gchandle = GCHandle.Alloc(pbData, GCHandleType.Pinned);
    var pbPtr = gchandle.AddrOfPinnedObject();

    while (dwRemaining > 0)
    {
     dataStream.Read(pbData, 0, dwSize);

     // Update message piece by piece     
     var bResult = Win32.CryptMsgUpdate(hMsg, pbPtr, dwSize, (dwRemaining <= dwSize));
     if (!bResult)
     {
      throw new Exception(
       "CryptMsgUpdate error #" + Marshal.GetLastWin32Error().ToString(),
       new Win32Exception(Marshal.GetLastWin32Error()));
     }

     dwRemaining -= dwSize;
     if (dwRemaining < dwSize)
      dwSize = (int)dwRemaining;

     // if (gchandle.IsAllocated)
     //  gchandle.Free();
    }
   }
   finally
   {
    if (gchandle.IsAllocated)
    {
     gchandle.Free();
    }
   }
  }

  internal static Win32.CertHandle GetCertContext(X509Certificate2 certificate)
  {
   var handle = Win32.CertDuplicateCertificateContext(certificate.Handle);
   GC.KeepAlive(certificate);
   return handle;
  }

  internal static bool GetPrivateKeyInfo(Win32.CertHandle safeCertContext, out Win32.CRYPT_KEY_PROV_INFO parameters)
  {
   parameters = new Win32.CRYPT_KEY_PROV_INFO();
   var invalidHandle = new Win32.SafeHandle(IntPtr.Zero);
   uint pcbData = 0;
   if (!Win32.CertGetCertificateContextProperty(safeCertContext, 2, invalidHandle.DangerousGetHandle(), ref pcbData))
   {
    if (Marshal.GetLastWin32Error() != -2146885628)
    {
     throw new CryptographicException(Marshal.GetLastWin32Error());
    }

    return false;
   }

   invalidHandle = Win32.LocalAlloc(0, new IntPtr(pcbData));
   if (!Win32.CertGetCertificateContextProperty(safeCertContext, 2, invalidHandle.DangerousGetHandle(), ref pcbData))
   {
    if (Marshal.GetLastWin32Error() != -2146885628)
    {
     throw new CryptographicException(Marshal.GetLastWin32Error());
    }

    return false;
   }

   parameters = (Win32.CRYPT_KEY_PROV_INFO)Marshal.PtrToStructure(invalidHandle.DangerousGetHandle(), typeof(Win32.CRYPT_KEY_PROV_INFO));
   invalidHandle.Dispose();
   return true;
  }
 }
}
Win32.cs
using System;
using System.Runtime.InteropServices;

using Microsoft.Win32.SafeHandles;

namespace NS1
{
 [ComVisible(false)]
 public class Win32
 {
  #region "CONSTS"

  public const int X509_ASN_ENCODING = 0x00000001;

  public const int PKCS_7_ASN_ENCODING = 0x00010000;

  public const int CMSG_SIGNED = 2;

  public const int CMSG_DETACHED_FLAG = 0x00000004;

  public const int AT_KEYEXCHANGE = 1;

  public const int AT_SIGNATURE = 2;

  // public const string szOID_OIWSEC_sha1 = "1.3.14.3.2.26";

  public const int CMSG_CTRL_VERIFY_SIGNATURE = 1;

  public const int CMSG_CERT_PARAM = 12;

  public const int CMSG_SIGNER_CERT_INFO_PARAM = 7;

  public const int CERT_STORE_PROV_MSG = 1;

  public const int CERT_CLOSE_STORE_FORCE_FLAG = 1;

  #endregion

  #region "STRUCTS"

  [StructLayout(LayoutKind.Sequential)]
  [ComVisible(false)]
  public struct CRYPT_ALGORITHM_IDENTIFIER
  {
   public string pszObjId;
   public BLOB Parameters;
  }

  [StructLayout(LayoutKind.Sequential)]
  [ComVisible(false)]
  public struct CERT_ID
  {
   public int dwIdChoice;
   public BLOB IssuerSerialNumberOrKeyIdOrHashId;
  }

  [StructLayout(LayoutKind.Sequential)]
  [ComVisible(false)]
  public struct CMSG_SIGNER_ENCODE_INFO
  {
   public int cbSize;
   public IntPtr pCertInfo;
   public IntPtr hCryptProvOrhNCryptKey;
   public int dwKeySpec;
   public CRYPT_ALGORITHM_IDENTIFIER HashAlgorithm;
   public IntPtr pvHashAuxInfo;
   public int cAuthAttr;
   public IntPtr rgAuthAttr;
   public int cUnauthAttr;
   public IntPtr rgUnauthAttr;
   public CERT_ID SignerId;
   public CRYPT_ALGORITHM_IDENTIFIER HashEncryptionAlgorithm;
   public IntPtr pvHashEncryptionAuxInfo;
  }

  [StructLayout(LayoutKind.Sequential)]
  [ComVisible(false)]
  public struct CERT_CONTEXT
  {
   public int dwCertEncodingType;
   public IntPtr pbCertEncoded;
   public int cbCertEncoded;
   public IntPtr pCertInfo;
   public IntPtr hCertStore;
  }

  [StructLayout(LayoutKind.Sequential)]
  [ComVisible(false)]
  public struct BLOB
  {
   public int cbData;
   public IntPtr pbData;
  }

  [StructLayout(LayoutKind.Sequential)]
  [ComVisible(false)]
  public struct CMSG_SIGNED_ENCODE_INFO
  {
   public int cbSize;
   public int cSigners;
   public IntPtr rgSigners;
   public int cCertEncoded;
   public IntPtr rgCertEncoded;
   public int cCrlEncoded;
   public IntPtr rgCrlEncoded;
   public int cAttrCertEncoded;
   public IntPtr rgAttrCertEncoded;
  }

  [StructLayout(LayoutKind.Sequential)]
  [ComVisible(false)]
  public struct CMSG_STREAM_INFO
  {
   public int cbContent;
   public StreamOutputCallbackDelegate pfnStreamOutput;
   public IntPtr pvArg;
  }

  [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
  [ComVisible(false)]
  internal struct CRYPT_KEY_PROV_INFO
  {
   internal string pwszContainerName;
   internal string pwszProvName;
   internal uint dwProvType;
   internal uint dwFlags;
   internal uint cProvParam;
   internal IntPtr rgProvParam;
   internal uint dwKeySpec;
  }

  #endregion

  #region "DELEGATES"

  [ComVisible(false)]
  public delegate bool StreamOutputCallbackDelegate(IntPtr pvArg, IntPtr pbData, int cbData, Boolean fFinal);

  #endregion

  #region "API"

  [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
  [ComVisible(false)]
  public static extern Boolean CryptAcquireContext(
    ref IntPtr hProv,
    String pszContainer,
    String pszProvider,
    int dwProvType,
    int dwFlags);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern IntPtr CryptMsgOpenToEncode(
   int dwMsgEncodingType,
   int dwFlags,
   int dwMsgType,
   ref CMSG_SIGNED_ENCODE_INFO pvMsgEncodeInfo,
   String pszInnerContentObjID,
   ref CMSG_STREAM_INFO pStreamInfo);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern IntPtr CryptMsgOpenToDecode(
   int dwMsgEncodingType,
   int dwFlags,
   int dwMsgType,
   IntPtr hCryptProv,
   IntPtr pRecipientInfo,
   ref CMSG_STREAM_INFO pStreamInfo);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern Boolean CryptMsgClose(IntPtr hCryptMsg);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern Boolean CryptMsgUpdate(
   IntPtr hCryptMsg,
   Byte[] pbData,
   int cbData,
   Boolean fFinal);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern Boolean CryptMsgUpdate(
   IntPtr hCryptMsg,
   IntPtr pbData,
   int cbData,
   Boolean fFinal);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern Boolean CryptMsgGetParam(
   IntPtr hCryptMsg,
   int dwParamType,
   int dwIndex,
   IntPtr pvData,
   ref int pcbData);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern Boolean CryptMsgControl(
   IntPtr hCryptMsg,
   int dwFlags,
   int dwCtrlType,
   IntPtr pvCtrlPara);

  [DllImport("advapi32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern Boolean CryptReleaseContext(
   IntPtr hProv,
   int dwFlags);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern IntPtr CertCreateCertificateContext(
   int dwCertEncodingType,
   IntPtr pbCertEncoded,
   int cbCertEncoded);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern bool CertFreeCertificateContext(IntPtr pCertContext);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern IntPtr CertOpenStore(
   int lpszStoreProvider,
   int dwMsgAndCertEncodingType,
   IntPtr hCryptProv,
   int dwFlags,
   IntPtr pvPara);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern IntPtr CertGetSubjectCertificateFromStore(
   IntPtr hCertStore,
   int dwCertEncodingType,
   IntPtr pCertId);

  [DllImport("Crypt32.dll", SetLastError = true)]
  [ComVisible(false)]
  public static extern IntPtr CertCloseStore(
   IntPtr hCertStore,
   int dwFlags);

  [DllImport("crypt32.dll", CharSet = CharSet.Auto, SetLastError = true)]
  internal static extern CertHandle CertDuplicateCertificateContext([In] IntPtr pCertContext);

  [DllImport("crypt32.dll", CharSet = CharSet.Auto, SetLastError = true)]
  internal static extern bool CertGetCertificateContextProperty([In] CertHandle pCertContext, [In] uint dwPropId, [In, Out] IntPtr pvData, [In, Out] ref uint pcbData);

  [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
  internal static extern SafeHandle LocalAlloc([In] uint uFlags, [In] IntPtr sizetdwBytes);

  [DllImport("kernel32.dll", SetLastError = true)]
  private static extern IntPtr LocalFree(IntPtr handle);

  #endregion

  public class SafeHandle : SafeHandleZeroOrMinusOneIsInvalid
  {
   public SafeHandle(IntPtr handle) : base(true)
   {
    SetHandle(handle);
   }


   public SafeHandle()
    : base(true)
   {
   }

   protected override bool ReleaseHandle()
   {
    return (LocalFree(handle) == IntPtr.Zero);
   }
  }

  public class CertHandle : SafeHandleZeroOrMinusOneIsInvalid
  {
   public CertHandle()
    : base(true)
   {
   }

   public CertHandle(bool ownsHandle)
    : base(ownsHandle)
   {
   }

   protected override bool ReleaseHandle()
   {
    return CertFreeCertificateContext(handle);
   }
  }
 }
}

среда, 27 марта 2013 г.

HttpWebRequest некоторые неочевидности

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


  • Сколько бы вы не создали клиентов, в реальности одновременно к серверу будут идти 2 запроса, остальные попадут в очередь. Планируйте это при реализации параллельности. Изменить количество можно установив ServicePoint.ConnectionLimit или уникальный ConnectionGroupName
  • Балансировка запросов в настоящий момент достаточно туповатая, поэтому, если вам в принципе достаточно небольшого количества одновременных коннекций, но некоторые могут занимать долгое время, вы можете получить нехорошую проблему: запрос зависнет в очереди. Происходит это тогда, когда кончаются все доступные коннекшены, запрос уходит в очередь к какому-либо из них, и продолжится когда тот закончится. Если при этом освобождаются другие — это ни на что не влияет. Т.е. вы можете получить пиковую нагрузку, очередь и толпу отвалившихся запросов, в случае если один будет долгим, а другие пристроятся ему в хвост.
  • Timeout на самом деле ограничивает время выполнения всего запроса, а не ожидания данных. Т.е. если вы не спеша получаете файл в 10Gb, вам плюнется таймаут. Не забудьте увеличить. А проблемы с долгим соединением решайте вручную парой Begin/End
  • Если вы используете Begin/End (BeginGetRequestStream, например), и решаете забросить запрос в случае долгого ответа, не забудьте сделать Abort, а то запрос может дойти, когда его уже никто не ждёт и не хочет обрабатывать
  • Несмотря на возможность установить AllowWriteStreamBuffering в false, память всё равно будет сжираться где-то в его недрах (возможно поправят поведение, ибо кажется багом, т.к. данные уходят по факту), так что при передаче гигабайт данных для надёжноти включите чанкинг
  • Размер данных, при которых будет отправка не регулируется, опытным путём установлено, что при чанкинге он 1024 байта. Т.е. если вы хотите медленно посылать небольшие порции данных на сервер, вы можете посылать их тупо в буфер, так что лучше не делать подобное и посылать всё целиком (получать данные с сервера маленькими кусками можно)
  • Если на сервере используется недоверенный https сертификат, то чтобы не получать ошибку соединения, надо подписаться на ServicePointManager.ServerVerificationCallback и там проверять сертификат на корректность. Метод статичный, так что следите за тем, чтобы не перехватывать чужие запросы (если в вашем приложении ещё кто-то посылает данные)
  • HttpWebRequest использует своё кеширование DNS, по умолчанию 2 минуты. Что странно, т.к. резолв идёт через системный DNS-кеш, т.е. накладных расходов не происходит. Можно изменить это время в ServicePointManager.DnsRefreshTimeout, и аккуратнее с KeepAlive, т.к. при его наличии будет держаться коннекшен к серверу некоторе время, и резолва не произойдёт.
  • Если сервер вам посылает данные очень медленно, то проверить не умер ли он, можно установив ServicePoint.SetTcpKeepAlive, что позволит пинговать сервер средствами TCP, и получить ошибку в случае если сервер реально пропал.
На этом пока всё, если будут ещё интересные моменты, то буду обновлять пост.

понедельник, 23 июля 2012 г.

MVC. А нужны ли Контроллеры?


Мне концепция MVC в принципе, достаточно нравится, но в мелочах сильно раздражает. У нас есть бесправные View, которые по факту про всё знают и активно занимаются навигацей. У нас есть тупые Модели, которые периодически берут на себя заботу о данных, и есть Контроллеры, занимающиеся непонятно чем, только не тем, чем нужно.
MVC позволяет писать гибко, но за эту гибкость приходится расплачиваться жёсткими гайдлайнами или же полной неразберихой. Кто должен готовить данные для View? Контроллер, Модель, внешний слой бизнес-логики? И если внешний слой, то он должен возвращать Модель, или модель должна его использовать для наполнения, или контроллер должен заполнить модель, имеющимися данными?
Модель должна быть ViewModel или настоящая, живая? А может быть просто DTO?

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

Все контроллеры в приложении примерно равны

В приложении любой контроллер делает примерно одну и ту же вещь: он содержит в себе набор команд и некий базовый функционал (например, работа с сессией или получение текущего пользователя). Навигация между одним контроллером или же между разными ничем не отличается (просто параметр у хелпера). Но при этом, по-хорошему передать управление другому контроллеру без хаков нельзя. Поэтому написать красивый диспатчер не получится, можно сделать редирект через клиента, но это издевательство над здравым смыслом. Или же можно сделать замечательный диспатчер, полностью раскроив всю структуру контроллеров и засунуть всю обработку куда-то внутрь, оставив наружу свою некую реализацию контроллера и маршрутизации.

Контроллеры разделены логически, а не функционально

Вот смотрите, есть у нас класс User и какой-нибудь UserManager, который отвечает за создание пользователей, получение данных, редактирование и удаление. Всё просто и логично. Теперь делаем приложение, контроллеры и получаем UserRegistrationController, UserProfileController, UserAdministrationController и использование пользователей в каких-нибудь RoleManagerController, GroupManagerContoller, LoginController и куче других. Можно пытаться сделать один большой контроллер, но получим помойку, т.к. функционал администратора управляющего пользователями сильно отличается от функционала нового, регистрирующегося пользователя. В результате, с одной стороны мы раскидываем работу с пользователями по куче контроллеров, с другой, при попытке их объединить мы получаем жуткую чехарду с функционалом и контекстом.

Контроллеры занимаются самыми разными вещами

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

Вот такая вот подлость с этими контроллерами. А как решать эту проблему — я не знаю. У меня не настолько светлый ум.  Но если хотите моего мнения (а если не хотите, то всё равно напишу): утащить всю логику в некое WebAPI со своей маршрутизацией. Это WebAPI будет заниматься получением и сохранением данных, не трогая контроллеры. А за самими контроллерами оставить простейшую логику по отдаче нужных страничек по урлу, максимально вырезав из них всё остальное. И идти дальше, разбираться с View и Models.

вторник, 26 июня 2012 г.

Циклы. Нужны ли они?

Не стоит воспринимать данный текст слишком серьёзно. В нём присутствует некоторая доля бреда

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

Давайте подумаем, где в этом красивом стройном мире место циклам? А места на самом деле и нет! Не нужны циклы при повседневной работе. Они могут быть запрятаны где-то внутри библиотек или вообще быть только на уровне компилятора. Без них можно совершенно спокойно обойтись, всему есть красивые замены. Перечислим, для чего используют циклы:

Преобразование

Типичная задача: взять из списка объектов "Пользователь" только имя и работать с ним дальше. Есть вполне сформировавшиеся абстракции, например map в jQuery или Select в .NET. Согласитесь, что второй пример кода выглядит гораздо понятнее первого, т.к. отсутствует лишний вспомогательный код:

List<string> names = new List<string>(); 
foreach(var user in users) 
    names.Add(user.Name); 

var names = users.Select(x => x.Name);

Фильтрация

Тоже частая задача. Выбрать всех активных пользователей. Тут стандартные схемы filter и Where:
List<User> resUsers = new List<User>(); 
foreach(var user in users) 
    if(user.IsActive) 
        resUsers.Add(user); 

var resUsers = users.Where(x => x.IsActive);

Аггрегация 

Куда же без неё! Например, нам нужно посчитать количество пользователей (да, у нас сейчас нет встроенной функции):
var cnt = 0; 
foreach(var user in users) 
    cnt++; 
А в нормальном стиле (в общем виде) это можно сделать как-то так, через аккумулятор:
var cnt = users.Aggregate((e, acc) => acc + 1);

Обработка

В данном случае результат передаётся куда-то дальше, основной объект никак не трансформируется. Тут нам на помощь приходят each в jQuery и ForEach в .NET. Например, мы хотим вывести имена пользователей на консоль:
//names - List<int> из первого примера 
foreach(var name in names)  
    Console.WriteLine(name);  

names.ForEach(Console.WriteLine);

И всё вместе

И конечно же, циклы используются сразу для всех задач вместе, ещё больше запутывая код и подменяющие реализацию задачи ненужными деталями реализации (которые, действительно, никому никогда не нужны и только мешают дальнейшему чтению кода).
Итак, выбираем имена активных пользователей и находим самое длинное:
List<string> names = new List<string>();  
foreach(var user in users)  
    if(user.IsActive) 
        names.Add(user.Name);  

var maxName = ""; 
foreach(var name in names) 
    if(maxName.Length < name.length)  
        maxName = name; 

//-------------------- 
var maxName = users 
    .Where(x => x.IsActive) 
    .Select(x => x.Name) 
    .Aggregate("", (e, acc) => e.Length > acc.Length ? e : acc);

Согласитесь, второй вариант, гораздо понятнее и короче. Сразу по тексту видно что он делает (и это я ещё не воспользовался готовым методом Max). В первом же случае проще всего догадаться о том, что делает код по названию переменной, но это в данном простом тестовом примере. А в реальной жизни всё гораздо запутаннее, ведь обязательно набегут оптимизаторы и цикл превратится в более короткую и простую версию, которая понятнее (вроде бы), но в результате вся логика прибита гвоздями и её изменение становится уже более сложной задачей, связанной с практически полным переписыванием куска кода:
var maxName = ""; 
foreach(var user in users)  
    if(user.IsActive && user.Name.Length > maxName.Length) 
        maxName = name; 

Результат

Что мы получили: циклы всегда можно заменить на простую комбинацию простых конкретных операций, в результате код будет легче читаться, легче сопровождаться и будет содержать меньше ошибок. Кроме того, мы избавляемся от врождённой болезни циклов — побочных эффектов (что сильно мешает оптимизациям даже на уровне компиляторов). Так что у них остаётся единственное преимущество — производительность. Но и она постепенно нивелируется развитием комплияторов и библиотек, так что может стоит отказаться от циклов прямо сейчас, и не быть слоупоком?


Напоследок

Чтобы не останавливаться на элементарных примерах, я решил привести чуть более сложный. А именно, реализацию сортировки. Классический quicksort из учебника выглядит примерно так (только главная часть):
public void Sort(int start, int end) 
{ 
    if (end <= start) return; 
    if (end - start == 1) 
    { 
        if (_array[end] < _array[start]) Swap(start, end); 
        return; 
    } 
    int el = _array[(start + end)/2]; 
    int endIdx = end; 
    int startIdx = start; 
    while(startIdx <= endIdx) 
    { 
        if (_array[startIdx] < el) startIdx++; 
        else 
            if (_array[endIdx] <= el) 
            { 
                Swap(startIdx, endIdx); 
                endIdx--; 
                startIdx++; 
            } 
            else endIdx--; 
    } 
    Sort(start, endIdx); 
    Sort(endIdx + 1, end); 
}
Данный код, конечно можно подсократить, но это уже тонкости и детали реализации и оптимизации. Тем не менее, даже на такой пример я потратил достаточно времени, пока его реализовал и выловил все замеченные ошибки. И в теперь разобраться как сортирует алгоритм за всеми конструкциями весьма сложно. А теперь посмотрите, что я написал сходу на LINQ:

public IEnumerable<int> Sort(IEnumerable<int> arr) 
{ 
    if (arr.Count() <= 1) return arr; 
    int el = arr.First(); 
    return Sort(arr.Where(x => x < el))
           .Concat(arr.Where(x => x == el))
           .Concat(Sort(arr.Where(x => x > el))); 
}
Всего 5 строчек, и то разбитых для удобства! И смотрите как просто объяснить теперь алгоритм: берём первый попавшийся элемент, берём из массива все элементы меньше него, сортируем их данным же алгоритом, добавляем элементы равные данному, и большие, отсортированные тем же алгоритмом. Всё просто, банально и понятно.
Не хочем quicksort, хотим сортировку выбором? Нет ничего проще:
public IEnumerable<int> Sort(IEnumerable<int> arr) 
{ 
    if (arr.Count() <= 1) return arr; 
    int elMax = arr.Max(); 
    return Sort(arr.Where(x => x < elMax)).Concat(arr.Where(x => x == elMax)); 
}

Видите, вполне чёткая логика и понятность. Так зачем вам ещё циклы?