Реализация чата на ASP.NET с использованием Long Polling

Автор: Вячеслав Гринин | веб-программирование | 13 Мар 2010 2:06 пп

Бродя по просторам интернета, на одном из сайтов я увидел простой чат. Просмотрев страницу в FireBug я понял, что никакими апплетами там и не пахнет, а значит чат реализован на простом JavaScript.

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

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

Непродолжительный поиск по интернетам дал мне ответ. И ответ этот “Long Polling“, а встречается еще и другое название технологии – Comet.

Итак, как же это работает?

По сути, это все тот же старый добрый AJAX, и использование объекта XmlHttpRequest. С одной большой разницей, заключенной на серверной стороне. Разница состоит вот в чем. Клиент посылает XHR-запрос и длительное время ожидает ответа сервера. А “длительное время” отклика обеспечивает сам сервер, который не сразу отдает данные клиенту, а лишь только тогда, когда у него в очереди появляются свежие данные, то есть когда ему действительно есть что сказать.

Клиент, дождавшись ответа, обрабатывает его (например, отображает сообщение на странице), а затем, без промедления, снова запрашивает сервер. И снова ждет.

Если же подав запрос клиент так и не дождался ответа из-за таймаута, то он снова запрашивает сервер. А откуда этот таймаут? А просто серверу нам нечего ответить, вот он и не отдает клиенту результат запроса. Ждет. Но не дождавшись, разрывает соединение по таймауту. Такая вот несложная схема.

Это и называется Long Polling, то есть – длительный опрос.

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

Итак… Без особых размышлений приведу здесь листинг кода и объясню принцип его действия.

using System;
using System.Collections.Generic;

// Класс описывающий одно сообщение от клиента и
// метод его сериализации
public class CometMessage
{
 public string UserName;
 public string Message;
 public string Serialize()
 {
 return "{'user': '" + UserName + "', 'message': '" + Message + "'}";
 }
}

// Собственно, серверная часть
public static class CometServer
{
 // вспомогательный объект для блокировки ресурсов
 // многопоточного приложения
 private static Object _lock = new Object();

 // Список, хранящий состояние всех подключенных клиентов
 private static List<CometAsyncState> _clientStateList =
   new List<CometAsyncState>();

 // Возвращаем сообщение каждому подключенному клиенту
 public static void PushMessage(CometMessage message)
 {
 lock (_lock)
 {
 // Пробегаем по списку всех подключенных клиентов
 foreach (CometAsyncState clientState in _clientStateList)
 {
 if (clientState.CurrentContext.Session != null)
 {
 // И пишем в выходной поток текущее сообщение
 clientState.CurrentContext.Response.Write(message.Serialize());
 // После чего завершаем запрос - вот именно после этого результаты
 // запроса пойдут ко всем подключенным клиентам
 clientState.CompleteRequest();
 }
 }
 }
 }

 // Срабатывает кажды раз при запуске клиентом запроса Long poll
 // так как при этом HttpContext клиента изменяется, то надо обновить
 // все изменившиеся данные клиента в списке, идентифицируемом по
 // гуиду, который у клиента в течение работы остается постоянным
 public static void UpdateClient(CometAsyncState state, String guid)
 {
 lock (_lock)
 {
 // ищем клиента в списке по его гуиду
 CometAsyncState clientState = _clientStateList.Find(s => s.ClientGuid
   == guid);
 if (clientState != null)
 {
 // и если он нашелся, то обновляем все его параметры
 clientState.CurrentContext = state.CurrentContext;
 clientState.ExtraData = state.ExtraData;
 clientState.AsyncCallback = state.AsyncCallback;
 }
 }
 }

 // Регистрация клиента
 public static void RegicterClient(CometAsyncState state)
 {
 lock (_lock)
 {
 // Присваиваем гуид и добавляем в список
 state.ClientGuid = Guid.NewGuid().ToString("N");
 _clientStateList.Add(state);
 }
 }

