2013年10月1日 星期二

[named pipe] 訊息傳送的協定 - 使用訊息長度

上次討論的是,使用分隔字元的訊息傳送協定的實作方式及需要注意的事項(http://www.dotblogs.com.tw/rickyteng/archive/2013/09/12/118266.aspx)(這一句好長!)。基本上使用分隔字元的方式,通常是用在文字訊息。因為把給人看的文字訊息中,比較不會使用到所謂控制字元(也就是那些看不到的字元),所以拿來用就比較不會有衝突。然而,若是傳送的是位元檔的內容,每一個 byte 可能的值都是 0~255,可以拿來當做分隔字元的 byte 等於是不存在。因此而這次要討論的是使用訊息長度為主的協定,通常就用來傳送非文字訊息。

使用訊息長度,意思就是每一次傳送,告訴對方要傳幾個位元組的資料。為了確保傳遞訊息長度的資訊一定送達,傳遞訊息長度的資料不能太長,而且最好是放在一開頭,這樣可以降低發生錯誤的機會。若是一開始訊息長度就失誤了,後面的訊息都會解譯錯誤。

使用訊息長度,最簡單的實作就是每次傳送的開頭 4 個位元組就是訊息長度。這次我們就來實作這種最簡易協定。

首先先改一下之前的範例,把 StreamReader / StreamWriter 這兩個方便的類別拿掉。

我們回歸到 NamedPipeServer / NamedPipeClient 原先提供的 Read / Write 方法。

Write 方法的改寫非常直覺,先把資料的 byte 陣列準備好,計算出所需長度。把長度轉換成 4 個位元組的表示,先寫出,再接著再將資料陣列寫出。

Sub SubHandleWrite(ByVal content As Byte())
    Dim Buffer As Byte()
    Dim BufferSize As Byte()
    ReDim Buffer(content.Length - 1)
    Array.Copy(content, Buffer, content.Length)
    BufferSize = BitConverter.GetBytes(Buffer.Length)
    PipeServerOut.Write(BufferSize, 0, 4)
    PipeServerOut.Write(Buffer, 0, Buffer.Length)
    TextBox3.Text = ""
    PipeServerOut.Flush()
End Sub

Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
    Try
        Dim ContentByte As Byte()
        ContentByte = System.Text.UTF8Encoding.UTF8.GetBytes(TextBox3.Text)
        SubHandleWrite(ContentByte)
    Catch ex As Exception
        CleanPipeServer()
        CleanThread()
        UpdateConnectionState(ConnectionState.Disconnected)
    End Try
End Sub

看一下 Read 方法的介面定義:

Public Overrides Function Read(ByVal buffer() As Byte, ByVal offset As Integer, ByVal count As Integer) As Integer

Read 方法回傳的值是讀取到的實際長度,傳入的參數則有預計要讀取的長度,用來放資料的陣列(buffer)。

對於接收方,讀到 byte 陣列之後,會要先判斷是資料或是長度資訊。雖然我們知道所有的訊息前 4 個位元組是長度資訊,但是每次 Read 方法都要先決定 buffer 的長度才能呼叫 Read。想想有點雞生蛋、雞生雞的問題。而大多數的人採用兩種策略來應付:

  1. 永遠先讀 4 個位元組,得知長度後,再準備足夠的 buffer,讀取訊息內容。若訊息內容較長,可以分次讀取。
  2. 永遠讀一定長度的位元組,再從最前面 4 個位元組算出長度,再算出需要讀取的次數,分次讀取。

Read 方法是阻塞式的跟之前的 ReadLine 一樣,行為就是讀到東西時,會馬上回傳,若讀不到東西時,會 Block 住,也就是卡住。Read 方法會回傳的另一種情況是 pipe 斷開。所以,此時要判斷是否為讀到東西而回傳,就是看 Read 方法的回傳值是否為 0。若是 Read 方法的回傳值為 0 就是 pipe 斷開;若是 Read 方法的回傳值不為 0,就是有讀到東西。

上述兩種策略說來都是一句,但是實際狀況可不是這麼簡單。問題就在讀取到的實際長度!最簡單的例子就是,你讀到的只有 2 個 byte 怎辦?來吧,讓我們用第一個策略來實作,捲起袖子見招拆招吧。

While True
    ' Read Length Step
    ReadCount = PipeServerIn.Read(ReadBuffer, 0, 4)
    Select Case ReadCount
        Case 0
            Exit While
        Case Is < 4
            ' Need Handle
        Case 4
            MsgLength = BitConverter.ToInt32(ReadBuffer, 0)
            While True
                ' Read Message Body
            End While
        Case Else
            ' God!
    End Select
End While

先讀取 4 個 byte,然後看看讀取到的數量,如果是 0,那就是斷掉了。如果小於 4,那就要另外處理(等下說明)。如果等於 4,就轉成訊息長度,然後進入接收訊息本體迴圈。如果都不是,那就看到神了!

讀到的數量小於 4,那就要等下一次讀取,所以要把這次讀到的保留下來吧!所以改寫成下面的樣子:

While True
    ' Read Length Step
    ReadCount = PipeServerIn.Read(ReadBuffer, 0, 4)
    Select Case ReadCount
        Case 0
            Exit While
        Case Is < 4
            Dim TmpByte As Byte()
            ReDim TmpByte(ReadCount - 1)
            Array.Copy(ReadBuffer, TmpByte, ReadCount)
            MsgLengthTmp.AddRange(TmpByte)
        Case 4
            MsgLength = BitConverter.ToInt32(ReadBuffer, 0)
            While True
                ' Read Message Body
            End While
        Case Else
            ' God!
    End Select
End While

眼尖的你有發現問題嗎?

當把這次讀到的保留下來後,讓程式做第二次讀取時,還是要讀 4 個位元組嗎?當然不是!所以就連在讀取訊息長度的階段 Read 方法,不是每次都讀 4 個位元組可以解決的。讓我們來修這個問題吧。所以就要改成下面的樣子:

Dim ReadBuffer As Byte()
ReDim ReadBuffer(3)
Dim MsgLengthTmp As List(Of Byte) = New List(Of Byte)
Dim TmpByte As Byte()

While True
    ' Read Length Step
    ReadCount = PipeServerIn.Read(ReadBuffer, 0, 4 - MsgLengthTmp.Count)
    If ReadCount = 0 Then
        Exit While
    End If
    ReDim TmpByte(ReadCount - 1)
    Array.Copy(ReadBuffer, TmpByte, ReadCount)
    MsgLengthTmp.AddRange(TmpByte)

    Select Case MsgLengthTmp.Count
        Case Is < 4
            ' Pass
        Case 4
            MsgLength = BitConverter.ToInt32(MsgLengthTmp.ToArray(), 0)
            MsgLengthTmp.Clear()
            While True
                ' Read Message Body
            End While
    End Select
End While

接下來要處理接受訊息本體,其考量與讀取訊息長度階段差不多。BUT,要多考量一個 buffer 長度的考量。因為訊息本體最長長度目前受到我們 4 個位元組的限制(Int32),只能有 256^4 / 2 = 2147483648 這麼長。一次讀取到的數量不一定是全部。所以分段接收還是要考慮進來。所以經過接收的部份如下:

MsgLength = BitConverter.ToInt32(MsgLengthTmp.ToArray(), 0)
MsgLengthTmp.Clear()
Dim ReadMsgBuffer As Byte()
Dim ReadMsgBufferSize As Integer = MsgLength
ReDim ReadMsgBuffer(ReadMsgBufferSize - 1)
Dim Offset As Integer = 0
While True
    ReadCount = PipeServerIn.Read(ReadMsgBuffer, Offset, MsgLength - Offset)
    UpdateTextBox2State(String.Format("Read {0} bytes", ReadCount))
    UpdateTextBox2State(BitConverter.ToString(ReadMsgBuffer, Offset, ReadCount))
    Offset += ReadCount
    If Offset = MsgLength Then
        UpdateTextBox2State("Recv a whole Msg")
        UpdateTextBox2State(System.Text.UTF8Encoding.UTF8.GetString(ReadMsgBuffer))
        Exit While
    End If
End While

目前這個程式是可以傳送文字,接收到之後更新在畫面上。

若是要傳送檔案,要怎麼修改。就留給大家吧。

image image

沒有留言:

張貼留言