You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

WCF双工绑定回调通道异常:二次启动注册失败及服务器崩溃

WCF双工客户端二次启动回调注册失败及服务器访问冲突问题排查与修复

咱们一步步拆解你遇到的WCF双工通信问题——这是典型的资源清理不当+过期回调引用堆积的组合问题,我会结合你的代码给出针对性的修复方案。

核心问题拆解

1. 二次启动超时的根源

从日志看,二次启动时客户端调用RegisterAgent直接超时。结合WSDualHttpBinding的特性:你的客户端回调地址是动态分配的(通过LocalPorts.GetNextAvailablePort生成),但第一次关闭时Close()调用逻辑不严谨,要么导致服务器端还持有旧的回调会话引用,要么客户端的ChannelFactory没有完全释放端口/内存资源,最终新代理无法建立有效连接。

2. 服务器崩溃的原因

移除Close()后,客户端的代理和ChannelFactory没有被正确释放,服务器端维护的回调列表里还留存着已失效的客户端通道引用。当服务器尝试向这些失效通道发送回调时,就会触发内存访问错误:"The thread tried to read from or write to a virtual address for which it does not have the appropriate access"。

3. 多客户端停止后的注册失败

当多个客户端突然异常停止(未执行正常关闭流程),服务器端的回调列表会堆积大量失效通道实例,这些实例占用的资源无法被及时回收,最终导致服务器资源耗尽,新客户端的注册请求无法被处理。


针对性修复方案

1. 修复客户端Proxy的正确释放逻辑

你的CloseProxy()方法的释放顺序和异常处理有漏洞,WCF客户端的正确释放流程需要先检查资源状态,再分阶段关闭/中止,且要给操作留合理超时。修改后的代码如下:

private void CloseProxy()
{
    // 先处理通道的关闭
    try
    {
        if (mProxy?.InnerChannel != null && mProxy.InnerChannel.State == CommunicationState.Opened)
        {
            mProxy.InnerChannel.Close(TimeSpan.FromSeconds(5)); // 给关闭操作留5秒超时
        }
    }
    catch (Exception ex)
    {
        mLog.Error.Write($"Failed to close channel: {ex.Message}", ex);
        mProxy?.InnerChannel.Abort(); // 异常时强制中止
    }

    // 再单独处理ChannelFactory的释放
    try
    {
        if (mProxy?.ChannelFactory != null && mProxy.ChannelFactory.State == CommunicationState.Opened)
        {
            mProxy.ChannelFactory.Close(TimeSpan.FromSeconds(5));
        }
    }
    catch (Exception ex)
    {
        mLog.Error.Write($"Failed to close channel factory: {ex.Message}", ex);
        mProxy?.ChannelFactory.Abort();
    }
}

关键优化点:

  • 关闭前先检查资源状态,避免对已关闭/失效的资源执行操作
  • 给Close()设置明确超时,避免无限等待
  • 通道和工厂的释放逻辑分离,一个失败不影响另一个的清理

2. 服务器端增加回调列表主动清理机制

服务器仅被动维护回调列表是不够的,需要定期检查并移除失效的回调通道:

// 服务器端AgentManagerService示例代码
public class AgentManagerService : IAgentManager
{
    private readonly List<IAgentCallback> _callbacks = new List<IAgentCallback>();
    private readonly Timer _callbackCleanupTimer;

    public AgentManagerService()
    {
        // 每分钟执行一次失效回调清理
        _callbackCleanupTimer = new Timer(CleanupInvalidCallbacks, null, TimeSpan.Zero, TimeSpan.FromMinutes(1));
    }

    public string RegisterAgent(string hostName)
    {
        var callback = OperationContext.Current.GetCallbackChannel<IAgentCallback>();
        lock (_callbacks)
        {
            if (!_callbacks.Contains(callback))
            {
                _callbacks.Add(callback);
            }
        }
        return Guid.NewGuid().ToString(); // 返回唯一标识
    }

