在 Exchange 中保持一组订阅和邮箱服务器之间的相关性

了解如何维护一组订阅与邮箱服务器之间的相关性。

相关性是请求和响应邮件序列与特定邮箱服务器的关联。 对于 Exchange 中的大多数功能,关联由服务器处理。 但是,通知是一个例外。 客户端负责维护与通知订阅的邮箱服务器的相关性。 此相关性使客户端和服务器之间的负载均衡器和客户端访问服务器能够将通知订阅和相关请求路由到维护订阅的邮箱服务器。 如果没有相关性,请求可能会路由到不包含客户端订阅的其他邮箱服务器,这可能导致返回 ErrorSubscriptionNotFound 错误。

如何维护相关性?

Exchange 中的相关性是基于 Cookie 的。 客户端通过在订阅请求中包含特定标头来触发 Cookie 的创建,然后订阅响应包含 Cookie。 然后,客户端会在后续请求中发送该 Cookie,以确保将请求路由到正确的邮箱服务器。

更具体地说,Exchange 中的相关性由以下项处理:

  • X-AnchorMailbox - 初始订阅请求中包含的 HTTP 标头。 它标识与同一邮箱服务器共享相关性的邮箱组中的第一个邮箱。

  • X-PreferServerAffinity — 包含在初始订阅请求中的 HTTP 标头,该标头具有 X-AnchorMailbox 标头,设置为 true,以指示客户端请求与邮箱服务器保持相关性。

  • X-BackEndOverrideCookie - 包含在初始订阅响应中的 Cookie,其中包含负载均衡器和客户端访问服务器用于将后续请求路由到同一邮箱服务器的 Cookie。

如何实现使用 EWS 托管 API 或 EWS 来维护相关性?

