Notifications - Push, Pull, Stream Notification #4


 Exchange 2013을 기반으로 개발을 진행하면서 맞닥뜨리게 된 케이스에 대해서 공유 하고자 한다. 이전에 잠깐 EWS(Exchange Web Service)를 통해 간단한 기능을 사용해본 경험이 전부라서 하나의 기능을 구현하기 위해 여러가지 방안에 대해서 바위에 계란치기로 직접 부딪혀 볼 수 밖에 없었다. 이런 힘들고 시간 싸움을 줄이는데 도움을 드리는데 일조 하고자 이곳에 공유 하고자 합니다. 비록 덜 정제 되고 문서 미비할 수 있지만 참고 사항으로 알아 두셨으면 합니다. 



 이번에는 지난 시간에 이어 Push와 Pull에 대해서 알아 보도록 하겠습니다. Push 방식은 Pull과 Stream 방식과는 다른 방식으로 MBX에서 이벤트를 보내주고 Listener에서 받아 주는 구조로 되어 있기 때문에 구독할 수 있는 별도의 서비스가 필요 합니다. "그림1" 를 보면 3번 스텝에 해당하는 서비스로 데이터를 받아서 처리를 하는 구조입니다.

구조에 따라 CAS 서버가 Register와 Listener 모두를 같이 담당할 수도 있고 별도의 서버로 구성을 하셔도 됩니다. 서버의 스펙과 사용자 수에 따라 조율하면 되겠습니다.


[그림1] Pull notification data flow



2. Push notifications

 "코드1"은 비 동기로 Push 이벤트 구독을 신청한다. "그림1"에서의 1번과 2번 과정을 수행하는 것이다.

////비동기를 관리할 수 있게 함 List<System.Threading.Tasks.Task> tasks = new List<System.Threading.Tasks.Task>(); #region Push notification test // Create our listener // "그림1"에서 3번 수행에 필요한 Listener를 활성화 시켜 줌 _listener = new PushConsoleListener(_logger);
_listener.EventReceived += new PushConsoleListener.ListenEventHandler(_listener_EventReceived); //사용자 정보를 가져와서 Push 이벤트 등록 신청 한다. foreach (var member in Members.GetUsers()) {     var m = member;     var task = System.Threading.Tasks.Task.Factory.StartNew(() =>     {         try         {             //Push 이벤트 구독 신청             SubscriptionAdd(m);         }         catch (Exception ex)         {             Log.WriteLine(string.Format("{0}, {1}""SubscriptionAdd"ex.Message));         }     });     tasks.Add(task); } #endregion //모든 비동기 겍체가 완료가 될때까지 대기 함. System.Threading.Tasks.Task.WaitAll(tasks.ToArray());

[코드1] Push Event 구독 요청


 "코드2"번은 "그림1"에서 2번에 해당하는 작업을 수행한다.

/// <summary> /// Push 이벤트 구독 신청 /// </summary> /// <param name="address">사용자 ID - 이메일 주소</param> /// <param name="waterMark">워터 마크 - 특정 시점의 item을 가져올 수 있음</param>

