IIS + .NET + Json. Пишем свой Application Server из песочницы

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

И так, что же такое «Сервер приложений»?

Попытаюсь сформулировать своими словами.
— «Сервер приложений» является одним из терминов, относящихся к архитектуре распределенных N-уровневых систем, где он занимает центральное место и находится между клиентами, выполняющими запросы на обработку и получение данных и хранилищем этих данных.
— Сервер приложений – это программное обеспечение, основанное на специальной архитектуре и технологиях, которые работают на стороне сервера. Специфика заключается в обслуживании множества клиентов как по их количеству так и по разнообразию их видов.
— Сервер приложений осуществляет необходимую обработку и трансформацию данных в определенных форматах, как поступивших от клиентов, так и отсылаемых им.
— Сервер приложений предоставляет API прикладного уровня, методы которого можно использовать для реализации того или иного клиентского приложения.

Предыстория данного проекта была такая.
Разрабатывая как-то очередное приложение на базе новомодной технологии Microsoft Windows Communication Faundation (далее просто WCF), возникло желание помимо толстого .NET клиента под Windows сделать к этому сервису веб интерфейс. Для освоения был выбран ExtJs. Нагуглив немного материала по теме «как подключить Ajax клиента к WCF сервису», были сделаны первые наброски. Макет заработал, но уже тогда начали возникать некоторые недовольства в способах достижения казалось бы элементарных вещей. Слишком много «ненужностей» на первый взгляд приходилось делать для этого, которые на практике были просто необходимы.

Еще немного истории.
Был опыт работы и несколько удачно реализованных проектов на WCF. В одной умной книжке от O’Reilly была рекомендация не делать в интерфейсе сервиса много методов (не больше десятка +-). Исходя из этого и из реальной необходимости иметь множество различных методов у сервиса, эволиционно пришло решение сделать один основной исполняемый метод плюс несколько дополнительных по-необходимости. Т.к. на тот момент речь шла только о .NET клиентах сервиса, то этот основной метод принимал имя вызываемого метода и сериализованные BinaryFormatter-ом параметры, плюс еще AssemblyQualifiedName на тот случай, когда методы были во внешних сборках.
Но такое решение было полностью несовместимым с Web-клиентами и пришлось возвращаться обратно к «плоским» методам, реализованным в интерфейсе сервиса.
Возникали проблемы и при работе с потоками, не смотря на поддержку MTOM. Мне так и не удалось заставить WCF нормально пропускать потоки через прокси сервера.
Еще одна неприятность в связке WCF + Ajax, это Json сериализация. Ну казалось уж куда проще, так нет, и здесь образовалась масса своих нюансов.
К тому же, практика все время показывала, что у клиентов, у которых внедрялись решения на базе WCF, очень часто возникают проблемы с установкой .NET 3.5 SP1 на сервера. То сам фреймворк не установлен, то .svc не зарегистрировано, что чаще всего, то еще чего-то.
По-маленьку, по-тихоньку, WCF переставал нравиться.

А в голове тем временем не давала покоя мысль, как же работают в интернете сервисы известных соцсетей, веб-проектов, различные App Store. Ведь не пхп единым. Понятное дело, что это не «Виндовс», но не в этом суть. Суть в том что, эти сервисы обеспечивают данными различные типы клиентов. Один сервис – множество разных клиентов, это же «круто» и «архи-важно» в наше время, но как сделать такое на .NET?

Было решено попробовать использовать .asmx WebService-ы из .NET 2.0, к тому же в .NET 3.5 появились расширения, позволяющие им взаимодействовать с Ajax миром. Скажу сразу, что-то получилось, что-то нет, но все равно оставалось чувство, что все эти расширения-дополнения для XML веб-сервисов к ним «за уши притянуты» и не родные они им.

