Создание Push Notification сервиса на основе WCF REST

В качестве вступления

Модель push-нотификаций является распространённой моделью для обмена сообщениями. Она подразумевает не получение информации по запросу, а немедленную её передачу отправителю при появлении этой информации на сервере.

Стандартный подход с ипользованием wsDualHttpBinding

Возможность создания push-механизма предоставляет и WCF. Этот фреймворк позволяет создать push-сервис с использованием wsDualHttpBinding контракта. Такой контракт позволяет для каждого запроса определить метод обратного вызова, который будет вызван при наступлении какого-либо события.
Если применить этот механизм к системе обмена сообщениями, то получим следующий алгоритм:

— Для каждого запроса на новые сообщения создаётся callback, который сохраняется в списке подписчиков на новые сообщения.
— При получении нового сообщения, система проходит по списку подписчиков и находит нужного нам получателя сообщения (а значит и нужный callback).
— Вызываем нужный нам callback-метод.
Ниже приведён пример использования wsDualHttpBinding для WCF сервиса:

— Создаём метод обратного вызова для запроса на новые сообщения
interface IMessageCallback
    { 
        [OperationContract(IsOneWay = true)]
        void OnMessageAdded(int senderId, string message, DateTime timestamp);
    }

— Создаём сервис контракт

    [ServiceContract(CallbackContract = typeof(IMessageCallback))]
    public interface IMessageService
    {
        [OperationContract]
        void AddMessage(int senderId, int recipientId, string message);

        [OperationContract]
        bool Subscribe();
    }

— Создаём сам сервис

Public class MessageService : IMessageService
{
     private static List<IMessageCallback> subscribers = new List<IMessageCallback>();

     public bool Subscribe(int id)
    {
         try
         {
               IMessageCallback callback =
               OperationContext.Current.GetCallbackChannel<IMessageCallback>();
               callback.id = id;
               if (!subscribers.Contains(callback))
                   subscribers.Add(callback);
               return true;
         }
          catch
         {
              return false;
         }
    }

     public void AddMessage(int senderId, int recipientId, string message)
    {
          subscribers.ForEach(delegate(IMessageCallback callback)
         {
              if ((((ICommunicationObject)callback).State == CommunicationState.Opened) && (callback.id ==     recipientId))
              {
                    callback.OnMessageAdded(recipientId, message, DateTime.Now);
              }
              else
              {
                    subscribers.Remove(callback);
               }
         });
     }
}

— Конфигурируем сервис в файле web.config
<system.serviceModel>
    <services>
      <service name="WCFPush.MessageService" behaviorConfiguration="Default">
        <endpoint address ="" binding="wsDualHttpBinding" contract="WCFPush.IMessage">
        </endpoint>
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="Default">
          <serviceMetadata httpGetEnabled="True"/>
          <serviceDebug includeExceptionDetailInFaults="False" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>

Сервис готов.

Однако, эта модель работает только в том случае, когда и сервер, и подписчики являются .NET-приложениями.

Использование RESTful подхода

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

Итак, создадим сервис, аналогичный по функциональности предыдущему, только на основе REST.
В случае с асинхронной моделью запрос состоит из двух частей: BeginRequestName и EndRequestName.

— Определим ServiceContract для REST-сервиса

	[ServiceContract]
	public interface IMessageService
	{
		[WebGet(UriTemplate = "AddMessage?senderId={senderId}&recipientId={recipientId}&message={message}")]
		[OperationContract ]
		bool AddMessage(int senderId, int recipientId, string message);

		[WebGet(UriTemplate = "Subscribe?id={id}")]
		[OperationContract(AsyncPattern = true)]
		IAsyncResult BeginGetMessage(int id, AsyncCallback callback, object asyncState);

		ServiceMessage EndGetMessage(IAsyncResult result);
	}

Обратите внимание: EndGetMessage не помечен аттрибутом OperationContact.

— Создадим класс для асинхронного результата, реализующий интерфейс IAsyncResult