 // Разрегистрация клиента
 public static void UnregisterClient(CometAsyncState state)
 {
 lock (_lock)
 {
 // Просто удаляем его из списка
 _clientStateList.Remove(state);
 }
 }
}

Итак:

6-14 строки – инкапсулируют класс сообщения от пользователя, как видим, там еще есть метод, преобразующий объект в JSON-строку. Вообще, по-хорошему, здесь надо пользоваться нормальным JSON-сериализатором, а не городить строку вручную.

Далее идет описание класса CometServer в котором и живет вся серверная логика LongPolling-технологии.

Заметим, что в переменной _clientStateList (строка 24) хранятся все подключенные на данный момент к серверу клиенты.

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

Метод UpdateClient (строка 51) по полученному им гуиду клиента, находит клиента в списке clientStateList и обновляет все его параметры, такие как HttpContext-например. Этим мы обеспечиваем всегда актуальное состояние списка клиентов, после каждого их реконнекта.

Метод RegicterClient (строка 69) /опечатка вышла в его имени, сами исправите при необходимости:)/ добавляет нового клиента в очередь.

Метод UnregisterClient (строка 80) соответственно – удаляет клиента из очереди. Что вполне законно. Зачем уведомлять клиента о новых сообщениях в чате, если он давно покинул чат?

Дальше идет описание класса CometAsyncState. Зачем мы его создали? Да чтобы хранить такие параметры клиента, как CurrentContext, AsyncCallback и ClientGuid. Ну то есть, чтобы отдельный поток, порожденный в пуле потоков и отвечающий за одного клиента, мог собственно функционировать и взаимодействовать с клиентом. Этот класс, по большому счету, всего лишь набор заглушек для методов интерфейса IAsyncResult плюс метод CompleteRequest(), который вызывает callback-функцию при завершении потока.

using System;
using System.Threading;
using System.Web;

public class CometAsyncState : IAsyncResult
{
    // Чтобы было где хранить все это и был создан
    // класс-наследник от IAsyncResult
    public HttpContext CurrentContext;
    public AsyncCallback AsyncCallback;
    public object ExtraData;
    public string ClientGuid;
    private Boolean _isCompleted;

    // Конструктор
    public CometAsyncState(HttpContext context,
      AsyncCallback callback, object data)
    {
        CurrentContext = context;
        AsyncCallback = callback;
        ExtraData = data;
        _isCompleted = false;
    }

    // Завершим запрос
    public void CompleteRequest()
    {
        // При завершении запроса просто выставим флаг
        // что он завершен
        // и вызовем callback
        _isCompleted = true;
        if (AsyncCallback != null)
        {
            AsyncCallback(this);
        }
    }

    #region IAsyncResult Members
    // И снова видим набор заглушек для интерфейса IAsyncResult

    public Boolean CompletedSynchronously
    {
        get
        {
            return false;
        }
    }

    public bool IsCompleted
    {
        get
        {
            return _isCompleted;
        }
    }

    public object AsyncState
    {
        get
        {
            return ExtraData;
        }
    }

    public WaitHandle AsyncWaitHandle
    {
        get
        {
            return new ManualResetEvent(false);
        }
    }
    #endregion
}

Осталась малость – клиентский JavaScript. Ниже я приведу его с моими подробными комментариями в самом коде и не буду его так детально разжевывать. Потому что весь он разжеван в начале статьи, где я описывал принцип работы LongPolling.

var clientGuid

$(document).ready(function() {
 // Подключаемся после загрузки страницы,
 // запускаем первый long polling
 Connect();
});

$(window).unload(function() {
 // При выгрузке страницы - запрашиваем сервер об отключении
 // клиента для экономии ресурсов
 Disconnect();
});

// Посылает lonp poll - запрос серверу
function SendRequest() {
 var url = './comet.ashx?guid=' + clientGuid;
 $.ajax({
 type: "POST",
 url: url,
 // Если запрос завершился успехом, значит сервер сообщил
 // о новых событиях - обрабатываем их
 success: ProcessResponse,
 // При ошибке (например таймауте), снова рекурсивно
 // посылаем запрос обеспечивая тем самым непрерывный
 // процесс прослушки серверных событий
 error: SendRequest
 });
}

