VB.NET - TCPクライアントからのデータが順不同

私は約1年間このプロジェクトに取り組んできました。基本的なクライアント\サーバーチャットプログラムです。改善の長い時間の後、私は私のサーバーの強さをテストすることにしました。

クライアントでは、できるだけ速く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
3
各メッセージとの接続を作成しているか、1つの接続を使ってすべてのメッセージを送信していますか?
追加された 著者 BlackICE,
すべてのメッセージに対して1つの接続。
追加された 著者 Andrew Paglusch,

3 答え

従来のTCP/IPネットワーキングミス。送信されるデータはメッセージまたはパケットであると想定していますが、実際はストリームです。あなたのクライアントがmessage1-message2-message3-message4を送信したとしましょう。あなたの読んだコールバックのサーバー側であなたは得るかもしれません:

message1-m

または

message1-message2-

または

message1-message2-message3-message4

または just

m

このように断片化したメッセージが得られたら、解析コード(コマンドの分割)に何が起こるかを考えてみてください。良いTCP/IPコードは、1回の読み取りで1バイトのデータを受け取ることができるはずです。それができないなら、あなたは問題にぶつかります。

The typical approach is to keep adding to a buffer and inspecting the buffer each time fまたは a completed message and then poping off just that message, leaving any partial message trailing in the buffer to get filled out later. Checks fまたは DOS attacks/problems like discarding the buffer if it gets too large (based on your protocol) should be added at some point as well.

2
追加された
フォローアップするには、さまざまな方法でメッセージを送信できるため、コードがいくつの方法で破損するかを予測することは困難です。時にはあなたの解読が失敗することもあります。 1回の受信で2つのメッセージを受け取った場合、 Worker()を1回ではなく2回呼び出すようです。
追加された 著者 tcarvin,
喜んで助けてください。答えが正しいと思われる場合は、受け入れたものとしてフラグを立ててください。あなたは、適切な時に回答を受け入れる時間がかかることが分かったら、将来の質問に答えるために時間をとることを、より多くのユーザが望むだろうと思っています:)
追加された 著者 tcarvin,
私はそれが起こっていることを恐れていた。しかし、どのようにメッセージの再配置を説明することができますか?病気は80番目のメッセージの後にメッセージ#4を得る。
追加された 著者 Andrew Paglusch,
重複した 'Call Worker()'コードを指摘してくれてありがとう。それはデバッグの狂乱からいくつかの残り物です:)
追加された 著者 Andrew Paglusch,
いくつかの作業コードを手に入れました。早急に投稿する予定です(明日も可)
追加された 著者 Andrew Paglusch,
            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はあなたがしようとしているものに対して効果がありません。私はクライアント上でSynclockをやってみようと思います。

1
追加された
はい、それは、私はあなたがそこに働くスレッドでスレッドを持っていたと思ったが、古いコードがなくなった...
追加された 著者 BlackICE,
スレッドの束を作成する場合は、それを呼び出しているかどうかに依存しますし、それぞれを作成してより多くのものを作成する必要がありますスレッドを行う。
追加された 著者 BlackICE,
できるだけクライアントコードを投稿してください。私はデバッガにアクセスすることができます。
追加された 著者 BlackICE,
私は誤って古いコードを投稿しました。最新のバージョンで更新しました。また、作成したスレッドが終了するまで、呼び出しを中断するように "Join()"しませんか?返信いただきありがとうございます!
追加された 著者 Andrew Paglusch,
ですから、Join()は関数呼び出しを順番に保つべきですか?
追加された 著者 Andrew Paglusch,
私はちょうどクライアントをsynclockingでそれをテストするつもりです...
追加された 著者 Andrew Paglusch,
ちょうど "クライアント"上のSyncLockは改善を示していません。メッセージはまだ混ざっています。
追加された 著者 Andrew Paglusch,
私はそれを投稿したばかりです
追加された 著者 Andrew Paglusch,

さて、誰もが(部分コマンドの「バッファ」を作っている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
1
追加された