【问题标题】:WCF Duplex: Send callback to a specific subscribed ASP.NET Webforms clientWCF Duplex:向特定订阅的 ASP.NET Webforms 客户端发送回调
【发布时间】:2015-11-11 17:14:15
【问题描述】:

我正在开发一个 ASP.NET 4.0 Webforms 客户端应用程序,该应用程序连接到 WCF 4.0 Duplex 服务以实现某种平面文件处理。当用户在 Page_Load 事件上进入页面时,我为客户端订阅了双工服务,这是因为在某些情况下我需要通知所有客户端:

A) 启动进程的客户端必须在进程启动时得到通知。

B) 处理文件时必须通知启动进程的客户端。

C) 启动进程的客户端必须在整个进程完成时得到通知。

D) 如果一个新的客户端(订阅者)在进程已经开始时进入,必须收到一个特定的通知。

E) 如果有多个客户端(订阅者)在其中一个启动进程时处于活动状态,则其他客户端必须收到特定通知。

我已经编写了这个逻辑,但是我在尝试完成特定的订阅者通知时遇到了很多问题,似乎 WCF 的 Web 应用程序的所有客户端/实例都被识别为相同,我收到了所有启动进程的客户端中的通知,如果我打开其他浏览器并启动新会话(在 ASP.NET 上),我会收到相同的通知,但没有具体内容。

在这里你可以看到我的代码的简化版本

WCF 服务接口

using System.ServiceModel;

namespace WcfService
{
    [ServiceContract(CallbackContract = typeof(IService1DuplexCallback))]
    public interface IService1
    {
        [OperationContract(IsOneWay = true)]
        void Subscribe(string idSesion);

        [OperationContract(IsOneWay = true)]
        void ProcessFiles(string idSesion);
    }

    public interface IService1DuplexCallback
    {
        [OperationContract(IsOneWay = true)]
        void NotifyProcessWorking();

        [OperationContract(IsOneWay = true)]
        void NotifyProcessStarted();

        [OperationContract(IsOneWay = true)]
        void NotifyFileProcessed(int id);

        [OperationContract(IsOneWay = true)]
        void NotifyProcessFinished();
    }
}

WCF 服务实现

using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel;
using System.Threading;
using System.Threading.Tasks;

namespace WcfService
{
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple)]
    public class Service1 : IService1
    {
        private static List<KeyValuePair<string, IService1DuplexCallback>> _clients = new List<KeyValuePair<string, IService1DuplexCallback>>();
        private static bool _isProcessStarted;
        private static string _sessionStarted = string.Empty;

        public void Subscribe(string idSesion)
        {
            lock (_clients)
            {
                if (!_clients.Any(x => string.Equals(x.Key, idSesion, StringComparison.InvariantCultureIgnoreCase)))
                {
                    var callback = OperationContext.Current.GetCallbackChannel<IService1DuplexCallback>();

                    if (callback != null)
                    {
                        var currentSubscriber = new KeyValuePair<string, IService1DuplexCallback>(idSesion, callback);
                        _clients.Add(currentSubscriber);
                    }
                }
            }

            if (_isProcessStarted)
            {
                NotifyProcessWorking(idSesion);
            }
        }

        public void ProcessFiles(string idSesion)
        {
            _isProcessStarted = true;
            _sessionStarted = idSesion;

            try
            {
                var mockFileCount = 23;
                var r = new Random();

                NotifyStarted();
                NotifyProcessWorking();

                Parallel.For(0, mockFileCount, (i) =>
                {
                    //Do a lot of specific validations... (time betweeen 5 secs and 2 minutes per file)
                    var time = r.Next(5000, 120000);

                    Thread.Sleep(time);

                    NotifyFileProcessed(i);
                });

                NotifyProcessFinished();
            }
            catch (Exception ex)
            {
                throw;
            }

            _isProcessStarted = false;
        }

        private static void NotifyStarted()
        {
            var c = _clients.FirstOrDefault(x => string.Equals(x.Key, _sessionStarted, StringComparison.InvariantCultureIgnoreCase));

            try
            {
                c.Value.NotifyProcessStarted();
            }
            catch (Exception ex)
            {
                lock (_clients)
                {
                    _clients.Remove(c);
                }
            }
        }

        private static void NotifyFileProcessed(int idFile)
        {
            var c = _clients.FirstOrDefault(x => string.Equals(x.Key, _sessionStarted, StringComparison.InvariantCultureIgnoreCase));

            try
            {
                c.Value.NotifyFileProcessed(idFile);
            }
            catch (Exception ex)
            {
                lock (_clients)
                {
                    _clients.Remove(c);
                }
            }
        }

        private static void NotifyProcessFinished()
        {
            foreach (var c in _clients)
            {
                try
                {
                    c.Value.NotifyProcessFinished();
                }
                catch (Exception ex)
                {
                    lock (_clients)
                    {
                        _clients.Remove(c);
                    }
                }
            }
        }

        private static void NotifyProcessWorking(string idSesion = "")
        {
            if (string.IsNullOrEmpty(idSesion))
            {
                foreach (var c in _clients)
                {
                    try
                    {
                        c.Value.NotifyProcessWorking();
                    }
                    catch (Exception ex)
                    {
                        lock (_clients)
                        {
                            _clients.Remove(c);
                        }
                    }
                }
            }
            else
            {
                var c = _clients.FirstOrDefault(x => string.Equals(x.Key, idSesion, StringComparison.InvariantCultureIgnoreCase));

                try
                {
                    c.Value.NotifyProcessWorking();
                }
                catch (Exception)
                {
                    lock (_clients)
                    {
                        _clients.Remove(c);
                    }
                }
            }
        }
    }
}

