【问题标题】:VB.NET - Data from TCP Client isn't in orderVB.NET - 来自 TCP 客户端的数据不按顺序排列
【发布时间】:2011-12-27 20:16:18
【问题描述】:

我已经从事这个项目大约一年了。这是一个基本的客户端\服务器聊天程序。经过长时间的改进,我决定测试一下我的服务器的强度。

在客户端,我尽可能快地向服务器发送了 200 条聊天消息(“FLOOD#1”...“FLOOD#200”)。结果:服务器立即崩溃。经过一些轻微的篡改,我能够让服务器在放弃之前处理 200 条消息中的 135 条。它不再崩溃,但发生了一些不同的事情。来自客户端的数据是按顺序接收的,但是当我将该消息传递给函数 (myForm.OnLineReceived) 时,数据完全无序。如果我在 OnLineRecieved 函数的调用之间添加一点延迟,则消息井井有条。

来自客户端的每条消息首先被加密,然后以 base64 编码。在末尾附加一个“-”,以便服务器可以轻松找到每个数据“包”的结尾。

我相信你们会很容易地发现并指出这是一个愚蠢的错误。感谢您查看;)

服务器代码:

Imports System.Net.Sockets
Imports System.Text

' The UserConnection class encapsulates the functionality of a TcpClient connection
' with streaming for a single user.
Public Class UserConnection

Private client As TcpClient
Private readBuffer(READ_BUFFER_SIZE) As Byte

Public UID As String = ""
Public isAdmin As Boolean
Public IpAddress As String
Public username As String = ""
Public Country As String = ""
Public ServerID As String = ""
Public Status As String = ""
Public UserComp As String = ""
Public OS As String = ""

Public SessionKey As String = ""
Public UsePublicKeyEncryption As Boolean = True

Public Version As Decimal = 0.0

Const READ_BUFFER_SIZE As Integer = 500

Private _commands As New System.Text.StringBuilder
Private command_count As Integer = 1

' Overload the New operator to set up a read thread.
Public Sub New(ByVal client As TcpClient) 'this runs every time a new client is added
    Me.client = client
    IpAddress = Me.client.Client.RemoteEndPoint.ToString.Substring(0, Me.client.Client.RemoteEndPoint.ToString.LastIndexOf(":")) 'ip address of client
    ' This starts the asynchronous read thread.  The data will be saved into
    ' readBuffer.
    Call Worker()
End Sub

Public Sub ForceKill()
    On Error Resume Next
    client.GetStream.Close()
    client.Close()
    client = Nothing
End Sub

Private Sub Worker()
    Try
        SyncLock client
            Dim tmp_byte(client.ReceiveBufferSize) As Byte
            Me.client.GetStream.BeginRead(tmp_byte, 0, client.ReceiveBufferSize, AddressOf RecieveDataAndSplit, Nothing)
            readBuffer = tmp_byte
        End SyncLock

    Catch
        Call myForm.OnLineReceived(Me, "D") 'this also calls ForceKill()
    End Try
End Sub


Public Event LineReceived(ByVal sender As UserConnection, ByVal Data As String)

' This subroutine uses a StreamWriter to send a message to the user.
Public Sub SendData(ByVal Data As String)
    ' Synclock ensure that no other threads try to use the stream at the same time.
    SyncLock client
        Dim writer As New IO.StreamWriter(client.GetStream)
        writer.Write(ToBase64(AES_Encrypt(Data, SessionKey)) & "-")
        ' Make sure all data is sent now.
        writer.Flush()
    End SyncLock
End Sub



