2013-01-07 121 views
1

我创建了一个Windows服务,该服务等待TCPClient连接并将任何消息转发给所有连接的客户端(发件人除外)。我的代码基于this示例。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() 

ConnectionInfo类:

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 

回答

1

TCP并不能保证一个侧面没有试图发送的数据可以检测损失的连接。您在设计应用程序协议时应该考虑到这一点。

你所看到的最常见的是由NAT或有状态防火墙造成的。实际上,如果您至少每十分钟不发送一次数据,至少可以期望某些客户端断开连接。他们的NAT设备或状态防火墙只是忘记了连接。直到它尝试发送数据时,双方都不会通知。

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

每个客户端都会通过向服务器发回一个虚拟消息来响应虚拟消息。如果客户在10分钟内未收到虚拟消息,则应考虑丢失连接,关闭连接并尝试再次连接。

尝试发送虚假消息的行为将会导致服务器检测到任何丢失的连接,但是您应该也可以考虑将任何与虚拟消息对应的客户端的连接视为无效准备发送下一个。当客户端没有收到虚拟消息时,它会知道连接丢失。消息交换将使NAT /防火墙条目保持活动状态。