可以使用相同的步骤来维护多个邮箱订阅及其邮箱服务器的相关性,无论你使用的是流式处理、拉取还是推送通知,以及针对 Exchange 本地服务器还是Exchange Online。

  1. 对于每个邮箱, 调用自动发现 并获取 GroupingInformation 和 ExternalEwsUrl 用户设置。 对于 SOAP 自动发现,使用 Setting 元素,对于 POX 自动发现,使用 GroupingInformation 元素。

  2. 使用自动发现响应中的 GroupingInformation 和 ExternalEwsUrl 设置,将具有相同 ExternalEwsUrl 和 GroupingInformation 串联值的邮箱放在同一组中。 如果任何组的邮箱超过 200 个,请进一步细分组,以便每个组的邮箱不超过 200 个。

  3. 为过程的其余部分创建和使用一个 ExchangeService 对象。 使用相同的 ExchangeService 对象时,cookie 和标头在设置) 时 (自动维护。 请注意,如果不打算将流式处理订阅分组到单个连接中,则可以为每个模拟用户创建不同的 ExchangeService 对象。

  4. 在按字母顺序对组中所有用户进行排序时,首先显示其用户名的用户发送订阅请求 (我们将此用户称为定位点邮箱用户) 。 请执行以下操作:

  1. 在订阅响应中,获取 X-BackEndOverrideCookie 值。 在此组中用户的每个后续订阅请求中包含此值。

  2. 对于组中的每个其他用户,请发送订阅请求并执行以下操作:

  • 包含 X-AnchorMailbox 标头,其值设置为组的定位点邮箱用户的 SMTP 地址。

  • 包含值设置为 true 的 X-PreferServerAffinity 标头。

  • 包括定位点邮箱用户的订阅响应中返回的 X-BackEndOverrideCookie。

  • (ExchangeImpersonation 类型) 使用 ApplicationImpersonation 角色。

    请注意,服务器同时使用 X-PreferServerAffinity 和 X-BackendOverrideCookie 值来执行到邮箱服务器的路由。 X-AnchorMailbox 标头也是必需的,但如果其他两个值有效,则服务器将忽略该标头。 如果 X-AnchorMailbox 和 X-PreferServerAffinity 位于请求中,并且不包括 X-BackendOverrideCookie,则 X-AnchorMailbox 值用于路由请求。

    由于 X-PreferServerAffinity 和 X-BackendOverrideCookie 值执行路由,因此,如果定位邮箱曾经移动到另一个组或服务器,则逻辑不会更改,因为 X-BackendOverrideCookie 会将请求路由到组的正确服务器。

  1. 为组发送单个 GetStreamingEventsGetEvents 请求,并执行以下操作:
  • 包括组中邮箱的每个单个订阅响应中返回的 SubscriptionId 值。

  • 如果组的订阅数超过 200 个,请创建多个请求。 请求中包含的 SubscriptionId 值的最大数目为 200。

  • 如果需要的连接数超过目标邮箱可用的连接数,请使用服务帐户模拟组的定位点邮箱;否则,请勿使用模拟。 理想情况下,你希望根据 GetStreamingEventsGetEvents 请求模拟唯一邮箱,以便永远不会遇到限制。

  • 如果需要 超过目标邮箱可用的连接数,请使用 ApplicationImpersonation;否则,请勿使用 ApplicationImpersonation。

  • 包括 X-PreferServerAffinity 标头并将其设置为 true。 如果使用在步骤 2 中创建的 ExchangeService 对象,则会自动包含此值。

  • 在定位邮箱用户的订阅响应) 中返回的 X-BackEndOverrideCookie (包含组的 X-BackEndOverrideCookie。 如果使用在步骤 2 中创建的 ExchangeService 对象,则会自动包含此值。

  1. 将返回的事件传递到单独的线程进行处理。

需要考虑哪些限制值?

规划通知实现时,需要考虑两个值:连接数和订阅数。 下表列出了每个 限制 设置的默认值以及设置的使用方式。 对于每个值,预算将分配给目标邮箱。 出于此原因,在许多情况下,使用模拟获取其他连接是必需的步骤。

表 1. 默认限制值

考虑领域 限制设置 默认值 说明
流式处理连接
默认挂起连接限制
10(Exchange Online)
Exchange 2013 的 3
帐户一次可以在服务器上打开的最大并发流式处理连接数。 若要在此限制内工作,请使用为目标邮箱分配的 ApplicationImpersonation 角色的服务帐户,并在获取流事件时模拟每个订阅 ID 组中的第一个用户。
拉取或推送连接
EWSMaxConcurrency
27
(请求的最大并发拉取或推送连接数,这些请求已收到但尚未响应,) 帐户可以同时在服务器上打开。
订阅
EWSMaxSubscriptions
20 个用于Exchange Online
Exchange 2013 的 5000
帐户一次可以具有的最大非到期订阅数。 在服务器上创建订阅时,此值将递减。

以下示例演示如何在任何目标邮箱与为目标邮箱分配了 ApplicationImpersonation 角色的服务帐户之间处理预算。

  • ServiceAccount1 (sa1) 模拟许多用户 (m1、m2、m3 等) 并为每个邮箱创建订阅。 请注意,创建订阅时,订阅所有者为 sa1,因此当 sa1 打开与订阅的连接时,EWS 强制这些订阅归 sa1 所有。

  • Sa1 可以通过以下方式打开连接:

  1. 没有模拟,因此连接将针对 sa1 收费。

  2. 通过模拟任何用户(例如 m1),使连接按 m1 的预算副本收费。 (M1 本身可以使用 Exchange Online 打开 10 个连接,模拟 m1 的所有服务帐户都可以使用复制的 budget 打开 10 个连接。)

  • 如果达到连接限制,可以使用以下解决方法:

    • 如果使用选项 1,管理员可以创建多个服务帐户来模拟其他用户。

    • 如果使用选项 2,则代码可以模拟其他用户(例如 m2)。

示例:维护一组订阅与邮箱服务器之间的相关性

好了,让我们看看它的运行情况。 下面的代码示例演示如何对用户进行分组,并使用 X-AnchorMailbox 和 X-PreferServerAffinity 标头以及 X-BackendOverrideCookie Cookie 来保持与邮箱服务器的相关性。 由于标头和 Cookie 在相关性情景中至关重要,因此此示例侧重于 EWS XML 请求和响应。 若要使用 EWS 托管 API 创建订阅请求和响应的正文,请参阅 在 Exchange 中使用 EWS 流式传输有关邮箱事件的 通知和在 Exchange 中使用 EWS 拉取有关邮箱事件的通知。 本部分包括维护相关性和将标头添加到请求的其他特定步骤。

此示例包含四个用户: alfred@contoso.com、 alisa@contoso.com、 ronnie@contoso.com和 sadie@contoso.com。 下图显示了用户的 GroupingInformation 和 ExternalEwsUrl 自动发现设置

图 1. 用于对邮箱进行分组的自动发现设置

此表格显示每个用户的 GroupingInformation 和 ExternalEwsUrl 值。

使用自动发现响应中的设置,邮箱按 GroupingInformation 和 ExternalEwsUrl 设置的串联值分组。 在此示例中,Alfred 和 Sadie 具有相同的值,因此它们位于一个组中,Alisa 和 Ronnie 共享相同的值,因此它们位于另一个组中。

图 2. 创建邮箱组

此表格显示如何使用 Autodiscover 设置创建邮箱组。

就此示例而言,我们将重点介绍组 A。我们将对组 B 使用相同的步骤,但对组使用不同的 X-AnchorMailbox 值。

使用 ApplicationImpersonation 创建定位点邮箱的订阅请求 (alfred@contoso.com) ,并将 X-AnchorMailbox 标头设置为其电子邮件地址,X-PreferServerAffinity 标头值为 true。 设置这两个标头值将触发服务器为响应创建 X-BackEndOverrideCookie。

如果使用 EWS 托管 API,请使用 HttpHeadersAdd 方法将两个标头添加到订阅请求,如下所示。

service.HttpHeaders.Add("X-AnchorMailbox", Mailbox.SMTPAddress);
service.HttpHeaders.Add("X-PreferServerAffinity", "true");

因此,阿尔弗雷德的订阅请求如下所示。

POST https://outlook.office365.com/EWS/Exchange.asmx HTTP/1.1
Content-Type: text/xml; charset=utf-8
Accept: text/xml
User-Agent: ExchangeServicesClient/15.00.0516.014
X-AnchorMailbox: alfred@contoso.com
X-PreferServerAffinity: true
Host: outlook.office365.com
<?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="https://schemas.xmlsoap.org/soap/envelope/">
  <soap:Header>
    <t:RequestServerVersion Version="Exchange2013" />
    <t:ExchangeImpersonation>
      <t:ConnectingSID>
        <t:SmtpAddress>alfred@contoso.com</t:SmtpAddress>
      </t:ConnectingSID>
    </t:ExchangeImpersonation>
  </soap:Header>
  <soap:Body>
    <m:Subscribe>
      <m:StreamingSubscriptionRequest>
        <t:FolderIds>
          <t:DistinguishedFolderId Id="inbox" />
        </t:FolderIds>
        <t:EventTypes>
          <t:EventType>NewMailEvent</t:EventType>
        </t:EventTypes>
      </m:StreamingSubscriptionRequest>
    </m:Subscribe>
  </soap:Body>
</soap:Envelope>

以下 XML 消息是对 Alfred 订阅请求的响应,它包括 X-BackEndOverrideCookie。 针对此组中用户的所有后续请求重新发送此 Cookie。 请注意,响应还包含其他 Cookie,例如 Exchange 2010 使用的 exchangecookie Cookie。 Exchange Online,Exchange Online作为Office 365的一部分,以及从 Exchange 2013 开始的 Exchange 版本,如果 exchangecookie 包含在后续订阅请求中,则忽略 exchangecookie。

HTTP/1.1 200 OK
Content-Type: text/xml; charset=utf-8
Set-Cookie: exchangecookie=ddb8c383aef34c7694132aa679744feb; expires=Thu, 25-Sep-2014 18:42:45 GMT; path=/;
    HttpOnly
Set-Cookie: X-BackEndOverrideCookie=CO1PR06MB222.namprd06.prod.outlook.com~1941996295; path=/; secure; HttpOnly
Set-Cookie: X-BackEndCookie=alfred@contoso.com=Ox8XKzcXLxg==; 
    expires=Wed, 25-Sep-2013 18:52:49 GMT; path=/EWS; secure; HttpOnly
<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="https://schemas.xmlsoap.org/soap/envelope/">
  <s:Header>
    <h:ServerVersionInfo MajorVersion="15"
                         MinorVersion="0"
                         MajorBuildNumber="775"
                         MinorBuildNumber="7"
                         Version="V2_4"
                         xmlns:h="http://schemas.microsoft.com/exchange/services/2006/types"
                         xmlns="http://schemas.microsoft.com/exchange/services/2006/types"
                         xmlns:xsd="http://www.w3.org/2001/XMLSchema"
                         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>
  </s:Header>
  <s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <m:SubscribeResponse xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages"
                         xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types">
      <m:ResponseMessages>
        <m:SubscribeResponseMessage ResponseClass="Success">
          <m:ResponseCode>NoError</m:ResponseCode>
          <m:SubscriptionId>JgBjbzFwcjA2bWIyMjIubmFtcHJkMDYucHJvZC5vdXRsb29rLmNvbRAAAAAUeGk+7JFdSaFM8/NI/gQQpVdgZX6H0Ag=</m:SubscriptionId>
        </m:SubscribeResponseMessage>
      </m:ResponseMessages>
    </m:SubscribeResponse>
  </s:Body>
</s:Envelope>

使用来自 Alfred 响应的 X-BackEndOverrideCookie 和 X-AnchorMailbox 标头,为 Sadie 创建订阅请求,该请求是 A 组萨迪订阅请求的其他成员,如下所示。

POST https://outlook.office365.com/EWS/Exchange.asmx HTTP/1.1
Content-Type: text/xml; charset=utf-8
Accept: text/xml
User-Agent: ExchangeServicesClient/15.00.0516.014
X-AnchorMailbox: alfred@contoso.com
X-PreferServerAffinity: true
Host: outlook.office365.com
Cookie: X-BackEndOverrideCookie=CO1PR06MB222.namprd06.prod.outlook.com~1941996295
<?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="https://schemas.xmlsoap.org/soap/envelope/">
  <soap:Header>
    <t:RequestServerVersion Version="Exchange2013" />
    <t:ExchangeImpersonation>
      <t:ConnectingSID>
        <t:SmtpAddress>sadie@contoso.com </t:SmtpAddress>
      </t:ConnectingSID>
    </t:ExchangeImpersonation>
  </soap:Header>
  <soap:Body>
    <m:Subscribe>
      <m:StreamingSubscriptionRequest>
        <t:FolderIds>
          <t:DistinguishedFolderId Id="inbox" />
        </t:FolderIds>
        <t:EventTypes>
          <t:EventType>NewMailEvent</t:EventType>
        </t:EventTypes>
      </m:StreamingSubscriptionRequest>
    </m:Subscribe>
  </soap:Body>
</soap:Envelope>

Sadie 的订阅响应如下所示。 请注意,它不包括 X-BackEndOverrideCookie。 客户端负责为将来的请求缓存该值。

HTTP/1.1 200 OK
Content-Type: text/xml; charset=utf-8
Set-Cookie: exchangecookie=640ea858f69d47ff8cce8b44c337f6d9; path=/
Set-Cookie: X-BackEndCookie=alfred@contoso.com=Ox8XKzcXLxg==; 
   expires= Wed, 25-Sep-2013 18:53:06 GMT; path=/EWS; secure; HttpOnly
<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="https://schemas.xmlsoap.org/soap/envelope/">
  <s:Header>
    <h:ServerVersionInfo MajorVersion="15"
                         MinorVersion="0"
                         MajorBuildNumber="775"
                         MinorBuildNumber="7"
                         Version="V2_4"
                         xmlns:h="http://schemas.microsoft.com/exchange/services/2006/types"
                         xmlns="http://schemas.microsoft.com/exchange/services/2006/types"
                         xmlns:xsd="http://www.w3.org/2001/XMLSchema"
                         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>
  </s:Header>
  <s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <m:SubscribeResponse xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages"
                         xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types">
      <m:ResponseMessages>
        <m:SubscribeResponseMessage ResponseClass="Success">
          <m:ResponseCode>NoError</m:ResponseCode>
          <m:SubscriptionId>JgBjbzFwcjA2bWIyMjIubmFtcHJkMDYucHJvZC5vdXRsb29rLmNvbRAAAAB4EQOy2pfrQJfM3hzs/nZJIZssan6H0Ag=</m:SubscriptionId>
        </m:SubscribeResponseMessage>
      </m:ResponseMessages>
    </m:SubscribeResponse>
  </s:Body>
</s:Envelope>

使用订阅响应中的 SubscriptionId 值,为组中的所有订阅创建了 GetStreamingEvents 操作请求。 由于此组中的订阅少于 200 个,因此它们都在一个请求中发送。 X-PreferServerAffinity 标头设置为 true,并且包含 X-BackEndOverrideCookie。

POST https://outlook.office365.com/EWS/Exchange.asmx HTTP/1.1
Content-Type: text/xml; charset=utf-8
Accept: text/xml
User-Agent: ExchangeServicesClient/15.00.0516.014
X-AnchorMailbox: alfred@contoso.com
X-PreferServerAffinity: true
Host: outlook.office365.com
Cookie: X-BackEndOverrideCookie=CO1PR06MB222.namprd06.prod.outlook.com~1941996295
<?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="https://schemas.xmlsoap.org/soap/envelope/">
  <soap:Header>
    <t:RequestServerVersion Version="Exchange2013" />
    <t:ExchangeImpersonation>
      <t:ConnectingSID>
        <t:SmtpAddress>sadie@contoso.com</t:SmtpAddress>
      </t:ConnectingSID>
    </t:ExchangeImpersonation>
  </soap:Header>
  <soap:Body>
    <m:GetStreamingEvents>
      <m:SubscriptionIds>
        <t:SubscriptionId>JgBjbzFwcjA2bWIyMjIubmFtcHJkMDYucHJvZC5vdXRsb29rLmNvbRAAAAB4EQOy2pfrQJfM3hzs/nZJIZssan6H0Ag=</t:SubscriptionId>
        <t:SubscriptionId>JgBjbzFwcjA2bWIyMjIubmFtcHJkMDYucHJvZC5vdXRsb29rLmNvbRAAAAAUeGk+7JFdSaFM8/NI/gQQpVdgZX6H0Ag=</t:SubscriptionId>
      </m:SubscriptionIds>
      <m:ConnectionTimeout>10</m:ConnectionTimeout>
    </m:GetStreamingEvents>
  </soap:Body>
</soap:Envelope>

然后,返回的事件将传递到单独的线程进行处理。

相关性有何变化?

在 Exchange 2010 中,订阅在客户端访问服务器上维护,如图 3 所示。 在 Exchange 2010 之后的 Exchange 版本中,订阅在邮箱服务器上维护,如图 4 所示。

图 3. 在 Exchange 2010 中维护相关性的过程

此插图显示 Exchange 2010 中客户端访问服务器上的活动订阅表的维护方式。

图 4. 在 Exchange Online 和 Exchange 2013 中维护相关性的过程

此插图显示 Exchange Server 和 Exchange Online 中的负载平衡器和客户端访问服务器路由如何向维护活动订阅表的邮箱服务器发出请求。

在 Exchange 2010 中,客户端只知道负载均衡器的地址,服务器返回的 exchangecookie 可确保将请求路由到正确的客户端访问服务器。 但是,在更高版本中,负载均衡器和客户端访问服务器角色必须相应地路由请求,然后才能到达邮箱服务器。 为此,需要其他信息,这就是引入了新标头和 Cookie 的原因。 Exchange 中的通知订阅、邮箱事件和 EWS 一文介绍了如何在 Exchange 2013 中维护订阅。

你可能会注意到,Exchange 2010 使用的 exchangecookie 仍由更高版本返回。 在请求中包含此 Cookie 没有任何危害,但 Exchange 的更高版本会忽略它。

另请参阅