    private void CleanupInvalidCallbacks(object state)
    {
        lock (_callbacks)
        {
            // 筛选所有状态非Opened的回调通道
            var invalidCallbacks = _callbacks.Where(c => 
                ((ICommunicationObject)c).State != CommunicationState.Opened).ToList();
            
            foreach (var callback in invalidCallbacks)
            {
                try
                {
                    ((ICommunicationObject)callback).Close(TimeSpan.FromSeconds(2));
                }
                catch
                {
                    ((ICommunicationObject)callback).Abort();
                }
                _callbacks.Remove(callback);
            }
        }
    }

    // 实现Ping方法
    public bool Ping() => true;
}

这个定时清理机制能主动回收服务器端的失效资源,避免堆积导致新客户端无法注册。

3. 优化客户端二次启动的会话重置逻辑

在CreateManagerProxy()中,增加重试机制和资源强制回收步骤,确保旧代理完全释放后再创建新代理:

private void CreateManagerProxy()
{
    Reset();
    var isUnbound = true;
    int retryCount = 0;
    const int maxRetries = 3;

    while (isUnbound && retryCount < maxRetries)
    {
        try
        {
            // 先彻底清理旧代理资源
            if (ManagerProxy != null)
            {
                ManagerProxy.ResetProxy();
                ManagerProxy = null;
                GC.Collect(); // 可选:强制触发垃圾回收,加速资源释放
                GC.WaitForPendingFinalizers();
            }

            ManagerProxy = new ProxyWrapper<AgentManagerProxy, IAgentManager>(CreateProxy);
            ManagerProxy.ChannelStateChanged += HandleProxyConnectionStateChanged;
            isUnbound = false;
        }
        catch (AddressAlreadyInUseException)
        {
            sLog.ErrorFormat("Port is already reserved, binding failed. Retrying after 1s...");
            retryCount++;
            Thread.Sleep(1000);
        }
        catch (TimeoutException)
        {
            sLog.ErrorFormat("Proxy creation timed out. Retrying after 2s...");
            retryCount++;
            Thread.Sleep(2000);
        }
        catch (Exception error)
        {
            sLog.ErrorFormat($"Failed to create proxy: {error}");
            throw;
        }
    }

    if (retryCount >= maxRetries)
    {
        throw new InvalidOperationException("Failed to create proxy after maximum retries");
    }
}

4. 调整WSDualHttpBinding的超时设置

你的Binding中ReceiveTimeout设为TimeSpan.MaxValue容易导致资源泄漏,同时默认SendTimeout过短,建议设置合理的超时值:

private static Binding CreateDuplexServiceHostBinding()
{
    var binding = new WSDualHttpBinding
    {
        ReceiveTimeout = TimeSpan.FromHours(1), // 避免用MaxValue,降低资源泄漏风险
        SendTimeout = TimeSpan.FromSeconds(30), // 增加发送超时,给服务器足够响应时间
        OpenTimeout = TimeSpan.FromSeconds(10),
        CloseTimeout = TimeSpan.FromSeconds(10),
        MaxReceivedMessageSize = int.MaxValue,
        ReaderQuotas = {
            MaxArrayLength = int.MaxValue,
            MaxBytesPerRead = int.MaxValue,
            MaxDepth = int.MaxValue,
            MaxNameTableCharCount = int.MaxValue,
            MaxStringContentLength = int.MaxValue
        },
        Security = {
            Mode = WSDualHttpSecurityMode.None,
            Message = { ClientCredentialType = MessageCredentialType.None }
        }
    };
    return binding;
}

验证步骤

  1. 先应用客户端CloseProxy()的修复,测试二次启动是否能成功注册回调
  2. 部署服务器端的回调清理机制,测试多客户端异常停止后新客户端的注册情况
  3. 模拟服务器回调场景,验证是否还会出现内存访问错误

内容的提问来源于stack exchange,提问作者btomas

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.07 21:52:59