Я не хочу сказать, что технологии WCF и WebService совсем не годятся для реализации Web 2.0, но повторюсь – то, что казалось должно делаться интуитивно просто, вызывало подчас такие «непонятки», что хотелось все бросить. Как-то сложно выглядела реализация простых вещей, а некоторые вещи и вовсе невозможно было сделать.
Можно было бы привести здесь список неприятных моментов, которые возникали в процессе разработки, но если честно – теперь уже даже вспоминать не хочется, после того, как все стало элегантно и просто.

Так должно же быть что-то такое в .NET пригодное для достижения поставленных целей? Наше спасение – это System.Net и System.Web.
И так, выбираем IHttpHandler.

Взглянем на HttpContext с его HttpRequest и HttpResponse. В них почти полностью реализованы низкоуровневые детали Http протокола из Webengine и выложены нам на блюдечке. Нам же остается только нарисовать «голубую каемочку».

/// <summary>
/// Enables processing of HTTP Web requests by a custom HttpHandler.
/// </summary>
/// <param name="httpContext">Контекст Http запроса.</param>
void IHttpHandler.ProcessRequest(HttpContext httpContext)

Мы получили запрос от какого-то клиента.
А дальше что нам с ним делать? Ответ – все что душе угодно. Да, да, именно так.
Только еще раз сформулируем, чего же мы хотим:
1. Один сервер приложений – множество типов клиентов (.NET, Java, Web + Ajax, Silverlight, iPhone, Android и т.д.);
2. Вызов необходимого прикладного метода сервера приложений из любого клиента;
3. Поддержка “GET” и “POST” запросов, т.е. передача параметров через Url или тело запроса;
4. Поддержка сжатия параметров и результатов выполнения методов, отсылаемых назад клиентам;
5. Работа с потоками в обе стороны без каких-либо усилий;
6. Быстрота, легкость, асинхронность, прозрачность и понятность решения.

При реализации 1-го пункта, я пришел к выводу, что существуют типы данных, которые есть почти во всех языках и платформах, совместимые между собой и пригодные для передачи их через границу среды (тоже мне называется – открыл Америку). Подчеркиваю слово «совместимые», т.к. это принципиальный момент:
символы, строки (с учетом кодировки), простые типы данных (различные виды чисел, булевы значения), массивы байт и потоки. Дата обычно имеет определенное строковое представление. Cюда же можно отнести массивы, списки и словари, как контейнеры вышеназванных типов данных.
Естественным выбором был Json, как формат обмена. Как вариант, была выбрана библиотека Json.NET от Newtonsoft.

И так, клиент должен уметь вызывать у сервера определенный прикладной метод (RPC) и получить от сервера понятный ему ответ. Вызываемый метод задается строкой с именем. Методу нужны параметры. Он должен их правильно распознать и обработать. Для большей гибкости, методы могут принадлежать не только самому серверу, но и любой сборке, которая серверу доступна, правда с учетом некоторых ограничений в плане безопасности.
Стиль REST популярен, но не совсем гибкий. Это «шаблонный» стиль. Шаг влево-вправо, и теряется универсальность. Остается незаменимый QueryString.

Сервер получает от клиента запрос вида:
/service.ashx?method=GetImage(“DSCN2099.JPG”)

Жирным выделены имена ключевых параметров.
Понятно, что метод принадлежит самому сервису, т.к. нет никаких других указаний, где его искать, в качестве параметра принимает имя запрашиваемой картинки. Где находится картинка, известно только вам как разработчику сервиса. На выбор: каталог в файловой системе; ресурс из какой-либо сборки; БД; runtime рисование; внешний ресурс и т.д. Это и есть прикладная логика.

Полный формат строки запроса:
/service.ashx?
session = xxxxxxxxxxxxxxxxx &    -- ИД сессии
class   = ПолноеИмяТипаСборки &  -- “FullTypeName, AssemblyName”
method  = ИмяМетода(Параметры) & -- (имя метода с учетом регистра)
format  = Json/DotNetBinary &    –- формат данных (расширяемо)
zip     = on/off                 -- сжаты ли входные параметры метода

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

Что вернуть клиенту? Зависит от самого клиентского приложения. Если картинка представляет собой миниатюру (thumbnail), то можно вернуть массив байт. Если это большая картинка, то лучше вернуть поток.