WCF 服务 Web.config

<?xml version="1.0"?>
<configuration>
  <appSettings/>
  <system.web>
    <compilation debug="true" targetFramework="4.0"/>
    <httpRuntime/>
  </system.web>
  <system.serviceModel>
    <services>
      <service name="WcfService.Service1">
        <endpoint address="" binding="wsDualHttpBinding" bindingConfiguration="FileProcessorDuplexBinding" 
                  name="FileProcessorDuplexEndPoint" contract="WcfService.IService1"/>
      </service>
    </services>
    <bindings>
      <wsDualHttpBinding>
        <binding name="FileProcessorDuplexBinding" closeTimeout="00:30:00" openTimeout="00:30:00" 
                 sendTimeout="00:30:00" receiveTimeout="00:30:00" maxBufferPoolSize="2147483647"
                 maxReceivedMessageSize="2147483647">
          <reliableSession inactivityTimeout="00:30:00"/>
          <readerQuotas maxDepth="2147483647" maxStringContentLength="2147483647" maxArrayLength="2147483647" 
                        maxBytesPerRead="2147483647" maxNameTableCharCount="2147483647"/>
          <security mode="None"/>
        </binding>
      </wsDualHttpBinding>
    </bindings>
    <behaviors>
      <serviceBehaviors>
        <behavior>
          <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true"/>
          <serviceDebug includeExceptionDetailInFaults="false"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <serviceHostingEnvironment  multipleSiteBindingsEnabled="true"/>
  </system.serviceModel>
  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true"/>
    <directoryBrowse enabled="true"/>
  </system.webServer>
</configuration>

ASP.NET WebForm 客户端用户界面

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="WebApplication.Default" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <script src="Scripts/jquery-2.1.4.min.js"></script>
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
        <div>
            <asp:Button ID="btnStart" runat="server" Text="Start Process" OnClientClick="Start();"/>
            <br/>
            <br/>
            <asp:Label ID="lblStatus" runat="server" Text="[Process Status]"></asp:Label>
        </div>
        <script>

            function Start() {

                var loc = window.location.href;
                var dataValue = "{}";

                $.ajax({
                    type: "POST",
                    url: loc + "/StartProcess",
                    contentType: 'application/json',
                    data: dataValue,
                    dataType: 'json',
                    error: function(XMLHttpRequest, textStatus, errorThrown) {
                        alert("Request: " + XMLHttpRequest.toString() + "\n\nStatus: " + textStatus + "\n\nError: " + errorThrown);
                    },
                    success: function(result) {
                    }
                });

            }

            setInterval(function () {

                var loc = window.location.href;
                var dataValue = "{ id: '1' }";

                $.ajax({
                    type: "POST",
                    url: loc + "/CheckMessage",
                    contentType: 'application/json',
                    data: dataValue,
                    dataType: 'json',
                    error: function(XMLHttpRequest, textStatus, errorThrown) {
                        alert("Request: " + XMLHttpRequest.toString() + "\n\nStatus: " + textStatus + "\n\nError: " + errorThrown);
                    },
                    success: function(result) {
                        processMessage(result.d);
                    }
                });


            }, 1000);

            function processMessage(msg) {

                if (msg) {
                    switch (msg) {
                    case "working":
                        alert("Process currently working");
                        $('[id$=lblStatus]').attr('disabled', true);
                        break;

                    case "started":
                        $('#<%=lblStatus.ClientID%>').html("Process started");
                        break;

                    case "finished":
                        $('#<%=lblStatus.ClientID%>').html("Process finished");
                        break;

                    default:
                        var data = msg.split(":");
                        $('#<%=lblStatus.ClientID%>').html("File Processed: " + data[1]);
                        break;
                    }
                }
            }
        </script>
    </form>