Public Sub RecieveDataAndSplit(ByVal ar As IAsyncResult) 'this is the FIRST function that incoming data is ran through
    Dim BytesRead As Integer
    Dim Content As String

    Try
        ' Ensure that no other threads try to use the stream at the same time.
        SyncLock client
            ' Finish asynchronous read into readBuffer and get number of bytes read.
            BytesRead = client.GetStream.EndRead(ar)
        End SyncLock
    Catch e As Exception
        Call myForm.OnLineReceived(Me, "D") 'couldn't read the stream from the client. Kill our connection with them :P
        Exit Sub
    End Try



    Try
        Content = Encoding.ASCII.GetString(readBuffer, 0, BytesRead)
    Catch ex As Exception
        Call Worker()
        Exit Sub
    End Try


    Dim commands() As String
    Try
        commands = LineTrim(Content).Split("-")
    Catch
    End Try


    Dim i As Integer = 0

    For i = 0 To commands.Length - 1

        If commands(i) <> "" Then

            Dim decrypted_content As String = AES_Decrypt(FromBase64(commands(i)), SessionKey)
            If decrypted_content <> "" Then

                'If decrypted_content = "D" Or Nothing Then
                '    client.GetStream.Close()
                '    client.Close()
                '    Call myForm.OnLineReceived(Me, decrypted_content)
                'Else

                Call myForm.OnLineReceived(Me, decrypted_content)
                Call Worker() 'reads the stream again
                'End If
            End If
        End If
    Next

End Sub
End Class

客户代码:

Public Sub SendData(ByVal data As String)
    Try
        If data = "D" Then 'telling server that we're closing
            ForceDisconnect(False)
        Else 'any other message
            Dim sendBytes As [Byte]()

            sendBytes = Encoding.ASCII.GetBytes(ToBase64(AES_Encrypt(data, SessionKey)) & "-")

            Dim networkStream As NetworkStream = tcp_client.GetStream()
            networkStream.Write(sendBytes, 0, sendBytes.Length)
            networkStream.Flush()
        End If
    Catch ex As Exception
        connection_state_toggle(False)

        Label1.ForeColor = Color.Black
        Label1.Text = "Idle"
    End Try



End Sub

【问题讨论】:

  • 您是为每条消息创建一个连接,还是只使用一个连接来发送所有消息?
  • 一个连接所有消息。

标签: vb.net tcp client buffer


【解决方案1】:

经典的 TCP/IP 网络错误。您假设发送的数据在消息或数据包中,但它确实是一个流。假设您的客户发送 message1-message2-message3-message4。在您的读取回调的服务器端,您可能会得到:

message1-m

message1-message2-

message1-message2-message3-message4

或者只是

m

想一想当您收到这样碎片化的消息时,您的解析代码(命令拆分)会发生什么情况。良好的 TCP/IP 代码应该能够在每次读取时接收一个字节的数据。如果不能,那么您一定会遇到问题。

典型的方法是不断添加到缓冲区,每次都检查缓冲区是否有完整的消息,然后仅弹出该消息,将任何部分消息留在缓冲区中以供以后填写。检查 DOS 攻击/问题,例如如果缓冲区太大(基于您的协议)则丢弃缓冲区。

【讨论】:

  • 接下来,很难预测代码可以破坏多少种方式,因为消息可以通过多种方式传递。我想有时你的解密会失败。如果您一次收到两条消息,看起来您会Call Worker() 两次而不是预期的一次。
  • 我担心这就是正在发生的事情。但是你怎么解释这些消息被重新排列呢?我会在第 80 条消息之后收到消息 #4。
  • 感谢您指出重复的“Call Worker()”代码。这是调试狂潮的一些残余物:)
  • 很高兴为您提供帮助。如果您认为答案正确,请将其标记为已接受。你会发现,如果更多用户看到你在适当的时候花时间接受答案,他们会愿意花时间回答你未来的问题:)
【解决方案2】:
            Dim thrd As New System.Threading.Thread(AddressOf RecieveDataAndSplit)
            thrd.Start(bytesFrom)
            thrd.Join()

您正在创建新线程来处理数据,无法保证这些线程如何获得 CPU 时间的顺序,因此它们被乱序添加的原因。


    SyncLock client.GetStream
        Dim tmp_byte(client.ReceiveBufferSize) As Byte
        Me.client.GetStream.BeginRead(tmp_byte, 0, client.ReceiveBufferSize, AddressOf RecieveDataAndSplit, Nothing)
        readBuffer = tmp_byte
    End SyncLock