И так, сервер приложений получив запрос, обрабатывает его (парсит), создает контекст выполнения указанного метода и вызывает метод на исполнение. Метод реализует прикладную логику и в конце возвращает return result; А куда дальше идет этот result? Не в воздух же. А возращается результат из метода обратно в ядро сервера приложений как универсальный object. Далее анализируется тип возвращаемого значения. Если это typeof(void), то больше ничего не делается, если это Stream, то он перенаправляется в выходной поток HttpResponse-а. Иначе — результат отдается Json сериализатору для преобразования в строку, а потом эта строка записывается через HttpResponse->Write().

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

Примерно так же реализована инфраструктура SOAP .asmx WebService. Только там тяжелый но всеядный XML, а здесь легкий и быстрый Json. .aspx и WCF – это тоже handler-ы.

Так что же с нашими клиентами, ради которых это все затевалось?

С .NET все просто.
Тут тебе на выбор и родной бинарный формат и Json, а HttpWebResponse->GetResponseStream() — это тот самый поток, который вернул наш прикладной метод “GetImage()” сервера приложений, разница только в типе этого потока, здесь это будет сетевой стрим — один из внутренних классов .NET Framework-а. Да это нам и неважно. Важно, что мы можем его прочитать и сделать например Image.FromStream() или просто сохранить в файл. Если бы сервер приложений вернул массив двоичных данных (byte[]), то это по сути тоже самое, т.к. нет другого способа кроме GetResponseStream(). Только мы должны преобразовать его обратно в этот самый массив. Что делать с этим потоком, решает разработчик клиентского приложения, на основе того прикладного API, который предоставляет ему сервер приложений.

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

Ajax
Представленная идеология сервера приложений очень удачно вписывается в реализацию клиентов на основе Ajax фреймворков. Ведь Json для них даже роднее чем XML.
Ext.Ajax.request({
   method: 'GET',
   url: '/service.ashx?method=GetImageInfo(“DSCN2099.JPG”)',
   success: function(response, options) {
      var result = Ext.decode(response.responseText);
   },
   failure: function(response, options) { ... }
});

Серверный метод:
public ImageInfo GetImageInfo(Json json)
{
   string fileName = json.AsString;
   string filePath = Path.Combine(IMAGES_DIR, fileName);
   return new ImageInfo(filePath);
}

Возращаемая методом “GetImageInfo()” структура ImageInfo, будет преобразована Json сериализатором во что-то наподобие следующей строки:
{
   "Name":  "DSCN2099.JPG",
   "Height":  1536,
   "Width":  2048,
   "PixelFormat":  137224,
   "RawFormat":  "Jpeg",
   "HorizontalResolution":  300.0,
   "VerticalResolution":  300.0,
   "ThumbImage":  "/9j/4AAQSkZJRgABAQ…",
   "FileInfo":  {
      "FileSize":  1849625,
      "CreationTime":  "\/Date(1246514398257+0400)\/",
      "FileAttributes":  32
   }
}

А сервер приложений отправит ее клиенту, где она будет декодирована в javascript объект.
Дату можно получить путем вызова:
var date = Date.parseDate(result.FileInfo.CreationTime, 'M$').format('d.m.Y h:i'),

а миниатюру:
var image = {
   xtype: 'box',
   autoEl: {
      tag: 'div',
      children: [{
         tag: 'img',
         src: String.format('data:image/jpg;base64,{0}', result.ThumbImage)
      }]
   },
   listeners: {
      render: function(comp) {
         comp.getEl().on({
            dblclick: function() {
               var url = '/service.ashx?method=GetImage(result.Name)';
               window.open(url, 'imageWindow', 'menubar=no, location=no, resizable=yes, scrollbars=yes, status=no, width=640, height=480');
            },
            scope: comp
         });
      }
   }
};

Наш base64 ThumbImage успешно преобразовался в картику соответсвующего размера, а двойной клик на ней откроет новое окно браузера и отобразит полноценную картинку.