</body>
</html>

ASP.NET WebForm 客户端代码隐藏

using System;
using System.Collections.Concurrent;
using System.ServiceModel;
using System.Web.Services;
using System.Web.UI;
using WebApplication.ServiceReference1;

namespace WebApplication
{
    [CallbackBehavior(ConcurrencyMode = ConcurrencyMode.Multiple, UseSynchronizationContext = false)]
    public partial class Default : Page, IService1Callback
    {
        private static ConcurrentQueue<string> _serviceReceivedMessages = new ConcurrentQueue<string>();
        private static string _sessionId = string.Empty;

        protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
            {
                _sessionId = Session.SessionID;

                var proxyDuplex = new Service1Client(new InstanceContext(new Default()));
                proxyDuplex.Subscribe(_sessionId);
            }
        }

        [WebMethod]
        public static void StartProcess()
        {
            var proxyDuplex = new Service1Client(new InstanceContext(new Default()));
            proxyDuplex.ProcessFiles(_sessionId);
        }

        [WebMethod]
        public static string CheckMessage(string id)
        {
            var message = string.Empty;

            _serviceReceivedMessages.TryDequeue(out message);

            return message ?? (message = string.Empty);
        }

        public void NotifyProcessWorking()
        {
            _serviceReceivedMessages.Enqueue("working");
        }

        public void NotifyProcessStarted()
        {
            _serviceReceivedMessages.Enqueue("started");
        }

        public void NotifyFileProcessed(int id)
        {
            _serviceReceivedMessages.Enqueue("processed:"+id);
        }

        public void NotifyProcessFinished()
        {
            _serviceReceivedMessages.Enqueue("finished");
        }
    }
}

ASP.NET WebForm 客户端 Web.config

<?xml version="1.0"?>
<configuration>
  <system.web>
    <compilation debug="true" targetFramework="4.0"/>
    <httpRuntime/>
  </system.web>
  <system.serviceModel>
    <bindings>
      <wsDualHttpBinding>
        <binding name="FileProcessorDuplexBinding" 
                 closeTimeout="00:30:00" openTimeout="00:30:00" receiveTimeout="00:30:00"
                 sendTimeout="00:30:00" maxBufferPoolSize="2147483647" maxReceivedMessageSize="2147483647"
                 clientBaseAddress="http://localhost:62778/TempUri">
          <reliableSession inactivityTimeout="00:30:00" />
          <readerQuotas maxDepth="2147483647" maxStringContentLength="2147483647"
            maxArrayLength="2147483647" maxBytesPerRead="2147483647" maxNameTableCharCount="2147483647" />
          <security mode="None" />
        </binding>
      </wsDualHttpBinding>
    </bindings>
    <client>
      <endpoint address="http://localhost:62778/Service1.svc" binding="wsDualHttpBinding"
        bindingConfiguration="FileProcessorDuplexBinding" contract="ServiceReference1.IService1"
        name="FileProcessorDuplexEndPoint" />
    </client>
  </system.serviceModel>
</configuration>

您可以在此处download 包含完整代码的 Visual Studio 2015 解决方案。

我想知道我的代码有什么问题,我认为这种行为是可能的,但不明白为什么 WCF 没有通知特定的客户端。

谢谢

更新 1

我做了所有更改@JuanK 建议我(当时)没有运气,行为仍然相同,我添加了一个新的控制台项目来测试相同的服务,并且在该项目中运行良好

但在 ASP.NET 项目中,错误仍在继续,第二个客户端获取所有通知

您可以在这里download VS 解决方案更新(此时)