public void SubscriptionAdd(string addressstring waterMark = null) {     Console.WriteLine("{0} 등록중"address);     //인증된 ExchangeService instance 가져오기     ExchangeService _service = EWSHelper.GetService();     var folderIds = new List<FolderId>()                 {                     WellKnownFolderName.Inbox                 };     var eventTypes = new List<EventType>();     //NewMail에 대해서만 이벤트를 받음 처리     eventTypes.Add(EventType.NewMail);  

//임시적으로 waterMark를 null로 할당( 이 기능으로 특정 시점의 item을 가져올 수 있음 ) //var waterMark = null;
    _service.ImpersonatedUserId = new ImpersonatedUserId(ConnectingIdType.SmtpAddressaddress); //옵션중에 frequency는 1분에 한번씩 Listener이 살아 있는지 체크하도록 하는 옵션이다. 1분부터 1440분까지 옵션을 줄 수가 있다.     PushSubscription pushSubscription = _service.SubscribeToPushNotifications(folderIdsnew Uri(_listener.URi), 1waterMarkeventTypes.ToArray());     memberAddress.TryAdd(pushSubscription.Idaddress);     Console.WriteLine(address + " push listener 등록 됨"); }

[코드2] Push 이벤트 구독 등록


 "코드1"과 "코드2"에서와 같이 MBX에 Push event 구독을 신청하였으면 이제 Listener을 할 수 있는 역할을 하는 서비스를 세팅해야 한다. ( "코드1"에서 해당 클래스를 활성화 시켜 주는 작업은 이미 수행 되었다. )


Push 이벤트는 MBX에서 XML 형식으로 Listener에 값을 넘겨 준다. 그러므로 XML에서 원하는 데이타를 추출하기 위해 XML Parser를 이용해서 작업을 하고 EventArgs를 만들어서 EventHandler를 넘겨 Event를 통해 처리하도록 하고 있다. 아래는 MBX에서 보내주는 XML 전문이다. 

<!-- 정상적인 메시지 -->
<?xml version="1.0" encoding="utf-8"?>
<soap11:Envelope xmlns:soap11="http://schemas.xmlsoap.org/soap/envelope/">
  <soap11:Header>
    <t:RequestServerVersion xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" Version="Exchange2013" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" />
  </soap11:Header>
  <soap11:Body>
    <m:SendNotification xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages">
      <m:ResponseMessages>
        <m:SendNotificationResponseMessage ResponseClass="Success">
          <m:ResponseCode>NoError</m:ResponseCode>
          <m:Notification>
            <t:SubscriptionId>DwBtbWJ4MDIuYnBpby5uZXQQAAAA8D/AEWNefUirlcJW7vLT2zE/ABKs3s8I</t:SubscriptionId>
            <t:PreviousWatermark>AQAAAKGzfQd17kZIovKBge0oWhrMcAAAAAAAAAE=</t:PreviousWatermark>
            <t:MoreEvents>false</t:MoreEvents>
            <t:StatusEvent>
              <t:Watermark>AQAAAKGzfQd17kZIovKBge0oWhrMcAAAAAAAAAE=</t:Watermark>
            </t:StatusEvent>
          </m:Notification>
        </m:SendNotificationResponseMessage>
      </m:ResponseMessages>
    </m:SendNotification>
  </soap11:Body>
</soap11:Envelope>

[코드3] MBX가 Listener에게 보내주는 XML 정보 (정상)


<!-- 비 정상적인 메시지 -->
<?xml version="1.0" encoding="utf-8"?>
<soap11:Envelope xmlns:soap11="http://schemas.xmlsoap.org/soap/envelope/">
  <soap11:Header>
    <t:RequestServerVersion xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" Version="Exchange2013" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" />
  </soap11:Header>
  <soap11:Body>
    <m:SendNotification xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages">
      <m:ResponseMessages>
        <m:SendNotificationResponseMessage ResponseClass="Error">
          <m:MessageText>이 구독의 이벤트를 검색할 수 없습니다. 구독을 다시 만들어야 합니다.</m:MessageText>
          <m:ResponseCode>ErrorReadEventsFailed</m:ResponseCode>
          <m:DescriptiveLinkKey>0</m:DescriptiveLinkKey>
          <m:MessageXml>
            <t:Value Name="SubscriptionId">DwBtbWJ4MDEuYnBpby5uZXQQAAAAPxYnRYDXgkCvXyhllmZHPpV2k0Sr3s8I</t:Value>
          </m:MessageXml>
        </m:SendNotificationResponseMessage>
      </m:ResponseMessages>
    </m:SendNotification>
  </soap11:Body>
</soap11:Envelope>

[코드4]MBX가 Listener에게 보내주는 XML 정보 (비 정상)


 이제 XML 파일을 파싱해서 얻어진 정보를 EventArgs로 사용하는 클래스를 먼저 살펴 보자. 이 클래스를 통해 Listener에 연결된 핸들러에서 편리하게 해당 정보를 얻어 사용할 수 있다. 

/// <summary>
/// EWS 2.0, Exchange 2013, Push notification 관련 XML에 관계된 EventArgs
/// </summary>
public class ListenEventArgs : EventArgs
{
    #region fields
    private string _xml = "";
    private string _subscriptionId = "";
    private EventType _eventtype = EventType.Status;
    private ItemId _itemId = new ItemId(new Guid().ToString());
    private FolderId _folderId = null;
    private FolderId _parentFolderId = null;
    private ItemId _oldItemId = null;
    private FolderId _oldFolderId = null;
    private FolderId _oldParentFolderId = null;
    #endregion
 
    #region Constructor
    /// <summary>
    /// XML 데이터를 가지고 인스턴스 생성
    /// </summary>
    /// <param name="XML">XML 데이타</param>
    public ListenEventArgs(string XML)
    {
        //XML에 notification 상세 정보가 있다.
        _xml = XML;
    }
 
    /// <summary>
    /// 인스턴스 생성
    /// </summary>
    /// <param name="SubscriptionId">구독 요청시 생성된 ID값</param>
    /// <param name="isError">에러여부</param>
    public ListenEventArgs(string SubscriptionIdbool isError)
    {
        this._subscriptionId = SubscriptionId;
        this.IsError = isError;
    }
 
    /// <summary>
    /// 인스턴스 생성
    /// </summary>
    /// <param name="SubscriptionId">구독 요청시 생성된 ID값</param>
    /// <param name="EventType">이벤트 타입</param>
    /// <param name="ItemId">ItemId</param>
    /// <param name="ParentFolderId">ParentFolderId</param>
    public ListenEventArgs(string SubscriptionIdEventType EventTypeItemId ItemIdFolderId ParentFolderId)
    {
        _subscriptionId = SubscriptionId;
        _eventtype = EventType;
        _itemId = ItemId;
        _parentFolderId = ParentFolderId;
    }
 
    /// <summary>
    /// 인스턴스 생성
    /// </summary>
    /// <param name="SubscriptionId"></param>
    /// <param name="EventType"></param>
    /// <param name="ItemId"></param>
    /// <param name="ParentFolderId"></param>
    /// <param name="OldItemId"></param>
    /// <param name="OldParentFolderId"></param>
    public ListenEventArgs(string SubscriptionIdEventType EventTypeItemId ItemIdFolderId ParentFolderIdItemId OldItemIdFolderId OldParentFolderId)
        : this(SubscriptionIdEventTypeItemIdParentFolderId)
    {
        _oldItemId = OldItemId;
        _oldParentFolderId = OldParentFolderId;
    }
 
    /// <summary>
    /// 인스턴스 생성
    /// </summary>
    /// <param name="SubscriptionId"></param>
    /// <param name="EventType"></param>
    /// <param name="FolderId"></param>
    /// <param name="ParentFolderId"></param>
    public ListenEventArgs(string SubscriptionIdEventType EventTypeFolderId FolderIdFolderId ParentFolderId)
    {
        _subscriptionId = SubscriptionId;
        _eventtype = EventType;
        _folderId = FolderId;
        _parentFolderId = ParentFolderId;
    }
 
    /// <summary>
    /// 인스턴스 생성
    /// </summary>
    /// <param name="SubscriptionId"></param>
    /// <param name="EventType"></param>
    /// <param name="FolderId"></param>
    /// <param name="ParentFolderId"></param>
    /// <param name="OldFolderId"></param>
    /// <param name="OldParentFolderId"></param>
    public ListenEventArgs(string SubscriptionIdEventType EventTypeFolderId FolderIdFolderId ParentFolderIdFolderId OldFolderIdFolderId OldParentFolderId)
        : this(SubscriptionIdEventTypeFolderIdParentFolderId)
    {
        _oldFolderId = OldFolderId;
        _oldParentFolderId = OldParentFolderId;
    }
    #endregion
 
    /// <summary>
    /// Push 구독 요청시 반환된 값
    /// </summary>
    public string SubscriptionId
    {
        get { return _subscriptionId; }
    }
 
    /// <summary>
    /// 이벤트 타입
    /// </summary>
    public EventType EventType
    {
        get { return _eventtype; }
    }
 
    /// <summary>
    /// Item unique Id
    /// </summary>
    public ItemId ItemId
    {
        get { return _itemId; }
    }
 
    /// <summary>
    /// Folder Id
    /// </summary>
    public FolderId FolderId
    {
        get { return _folderId; }
    }
 
    /// <summary>
    /// Parent folder Id
    /// </summary>
    public FolderId ParentFolderId
    {
        get { return _parentFolderId; }
    }
 
    /// <summary>
    /// Old Item Id
    /// </summary>
    public ItemId OldItemId
    {
        get { return _oldItemId; }
    }
 
    /// <summary>
    /// Old Parent Folder Id
    /// </summary>
    public FolderId OldParentFolderId
    {
        get { return _oldParentFolderId; }
    }
 
    /// <summary>
    /// MBX 주소
    /// </summary>
    public string RemortEndpoint { getset; }
 
    /// <summary>
    /// 워터마크
    /// </summary>
    public string Watermark { getset; }
 
    /// <summary>
    /// 이전 워터마크
    /// </summary>
    public string PreviousWatermark { getset; }
 
    /// <summary>
    /// 에러여부
    /// </summary>
    private bool isError = false;
 
    /// <summary>
    /// 에러여부
    /// </summary>
    public bool IsError
    {
        get
        {
            return isError;
        }
        set
        {
            isError = value;
        }
    }
 
    /// <summary>
    /// Push server에서 보낸 준 XML Data
    /// </summary>
    public string Xml
    {
        get { return _xml; }
        set { _xml = value; }
    }
}

[코드5] XML에서 파싱된 정보를 저장하는 클래스


 다음 포스트에서 Listener에서 XML을 파싱하는 클래스부터 다뤄보도록 하겠다.

+ Recent posts