Для примера еще такой трюк. Строка из таблицы стилей. Иконки находятся в ресурсной сборке:
.loading
{
   background-image: url(/service.ashx?method=LoadIcon%28%22loading.gif%22%29) !important;
}

Ну что ж, вроде пока неплохо. Два клиента у нас уже есть.

Попробуем Silverlight?
Отличия от «толстого» .NET клиента заключаются в полном отсутствии синхронных метотов у HttpWebRequest и HttpWebResponse. Для работы с Json есть реализация Json.NET для Silverlight. В остальном практически тоже самое, что и для настольных .NET приложений.
Для упрощения взаимодействия с сервером приложений, разработан специальный класс Request, который формирует строку запроса и выполняет его. Он инкапсулирует в себе работу с HttpWebRequest и HttpWebResponse:
RequestParams requestParams = new RequestParams();
requestParams.Method.Name = "GetImage(fileName)";
requestParams.Method.Params.Add("fileName", "DSCN2099");

Request request = new Request("http://localhost/AppService");
request.Execute(requestParams, Action<RequestCompletedEventArgs> onRequestCompleted);

Т.е. подготовив параметры запроса, мы просто вызываем серверный метод и обрабатываем результат в методе обратного вызова. Проще не бывает!

На очереди Java.
Основной класс — это HttpURLConnection, который имеет метод getInputStream(). Называется – найди два отличия от .NET (не считая названия). Идея таже самая – создаются вспомогательные классы RequestParams и Request. Класс RequestParams – это имя вызываемого метода и его параметры, а Request инкапсулирует логику работы с HttpURLConnection. Для Json сериализации используется библиотека от Google – Gson, которую можно использовать и для разработки под Android. Все что приходит в Json формате с сервера приложений, реализованного на антагонистической по отношению к Java платформе, без проблем ею «переваривается». Единственное, что по умолчанию не понимает Java – это формат даты от MS. Но Gson расширяемая библиотека, и проблема решается просто:
public class MSDateJsonSerializer implements JsonSerializer<Date> {
   public JsonElement serialize(Date date, Type typeOfT, JsonSerializationContext context) {
      return new JsonPrimitive("/Date(" + date.getTime() + ")/");
   }
}

public class MSDateJsonDeserializer implements JsonDeserializer<Date> {
   public Date deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
      String jsonDateToMilliseconds = "\\/(Date\\((.*?)(\\+.*)?\\))\\/";
      Pattern pattern = Pattern.compile(jsonDateToMilliseconds);
      Matcher matcher = pattern.matcher(json.getAsJsonPrimitive().getAsString());
      String result = matcher.replaceAll("$2");
      return new Date(new Long(result));
   }
}

Потоки из .NET также совместимы с Java (потоки они и в Африке потоки):
ImageIcon icon = new ImageIcon(“http://localhost/AppService/service.ashx? method=GetImage('DSCN2099.JPG')”);

Итак, мы имеем уже 4 клиента, а с учетом того, что Android приложения пишутся на Java, то все 5.

Думаю, теперь становится понятно, что ограничений по типу клиентов практически нет, и все платформы, умеющие работать с Http и Json, смогут взаимодействовать с нашим сервером приложений.
+6
11 июля 2011, 00:57
10
YuriyM 2,5

комментарии (19)

0
RomanNikitin #
Я такое делал на основе ASP.NET MVC3.
Там это гораздо управляемей и быстрей в реализации выходит.
Легкий маппинг на урл (api.service.com/get-user/1/json)
Версионировать легко. (api.service.com/v2/get-user/2/xml);

Но подумываю переписать на WCF — в плюсах вижу:
+ разные биндинги (http, socket)
+ разные возвращаемые типы (protocol buffers, json, xml etc). Тут важно разобраться с REST Starter Kit.

Отсюда например в другой .net можно импортировать вебсервис, а Java и другие будут общаться c REST API через http & json.

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

Еще мне кажется, что пример с загрузкой и отдачей картинки — не самый удобный для демонстрации API. Обычно это отдельный низкоуровневый хэндлер для отдачи изображений и к нему совсем другие требования, чем например к выборке например пользователей.
0
YuriyM #
Похоже, я упустил из вида описание самой «фишки» предлагаемого подхода. У сервера приложений всего лишь одна точка входа — "/service.ashx", никаких других хэндлеров нет и они не нужны! Этот базовый адрес с помощью несложного клиентского API, который легко пишется для любого типа клиента, динамически выстраивается в нужный url, а на сервере приложений HttpRequest превращается в вызов через Reflection соответствующего метода из указанной сборки.
В следующей статье постараюсь развить эту тему, чтобы идея была более понятна и «прочувствована» :)
+3
chaliy #
Непонял в чем фишка, уже ж есть WCF Web API. Там все заимплпменченно, и УРЛЫ нормальные (service.ashx < — что это за хрень?), так как там используется System.Web.Routing.