我现在没有要调试的东西,但我想知道 GetStream 是否会在您每次调用它时返回不同的对象引用,从而使 Synclock 对您尝试执行的操作无效。我会尝试只在客户端上进行同步。

【讨论】:

  • 我不小心发布了一些旧代码。我已经用我的最新版本更新了它。并且“Join()”不是告诉调用线程暂停执行直到创建的线程退出吗?感谢您的回复!
  • 是的,确实如此,我以为你有一个线程在那里工作,但是旧代码已经消失了......
  • 那么,拥有 Join() 应该保持函数调用的顺序,对吧?
  • 取决于你在哪里调用它,如果你创建一堆线程,然后加入每个线程,它不会保持它们的顺序,如果你创建一个并在创建更多线程之前加入它应该,但那做线程有什么意义呢。
  • 仅“客户端”上的 SyncLock 没有任何改进。消息仍然混乱。
【解决方案3】:

好的,感谢大家的投入(主要是 tcarvin 的想法,即制作部分命令的“缓冲区”),我已经设法让一些漂亮的代码工作了!

希望这可以帮助其他人摆脱我自己经历的痛苦日子。

这是 100% 工作的代码:

Imports System.Net.Sockets
Imports System.Text

' The UserConnection class encapsulates the functionality of a TcpClient connection
' with streaming for a single user.
Public Class UserConnection

Private client As TcpClient

Private income_message_buffer As New System.Text.StringBuilder 'all new messages are added onto the end of this. messages are pulled from the beginning in a timely manner

Public Sub Run(ByVal client As TcpClient)
    Me.client = client
    Call MessageParser()
End Sub

Public Sub ForceKill()
    On Error Resume Next
    client.GetStream.Close()
    client.Close()
    client = Nothing
End Sub

Private Sub MessageParser()
    Do

        If client.Connected = True Then
            If client.GetStream.DataAvailable = True Then
                Dim tmp_byte(client.ReceiveBufferSize) As Byte
                Dim BytesRead As Integer
                Dim content As String

                SyncLock client
                    BytesRead = Me.client.GetStream.Read(tmp_byte, 0, client.ReceiveBufferSize)
                End SyncLock

                Try
                    content = Encoding.ASCII.GetString(tmp_byte, 0, BytesRead)
                    income_message_buffer.Append(LineTrim(content))
                Catch ex As Exception

                End Try


            End If
        End If


        Dim EndOfFirstMessage As Integer = income_message_buffer.ToString.IndexOf("-") 'gets the first occurace of "-" in the buffer
        If EndOfFirstMessage >= 0 Then
            Dim message As String = income_message_buffer.ToString.Substring(0, EndOfFirstMessage) 'gets everything before the "-"
            income_message_buffer.Remove(0, EndOfFirstMessage + 1) 'removes the first message AND the "-"
            Call ParseMessage(message)
        End If



    Loop
End Sub

Public Event LineReceived(ByVal sender As UserConnection, ByVal Data As String)

' This subroutine uses a StreamWriter to send a message to the user.
Public Sub SendData(ByVal Data As String)
    ' Synclock ensure that no other threads try to use the stream at the same time.
    SyncLock client
        Dim writer As New IO.StreamWriter(client.GetStream)
        writer.Write(ToBase64(AES_Encrypt(Data, SessionKey)) & "-")
        ' Make sure all data is sent now.
        writer.Flush()
    End SyncLock
End Sub



Public Sub ParseMessage(ByVal message As String) 'this is the FIRST function that incoming data is ran through

    Dim decrypted_content As String = AES_Decrypt(FromBase64(message), SessionKey)

    If decrypted_content <> "" Then
        Call myForm.OnLineReceived(Me, decrypted_content)
    End If


End Sub
End Class

【讨论】:

    猜你喜欢
    • 2014-11-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-12-30
    • 1970-01-01
    相关资源
    最近更新 更多