В качестве вступления
Модель 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 реквесты, в том числе и из обычного браузера.
