1

我创建了一个 Windows 服务,它等待 TCPClient 连接并将任何消息中继到所有连接的客户端(发送者除外)。我的代码基于这个例子。

一个客户端在触发事件时连接,发送一些进度更新,然后断开连接。其他客户端是接收和显示更新的前端应用程序。

如果这些客户端闲置了几个小时,它们似乎会在没有任何错误\警告的情况下断开连接。我找不到任何空闲时间的相关超时,我有什么遗漏吗?

服务代码:

Protected Overrides Sub OnStart(ByVal args() As String)
    _Listener = New TcpListener(IPAddress.Any, 1314)
    _Listener.Start()
    ListenForClient()
    _ConnectionMontior = Task.Factory.StartNew(AddressOf DoMonitorConnections, New MonitorInfo(_Listener, _Connections), TaskCreationOptions.LongRunning)
End Sub

Private Sub ListenForClient()
    Dim info As New ConnectionInfo(_Listener)
    _Listener.BeginAcceptTcpClient(AddressOf DoAcceptClient, info)
End Sub

Private Sub DoAcceptClient(result As IAsyncResult)
    Try
        Dim monitorInfo As MonitorInfo = CType(_ConnectionMontior.AsyncState, MonitorInfo)
    If monitorInfo.Listener IsNot Nothing AndAlso Not monitorInfo.Cancel Then
        Dim info As ConnectionInfo = CType(result.AsyncState, ConnectionInfo)
        monitorInfo.Connections.Add(info)
        info.AcceptClient(result)
        ListenForClient()
        info.AwaitData()
    End If
    Catch ex As Exception
        WriteToEventLog("DoAcceptClient: " & ex.Message)
    End Try
End Sub

Private Sub DoMonitorConnections()

    Try

        'Create delegate for updating output display
        ' Dim doAppendOutput As New Action(Of String)(AddressOf AppendOutput)

        'Get MonitorInfo instance from thread-save Task instance
        Dim monitorInfo As MonitorInfo = CType(_ConnectionMontior.AsyncState, MonitorInfo)

        'Implement client connection processing loop
        Do
            'Create temporary list for recording closed connections
            Dim lostConnections As New List(Of ConnectionInfo)

            'Examine each connection for processing
            For Each info As ConnectionInfo In monitorInfo.Connections
                If info.Client.Connected Then
                    'Process connected client
                    If info.DataQueue.Count > 0 Then
                        'The code in this If-Block should be modified to build 'message' objects
                        'according to the protocol you defined for your data transmissions.
                        'This example simply sends all pending message bytes to the output textbox.
                        'Without a protocol we cannot know what constitutes a complete message, so
                        'with multiple active clients we could see part of client1's first message,
                        'then part of a message from client2, followed by the rest of client1's
                        'first message (assuming client1 sent more than 64 bytes).
                        Dim messageBytes As New List(Of Byte)
                        While info.DataQueue.Count > 0
                            messageBytes.Add(info.DataQueue.Dequeue)
                        End While

                        'Relay the message to all clients except the sender
                        For Each inf As ConnectionInfo In monitorInfo.Connections
                            If inf.Client.Connected Then
                                Dim msg As String = info.Client.Client.RemoteEndPoint.ToString & "|" & System.Text.Encoding.ASCII.GetString(messageBytes.ToArray)
                                If Not inf.Client.Client.RemoteEndPoint.ToString = msg.Split("|")(0) Then
                                    inf.Client.Client.Send(messageBytes.ToArray)
                                End If
                            End If
                        Next

                    End If
                Else
                    'Record clients no longer connected
                    lostConnections.Add(info)
                End If
            Next

            'Clean-up any closed client connections
            If lostConnections.Count > 0 Then
                While lostConnections.Count > 0
                    monitorInfo.Connections.Remove(lostConnections(0))
                    lostConnections.RemoveAt(0)
                End While
            End If

            'Throttle loop to avoid wasting CPU time
            _ConnectionMontior.Wait(1)
        Loop While Not monitorInfo.Cancel

        'Close all connections before exiting monitor
        For Each info As ConnectionInfo In monitorInfo.Connections
            info.Client.Close()
        Next
        monitorInfo.Connections.Clear()

    Catch ex As Exception
        WriteToEventLog("DoMonitorConnections" & ex.Message)
    End Try