Короче, велосипед который в теории был бы оправдан лет пять тому назад. Минусявка от меня, а то позору за платформу не обрешся.
0
sashaeve #
service.ashx — хэндлер. Такие файлы, как правило, исключают из таблицы роутов. С WCF надо играться, а тут добавил файл и сервис готов — для простых кейсов можно использовать.
0
chaliy #
Я знаю что такое ashx. С роутингом, одна строка кода и без всяких файлов готов сервис. С нормальными урлами. На написание этой статьи ушло больше времени че на «играться» с WCF и роутингом.
0
chaliy #
Для затравки, это код для регисрации.
RouteTable.Routes.MapServiceRoute("Commands");
А это код сервиса.
[ServiceContract]
public class CommandsService
{
[WebGet(UriTemplate = "")]
public CommandDef Get()
{
return new CommandDef{};
}
}

... Странно да?
0
YuriyM #
Надо просто попробовать. Сделать WCF сервис и написать для него четыре простых клиента, как в моем примере, с вызовом всего лишь 2-х методов: GetImageInfo() и GetImage(). И посмотреть во что это выльется.
service.ashx — всего лишь фабрика обработчиков, что ненормального в url? REST стиль — это всего-лишь один из способов адресации. Суть же не в этом.
0
chaliy #
Я уже попробовал, код сразу же над вашим коментраием. Подставьте там нужные методы, а клиенты такими и остантуся, сервис вернет самый обычный JSON. Урлы будут ввиде localhost/Commands/xxxx, под хххх будет то как UriTemplate.

Более того, оно еще и принимать JSON может(уже обьектом), и формат выбирает не через урл (разве что для девелоперский целей), а правильно чере Accrpt хеадер.
0
YuriyM #
Вы лукавите, с WCF не все так просто. В Вашем шаблоне еще нет Web.config-а, метод не сможет вернуть Stream, сложные Json структуры для него не «съедобны». Добавить метод — пересобрать сервис.
К тому же WCF «зашит» как синхронный хэндлер, что вызывает подозрение в ограничениях, таких же как обработка .aspx
Ах, если бы WCF был так хорош…
0
chaliy #
1) У меня нет Web.config-а, потому что он не нужен.
2) WCF может вернуть HttpResponseMessage

public HttResponseMessage GetImage(string filePath){
return new HttResponseMessage{Content = new StreamContent(File.Read(filePath))}
}


Писал по памяти, но суть такова. Причем при желании в 10ть строк можно написать форматтер чтобы возвращало стрим. На самом то деле я не уверен что там Стрим неззя возвращать, это я так сказать вам верю.

3) Конешно же пересобрать. Я же нехочу чтобы кто-то мог свободно вызвать CurrentConnections(«MyaConnection») и получить стоку соединения с БД ;).

4) Что-то не понял о каких ограничениях речь. Хотя WCF Web API собсно имплементит и IHttpAsyncHandler и IHttpHandler
0
chaliy #
Такс, ради вас проверил можно ли возвращать Stream. Можно. Никаких вообще проблем:

public Stream GetImage(string filePath){
return File.OpenRead(filePath);
}


