[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 SubPrivate 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。想想有點雞生蛋、雞生雞的問題。而大多數的人採用兩種策略來應付:
- 永遠先讀 4 個位元組,得知長度後,再準備足夠的 buffer,讀取訊息內容。若訊息內容較長,可以分次讀取。
- 永遠讀一定長度的位元組,再從最前面 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
目前這個程式是可以傳送文字,接收到之後更新在畫面上。
若是要傳送檔案,要怎麼修改。就留給大家吧。