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 程式關掉。