Забыл охватить «сложные Json структуры для него не «съедобны»», вопервых, оно все что надо хавает. Даже если несхавает, подрубить JSON.NET туда, это приблизительно 8 строк кода ;), а можно просто подрубить.
0
YuriyM #
«Сложные» я имел ввиду вложенные, т.е. граф объектов.
Поддержка IHttpAsyncHandler — это гуд, потому как родной .svc — только синхронный.
Все хорошо, но разве не утомительно помимо самих методов регистрировать еще мапинги на них и каждый метод аннотировать атрибутом?
А в общем, Web API, которое Вы упомянули — хорошее дополнение к WCF, но только надо сказать, что это коммунити проект, ну типа моего :)
А подключение методов без перекомпиляции сервиса очень удобно. Пример — вы автор уже работающего сервиса, но ему нужна доп.функциональность. Можно отдать реализацию функционала другому разработчику без предоставления исходников сервиса, не все же самому писать. А вызов системных функций и из GAC конечно же блокирован.

Вы не знаете, реализована ли в Web API работа с потоковым видео и дозагрузка файлов?
0
chaliy #
1) Потдерижвает вложенные, я даже проверять не буду.
2) Родной .svc, может быть асинхронным, метод должен вернуть IAsyncResult
3) Конвеншен чтобы убрать атрибуты пишется на раз два три. Причем ладно там WCF, тот же ASP.NET MVC все это может (это мой старючий пост).
4) У вашего «очень удобно» даж коментировать не буду, вы че реально считате что в локальном коде неззя найти интересной инфы? Вас даже задосить будет на раз два три.
5) «видео и дозагрузка файлов» без понятия заимплеменчено ли. Быстрее всего нет. Но что-то вы многовато хотити от application server.
0
YuriyM #
Прикладную сборку из Bin-а невозможно выкачать, так же как и загрузить, Вы же это знаете. А задосить можно любой сервис, это никак не связано.
Кстати, у меня реализованы виртуальные хосты, т.е. домены внутри домена самого сервера приложений. Есть админское API, через который я могу удаленно инсталлировать, запускать/останавливать и удалять приложения.
Да, именно, хочу многое, чтобы не надо было думать в ходе дальнейшей прикладной разработки, как реализовать то, что уже изначально должно быть. Поэтому — читаем еще раз тему топика, где жирными словами написано "… свой Application Server".
А Вам предлагаю написать о своей практике использования WCF Web API. Еще круче, если будет описание «внутренностей».
0
Shersh #
Я вот не пойму, а как я, разработчик, узнаю какие у есть методы API у вас по вашему урлу?

Вот с WCF понятно, я подключил и знаю, какие методы у меня есть. А здесь не могу понять… Объясните?
–1
YuriyM #
Если имеется ввиду автогенерация клиентских прокси, то как правило в общедоступной среде такая возможность на сервере отключается, а прикладным разработчикам предоставляются уже готовые их реализации. Пример: все публичные веб-проекты предоставляют уже готовый клиентский API под конкретную платформу без необходимости для вас исследовать сервис на наличие методов. Мало ли какие методы есть у сервиса, вы ничего о них знать не должны в принципе. С другой стороны, вы как разработчик сервиса прекрасно знаете все о своем детище :)
0
mezastel #
Вообще в REST-парадигме сервисы не являются самодокументирующимся. Нет никакого WSDL. Если нет публичной документации, остается только уповать на то, что авторы используют HATEOAS, то есть выбрасывают назад линки на возможные дополнительные операции.
0
chaliy #
Слушай, а я чето не могу найти где почитать про конкретные реализации HATEOAS. В OData это есть, но интересно поглядеть не МС вей.

В OData оно для JSON кстати заимплеменчено как __metadata поле, а там уже ест uri представляющий ресурс.
0
YuriyM #
Я тут еще раз глянул в Вики по вопросу, что же такое REST. И еще больше убедился в том, что критика в мой адрес по вопросу, почему представленная реализация не подпадает под шаблон REST, не правомерна.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.