// Регистрируемся на сервере
function Connect() {
 var url = './comet.ashx?cmd=register';
 $.ajax({
 type: "POST",
 url: url,
 success: OnConnected,
 error: ConnectionRefused
 });
}

// Разрегистрируемся на сервере
function Disconnect() {
 var url = './comet.ashx?cmd=unregister';
 $.ajax({
 type: "POST",
 url: url
 });
}

// Обработка сообщений, принятых с сервера
function ProcessResponse(transport) {
 eval('var d=' + transport + ';');
 document.getElementById("content").innerHTML +=
    ' <strong>' + d.user + '</strong> : "' + d.message + '"<br/>';
 // После отображения результатов запроса -
 // снова циклично делаем запрос.
 SendRequest();
}

// После регистрации на сервере сохраняем наш guid и
// посылаем первый long poll запрос на сервер
function OnConnected(transport) {
 clientGuid = transport;
 SendRequest();
}

// Если подключиться не удалось, то ждем три мекунды
// и опять пробуем подключиться
function ConnectionRefused() {
 $("#content").html("не удалось подключиться к серверу.
    Попробуем через 3 секунды...");
 setTimeout(Connect(), 3000);
}

// Отправка сообщения на сервер
function clickSendMessage() {
 var userName = document.getElementById("userName").value;
 var message = document.getElementById("message").value;
 var url = './comet.ashx?cmd=send&message=' + message + '&user=' + userName;
 $.ajax({
 type: "POST",
 url: url
 });
}

Ну и разумеется, код серверного хэндлера comet.ashx, который обрабатывает команды клиентского скрипта. Скажу сразу, хэндлер этот асинхронный, а потому унаследован не от IHttpHandler, а от IHttpAsyncHandler

< %@ WebHandler Language="C#" Class="CometAsyncHandler" %>

using System;
using System.Web;
using System.Threading;

public class CometAsyncHandler : IHttpAsyncHandler,
  System.Web.SessionState.IRequiresSessionState
{
    #region IHttpAsyncHandler Members

    public IAsyncResult BeginProcessRequest(HttpContext ctx,
      AsyncCallback cb, Object obj)
    {
        // Готовим объект для передачи его в QueueUserWorkItem
        CometAsyncState currentAsyncState =
          new CometAsyncState(ctx, cb, obj);

        // Добавляем в тредпул новый ждущий поток
        ThreadPool.QueueUserWorkItem(new WaitCallback(RequestWorker),
           currentAsyncState);

        return currentAsyncState;
    }

    public void EndProcessRequest(IAsyncResult ar)
    {
    }

    #endregion

    #region IHttpHandler Members
    // IHttpHandler Members - просто пустые заглушки,
    // так как нам не требуется реализация синхронных методов

    public bool IsReusable
    {
        get
        {
            return true;
        }
    }

    public void ProcessRequest(HttpContext context)
    {
    }

    #endregion

    // Основная функция рабочего потока
    private void RequestWorker(Object obj)
    {
        // obj - второй параметр
        // при вызове ThreadPool.QueueUserWorkItem()
        CometAsyncState state = obj as CometAsyncState;

        string command =
          state.CurrentContext.Request.QueryString["cmd"];
        string guid =
          state.CurrentContext.Request.QueryString["guid"];

        switch (command)
        {
            case "register":
                // Регистрируем клиента в очереди сообщений
                CometServer.RegicterClient(state);
                state.CurrentContext.Response.Write(
                  state.ClientGuid.ToString());
                state.CompleteRequest();
                break;
            case "unregister":
                // Удаляем клиента из очереди сообщений
                CometServer.UnregisterClient(state);
                state.CompleteRequest();
                break;
            case "send":
                // Отсылка сообщения
                string message =
                  state.CurrentContext.Request.QueryString["message"];
                string userName =
                  state.CurrentContext.Request.QueryString["user"];
                CometServer.PushMessage(new CometMessage() {
                  Message = message, UserName = userName });
                state.CompleteRequest();
                break;
            default:
                // При реконнекте клиента
                if (guid != null)
                {
                    CometServer.UpdateClient(state, guid);
                }
                break;

        }
    }
}

Основная логика программы кроется в методе RequestWorker (строки 51-95), который в зависимости от полученной с клиента команды, выполняет либо регистрацию/разрегистрацию клиента, либо отправку сообщения. Либо обновление данных о клиенте при его реконнекте.

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

Вот и все. Надеюсь, что статья оказалось полезной для вас.

Прочтите еще:

Разбор XML-файлов в .NET

Автор: Вячеслав Гринин | веб-программирование | 27 Фев 2010 4:30 пп

Заметил я вот какую странную вещь – мало я пишу про ASP.NET, несмотря на то, что сам я в основном программирую именно под ASP.NET. И решил я начинать исправлять эту ситуацию потихоньку. И теперь в нашем блоге будет появляться все больше статей, посвященных ASP.NET, да и вообще технологии .NET. И сегодня будет именно такая статья. Посвящена она работе с XML-файлами.

Вы уже наверное заметили, что я люблю парсить XML-файлы. Да просто формат этот очень уж удобный для передачи и хранения различных данных. Взять вот хотя бы тот же gismeteo-информер, который я уже успел распарсить несколькими различными методами. И все эти методы касались языка JavaScript. А теперь попробуем распарсить его и в .NET. Но давайте уж возьмем не сам по себе информер, а кое-какие другие XML-данные с сайта gismeteo.

Исходные данные: Mozilla с установленным FireBug, открытая в нем страница http://informer.gismeteo.ru/getcode/xml.php. Видите внизу три списка, соответственно: стран, регионов и городов? Так вот, когда Вы в нем выбираете страну, то в списки регионов и городов загружаются регионы и города выбранной страны. При этом происходит запрос на сервер gismeteo вот такого вида: http://informer.gismeteo.ru/getcode/index.php. При этом, в POST-параметрах запроса передаются параметры: a1=156 (для регионов), a2n=156 (для городов). 156 – это код России в справочниках gismeteo.

А назад мы получаем XML-файл вот такого вида (здесь я приведу лишь кусок файла):

<cities>
<city value="11845" old_id="99855">Абаза</city>
<city value="4723" old_id="29865">Абакан</city>
<city value="4659" old_id="29485">Абан</city>
<city value="4546" old_id="28581">Абатский</city>
<city value="4607" old_id="28815">Абдулино</city>
<city value="11657" old_id="99666">Абинск</city>
<city value="12874" old_id="89157">Автуры</city>
<city value="3967" old_id="23383">Агата</city>
</cities>

Итак захотел я распарсить именно этот файл. Скажу даже зачем мне это нужно. А вот нужно мне по названию города на русском языке получить соответствующий XML-документ с данными о погоде в этом городе. А XML этот лежит по вот такому пути: http://informer.gismeteo.ru/xml/99855_1.xml , где 99855 – это аттрибут old_id нужного мне города. Понятно, что разобранный файл я сохраню затем в базу данных, откуда и буду выбирать айдишники нужных мне городов. Наверное, можно было попросить эту базу у gismeteo, но я так и не нашел на их сайте работающих e-mail’ов. :) Да и не ищем мы легких путей. Будем самостоятельно парсить их базу .

Итак, вот готовый код для разбора подобного рода XML-файла:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.XPath;
using System.IO;

namespace ParseXML
{
class Program
{
static void Main(string[] args)
{
// Собственно XML
string xml = @"
<cities>
<city value=""11845"" old_id=""99855"">Абаза</city>
<city value=""4723"" old_id=""29865"">Абакан</city>
<city value=""4659"" old_id=""29485"">Абан</city>
<city value=""4546"" old_id=""28581"">Абатский</city>
<city value=""4607"" old_id=""28815"">Абдулино</city>
<city value=""11657"" old_id=""99666"">Абинск</city>
<city value=""12874"" old_id=""89157"">Автуры</city>
<city value=""3967"" old_id=""23383"">Агата</city>
</cities>";
// ЗАгружаем строку в XPathDocument
XPathDocument xPathDoc = new XPathDocument(new StringReader(xml));
XPathNavigator xPathNav = xPathDoc.CreateNavigator();
// Выбираем нужные узлы
XPathNodeIterator xPathIter = xPathNav.Select("/cities/city");
// Пробегаем по всем узлам
foreach (XPathNavigator nav in xPathIter)
{
// Метод GetAttribute извлекает аттрибут узла
int id = Int32.Parse(nav.GetAttribute("value", ""));
int old_id = Int32.Parse(nav.GetAttribute("old_id", ""));
// Свойство Value узла дает нам содержимое XML-узла, то есть строку,
// которая содержится между открывающим и закрывающим XML-тегами
Console.WriteLine("{0} \t id={1}, \t http://informer.gismeteo.ru/xml/{2}_1.xml",
nav.Value, id, old_id);
}
Console.ReadLine();
}
}
}

Вот и все. Думаю, что код этот очень простой и в дополнительном комментировании не нуждается.

Кстати, скачать бесплатно мобильные игры java Вы можете на сайте mobi4uk4a.ru. Я скачал себе набор пасьянсов, теперь есть чем заняться пока еду в метро.

Нам и нашим четвероногим друзьям нужна квалифицированная ветеринарная помощь, ведь только в этом случае можно быть уверенным, что Ваш домашний любимец будет здоров и проживет долгую жизнь рядом с Вами. Не стоит надеяться на “авось”, если Ваш питомец заболел, и ни в коем случае нельзя заниматься самолечением – сразу идите в ветеринарную клинику “Биоконтроль”

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

Прочтите еще:

AJAX с помощью jQuery и ASP.NET web-сервисов

Автор: Вячеслав Гринин | веб-программирование | 21 Сен 2009 5:14 пп

Если вы создаете сайты под ASP.NET, то возможно вам уже довелось использовать AJAX-framework, предоставляемый Microsoft. Тот самый, что использует ScriptManager, rfк в примере, приведенном ниже:

1) Содержимое страницы Default.aspx:

