C# Modbus/tcp - 连接挂起问题

3
我已经编写了一个Windows服务,使用NModbus库通过TCP执行Modbus WriteMultipleRegisters函数调用到第三方设备,每10分钟执行一次(System.Threading.Timer的滴答声)。
偶尔,在网络故障期间,此连接会挂起打开。由于该设备一次只接受一个Modbus连接,而其他连接被拒绝,因此所有下一个滴答声期间的连接都会失败,并引发SocketException - ConnectionRefused。
但是,设备会自动关闭不响应的连接,时间很短。某些东西必须保持连接在我的一侧,甚至保持两天。更重要的是,当我的服务重新启动时,一切都恢复正常。因此,明确存在一些遗忘的开放连接。但是我无法在开发中重现此错误,所以我不知道/何时……连接会挂起。我只知道下一个连接会被拒绝。
我使用以下代码部分进行modbus函数调用:
using (TcpClient client = new TcpClient(device.ip, 502))
    {
    using (Modbus.Device.ModbusIpMaster master = Modbus.Device.ModbusIpMaster.CreateIp(client))
          {
          master.WriteMultipleRegisters(500, new ushort[] { 0xFF80 });
    }  
}

device.ip是包含设备IP地址的字符串 - 这是正确的,通过SocketException细节进行了确认。

由于我使用了using语句,因此两个对象都调用了dispose方法。我已经查看了NModbus源代码,一切都被正确地处理了。

你有任何想法吗?为什么使用这段代码连接没有关闭?


你确定 .Dispose 应该关闭连接吗?我在另一个网络库中遇到了这个问题。它可能只是清理资源而没有真正地 "关闭" 任何东西。 - nemec
是的,我已经遵循了.Dispose调用库和TcpClient.Dispose被调用。NModbus使用unme库进行处理(文件:https://code.google.com/p/unme/source/browse/trunk/src/Unme.Common/DisposableUtility.cs) - Rfilip
1
“Shutdown”和“Dispose”是两种非常不同的方法。“Shutdown”向另一端发送“我们完成了”的消息,而“Dispose”并不总是保证这样做。此外,与任何C#问题无关,不要忘记TCP连接直到超时过期才会真正关闭(以防止任何挂在互联网基础设施上的数据被传递到同一端口上新打开的连接)。处理优雅的关闭非常重要,因为任何悬留的TCP连接可能需要2-4分钟才能真正关闭,这实际上可能会使.NET终结器队列停滞... - Luaan
@Luaan 关闭连接?我在TcpClient文档和源代码中都没有看到这个方法,VS也无法识别它。也许你是指Close方法,但Close只是Dispose的别名(详见对ZaXa回答的评论)。不过感谢你提供有关TCP连接关闭的信息。 - Rfilip
你是否正在使用多线程? - landoncz
除了计时器的滴答声,我不使用多线程。 - Rfilip
3个回答

4

我同意nemec的观点。如果你查看TcpClient.Dispose的文档,它并没有明确提到关闭连接。它默认释放托管和非托管资源,但可能无法正确地拆除连接。

尝试将您的代码更改为:

using (TcpClient client = new TcpClient(device.ip, 502))
{
    try 
    {
        using (Modbus.Device.ModbusIpMaster master = Modbus.Device.ModbusIpMaster.CreateIp(client))
        {
            master.WriteMultipleRegisters(500, new ushort[] { 0xFF80 });
        }
    }
    catch(Exception e) 
    {
        // Log exception
    }
    finally 
    {
        client.Close();
    }
}

这样做的好处是在释放之前进行了干净的关闭,即使Modbus协议抛出某种异常,它也会进行清理。


也许文档没有明确说明,但我使用ILSpy(http://ilspy.net/)查看了System.Net.TcpClient源代码,并发现Close只是Dispose的别名,因为它除了记录日志外只调用Dispose。然后在Dispose中包括Socket和NetworkStream的所有释放/关闭操作... - Rfilip
2
根据文档,它不仅仅是一个别名。"Close 方法标记实例为已释放,并请求相关的 Socket 关闭 TCP 连接。基于 LingerState 属性,当数据仍有待发送时,在调用 Close 方法后 TCP 连接可能会保持打开状态一段时间。在底层连接完成关闭时不提供任何通知。 调用此方法最终将导致相关 Socket 的关闭,并且如果已创建 NetworkStream,则还将关闭用于发送和接收数据的相关 NetworkStream。" - ZaXa
你在部署版本中使用的.NET运行时与开发版本相同吗?你提到在开发过程中没有看到它。 - ZaXa
代码始终是关于类行为的真相。文档对代码运行来说毫无意义。这些方法的文档写得不太好。方法的源代码:http://pastebin.com/2wE0QCiY。 是的,但与网络相关的错误很难复现。 - Rfilip
我基本上已经没有更多的想法了。但是关于诊断,你是否尝试验证你的应用程序是否保持套接字打开状态?可能使用TCPView - ZaXa

2

不,我没有修改这个属性。 - Rfilip

1
这不是一个答案,而是一个带有代码的评论。我们在一些安装的电脑上遇到了同样的问题,但并非所有电脑都有此问题。该问题本身也非常间歇性,有时候会几个月都不出现。我希望有人能找到答案。以下是我们的强制销毁/重新连接代码,无法正常工作
        try
        {
            try
            {
                try
                {
                    // Close the stream
                    var stream = _tcpClient.GetStream();
                    if (stream != null)
                        stream.Close();
                }
                catch { }

                try
                {
                    // Close the socket
                    if (_tcpClient.Client != null)
                        _tcpClient.Client.Close();
                }
                catch { }

                // Close the client
                _tcpClient.Close();
                _tcpClient = null;
            }
            catch { }

            if (_device != null)
            {
                _device.Dispose();
                _device = null;
            }
        }
        catch { }

        System.Threading.Thread.Sleep(1000);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接