동적 사용자의 Push Notification 등록


목차
  1. 파워쉘로 사용자 정보 알아내기
  2. Push Notification 등록 요청


 이전 포스트에서 파워쉘로 Active 사용자를 알아 내는 방법을 살펴 보았다. ( 2013/05/03 - [기타] - [Exchange] 동적 사용자의 Push Notification 등록 #1 ) 이제 이번 포스트에서는 Push Notification을 신청하도록 하자.


아래 '코드1'을 통해 Active mode의 Database에서 사용자를 가져와 Push Notification을 요청하는 코드이다.

/// <summary> /// Active Database를 이용해서 사용자 알아 내기 /// </summary> private static void RequestDatabase() {     // 요청자 사용자 리스트     var requestList = new List<string>();     // 요청된 사용자의 SubscriptionId 리스트     var subscriptionIdList = new ConcurrentDictionary<stringstring>();     // Task 리스트     var taskList = new List<System.Threading.Tasks.Task>();     // 파워쉘 명령으로 알아 내기     using (var powershellInvoker = new ExchangePowerShellWrapper(new AppConfigExchangePowerShellConfig()))     {         // 파워쉘 실행         foreach (var databaseItem in powershellInvoker.PipelineScriptInvoke("Get-MailboxDatabaseCopyStatus -active | select-object DatabaseName"))         {             // DatabaseName을 추출한다.             var databaseName = databaseItem.Members["DatabaseName"].Value.ToString();             // Get-Mailbox 파워쉘을 실행하여 사용자의 WindowsEmailAddress를 알아 낸다.             foreach (var user in powershellInvoker.PipelineScriptInvoke("Get-Mailbox -Database \"" + databaseName + "\" | select-object WindowsEmailAddress"))             {                 // 사용자의 이메일 주소                 var windowsEmailAddress = user.Members["WindowsEmailAddress"].Value.ToString();                 var databaseNameTemp = databaseName;                 var temp = System.Threading.Tasks.Task.Factory.StartNew(() =>                 {                     //GetRequest(databaseNameTemp, windowsEmailAddress);                     // List에 관리가 되도록 추가 한다.                     subscriptionIdList.TryAdd(GetRequestNotification(windowsEmailAddress), windowsEmailAddress);                 });                 // waiting 하기 위해 리스트에 넣어 둔다.                 taskList.Add(temp);             }         }     }     // 작업이 완료 될 때까지 대기 하도록 한다.     System.Threading.Tasks.Task.WaitAll(taskList.ToArray()); } /// <summary> /// 웹 서비스로 Subscription 신청 /// </summary> /// <param name="smtpAddress">메일 주소</param> private static string GetRequestNotification(string smtpAddress) {     try     {         string subscriptionId = null;         PushSubscription pushSubscription = null;         var ewsService = new ExchangeService(Microsoft.Exchange.WebServices.Data.ExchangeVersion.Exchange2013)         {             // 인증 정보 세팅             Credentials = new NetworkCredential("domain\\id""password")         };         // 이벤트 종류         var events = new List<EventType>() { EventType.CreatedEventType.Deleted };         // 폴더 범위         var folders = new List<FolderId>() { new FolderId(WellKnownFolderName.Inbox) };         pushSubscription = ewsService.SubscribeToPushNotifications(folders.ToArray(),                                                     new Uri("https://owa.mail.com/ews/exchange.asmx"),                                                     1,                                                     null,                                                     events.ToArray());         // Push Notification 요청하면 관리가 되도록 uniqueId를 반환한다.         subscriptionId = pushSubscription.Id;         return subscriptionId;     }     catch (Exception ex)     {         Console.WriteLine("{0}, {1}, {2}"smtpAddressex.Messagecount++);         throw;     } }

[코드1] Active mode database 사용자 추가


  RequestDatabase() 함수에서는 Database에서 사용자 정보를 추출하고, GetRequestNotification() 함수에서는 Exchange에 Push Notification 요청한다.






동적 사용자의 Push Notification 등록


목차
  1. 파워쉘로 사용자 정보 알아내기
  2. Push Notification 등록 요청


 여러 익스체인지 서버가 동시에 서비스가 되고 있고 Active Db, Passive Db가 서로간의 서버간에 복제가 되도록 구성이 되었을 때 Push Notification을 어떻게 등록할 것인지에 대한 한정된 주제에 대해서만 언급하도록 하겠다. 이 제한된 구성은 수 많은 MBX서버가 고 가용성을 위해 다른 서버에게 복제가 되도록 구성이 된 구조에서 파워쉘을 이용해 Active 된 Database 사용자만을 가져와 Push notification을 요청 하도록 한다.


 이제 진행해 보도록 하자. MBX 서버에는 내부적으로 Active mode Database가 있고 Passive mode Database가 존재하고 있다. 그중에서 메일을 직접적으로 받고 있는 사용자는 Active mode Database에 있는 사용자만이 익스체인지 MBX에서 메일을 받고 처리하며 받은 메일에 대해서 다른 서버에 복제되도록 구성된 Database에 복사가 된다. 그렇다면 현재 서버에서 Active mode의 Database를 가져오는 방법을 우선 알아야 할 것이다. 아래 '코드1'과 같은 방법으로 활성화된 Database를 가져올 수가 있다.

Get-MailboxDatabaseCopyStatus -active | select-object DatabaseName

[코드1] Active mode Database


 이 코드를 수행하면 현재 서버에서 활성화된 Database를 알수가 있다. ( DatabaseName만 필요하기에 Select-Object를 통해 필터링을 하였다 )



[그림1] '코드1'의 수행 결과


 이렇게 알아낸 DatabaseName중에 'HADB01' 사용자의 정보를 알아 내는 구문은 '코드2'와 같이 실행하면 나온다.


Get-Mailbox -Database "HADB01"

[코드2] 사용자 알아 내기




[그림2] 사용자 알아내기 수행 결과 화면


 위와 같은 방법으로 현 서버에서 작업이 되는 사용자의 정보를 알아 낼 수 있게 되었다. 이 정보를 가지고 Push Notification 하는 방법은 다음 포스트에서 이어 가도록 하겠다.





Notifications - Push, Pull, Stream Notification #6


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



 이제 마지막으로 남은 Pull Notification에 대해서 알아 보도록 하자. 원래 바로 이전 포스트에서 Pull까지 마치려고 하였으나 Listener의 소스가 너무 길어서 한번에 언급하기에는 다소 무리라고 판단이 되었다. 그래서 6회까지 이어지게 되었지만 너그러이 이해해 주시면 좋겠습니다. 


 본격적인 진행에 앞서서 Exchange  2013에서는 Pull Notification 관련 이벤트가 전형 발생하지 않고 있다. Powershell에서 Exchange 설정을 수정해야만 동작하는지는 확인하지 못하였지만 기본 상태에서는 동작하지 않는다. 그렇지만 Exchange 2010을 위해서 코드만이라도 살펴 보도록 하겠다. 


 Pull Notification 방식은 다른 어떤 방법보다 간단한 구조라서 사용 법이 간단하다.

PullNotificationListener pull = null;
 
for (int i = 0i < 1000i++)
{
    foreach (var member in Members.GetUsers())
    {
        try
        {
            string watermask = null;
            watermask = "AQAAAOlS/PGuZXZNiGy4pQo8RMLUKhkAAAAAAAA=";
            //watermask를 포함한 이후의 이벤트를 가져온다.
            pull = new PullNotificationListener(memberwatermask);
            Console.WriteLine(member);
            pull.Pulling();
            Thread.Sleep(10000);
        }
        catch (Exception ex) { Console.WriteLine(member + " Error " + ex.Message); }
    }
}

[코드1] Pull Notification 객체로 요청한다.


 위 코드는 PullNotificationListener을 실행 하도록 요청하는 클래스이며 "코드2"에서 실질적으로 Pulling을 진행 한다. 

/// <summary>
/// Pull Notification 이벤트 처리 클래스
/// </summary>
public class PullNotificationListener
{
    /// <summary>
    /// 사용자 email 주소
    /// </summary>
    private string address = null;
    /// <summary>
    /// Watermask 값
    /// </summary>
    private string watermark = null;
 
    /// <summary>
    /// 생성자
    /// </summary>
    /// <param name="address"></param>
    /// <param name="watermark"></param>
    public PullNotificationListener(string addressstring watermark)
    {
        this.address = address;
        this.watermark = watermark;
    }
 
    /// <summary>
    /// Pull notification을 실행한다.
    /// </summary>
    public void Pulling()
    {
        var service = EWSHelper.GetService(address);
 
        //timeOut를 10분으로 세팅 하였다.
        var pullSubscription = service.SubscribeToPullNotifications(EWSHelper.GetNotificationFolderId(), 10watermarkEWSHelper.GetNotificationEventTypes());
 
        //비 동기 실행
        //IAsyncResult result = pullSubscription.BeginGetEvents(new AsyncCallback(Subscription), pullSubscription);
        //비 동기 실행이 완료 할 때까지 대기 하도록 한다.
        //WaitHandle.WaitAll(new[] { result.AsyncWaitHandle });
 
 
        //동기 실행
        var events = pullSubscription.GetEvents();
 
        Console.WriteLine("start");
        foreach (var itemEvent in events.ItemEvents)
        {
            Console.WriteLine("{0}, {1}, {2}, watermark {3}"addressitemEvent.ItemIdDateTime.Now.ToString());
        }
        Console.WriteLine("end");
    }
 
    public void Subscription(IAsyncResult result)
    {
        Console.WriteLine("Subscription Start");
 
        //PullSubscription 변환
        var pullSubscription = result.AsyncState as PullSubscription;
 
        var events = pullSubscription.EndGetEvents(result);
            
        foreach (var itemEvent in events.ItemEvents)
        {
            Console.WriteLine("{0}, {1}, {2}, watermark {3}"addressitemEvent.ItemIdDateTime.Now.ToString());
        }
 
        //구독을 중단한다.
        //pullSubscription.Unsubscribe();
 
        Console.WriteLine("Subscription End");
    }
}

[코드2] Pulling 클래스


 코드에 대한 부가 설명을 하자면 "코드2"에서 동기 방식과 비 동기 방식으로 수행하는 코드가 작성되어 있지만 비 동기 방식은 주석처리가 되어 있고 동기 방식으로 수행 되도록 작성되었다. Exchange 2013에서 두가지 방식으로 테스트를 진행 하였으나 둘다 동작하지는 않았다. 어느 곳의 공개된 소스를 찾아 봐도 위와 같은 흐름으로 되어 있으며 더 이상 진행하지를 못하였다. ( 비 동기 실행의 코드 주석을 풀면 비 동기로 실행이 되고 있다 )


결론적으로 Exchange 2013에서는 Pull, Stream notification 방식으로 이벤트 정보를 받아서 치리해야 한다.

Notifications - Push, Pull, Stream Notification #5


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



 이번에는 Listener에 대해서 살펴 보자. 이 클래스는 MBX에서 보내준 Push 정보인 XML을 HttpListener을 통해 받아 ListenEventArgs로 변환해 주고 연결된 이벤트(EventReceived)를 통해 핸들러로 넘겨 주는 작업을 한다. 이제 아래 코드에서 Listener의 전체 코드를 살펴 보자

/// <summary>
/// Push Notification을 보내 주면 HttpListener을 통해 받은 데이터를 파싱하고 연결된 이벤트를 발생한다.
/// </summary>
public class PushConsoleListener
{
    #region Fidles
    /// <summary>
    /// HttpListener을 통해서 MBX에서 보내준 정보를 받는 역할을 한다.
    /// </summary>
    private HttpListener _Listener = null;
    private string _ListenURi = "";
    private int _ListenPort = int.Parse(ConfigurationManager.AppSettings["PullNotificationListenPort"].ToString()); //36728
    private List<string> _Requests;
    /// <summary>
    /// MBX에서 정보를 보내주고 넘겨줄 리턴 값
    /// </summary>
    private string _NotificationResponse = "";
    //내부 로그 작업에 필요한 객체
    private ClassLogger _Logger = null;
    private string _subscriptionId = "";
    #endregion
 
    //이벤트 델리게이트 선언
    public delegate void ListenEventHandler(object senderListenEventArgs a);
    //이벤트 핸들로 선언
    //별도의 핸들러에서 MBX에서 보내준 정보를 가지고 작업을 할 수 있도록 해준다.
    //다른 클래스에서는 XML파싱이나 HttpListenr에 대해서 전혀 고려할 필요 없어 이 이벤트 처리에 대해서만 신경 쓰면 된다.
    public event ListenEventHandler EventReceived;
 
    //노드를 찾아가기 쉽도록 하기 위해 선엄 됨
    const string m = "http://schemas.microsoft.com/exchange/services/2006/messages";
    const string t = "http://schemas.microsoft.com/exchange/services/2006/types";
 
    public PushConsoleListener(ClassLogger Logger)
    {
        #region 하드코딩(이해하기 쉽도록 수정 된 코드)
        _NotificationResponse = "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
        _NotificationResponse += "<soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:m=\"http://schemas.microsoft.com/exchange/services/2006/messages\" xmlns:t=\"http://schemas.microsoft.com/exchange/services/2006/types\" xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">";
        _NotificationResponse += "  <soap:Body>";
        _NotificationResponse += "    <m:SendNotificationResult>";
        _NotificationResponse += "      <m:SubscriptionStatus>OK</m:SubscriptionStatus>";
        _NotificationResponse += "    </m:SendNotificationResult>";
        _NotificationResponse += "  </soap:Body>";
        _NotificationResponse += "</soap:Envelope>";
        #endregion
 
        #region 리소스 파일에서 읽어 옴
        //리소스에 포함된 XML 파일 정보륵 가져온다.
        //이 작업은 MBX에서 HttpListener로 보내주고 정상적으로 Listen을 했는지 여부를 보내주기 위한 정보를 가져온다.
        //예제를 들기 위한 코드로서 에러가 발생 한다면 하드코딩된 데이터를 가지고 작업하면 됩니다.
        StreamReader oReader = new StreamReader(Assembly.GetExecutingAssembly().GetManifestResourceStream("NoneProcess.XML.EWSSendNotificationResult.xml"));
        _NotificationResponse = oReader.ReadToEnd();
        oReader.Close();
        #endregion
 
        _Requests = new List<string>();
        _Listener = new HttpListener();
 
        _Logger = Logger;
 
        StartListening();
    }
 
    ~PushConsoleListener()
    {
        Close();
    }
 
    /// <summary>
    /// 핸들러가 연결되어 있으면 이벤트를 발생 시켜서 정보를 넘겨 준다.
    /// </summary>
    /// <param name="e"></param>
    protected virtual void OnNotificationReceived(ListenEventArgs e)
    {
        ListenEventHandler handler = EventReceived;
 
        if (handler != null)
        {
            //이벤트고 연결되어 있으면 발생 시킴.
            handler(thise);
        }
    }
 
    private void StartListening()
    {
        //환결 설정 파일에서 HttpListener이 어떤 서버에서 구동되는지 가져 온다.
        //예로 테스트나 구동되는 Host의 IP를 넣어 주면 된다.
        //HttpListener은 관리자 권한으로 실행 해야 정상적인 동작을 할 수 있다.
        string sMachineName = ConfigurationManager.AppSettings["PullNotificationListenerUrlPrefix"].ToString();   //127.0.0.1 Environment.MachineName;
        //try
        //{
        //    System.DirectoryServices.ActiveDirectory.Domain oDomain = System.DirectoryServices.ActiveDirectory.Domain.GetComputerDomain();
        //    if (!String.IsNullOrEmpty(oDomain.Name))
        //        sMachineName = sMachineName + "." + oDomain.Name;
        //}
        //catch { }
 
 
        //_ListenURi = "http://" + sMachineName + ":" + _ListenPort + "/" + AppDomain.CurrentDomain.ToString() + "/";
        _ListenURi = "http://" + sMachineName + ":" + _ListenPort + "/" + "NoneProcessEWSTest" + "/";
        _Listener.Prefixes.Clear();
        //HttpLisener에 Prefix를 세팅해서 응답 받을 수 있도록 세팅 함.
        _Listener.Prefixes.Add(_ListenURi);
 
        //호스팅 정보를 콘솔에 알려 준다.
        Console.WriteLine(_ListenURi);
 
        try
        {
            //HttpListenr을 시작한다.
            //위에서 세팅된 ListenURi 주소로 호스팅을 시작한다.
            _Listener.Start();
            //세팅된 Prefix로 접속하면 GetContext에서 AsyncCallback가 실행 되도록 한다.
            _Listener.BeginGetContext(new AsyncCallback(ListenerCallback), _Listener);
        }
        catch (Exception ex)
        {
            throw;
        }
    }
 
    /// <summary>
    /// 실행 될 포트
    /// </summary>
    public int Port
    {
        get
        {
            return _ListenPort;
        }
        set
        {
            if (value == _ListenPortreturn;
            _Listener.Stop();
            _ListenPort = value;
            StartListening();
        }
    }
 
    /// <summary>
    /// 실행 될 URI 주소
    /// </summary>
    public string URi
    {
        get
        {
            return _ListenURi;
        }
    }
 
    /// <summary>
    /// Subscription Id
    /// </summary>
    public string SubscriptionId
    {
        get
        {
            return _subscriptionId;
        }
        set
        {
            _subscriptionId = value;
        }
    }
 
    /// <summary>
    /// MBX에서 보내준 정보를 받을 때 마다 Callback가 실행 된다.
    /// </summary>
    /// <param name="result"></param>
    public void ListenerCallback(IAsyncResult result)
    {
        //SubscribeToPushNotifications 등록 시 설정 된 frequency 설정 값대로 특정 주기로 서비스가 살아 있는지 확인 한다.
        //정상적으로 동작하지 않으면 MBX에서는 Push를 중단한다.
        //Console.WriteLine("Exchange서버에서 listener가 살아 있는지 점검 : {0}", DateTime.Now.ToString());
        try
        {
            HttpListener listener = (HttpListener)result.AsyncState;
 
            System.Threading.Tasks.Task.Factory.StartNew(() =>
            {
                //결과값을 받아 오기 위한 처리
                HttpListenerContext context = listener.EndGetContext(result);
                //HttpListenerContext에서 Request객체를 할당
                HttpListenerRequest request = context.Request;
 
                //Console.WriteLine("Local end point: {0}", request.LocalEndPoint.ToString());
                //Console.WriteLine("Remote end point: {0}", request.RemoteEndPoint.ToString());
                                        
                string sRequest = "";
 
                using (StreamReader reader = new StreamReader(request.InputStream))
                {
                    //MBX에서 보내준 XML 파일을 읽어 온다.
                    sRequest = reader.ReadToEnd();
                }
 
                //파싱 및 이벤트 전달 하도록 한다.
                DetermineResponse(sRequestcontext.Responserequest);
            });
 
            _Listener.BeginGetContext(new AsyncCallback(ListenerCallback), _Listener);
        }
        catch (Exception ex) { Console.WriteLine(ex.Message); }
    }
 
    /// <summary>
    /// 파싱 및 이벤트 전달 하도록 한다.
    /// </summary>
    /// <param name="Request">XML 전문 정보</param>
    /// <param name="Response">HttpListenerResponse</param>
    /// <param name="request">HttpListenerRequest</param>
    private void DetermineResponse(string RequestHttpListenerResponse ResponseHttpListenerRequest request)
    {
        //정상적인 XML 파일인지 체크
        if (Request.Contains("exchange"&& Request.Contains("SendNotificationResponseMessage"))
        {
            //MBX에 정상적으로 이벤트를 받았다고 리턴 해줌.
            WriteSubscriptionResponse(Response);
            //파싱하고 이벤트 발생시킴
            RaiseEvents(Requestrequest);
        }
 
        Response.OutputStream.Flush();
        //Response의 Stream을 닫는다.
        Response.Close();
    }
 
    /// <summary>
    /// 정상적으로 이벤트를 받았다고 리턴 해준다.
    /// </summary>
    /// <param name="Response">HttpListenerResponse</param>
    private void WriteSubscriptionResponse(HttpListenerResponse Response)
    {
        //정상적이라고 Response에 넘긴다.
        byte[] buffer = System.Text.Encoding.UTF8.GetBytes(_NotificationResponse);
        Response.ContentLength64 = buffer.Length;
        Response.ContentType = "text/xml";
        using (Stream output = Response.OutputStream)
        {
            output.Write(buffer0buffer.Length);
        }
 
        Log(_NotificationResponse"Notification Response");
    }
 
    /// <summary>
    /// XML 파싱해서 HttpListenEventArgs를 생성하고 이벤트를 
    /// </summary>
    /// <param name="Notifications"></param>
    /// <param name="request"></param>
    private void RaiseEvents(string NotificationsHttpListenerRequest request)
    {
        // Process the notifications
        XmlDocument oNotifications = new XmlDocument();
        try
        {
            //XMLDocument로 읽어 들인다. - XML 객체로 인식하도록 한다.
            oNotifications.LoadXml(Notifications);
        }
        catch (Exception ex)
        {
            return;
        }
 
        var responseClassValue = "Error";
        try
        {
            //특정 테그를 검색해서 원하는 노드의 Attribute의 값을 가져온다.
            responseClassValue = oNotifications.GetElementsByTagName("SendNotificationResponseMessage"m)[0].Attributes["ResponseClass"].Value;
        }
        catch { }
 
        //성고 여부 체크
        if (responseClassValue == "Success")
        {
            XmlNodeList nodeListNotifications = oNotifications.GetElementsByTagName("Notification"m);
            if (nodeListNotifications.Count == 0return;
 
            //SubscriptionId를 가져온다.
            XmlNode nodeSubscriptionId = oNotifications.GetElementsByTagName("SubscriptionId"t)[0];
            string subscriptionId = nodeSubscriptionId.InnerText;
 
            // SubscriptionId를 체크해서 정합성 여부를 체크 한다.
            // 이곳에서는 여러 사용자에 대해서 이벤트를 처리 하도록 되어 있기에 정합성 체크를 무시 한다.
            //if (!String.IsNullOrEmpty(_subscriptionId))
            //{
            //    if (subscriptionId != _subscriptionId)
            //        return;
            //}
 
            foreach (XmlNode node in nodeListNotifications[0].ChildNodes)
            {
                //이벤트 종류에 따라 XML 정보가 상이하여 다르게 처리 해야 함.
                switch (node.LocalName)
                {
                    case "StatusEvent":
                        break;
 
                    case "NewMailEvent":
                        //NewMail 관련 이벤트를 발생 시키며
                        //XML에서 관련 정보를 파싱해서 ListenEventArgs instance를 생성해서 넘겨 준다.
                        OnNotificationReceived(new ListenEventArgs(subscriptionId,
                        EventType.NewMail,
                        ExtractItemId("ItemId"node), ExtractFolderId("ParentFolderId"node)) { RemortEndpoint = request.RemoteEndPoint.ToString(), Watermark = GetWatermark(node), PreviousWatermark = GetPreviousWatermark(node) });
                        break;
 
                    case "CreatedEvent":
                        //CreatedEvent 관련 이벤트를 발생 시키며
                        //XML에서 관련 정보를 파싱해서 ListenEventArgs instance를 생성해서 넘겨 준다.
                        if (node.InnerXml.Contains("ItemId"))
                        {
                            OnNotificationReceived(new ListenEventArgs(subscriptionId,
                                EventType.Created,
                                ExtractItemId("ItemId"node), ExtractFolderId("ParentFolderId"node)) { RemortEndpoint = request.RemoteEndPoint.ToString(), Watermark = GetWatermark(node), PreviousWatermark = GetPreviousWatermark(node) });
                        }
                        else
                        {
                            OnNotificationReceived(new ListenEventArgs(subscriptionId,
                                EventType.Created,
                                ExtractFolderId("FolderId"node), ExtractFolderId("ParentFolderId"node)) { RemortEndpoint = request.RemoteEndPoint.ToString(), Watermark = GetWatermark(node), PreviousWatermark = GetPreviousWatermark(node) });
                        }
                        break;
 
                    case "MovedEvent":
                        //MovedEvent 관련 이벤트를 발생 시키며
                        //XML에서 관련 정보를 파싱해서 ListenEventArgs instance를 생성해서 넘겨 준다.
                        if (node.InnerXml.Contains("ItemId"))
                        {
                            OnNotificationReceived(new ListenEventArgs(subscriptionId,
                                EventType.Moved,
                                ExtractItemId("ItemId"node), ExtractFolderId("ParentFolderId"node),
                                ExtractItemId("OldItemId"node), ExtractFolderId("OldParentFolderId"node)) { RemortEndpoint = request.RemoteEndPoint.ToString(), Watermark = GetWatermark(node), PreviousWatermark = GetPreviousWatermark(node) });
                        }
                        else
                        {
                            OnNotificationReceived(new ListenEventArgs(subscriptionId,
                                EventType.Moved,
                                ExtractFolderId("FolderId"node), ExtractFolderId("ParentFolderId"node),
                                ExtractFolderId("OldFolderId"node), ExtractFolderId("OldParentFolderId"node)) { RemortEndpoint = request.RemoteEndPoint.ToString(), Watermark = GetWatermark(node), PreviousWatermark = GetPreviousWatermark(node) });
                        }
                        break;
 
                    case "CopiedEvent":
                        //CopiedEvent 관련 이벤트를 발생 시키며
                        //XML에서 관련 정보를 파싱해서 ListenEventArgs instance를 생성해서 넘겨 준다.
                        if (node.InnerXml.Contains("ItemId"))
                        {
                            OnNotificationReceived(new ListenEventArgs(subscriptionId,
                                EventType.Copied,
                                ExtractItemId("ItemId"node), ExtractFolderId("ParentFolderId"node),
                                ExtractItemId("OldItemId"node), ExtractFolderId("OldParentFolderId"node)) { RemortEndpoint = request.RemoteEndPoint.ToString(), Watermark = GetWatermark(node), PreviousWatermark = GetPreviousWatermark(node) });
                        }
                        else
                        {
                            OnNotificationReceived(new ListenEventArgs(subscriptionId,
                                EventType.Copied,
                                ExtractFolderId("FolderId"node), ExtractFolderId("ParentFolderId"node),
                                ExtractFolderId("OldFolderId"node), ExtractFolderId("OldParentFolderId"node)) { RemortEndpoint = request.RemoteEndPoint.ToString(), Watermark = GetWatermark(node), PreviousWatermark = GetPreviousWatermark(node) });
                        }
                        break;
 
                    case "DeletedEvent":
                        //DeletedEvent 관련 이벤트를 발생 시키며
                        //XML에서 관련 정보를 파싱해서 ListenEventArgs instance를 생성해서 넘겨 준다.
                        if (node.InnerXml.Contains("ItemId"))
                        {
                            OnNotificationReceived(new ListenEventArgs(subscriptionId,
                                EventType.Deleted,
                                ExtractItemId("ItemId"node), ExtractFolderId("ParentFolderId"node),
                                ExtractItemId("OldItemId"node), ExtractFolderId("OldParentFolderId"node)) { RemortEndpoint = request.RemoteEndPoint.ToString(), Watermark = GetWatermark(node), PreviousWatermark = GetPreviousWatermark(node) });
                        }
                        else
                        {
                            OnNotificationReceived(new ListenEventArgs(subscriptionId,
                                EventType.Deleted,
                                ExtractFolderId("FolderId"node), ExtractFolderId("ParentFolderId"node),
                                ExtractFolderId("OldFolderId"node), ExtractFolderId("OldParentFolderId"node)) { RemortEndpoint = request.RemoteEndPoint.ToString(), Watermark = GetWatermark(node), PreviousWatermark = GetPreviousWatermark(node) });
                        }
                        break;
 
                    case "ModifiedEvent":
                        //ModifiedEvent 관련 이벤트를 발생 시키며
                        //XML에서 관련 정보를 파싱해서 ListenEventArgs instance를 생성해서 넘겨 준다.
                        if (node.InnerXml.Contains("ItemId"))
                        {
                            OnNotificationReceived(new ListenEventArgs(subscriptionId,
                                EventType.Modified,
                                ExtractItemId("ItemId"node), ExtractFolderId("ParentFolderId"node),
                                ExtractItemId("OldItemId"node), ExtractFolderId("OldParentFolderId"node)) { RemortEndpoint = request.RemoteEndPoint.ToString(), Watermark = GetWatermark(node), PreviousWatermark = GetPreviousWatermark(node) });
                        }
                        else
                        {
                            OnNotificationReceived(new ListenEventArgs(subscriptionId,
                                EventType.Modified,
                                ExtractFolderId("FolderId"node), ExtractFolderId("ParentFolderId"node),
                                ExtractFolderId("OldFolderId"node), ExtractFolderId("OldParentFolderId"node)) { RemortEndpoint = request.RemoteEndPoint.ToString(), Watermark = GetWatermark(node), PreviousWatermark = GetPreviousWatermark(node) });
                        }
                        break;
                }
            }
        }
        else
        {
            //subscriptionId를 가져온다.
            XmlNode nodeSubscriptionId = oNotifications.GetElementsByTagName("MessageXml"m)[0];
            string subscriptionId = nodeSubscriptionId.InnerText;
 
            XmlNode nodeMessageText = oNotifications.GetElementsByTagName("MessageText"m)[0];
            string messageText = nodeMessageText.InnerText;
 
            OnNotificationReceived(new ListenEventArgs(subscriptionIdtrue) { Xml = messageText });
        }
    }
 
    private ItemId ExtractItemId(string TagXmlNode Node)
    {
        // Extract an ItemId from the node
        string itemId = "";
        string changeKey = "";
 
        foreach (XmlNode node in Node.ChildNodes)
        {
            if (node.LocalName == Tag)
            {
                // This is our ItemId
                itemId = node.Attributes["Id"].Value;
                changeKey = node.Attributes["ChangeKey"].Value;
                break;
            }
        }
        if (!String.IsNullOrEmpty(itemId))
        {
            ItemId id = new ItemId(itemId);
            //id.ChangeKey = changeKey;
            return id;
        }
        return null;
    }
 
    /// <summary>
    /// XML에서 FolderId 가져오기
    /// </summary>
    /// <param name="Tag"></param>
    /// <param name="Node"></param>
    /// <returns></returns>
    private FolderId ExtractFolderId(string TagXmlNode Node)
    {
        string folderId = "";
        string changeKey = "";
 
        foreach (XmlNode node in Node.ChildNodes)
        {
            if (node.LocalName == Tag)
            {
                //ItemId
                folderId = node.Attributes["Id"].Value;
                changeKey = node.Attributes["ChangeKey"].Value;
                break;
            }
        }
 
        if (!String.IsNullOrEmpty(folderId))
        {
            FolderId id = new FolderId(folderId);
            //id.ChangeKey = changeKey;
            return id;
        }
        return null;
    }
 
    /// <summary>
    /// XML에서 워터마크 가져오기
    /// </summary>
    /// <param name="Node">XML</param>
    /// <returns></returns>
    public string GetWatermark(XmlNode Node)
    {
        string returnValue = string.Empty;
 
        foreach (XmlNode node in Node.ParentNode.ChildNodes)
        {
            if (node.LocalName == "StatusEvent")
            {
                foreach (XmlNode n in node.ChildNodes)
                {
                    if (n.LocalName == "Watermark")
                    {
                        returnValue = n.Value;
                    }
                }
            }
        }
 
        return returnValue;
    }
 
    /// <summary>
    /// XML에서 이전 워터마크 가져오기
    /// </summary>
    /// <param name="Node">XML</param>
    /// <returns></returns>
    public string GetPreviousWatermark(XmlNode Node)
    {
        string returnValue = string.Empty;
 
        foreach (XmlNode node in Node.ParentNode.ChildNodes)
        {
            if (node.LocalName == "PreviousWatermark")
            {
                returnValue = node.InnerText;
            }
        }
 
        return returnValue;
    }
 
    private void Log(string Detailsstring Description = "")
    {
        try
        {
            if (_Logger == nullreturn;
            _Logger.Log(DetailsDescription);
        }
        catch { }
    }
 
    public void Close()
    {
        // Release the listener
        try
        {
            _Listener.Stop();
            _Listener.Close();
        }
        catch { }
    }
}

[코드1]Listener의 전체 소스


 위 소스에서 XML을 파싱하는 부분의 코드는 EWS의 기본 로직에 따라 해당하는 ListenEventArgs를 생성하고 있다. 예로 XML 파싱에서 MovedEvents가 발생되었다고 판독이 되면 ItemId가 있는지 체크하고 있으면 기존 아이템처럼 수행하면 되고 아니면 Folder 처럼 동작하도록 되었다. Exchagne 에서는 모든 아이템이 동등한 객체처럼 관리가 되고 있기 때문이다. Folder가 옮겨져도 MovedEvents가 발생하고 메일이 옮겨져도 MovedEvents가 발생하기 때문이다. 다른 아이템도 마찬가지로 이벤트가 동작하게 되어 있다.


 이번 "코드1"의 소스 길이가 다른 사항에 비해 상단히 길게 작성이 되어 있다. 쉽게 이해할 수 없을 수도 있으나 디버깅이나 실행 흐름을 따라가다가 보면 어렵지 않게 이해할 수 있으리라 기대한다. 다만 조급히 생각하지 않고 시간 투자만 한다면 어렵지 않을것이다. 다만 이곳에서 직접적으로 보여줄 수 없는 부분이 서버에 대한 정보라고 할수 있을 것이다. ConfigurationManager을 통해서 설정 파일에서 읽어 오는 부분은 옆에 주석으로 해당 값에 대한 예시를 같이 적어 두었으니 구동 환경에 맞게 수정하면 될 것이다. 그리고 여기에 리소스로 포함되는 XML 파일을 보여주는 것으로 이번 Push Notification을 마치도록 하겠다.


<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <m:SendNotificationResult>
      <m:SubscriptionStatus>OK</m:SubscriptionStatus>
    </m:SendNotificationResult>
  </soap:Body>
</soap:Envelope>

[코드2] 리소스에 포함되는 XML 파일 ( MBX에 정상적으로 수신되었다고 리턴 값을 보내는 정보)








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을 파싱하는 클래스부터 다뤄보도록 하겠다.

Notifications - Push, Pull, Stream Notification #1


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



 Exchange에는 메일(Item, 아이템 - 메일, 캘린더, 일정, 명함, ... - 이곳에서는 메일에 대해서만 언급 하도록 하겠다.)의 상태( 아래 "리스트1" 참고 )에 대해서 알아 낼 수 있는 방법이 Exchange에서 제공하는 EWS를 통해서 Notification이벤트로 알아 낼 수 있다.

public enum EventType
{
    Status = 0,
    NewMail = 1,
    Deleted = 2,
    Modified = 3,
    Moved = 4,
    Copied = 5,
    Created = 6,
    FreeBusyChanged = 7,
}

[리스트1] Notification 이벤트 타입


 EWS를 통해서 Notification을 알아 내는 방법은 아래와 같다.

  • Pull Notification
  • Push Notification
  • Stream Notification ( 2010 SP1 ↑ 지원 )


각 사용법에 대한 장단점은 아래 "표1", "표2", "표3"을 참고 하기 바란다.




Push Notification vs Pull Notification vs Stream Notification

Push Notifications

Pros

Cons

Nearly instantaneous notifications
거의 실시간으로 Notification을 받을 수 있고

Have to write a listener
http 리스너를 할 수 있는 서버가 필요

No wasted traffic

적은 트래픽

Listener must be addressable by the Exchange server

리스너를 받기 위해서 IP기반 주소가 필요

Does not require CAS affinity

CAS를 이용하지 않는다

 

[표1] Push Notification

 

Pull Notifications

Pros

Cons

Same simple request/response protocol as all other EWS web methods

EWS 웹 메소드와 같이 간단하게 사용

Receive notifications as frequently as client polls

클라이언트 Polling을 통해 자주 요청해야 한다.

Client does not need to be addressable (can be behind a proxy or firewall)

IP기반 주소가 필요 없다.

Wasted traffic

Polling 때문에 낭비되는 트래픽이 발생

Authentication is handles in the same way as for all other EWS web methods

다른 EWS 메소드와 같은 방식으로 인증

Fine tuning required to get optimal polling interval

최선의 Polling 주기를 조율해야 한다.

[표2] Pull Notification

 

Stream Notifications
(아직 공식적인 문서는 없으며 Pull, Push를 기반으로 작성 되었다.)

Pros

Cons

Same simple request/response protocol as all other EWS web methods

EWS 웹 메소드와 같이 간단하게 사용

Watermask를 사용해서 지난 메세지를 받을 수 없다.

(SyncFolderItems 으로 가능한지 테스트 중)

Client does not need to be addressable (can be behind a proxy or firewall)

IP기반 주소가 필요 없다.

즉시적인 Notifications을 받을 수 없다.

그렇지만 연결된 상태에서는 특정 주기(기본 5)마다 이벤트를 구독할 수 있다. - 이벤트 방식으로 동작

Authentication is handles in the same way as for all other EWS web methods

다른 EWS 메소드와 같은 방식으로 인증

 

[표3] Stream Notification






 다음은 전체적이 흐름을 이해하기 쉽도록 간단하게 도식을 살펴 보도록 하자. 

[그림1] Push notification


Push notification은 Register에서 CAS에 Push사용자를 등록하고 MBX에서는 Listener에게 발생된 이벤트를 즉시적으로 넘겨 준다. 다른 notification 방식보다 복잡하며 MBX에서 접근 할 수 있는 Listener 서버가 필요 하다. 그렇지만 세가지 방식중에서 가장 실시간 이벤트 정보를 받아 볼 수 있으므로 실시간 연동이 필요한 부분에서는 고려해 볼 만하다. 그렇지만 MBX에서 데이터를 XML 형식으로 넘겨 주기 때문에 Listener에서 XML을 파싱하는 작업이 수반되어야 한다.



[그림2] Pull notification


Pull notification은 CAS에 각 사용자를 Pulling하고 받은 정보를 받아와 처리한다. 다음 이벤트를 받기 위해서는 다시 같은 프로세스를 다시 실행해야 한다. Pull은 Receiver에서 주도적으로 CAS에 질의를 해야 하며 필요 없는 트래픽이 많이 발생 할 수 있거나 Pulling 주기에 따라 시간 차이가 발생 할 수 있다. 실 시간적인 처리가 필요한 시스템 연동에서는 데이터 불일치가 발생할 수 있다.



[그림3] Stream notification


 Stream notification은 Pull과 Push의 장점을 합쳐서 만든 방식이다. 1번 처럼 한번 등록이 되면 DisConnection이 되기 전까지는 주기적(기본적으로는 5초 단위)으로 CAS에서 Receiver에게 이벤트를 보내 준다. 그리고 EWS와 같은 방식으로 메소드를 사용할 수 있으며 별도의 파싱 작업을 해야 할 필요는 없다. 그렇지만 현 시점에서는 Watermask를 통해 지난 이벤트를 가지고 올 수 있는 방안을 제공하지 않고 있다.SyncFolderItems 으로 지난 Notification을 가져올 수 있는지 테스트 중에 있다. 관련 자료 - Stream Notification )


 지금까지 Notification을 구독하는 컨셉에 대해서 알아 보았다면 다음 포스트에서는 실 서버 환경에서 구축하는 방안에 대해서 알아 보도록 하겠다.


Exchange flow diagram V1.pptx

위 다이어그램 관련 작업하며 추가하고 있는 자료입니다. 시간이 지나면서 점점더 추가 하도록 하겠습니다.

+ Recent posts