<asp:ScriptManager ID="MainSM" ScriptMode="Debug" runat="server">
 <Services>
 <asp:ServiceReference Path="~/usersrv.asmx" InlineScript="false" />
 </Services>
 </asp:ScriptManager>

2) Содержимое файла script.js:

// вызов ajax-метода GetPoint
usersrv.GetPoint(id,OnUSGetPointSucc,OnUSGetPointErr);
// callback успешного вызова
function OnUSGetPointSucc(res)
{
 alert('success');
}
// callback ошибки
function OnUSGetPointErr(err)
{
 alert('success');
}

3) Содержимое файла usersrv.asmx:

<%@ WebService Language="C#" CodeBehind="~/App_Code/usersrv.cs" %>

4) Содержимое файла usersrv.cs:

[WebService(Namespace = "http://tempuri.org/")]
[System.Web.Script.Services.ScriptService]
public class usersrv : System.Web.Services.WebService
{
[WebMethod]
public Dictionary<string, object> GetPoint(long p_id)
 {
 Dictionary<string, object> res = new Dictionary<string, object>();
 try
 {
   // готовим возвращаемые параметры и сохраняем их в res
 }
 catch (Exception)
 {
 res["Error"] = U.GTFR("userpoints_err");
 }
 return res;
 }
}

