2013年10月1日 星期二

[vb.net] 變數初始值的指定

因為在 C# 用習慣了,換到 vb.net 有點卡卡的,原先用 vb6 又沒有這種語法。

基本型別是沒有什麼問題。

常常會忘記的就是物件的初始值指定、集合物件(collection, dictionary) 的內容值指定這兩件事。

所以寫下來備忘。

對於物件的初始值指定,是使用 With 關鍵字。在大括號裡給予初始值。

Class Apple
    Public Place As String
    Public weight As Integer
End Class

Dim a As Apple = New Apple With {.Place = "Taiwan", .weight = 1}

對於集合物件,則是使用 From 關鍵字。在大括號裡給予初始值。這東西要 VisualStudio 2010 之後才有支援(又多了一個使用 C# 的理由)。太長可以換行。

Dim PostLabel As Dictionary(Of String, String) = New Dictionary(Of String, String) From
    {
        {"Customer", "Forest"},
        {"Address", "Cali"},
        {"Code", "302"}
    }

Dim Box As Collection = New Collection From {"Apple", 2, {"Made", "HC, Taiwan"}, PostLabel, a}

不過,寫到這,我有的疑問,key / value pair 是用大括號加逗點,list 也是。

一下子,上面的 collection,不曉得 {"Made", "HC, Taiwan"} 這一項是怎麼解釋?

寫了個小程式測試一下

For Each obj In Box
    Console.WriteLine("Type:{0}", obj.GetType().Name)
Next

得到的結果是

Type:String
Type:Int32
Type:String
Type:Dictionary`2
Type:Apple

嗯,得到了一個 String 的型別?!

如果把該項換成 {"Made", "HC, Taiwan", "XX"},則會得到「型別初始設定式發生例外狀況」的錯誤。所以要記得這個特性才行。自己不要誤以為是正確的。

若是想要得到一個 string array,多出一組大括號,像是這樣

Dim Box As Collection = New Collection From {"Apple", 2, {{"Made", "HC Taiwan"}}, PostLabel, a}

得到的結果是

Type:String
Type:Int32
Type:String[]
Type:Dictionary`2
Type:Apple

但我想試著弄出 dictionary 來,試寫成

Dim Box As Collection = New Collection From {"Apple", 2, {{"Made", "HC Taiwan"}, {"Make", "TP Taiwan"}}, PostLabel, a}

得到錯誤

型別 '1-維陣列屬於 String' 的值無法轉換成 'String'。

所以目前還沒辦法隨心所欲呢。

[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

2013年9月23日 星期一

[named pipe] 訊息傳送的協定 - 使用分隔字元

在之前的例子中,在訊息傳送使用的是 StreamWriter 類別,訊息接收使用的是 StreamReader 類別。
接下來就是要討論幾種訊息傳送接收的方式。

在之前的例子中,使用了 WriteLine 方法傳送訊息,及 ReadLine 方法接收訊息。
這代表傳送端及接收端都「同意」使用「換行」這個字元當做一個訊息的結束。

在早期使用無線電通話機時,兩邊的人都必須要在說完話之後,加一個「over」,
讓對方知道自方說完一句話。使用 WriteLine、ReadLine 也是一樣的道理。

然後,這種方式,會有什麼問題呢?

首先,我們來看看在訊息中有換行字元會發生什麼事?

為了要看清楚接下來的範例,要改一下程式。
在 Server 端及 Client 端,接收到訊息後會將訊息更新到 TextBox2,負責處理的函式如下:

Sub UpdateTextBox2State(ByVal desc As String)
    If Me.InvokeRequired Then
        Me.Invoke(deleUpdateTextBox2State, New Object() {desc})
    Else
        TextBox2.Text += DateTime.Now.ToString() & desc & vbNewLine
    End If
End Sub

原本是「TextBox2.Text += desc & vbNewLine」,在 desc 前加上時間綽戳記。
如此每收到一個訊息,就會連同時間戳記一起寫下,就可觀察到不同次收到的訊息為何。

接下來,在 server 端的 TextBox2 裡,填入以下文字:

named pipe 測試1
分隔符號在訊息中

image

按下 Send 鈕之後,Client 端的畫面為:

image

原來預期一次傳送的訊息,被接收端認為是兩次訊息。

訊息斷裂後的兩次訊息,對於人類來說是還好,但對於程式來說,一定會造成無法辨識的結果。

所以收送的兩方一定要嚴守通訊的要求,才不會有意外發生。

但是,有些情況下,非得要傳送這種訊息中有換行字元的,例如一首詩。
那麼最常使用的方式,就是把訊息中的換行字元替換成其他的字元。
換成其他字元的方法,可以解決換行字元的問題,但會衍生出「若訊息中,有『其他字元』該怎麼辦?」的問題。畢竟很難保證,訊息中不會有你用來取代換行字元的字元。於是實務上會看到許多工程師,會用兩個以上的字元來替代訊息中的換行字元後傳送出去,在接收端再換回換行字元。
這樣就會把問題避開了嗎?不行!不管你用的替代字元或字串有多複雜,一個最簡單的例子就會突破了!那就是,若有人在訊息中問你,你的替代字串是什麼?你回應的時候,不就又被抓來當換行字元了嗎?不過,這種情況真的很少,只有工程師自己會玩這種把戲啦!一般使用者不會想來搞死工程師的。

還有另外一種方法,是把整個訊息編碼(encode),例如 base64,編碼完沒有換行字元,就可以用來傳送,接收端接到再解碼(decode)。兩種方式端看效率以及應用情況來選擇。有時,兩端的程式並非同一個人或同一公司撰寫,因此實務才會有許多解法出現,但不脫字元取代及訊息編碼(encode)這兩大類。

[python] unittest in IDLE

最近開始抄別人的 code 來學習。在 IDLE 試著要用 unittest 做測試,結果發生以下問題:

----------------------------------------------------------------------
Ran 1 test in 0.054s

OK

Traceback (most recent call last):
  File "D:\test.py", line 150, in <module>
    unittest.main()
  File "C:\Python27\lib\unittest\main.py", line 95, in __init__
    self.runTests()
  File "C:\Python27\lib\unittest\main.py", line 231, in runTests
    sys.exit(not self.result.wasSuccessful())
SystemExit: False

我找到網路上的解法,真的是可以用。

if __name__ == '__main__':
unittest.main(exit=False)

參考:http://stackoverflow.com/questions/2457068/using-idle-to-run-python-pyunit-unit-tests

2013年9月15日 星期日

[named pipe] .Net 的 named pipe,堪用的第一步。Client 端

經過 Server 端程式的字很多轟炸之後,再來看 Client 端程式會覺得很開心。不但 thread 變少,要考慮的狀況也少很多。復習一下,pipe name 與 pipe direction 的關係。

image

看圖可以比較清楚的知道,在 Client 端,<pipe name>_1 是 Out,屆時要連接的是 StreamWriter,而 <pipe name>_2 是 In,屆時要連接 StreamReader。正好跟 Server 端程式反過來。由此可以注意到,所謂的 In, Out 是對本身程式而言,而不是對 Pipe 的方向。同一個 pipe,對於 pipe 兩端的程式來說,in,out 就是剛好相反。所以為了自己也為了別人好,就不要把 pipe 取名 xxx_in 或 xxx_out。要是這樣取名字一定會害死人的。

接下來介紹與 server 端連接的函式。Client 端使用的是 NamedPipeClientStream 這個物件。因為 windows 的 pipe 是可以跨電腦的,所以 client 端的 pipe 在呼叫建構函式的時候,是要清楚告知是哪台電腦的 pipe。在這裡選擇三個參數的建構函式。第一個是 host name,輸入 "." 是告知使用本機。在實例化後,只要呼叫 Connect 即可與 server 端連接。在這裡一樣在處理 in 這個 pipe 需要用一個 thread 來呼叫 SubHandleRead。

Sub ConnectToServer()
    Dim pipename1 As String = TextBox1.Text & "_1"
    Dim pipename2 As String = TextBox1.Text & "_2"
    PipeClientIn = New NamedPipeClientStream(".", pipename2, PipeDirection.In)
    PipeClientOut = New NamedPipeClientStream(".", pipename1, PipeDirection.Out)

    Try
        UpdateConnectionState(ConnectionState.Waiting)
        PipeClientIn.Connect()
        PipeClientOut.Connect()
        ThreadIn = New Thread(AddressOf SubHandleRead)
        ThreadIn.Start()
        UpdateConnectionState(ConnectionState.Connected)
    Catch ex As Exception
        UpdateConnectionState(ConnectionState.Disconnected)
        PipeClientIn = Nothing
        PipeClientOut = Nothing
    End Try
End Sub

因為在 PipeClientIn 斷開之後,不需要自動重連,因此 SubHandleRead 非常簡單,斷開就離開。

Sub SubHandleRead()
    Dim RStream As StreamReader
    RStream = New StreamReader(PipeClientIn)
    While True
        Dim ReadStringLine As String = RStream.ReadLine
        If ReadStringLine Is Nothing Then
            Exit While
        End If
        UpdateTextBox2State(ReadStringLine)
    End While
    UpdateConnectionState(ConnectionState.Disconnected)
    CleanPipeClient()
End Sub

斷開的情境有兩個。一個是 Client 主動斷開,一個是 Server 主動斷開。

在 Server 主動斷開的情況下,SubHandleRead 裡 RStream.ReadLine 方法會傳回 Nothing,接下來離開迴圈。在更新完 GUI 的資訊後,最好把 PipeClientIn 及 PipeClientOut 都 Close 確保有釋放資源。SubHandleRead 離開後 ThreadIn 也就結束了。

在 Client 主動斷開的情況下,是由 PipeClientIn 執行 Close 方法,接下來 SubHandleRead 裡 RStream.ReadLine 方法會傳回 Nothing,然後離開迴圈。按照道理說,PipeClientIn 及 PipeClientOut 都執行過 Close 了,所以可不用執行 CleanPipeClient()。但因為執行也無妨。同時,離開了 SubHandleRead 後,ThreadIn 也停了,所以可放心不會有問題。

傳送資料的方式很簡單,只要在畫面下方的文字框,輸入文字,按下 Send 鈕就可以了。

image

Server 端程式跟 Client 端程式的寫法都一樣:

Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
    Dim WStream As StreamWriter
    WStream = New StreamWriter(PipeServerOut)
    Try
        WStream.WriteLine(TextBox3.Text)
        TextBox3.Text = ""
        WStream.Flush()
    Catch ex As Exception
        CleanPipeServer()
        CleanThread()
        UpdateConnectionState(ConnectionState.Disconnected)
    End Try
End Sub

到此,基本的 Server 端程式與 Client 端程式的行為都已跟需求一樣。暫時休息一下。

 

參考:

http://msdn.microsoft.com/en-us/library/system.io.pipes.namedpipeserverstream.aspx

[named pipe] .Net 的 named pipe,堪用的第一步。Server 端

首先要先定義 pipe name 與 pipe in 及 out 的關係。

image

Server 與 Client 之間有兩條 pipe,所以會需要兩個名字,然而,為方便使用者輸入的簡化,當使用者輸入了<pipe name> 之後,實際建立的兩條 pipe 的名字是 <pipe name>_1 與 <pipe name>_2。假設使用者輸入的 pipe name 是 test,Server 端會建立 test_1 與 test_2 兩條 pipe 等待連接。

在這兩條 pipe 裡,Server 端以<pipe name>_1 為輸入端,<pipe name>_2 為輸出端。.Net 為我們準備好的 Server 端物件是 NamedPipeServerStream。設定 PipeServer 的方式僅需給予 pipe 的名字及方向。

當使用者輸入 pipe name 之後,按下 Open 鈕,會執行以下 sub:

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
    If TextBox1.Text <> "" Then
        IWantStop = False
        ServerSetup()
    End If
End Sub

Sub ServerSetup()
    Dim pipename1 As String = TextBox1.Text & "_1"
    Dim pipename2 As String = TextBox1.Text & "_2"
    PipeServerIn = New NamedPipeServerStream(pipename1, PipeDirection.In)
    PipeServerOut = New NamedPipeServerStream(pipename2, PipeDirection.Out)
    ThreadWaitConnectionIn = New Thread(AddressOf SubWaitConnectionIn)
    ThreadWaitConnectionOut = New Thread(AddressOf SubWaitConnectionOut)
    ThreadWaitConnectionIn.Start()
    ThreadWaitConnectionOut.Start()
    UpdateConnectionState(ConnectionState.Waiting)
End Sub

ServerSetup 前面 4 行就是在處理組合 pipe name 及設定 PipeServer。原本接下來應該是呼叫 WaitForConnection 等待連接,因為此呼叫會 block,所以另外用 thread 來做呼叫的處理。對於 PipeServerOut 的 WaitForConnection 比較單純,我僅使用一個 sub 來處理,在連接來時,若 PipeServerIn 也連接上時,才更新 GUI 上的狀況說明。

Sub SubWaitConnectionOut()
    PipeServerOut.WaitForConnection()
    If PipeServerIn.IsConnected Then
        UpdateConnectionState(ConnectionState.Connected)
    End If
End Sub

在 PipeServerOut 實例化後,執行以下動作,使用另一個 thread 來等待:

ThreadWaitConnectionOut = New Thread(AddressOf SubWaitConnectionOut)
ThreadWaitConnectionOut.Start()

在接收端 PipeServerIn 這個物件,在被連接後,要開始處理接收輸入的動作。然後,處理接收這件事,也要用一個 thread 來處理。於是 PipeServerIn 的 WaitForConnection 處理 sub 如下:

Sub SubWaitConnectionIn()
    PipeServerIn.WaitForConnection()
    ThreadIn = New Thread(AddressOf SubHandleRead)
    ThreadIn.Start()
    If PipeServerOut.IsConnected Then
        UpdateConnectionState(ConnectionState.Connected)
    End If
End Sub

在處理輸入這個 SubHandleRead,有一半是真的處理輸入,另一半是處理 PipeServer 重設的工作。不小心,又用了專用的 thread 來做事。因為 SubHandleRead 被 ThreadIn 所使用,在做重設定時 ThreadIn 必須先結束才能重設。我是不得已才這麼做才又多一個 thread 來呼叫 ReSetUp 的。

Sub SubHandleRead()
    Dim RStream As StreamReader
    RStream = New StreamReader(PipeServerIn)
    While True
        Dim ReadStringLine As String = RStream.ReadLine
        If ReadStringLine Is Nothing Then
            Exit While
        End If
        UpdateTextBox2State(ReadStringLine)
    End While
    UpdateConnectionState(ConnectionState.Disconnected)
    CleanPipeServer()
    If IWantStop = False Then
        If Not ThreadReSetup Is Nothing Then
            If ThreadReSetup.IsAlive Then
                ThreadReSetup.Abort()
            End If
        End If
        ThreadReSetup = New Thread(AddressOf ReSetup)
        ThreadReSetup.Start()
    End If
    CleanThread()
End Sub

Sub ReSetup()
    Thread.Sleep(500)
    ServerSetup()
End Sub

在處理輸入這一半,我使用了非常非常偷懶的 ReadLine 方法,在 StreamReader 的幫助下,只要讀到換行符號,就會從 ReadLine 方法得到資料,該資料不含換行符號。如果,Client 端的連接斷掉了,ReadLine 得到的是 Nothing。所以,得到 Nothing 的時候,就要離開處理迴圈。假如,Client 斷開的原因是故意的,那就不直接重建 Server。但,Client 斷開的原因是不小心的, IWantStop 會是 False,此時要開始執行重建 Server 的動作,回到等待連接的狀態。

而什麼時候是故意斷開的呢?就是按下 Close 鈕的時候:

Private Sub Button3_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button3.Click
    IWantStop = True
    CleanPipeServer()
    CleanThread()
    UpdateConnectionState(ConnectionState.Disconnected)
End Sub

在清理 Thread 這件事,我用了很簡單的方法,就是用力地 Abort!為了保證每個 thread 一定都會執行 Abort,我用 Try Catch 把每行執行都包起來,就減少了一些 thread 物件是不是有實例化的檢查。

Sub CleanThread()
    Try
        ThreadWaitConnectionIn.Abort()
    Catch ex As Exception

    End Try
    Try
        ThreadWaitConnectionOut.Abort()
    Catch ex As Exception

    End Try
    Try
        ThreadIn.Abort()
    Catch ex As Exception

    End Try
    ThreadWaitConnectionIn = Nothing
    ThreadWaitConnectionOut = Nothing
    ThreadIn = Nothing
End Sub

然而在清理 PipeServerIn、PipeServerOut 這兩個物件時,比較複雜一點點。當有 Client 端連接上時,讓 PipeServerIn 直接呼叫 Close 方法,就可以斷開。雖然我很直覺地試過要先呼叫 Disconnect,結果就是 block 住,反而無法動彈。PipeServerOut 則需要呼叫 Disconnect,它可以保證所有寫出的字都被讀取之後才斷開。

另一個流程是,PipeServerIn、PipeServerOut 沒有連接上,也就是正在等待連接,而 Server 程式想要關掉,此時 WaitForConnection 卻無法取消!因為 WaitForConnection 的無法取消,導致 thread 的資源沒有釋放乾淨。為了要離開 WaitForConnection,網路上有人提出一個解法,就是自己連上馬上斷掉。而我也採用了這個解法。準備了 PipeClientIn、PipeClientOut 連接,然後 Close。

Sub CleanPipeServer()
    Try
        If PipeServerIn.IsConnected Then
            'PipeServerIn.Disconnect() ' no need
            PipeServerIn.Close()
        Else
            PipeServerIn.Close() 'It Needs to close first. Client_Close_First will need this line.
            Dim pipename1 As String = TextBox1.Text & "_1"
            Dim PipeClientOut = New NamedPipeClientStream(".", pipename1, PipeDirection.Out)
            PipeClientOut.Connect(500)
            PipeClientOut.Close()
            PipeClientOut = Nothing
        End If
    Catch ex As Exception

    End Try
    Try
        If PipeServerOut.IsConnected Then
            PipeServerOut.Disconnect()
            PipeServerOut.Close()
        Else
            Dim pipename2 As String = TextBox1.Text & "_2"
            Dim PipeClientIn = New NamedPipeClientStream(".", pipename2, PipeDirection.In)
            PipeClientIn.Connect(500)
            PipeClientIn.Close()
            PipeClientIn = Nothing
        End If
    Catch ex As Exception

    End Try
    PipeServerIn = Nothing
    PipeServerOut = Nothing
End Sub

到這裡,Server 端程式就說明完畢了。

[named pipe] .Net 的 named pipe,堪用的第一步。

NamedPipeServerStream 這個類別從 .Net 3.5 開始提供。使用者可以 io stream 的概念來操作資料的傳送。與它相對應的是 NamedPipeClientStream。由這兩個類別可以完成 named pipe 的範例。

在 msdn 的範例中,NamedPipeServerStream 是首先被啟用,以等待另一端來連接。程式宣告了一個 NamedPipeServerStream,名字叫 testpipe,pipe 的方向是 out。接下來便是等待對方來連接。這時,才是 NamedPipeClientStream 要來連接的時機。在 Client 連接之後,Server 只是寫一行字就準備關掉了。而 Client 也是讀一行字就準備關掉了。這樣的情境(Sceanrio),可以示範,但在實務上是不能用的。目前教育大概也是這樣吧,學校以為把東西教了,學生就可以用來工作了,學生也以為是這樣,公司也以為是這樣。結果,就整件事來說,這只是半套,另外半套得要有人教。但是全部的人都以為學生學了全套,到了公司發現不對,就怪學生或是學校,這情況就是亂象。雖然另外半套簡單到死,公司再不願意教的,就剩補習班要教了。雖然大家嫌補習班,證照班教的太基本、太無聊、太簡單。啊~套一句我最新的名言:「你不會的那一點,就是重點」。我離題了就此打住,希望有教學生的人可以幫幫學生們,不要被大題目嚇傻了,不要被大老闆嚇傻了。台灣的年輕人能力不輸人的。

接下來我這裡嘗試比較一般會使用的情境,會有幾個要求:

  1. Server 端要能等待連線,在 client 斷掉後回到等待下一次連線。
  2. Server 端要能傳送資料,也要能接收資料。Client 也是。
  3. 使用 GUI (也就是 windows form 程式)顯示接收到的訊息,以及輸入要傳送的訊息。

因為以上的要求,程式就變得複雜一點:

  1. 因為 .WaitForConnection 呼叫後會 block 住,等到 client 連接才回來,為避免 GUI 卡主,因此選擇用 thread 來處理。
  2. 同 1 的理由,不論 Server 或 Client 的 ReadLine 也會 block 住,也選擇用 thread 來處理。
  3. 為了第二點,Server 端及 Client 端中間,開兩個 pipe,一個 in,一個 out。

你會發現,不過多了三個要求,程式碼多了一大堆!例如,什麼叫做 client斷掉,怎麼判斷?MSDN 的範例中沒看到。收兩行以上的程式怎麼寫?MSDN 的範例中沒看到。多看別人寫的程式碼,尤其是 open source 的專案,是學寫程式的好範本!這讓我覺得以前的電腦書比較好,會一步步教,很仔細。但現在基礎的書大多跳掉細節只講觀念,反而又無法做出一個完整的東西。奇怪這一篇很常在抱怨哩…。

好,那先介紹 GUI 畫面,下次再看程式碼吧…。

image image

正常的操作情境(Scenario)是:

  1. Server 程式啟動,Label1 位置會是紅字底的文字 Disconnected image 說明程式是 Disconnected 狀態。
  2. 在 Server 畫面的 Open 右方的文字框,輸入 Pipe Name。
  3. 按下 Open 鈕後,Label1 位置會是黃字底的文字 Waiting image
  4. Client 程式啟動,Label1 位置會是紅字底的文字 Disconnected image
  5. 在 Client 畫面的 Open 右方的文字框,輸入 Pipe Name。
  6. 按下 Open 鈕後,兩隻程式的 Label1 位置會是綠字底的文字 Connected image
  7. 接下來就可以在畫面最下方的方塊輸入文字,按下 Send 鈕,另一方就會收到文字了。
  8. 按下 Client 畫面的 Close 鈕,Client 程式會變成 Disconnected 狀態。而 Server 程式會變成 Waiting 狀態。Client 程式關掉。
  9. 按下 Server 畫面的 Close 鈕,Server 程式會變成 Disconnected 狀態。Server 程式關掉。