Как стать автором
Обновить

IIS + .NET + Json. Пишем свой Application Server

Время на прочтение10 мин
Количество просмотров11K
В данной статье я попытался рассказать о своем видении, что такое сервер приложений, взаимодействующий с различными клиентами, и как на .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, смогут взаимодействовать с нашим сервером приложений.
Теги:
Хабы:
Всего голосов 16: ↑11 и ↓5+6
Комментарии19

Публикации

Истории

Работа

.NET разработчик
58 вакансий

Ближайшие события