Этот метод достаточно прост и удобен, если бы не одно “но” – он весит 132 Кб. И обладает еще одним интересным свойством: если в нашем проекте есть несколько страниц, использующих AJAX, то каждая из них будет заново загружать этот скрипт, потому что при загрузке скрипта на странице используется примерно следующий URL http://domainname/ScriptResource.axd?d=TvjJs2RlM8MX3pIUhEsdnZKUzGh-9Wr9nvNBZJbxf-xzq-Wvzbpj6FfMJqZqOPR8Pku52J2zBqleUlYtVE8XCyfyo3kcbSnhbwzYc2LkdfQ1&t=ffffffffee41303f и для каждой страницы в пределах вашего проекта будет свой ScriptManager, а значит и свой уникальный ID в query-string. А значит скрипт этот для каждой страницы будет браться не из кэша, а запрашиваться заново с сервера.

В общем, мне все это не понравилось и я заинтересовался, как можно сэкономить трафик и перейти на AJAX.jQuery. Тем более, что он уже использовался в этом же проекте для других целей. Сказано – сделано! И вот какое решение я нашел.

Собственно, модификации пришлось подвергнуть как сами клиентские JavaScript-ы, так и методы, вызываемые на стороне сервера. Связано это с тем, что мне хотелось передавать в качестве параметров для web-сервиса сложные объекты, а не просто строки и целые значения. И получать в ответ хотелось тоже сложные объекты, а не просто строки.

