Pull to refresh

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

Reading time 5 min
Views 13K
В качестве вступления

Модель 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 реквесты, в том числе и из обычного браузера.
Tags:
Hubs:
+26
Comments 16
Comments Comments 16

Articles