End Sub

客户代码:

 _ServerAddress = IPAddress.Parse(ServerIP)
 _Connection = New ConnectionInfo(_ServerAddress, 1314, AddressOf InvokeAppendOutput)
 _Connection.AwaitData()

连接信息类:

Public Class ConnectionInfo
Private _AppendMethod As Action(Of String)
Public ReadOnly Property AppendMethod As Action(Of String)
    Get
        Return _AppendMethod
    End Get
End Property

Private _Client As TcpClient
Public ReadOnly Property Client As TcpClient
    Get
        Return _Client
    End Get
End Property

Private _Stream As NetworkStream
Public ReadOnly Property Stream As NetworkStream
    Get
        Return _Stream
    End Get
End Property

Private _LastReadLength As Integer
Public ReadOnly Property LastReadLength As Integer
    Get
        Return _LastReadLength
    End Get
End Property

Private _Buffer(255) As Byte

Public Sub New(address As IPAddress, port As Integer, append As Action(Of String))
    _AppendMethod = append
    _Client = New TcpClient
    _Client.Connect(address, port)
    _Stream = _Client.GetStream
End Sub

Public Sub AwaitData()
    _Stream.BeginRead(_Buffer, 0, _Buffer.Length, AddressOf DoReadData, Me)
End Sub

Public Sub Close()
    If _Client IsNot Nothing Then _Client.Close()
    _Client = Nothing
    _Stream = Nothing
End Sub

Private Const MESSAGE_DELIMITER As Char = ControlChars.Cr
Dim sBuilder As New System.Text.StringBuilder

Private Sub DoReadData(result As IAsyncResult)

    Dim info As ConnectionInfo = CType(result.AsyncState, ConnectionInfo)

    Try
        If info._Stream IsNot Nothing AndAlso info._Stream.CanRead Then
            info._LastReadLength = info._Stream.EndRead(result)
            If info._LastReadLength > 0 Then
                Dim message As String = System.Text.Encoding.UTF8.GetString(info._Buffer, 0, info._LastReadLength)


                If (message.IndexOf(MESSAGE_DELIMITER) > -1) Then

                    Dim subMessages() As String = message.Split(MESSAGE_DELIMITER)

                    sBuilder.Append(subMessages(0))
                    If Not info._Client.Client.LocalEndPoint.ToString = sBuilder.ToString.Split("|")(0) Then
                        info._AppendMethod(sBuilder.ToString)
                    End If

                    sBuilder = New System.Text.StringBuilder

                    If subMessages.Length = 2 Then
                        sBuilder.Append(subMessages(1))
                    Else
                        For i As Integer = 1 To subMessages.GetUpperBound(0) - 1
                            'MessageBox.Show(subMessages(i))
                            info._AppendMethod(subMessages(i))
                        Next
                        sBuilder.Append(subMessages(subMessages.GetUpperBound(0)))
                    End If
                Else
                    sBuilder.Append(message)
                End If
            End If
        End If

        info.AwaitData()

    Catch ex As Exception
        info._LastReadLength = -1
    End Try
End Sub
End Class
4

1 回答 1

1

TCP 不保证不尝试发送数据的一方可以检测到连接丢失。在设计应用程序协议时,您应该考虑到这一点。

您看到的最常见的原因是 NAT 或状态防火墙。实际上,如果您不至少每十分钟发送一次数据,您可以预期至少有一些客户端会断开连接。他们的 NAT 设备或状态防火墙只是忘记了连接。在尝试发送数据之前,双方都不会注意到。

我建议创建某种虚拟消息,服务器每五分钟将其发送给所有客户端。基本上,这只是一小部分数据,可以唯一标识为仅用于保持连接活动。

每个客户端通过将虚拟消息发送回服务器来响应虚拟消息。如果客户端在十分钟内没有收到虚拟消息,它应该认为连接丢失,关闭它,然后重新尝试连接。

仅尝试发送虚拟消息的行为将导致服务器检测到任何丢失的连接,但您可能还应该将任何在您准备好时尚未响应虚拟消息的客户端的连接视为已死发送下一个。客户端在没有收到虚拟消息时会知道连接丢失。消息交换将使 NAT/防火墙条目保持活动状态。

于 2013-01-07T11:18:21.160 回答