【问题讨论】:

    标签: c# asp.net wcf webforms wcf-binding


    【解决方案1】:

    WCF 没有通知特定的客户端,因为您已经编写了该服务必须向所有人发送通知的代码。

    private static void NotifyProcessFinished()
    {
        //FOR EACH CLIENT
        foreach (var c in _clients)
        {
            try
            {
                c.Value.NotifyProcessFinished();
            }
            catch (Exception ex)
            {
                lock (_clients)
                {
                    _clients.Remove(c);
                }
            }
        }
    }
    

    另一方面,我在这里修复了一些 ServiceBehavior 和 静态字段 问题:

    [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession, ConcurrencyMode = ConcurrencyMode.Multiple)]
    public class Service1 : IService1
    {
        private static List<KeyValuePair<string, IService1DuplexCallback>> _clients = new List<KeyValuePair<string, IService1DuplexCallback>>();
        private static bool _isProcessStarted;
        private string _sessionStarted = string.Empty;
    
        public void Subscribe(string idSesion)
        {
            lock (_clients)
            {
                if (!_clients.Any(x => string.Equals(x.Key, idSesion, StringComparison.InvariantCultureIgnoreCase)))
                {
                    var callback = OperationContext.Current.GetCallbackChannel<IService1DuplexCallback>();
    
                    if (callback != null)
                    {
                        var currentSubscriber = new KeyValuePair<string, IService1DuplexCallback>(idSesion, callback);
                        _clients.Add(currentSubscriber);
                    }
                }
            }
    
            if (_isProcessStarted)
            {
                NotifyProcessWorking(idSesion);
            }
        }
    
        public void ProcessFiles(string idSesion)
        {
            _isProcessStarted = true;
            _sessionStarted = idSesion;
    
            try
            {
                var mockFileCount = 2;
                var r = new Random();
    
                NotifyStarted();
                NotifyProcessWorking();
    
                Parallel.For(0, mockFileCount, (i) =>
                {
                    //Do a lot of specific validations... (time betweeen 5 secs and 2 minutes per file)
                    var time = 5000;//r.Next(5000, 120000);
    
                    Thread.Sleep(time);
    
                    NotifyFileProcessed(i);
                });
    
                NotifyProcessFinished();
            }
            catch (Exception ex)
            {
                throw;
            }
    
            _isProcessStarted = false;
        }
    
        private void NotifyStarted()
        {
            var c = _clients.FirstOrDefault(x => string.Equals(x.Key, _sessionStarted, StringComparison.InvariantCultureIgnoreCase));
    
            try
            {
                c.Value.NotifyProcessStarted();
            }
            catch (Exception ex)
            {
                lock (_clients)
                {
                    _clients.Remove(c);
                }
            }
        }
    
        private void NotifyFileProcessed(int idFile)
        {
            var c = _clients.FirstOrDefault(
                x => string.Equals(x.Key, _sessionStarted,
                StringComparison.InvariantCultureIgnoreCase)
                );
    
            try
            {
                c.Value.NotifyFileProcessed(idFile);
            }
            catch (Exception ex)
            {
                lock (_clients)
                {
                    _clients.Remove(c);
                }
            }
        }
    
        private void NotifyProcessFinished()
        {
            //STILL SAME YOU HAVE IT. JUST IN CASE
            foreach (var c in _clients)
            {
                try
                {
                    c.Value.NotifyProcessFinished();
                }
                catch (Exception ex)
                {
                    lock (_clients)
                    {
                        _clients.Remove(c);
                    }
                }
            }
        }
    
        private static void NotifyProcessWorking(string idSesion = "")
        {
            if (string.IsNullOrEmpty(idSesion))
            {
                foreach (var c in _clients)
                {
                    try
                    {
                        c.Value.NotifyProcessWorking();
                    }
                    catch (Exception ex)
                    {
                        lock (_clients)
                        {
                            _clients.Remove(c);
                        }
                    }
                }
            }
            else
            {
                var c = _clients.FirstOrDefault(x => string.Equals(x.Key, idSesion, StringComparison.InvariantCultureIgnoreCase));
    
                try
                {
                    c.Value.NotifyProcessWorking();
                }
                catch (Exception)
                {
                    lock (_clients)
                    {
                        _clients.Remove(c);
                    }
                }
            }
        }
    }
    

    如果您有单个实例行为和静态字段:静态字段在所有连接之间共享,因此例如_sessionStarted 始终获取上次连接的状态。

    因此,我已将服务行为更改为 PerSession,以允许每个特定会话/连接保留非静态字段,而 List&lt;KeyValuePair&lt;string, IService1DuplexCallback&gt;&gt; _clients 等静态字段仍在它们之间共享。 这也意味着一些方法现在是非静态方法。

    【讨论】:

    • 我认为这不是主要问题,因为您只是检查其中一个通知,还有其他一些适用于特定客户的通知也不起作用。
    • 我已经尝试了您的新代码,但不幸的是行为保持不变。如果您将 var _isProcessStarted 和 _sessionStarted 更改为非静态,新客户端将永远不会知道进程是否已经启动,因此该客户端的通知都不会起作用。请记住,主要场景是:1 客户端启动进程,如果新客户端到达(到页面)服务必须通知他进程正在工作并且不允许他做任何事情,直到当前进程结束。实际上在所有情况下都会通知所有客户,即使通知不是针对他们的。
    • 您是否将服务行为更改为如代码所示的InstanceContextMode = InstanceContextMode.PerSession?
    • 对于进程启动的限制,只是将 _isProcessStarted 设置为一个静态字段。
    猜你喜欢
    • 2015-10-07
    • 1970-01-01
    • 2012-05-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-12-25
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多