public class MessageAsyncResult : IAsyncResult
	{
		public AsyncCallback Callback { get; set; }

		private readonly object accessLock = new object();
		private bool isCompleted = false;
		private ServiceMessage result;

		private int recipientId;

		private object asyncState;

		public MessageAsyncResult(object state)
		{
			asyncState = state;
		}

		public int RecipientId
		{
			get
			{
				lock (accessLock)
				{
					return recipientId;
				}
			}
			set
			{
				lock (accessLock)
				{
					recipientId = value;
				}
			}
		}


		public ServiceMessage Result
		{
			get
			{
				lock (accessLock)
				{
					return result;
				}
			}
			set
			{
				lock (accessLock)
				{
					result = value;
				}
			}
		}

		public bool IsCompleted
		{
			get
			{
				lock (accessLock)
				{
					return isCompleted;
				}
			}
			set
			{
				lock (accessLock)
				{
					isCompleted = value;
				}
			}
		}

		public bool CompletedSynchronously
		{
			get
			{
				return false;
			}
		}

		public object AsyncState
		{
			get
			{
				return asyncState;
			}
		}

		public WaitHandle AsyncWaitHandle
		{
			get
			{
				return null;
			}
		}

}

Помимо реализации интерфейса, в этом классе также хранится Id получателя сообщения (recipientId), а также само сообщение, которое будет доставлено отправителю(result).

— Теперь реализуем сам сервис

[ServiceBehavior(
	InstanceContextMode = InstanceContextMode.PerCall,
	ConcurrencyMode = ConcurrencyMode.Multiple)]
	public class MessageService : IMessageService
	{
		private static List<MessageAsyncResult> subscribers = new List<MessageAsyncResult>();

		public bool AddMessage(int senderId, int recipientId, string message)
		{
			
			subscribers.ForEach(delegate(MessageAsyncResult result)
			{
				if (result.RecipientId == recipientId)
				{
				
					result.Result = new ServiceMessage(senderId, recipientId, message, DateTime.Now);
					result.IsCompleted = true;
					result.Callback(result);
					subscribers.Remove(result);

				}
				
			});
			return true;
		}

		public IAsyncResult BeginGetMessage(int id, AsyncCallback callback, object asyncState)
		{
			MessageAsyncResult asyncResult = new MessageAsyncResult(asyncState);
			asyncResult.Callback = callback;
			asyncResult.RecipientId = id;
			subscribers.Add(asyncResult);
			return asyncResult;
		}

		public ServiceMessage EndGetMessage(IAsyncResult result)
		{
			return (result as MessageAsyncResult).Result;
		}
	}

Когда приходит запрос на получение нового сообщения, создаётся асинхронный результат, который добавляется в список подписчиков. Как только приходит сообщение для данного подписчика, свойство IsCompleted для данного IAsyncResult устанавливается в true, и вызывается метод EndGetMessage. В EndGetMessage отправляется ответ подписчику.

— Осталось сконфигурировать сервис в файле web.config
<system.serviceModel>
    <bindings>
      <webHttpBinding>
	<binding name="webBinding">
	</binding>
      </webHttpBinding>
    </bindings>
    <services>
      <service name=" WCFPush.MessageService" behaviorConfiguration="Default">
        <endpoint address="" contract="WCFPush.IMessageService" behaviorConfiguration="web" bindingConfiguration="webBinding" binding="webHttpBinding">
        </endpoint>
      </service>
    </services>
    <behaviors>
      <endpointBehaviors>
        <behavior name="web">
          <webHttp />
        </behavior>
      </endpointBehaviors>
      <serviceBehaviors>
        <behavior name="Default">
          <serviceMetadata httpGetEnabled="true" />
          <serviceDebug includeExceptionDetailInFaults="false" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
  </system.serviceModel>

Сервис готов.

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

Заключение