Все это можно легко и просто сделать, передавая как в ту так и в другую сторону все же строки, но строки в формате JSON, который является родным для JavaScript. Да к тому же ASP.NET обладает встроенными возможностями сериализации/десериализации JSON.

Правда, для сериализации объекта в JSON на клиентской стороне пришлось использовать еще одну дополнительную библиотеку json2.js. Таким образом, обе библиотеки весят около 67 Кб. Да плюс ко всему в случае нескольких ajax-страниц в проекте, скрипт загрузится всего один раз, и для остальных страниц будет браться из кэша браузера.

Но от слов – к делу!  Вот содержимое скрипта, выполняемого на клиентской стороне:

// готовим передаваемые параметры в переменную par, здесь - всего лишь эмуляция
 var par = new Object();
 par.a = 5;
 par.b = 'data';
 par.c = new Array();
 par.c[0] = 5;
 par.c[1] = 10;
 // сериализуем объект
 var ser = JSON.stringify(par);
 // формируем ajax-запрос
 $.ajax({
 type: "POST",
 url: "usersrv.asmx/GetPoint",
 data: "{'par':'"+ser+"'}",
 contentType: "application/json; charset=utf-8",
 dataType: "json",
 success: function(r) {
 var res=r.d;
 // сериализуем для наглядности полученные данные и выводим их
на страницу
 // в реальном приложении здесь мы просто будем использовать готовые
данные
 // в переменной res без сериализации
 document.getElementById("result").innerHTML=JSON.stringify(r);
 },
 error: function(err) {
 // обработка ошибки
 alert('error: '+err.responseText);
 }
 });
 }

А вот содержимое usersrv.cs:

using System;
using System.Collections;
using System.Linq;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Xml.Linq;
using System.Web.Script.Serialization;

[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.Web.Script.Services.ScriptService]
public class usersrv : System.Web.Services.WebService
{
 public usersrv() { }

 [WebMethod]
 public ResponseType GetPoint(string par)
 {
 ResponseType response = new ResponseType();
 RequestType request = new RequestType();
 try
 {
 // десериализуем полученные параметры
 JavaScriptSerializer ser = new JavaScriptSerializer();
 request = ser.Deserialize<RequestType>(par);
 }
 catch (Exception)
 {
 response.isError = true;
 response.message = "Parameters is invalid";
 return response;
 }

 // преобразуем переданные параметры и готовим ответ
 // здесь - всего лишь эмуляция обработки данных
 if (request.c != null && request.a == request.c.Length)
 {
 response.isError = false;
 response.message = "That's good";
 }
 else
 {
 response.isError = true;
 response.message = "Length is not the same";
 }
 return response;
 }
}

А вот описание двух воспомогательных классов:

public class RequestType
{
    public int a;
    public string b;
    public int[] c;
}
public class ResponseType
{
    public string message;
    public bool isError;
}

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

Ссылка на архив с готовым проектом здесь.

Прочтите еще: