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; }
验证步骤
- 先应用客户端
CloseProxy()的修复,测试二次启动是否能成功注册回调 - 部署服务器端的回调清理机制,测试多客户端异常停止后新客户端的注册情况
- 模拟服务器回调场景,验证是否还会出现内存访问错误
内容的提问来源于stack exchange,提问作者btomas