Таким образом можно реализовать Push-сервис для обмена сообщениями “в реальном времени”, основываясь на REST запросах. Такой сервис может использоваться с любого клиента, поддерживающиего RESTful реквесты, в том числе и из обычного браузера.
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 16

    +1
    Давно искал статью по этой теме. Спасибо.
      +3
      1) Судя по описанию, вы придумали Long Polling. Это решение крайне плохо масштабируется.

      2) Неоптимальная синхронизация. lock не нужен для чтения примитивных типов, а для записи вместо него достаточен вызов Thread.MemoryBarrier()
        –5
        1) Судя по описанию, вы придумали Long Polling. Это решение крайне плохо масштабируется.

        ты серьезно бротюнь? Или ты про принципиалную неспособность дотнета решать C10K?
          +4
          Я серьёзно. Вот тут например
          tomasz.janczuk.org/2009/08/performance-of-http-polling-duplex.html
          результаты замеров. Банально Worker Threads заканчиваются.

          Что касается неуместного выпада в сторону .Net… Ты серьёзно бротюнь?
          С10К задача решаемая веб-сервером, в данном случае IIS (Hostable WebCore, http.sys), а не языком программирования или платчормой. Apache (написанный на Си) или TomCat не справляются с ней точно так же как и IIS, как, впрочем, и любой другой сервер приложений. Язык тут не при чём.
            0
            всю жизнь думал что асинхрнное io на стороне приложения, делает long polling и вообще C10K не проблемой для 98% веб приложений.
            0
            ну при чем тут .net?!

            возьмем ту же Java. Так вот, например, чтобы BlazeDS или LCDS держали long polling более 10K клиентов необходимо использовать Java NIO, а лучше NIO2.
            так вот в .net, начиная с версии 1.0 асинхронный IO поддерживается из коробки.

            и как заметили чуть ниже, платформа и язык тут не при чем.
              0
              я о том и говорю, ни в жизнь нигде лонг полинг не имел проблем с масштабируемостью, но у товарища adontz они есть, поэтому интересуюсь дело в дот нете или в нем.
                0
                могу согласится с вами, если речь про вертикальное масштабирование.

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

                однако, учитывая и это, главная проблема все-таки кроется в использовании threaded-server'ов. т.к. цитата
                >>Банально Worker Threads заканчиваются

                чтобы не разводить холивар на тему threaded vs evented сервера и т.д., скажу, что оба подхода достойны и должны использоваться каждый для своих целей.
                0
                Асинхронный I/O .Net тут не при чём, WCF для HTTP использует компонент операционной системы — HTTP.sys
                  0
                  >>Асинхронный I/O .Net тут не при чём
                  вы действительно считаете, что async не при чем? тогда как же происходит long polling в WCF, если он хостится вне IIS, например?

                  и

                  >>WCF для HTTP использует компонент операционной системы — HTTP.sys
                  понятное дело, однако все-таки WCF работает через стандартные механизмы самого I/O .NET'a, но никак не напрямую с драйвером системы.
                    0
                    Именно для HTTP работа идёт со специализированным драйвером. Используется класс-обёртка HttpListener
                    msdn.microsoft.com/en-us/library/system.net.httplistener.aspx

                    Нарошной ститьи я сейчас с ходу не нашёл, но по ссылкам ниже упоминается использование HTTP.sys и вызванные этим особенности.
                    msdn.microsoft.com/en-us/library/ms733768.aspx
                    msdn.microsoft.com/en-us/magazine/cc163570.aspx
                      0
                      именно HttpListener имел ввиду. скажу больше — благодаря ему (http.sys) IIS может шарить порты с WCF
                        0
                        Ну так асинхронный I/O .Net — это обёртка над IOCP, Socket.BeginSend/Socket.BeginReceive.

                        HttpListener это вообще не API ввода-вывода, вы там с HttpListenerContext работаете и уже распарсенным HttpListenerRequest. Никакого Read/Receive или Write/Send там нет. HttpListener — это расширяемая платформа веб-сервера.
                          0
                          >>Ну так асинхронный I/O .Net — это обёртка над IOCP, Socket.BeginSend/Socket.BeginReceive.
                          кто спорит?
                          >>HttpListener — это расширяемая платформа веб-сервера.
                          его использует и Cassini, да сами делали на нем сервер.
                          >>Никакого Read/Receive или Write/Send там нет.
                          вы же начинаете асинхронно принимать и отпускать запросы. а если асинхронно записываете в response? что представляет собой response? system.io.stream. а этот класс разве не имеет async. вообще на счет и async и java я начал, чтобы показать, что никакого отставания .net как такого нет по сравнению с др. платформами.
                      0
                      WCF для работы с HTTP всегда исспользует модуль HTTP.sys.
                      Попытайтесь запустить свой сервис без привилегий админа, сразу получите исключение, которое лечится конфигурированием urlacl именно для модуля HTTP.sys.
                        0
                        я не говорю, что HTTP.sys не используется — просто под стандартными механизмами имелся ввиду тот же самый HttpListener, который и есть wrapper вокруг драйвера.
                        хотелось показать, что WCF полностью написан с использованием managed кода.

            Only users with full accounts can post comments. Log in, please.