寫了個類似Proxy功能的小程式,接受遠端過來的連線,從NetworkStream讀取指令,執行作業後將結果透過NetworkStream傳回去,達到Proxy的效果。

我用個簡化版範例示意,為了方便直接使用telnet測試,程式會接收NetworkStream傳來的內容解讀為文字,並以換行符號\x0a為憑切割字串(程式範例中未用StreamReader是因為原本傳遞格式為byte[],非文字內容),當文字內容為exit時則結束作業關閉連線,否則將文字內容轉為大寫後再傳回去。

using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Net;
using System.Threading;
using System.IO;
using System;
 
namespace TCPServer
{
    class Program
    {
        static void Main(string[] args)
        {
            TcpListener listener = new TcpListener(
                    IPAddress.Parse("127.0.0.1"), 1234);
            listener.Start();
            while (true)
            {
                TcpClient clnt = listener.AcceptTcpClient();
                Console.WriteLine("Connection from {0}...",
                    ((IPEndPoint)clnt.Client.RemoteEndPoint).Address.ToString());
 
                Thread thd = new Thread(() =>
                {
                    NetworkStream stm = clnt.GetStream();
                    MemoryStream data = new MemoryStream();
                    Encoding big5 = Encoding.GetEncoding("big5");
 
                    while (clnt.Connected)
                    {
                        if (clnt.Available > 0)
                        {
                            byte[] buff = new byte[clnt.Available];
                            stm.Read(buff, 0, buff.Length);
                            data.Write(buff, 0, buff.Length);
                            //偵測是否輸入換行符號
                            if (buff.Last() == '\x0a')
                            {
                                //接入指令並轉為大寫
                                string cmd = big5.GetString(data.ToArray()).ToUpper();
                                //如指令為EXIT則結束
                                if (cmd.StartsWith("EXIT"))
                                    clnt.Client.Close();
                                else
                                {
                                    //傳回轉大寫的輸入內容
                                    byte[] resp = big5.GetBytes(cmd);
                                    stm.Write(resp, 0, resp.Length);
                                    //清空接收內容暫存區
                                    data.SetLength(0);
                                }
                            }
                        }
                        Thread.Sleep(20);
                    }
                    stm.Close();
                    Console.WriteLine("Connection Closed!");
                });
                thd.Start();
            }
        }
    }
}

執行程式後,開個Command Prompt,telnet localhost 1234就可進行測試。一切如預期,輸入文字按下Enter,文字內容就會變成大寫再傳回來;輸入exit按Enter,連線就可結束。

但這段程式有個問題,雖然我們用了TcpClient.Connected檢查連線狀態,但若在測試時直接關閉Command Prompt或結束Telent程式,程式並不會偵測到已經斷線的事實! 依照MSDN文件對TcpClient.Connected的說明:

Connected 屬性會取得上次 I/O 作業的 Client 通訊端連接狀態。當它傳回 false 時,即表示 Client 通訊端不是從未連接過,就是不再連接了。

因為 Connected 屬性只反映最近一次作業的連接狀態,所以您應嘗試傳送或接收訊息,以判斷目前的狀態。訊息傳送失敗之後,這個屬性就不再傳回 true。請注意,這種行為是設計上的預期行為。您可能無法很穩定地測試連接的狀態,原因是有可能在測試和收發 (訊息) 之間就失去該連接。您的程式碼應假設該通訊端是連接的,然後再小心處理傳輸失敗的情況。

原來Connected屬性反映的是前一次傳送接收資料的狀態,若沒有再從NetworkStream讀寫資料,它的狀態將一直保持不變。要解決這個問題,就必須執行NetworkStream.Read()或NetworkStream.Write()實測一下連線狀態,而我不想多傳資料到Client端,因為Client端必須加入邏輯忽視測試連線性質的多餘傳輸;若要由NetworkStream()試讀取一個byte,又會打亂原本TcpClient.Available有資料才讀取的單純邏輯。參考網路上的範例後,最後決定仿效StreamReader.Peek()的做法,只試讀資料,不要真的將資料取出,就不會影響原來的程式流程。Socket類別提供實踐Peek()的方法,Socket.Poll()Socket.Receive(), SocketFlags.Peek,因此只需稍加修改程式:

                    bool closed = false;
                    byte[] testByte = new byte[1];
 
                    while (clnt.Connected && !closed)
                    {
                        if (clnt.Available > 0)
                        {
                            byte[] buff = new byte[clnt.Available];
                            stm.Read(buff, 0, buff.Length);
                            data.Write(buff, 0, buff.Length);
                            //偵測是否輸入換行符號
                            if (buff.Last() == '\x0a')
                            {
                                //接入指令並轉為大寫
                                string cmd = big5.GetString(data.ToArray()).ToUpper();
                                //如指令為EXIT則結束
                                if (cmd.StartsWith("EXIT"))
                                    clnt.Client.Close();
                                else
                                {
                                    //傳回轉大寫的輸入內容
                                    byte[] resp = big5.GetBytes(cmd);
                                    stm.Write(resp, 0, resp.Length);
                                    //清空接收內容暫存區
                                    data.SetLength(0);
                                }
                            }
                        }
                        try
                        {
                            //使用Peek測試連線是否仍存在
                                 if (clnt.Connected 
                                && clnt.Client.Poll(0, SelectMode.SelectRead))
                                closed = 
                            clnt.Client.Receive(testByte, SocketFlags.Peek) == 0;
                        }
                        catch (SocketException se)
                        {
                            closed = true;
                        }
                        Thread.Sleep(20);
                    }
                    stm.Close();
                    Console.WriteLine("Connection Closed!");

如此,程式就能偵測到連線已斷,主動結束程序囉!


Comments

# by 小小偉

大大,看了你的文章很實用,不過小弟現在遇到一個問題就是,如果我的 Client 端是在 Recv 主伺服器端,那這樣我就不能用 Read 模式和 Peek 方法了,那小弟該怎麼辦?真的就是嘗試 Send 出 byte 的來測試是不是連線中斷了嗎?

# by Jeffrey

to 小小偉,有點不解,為何Client與Recv在同一台無法使用Peek? (文章裡的測試 telnet localhost 就在本機完成的)

# by che

板主你好, 我的問題跟小小偉很像, 同一個方案內 各有一個 TCPServer / TCPClient 。 在 TCPServer 專案內可以 檢測 Clinet 是否斷線 但 在 TCPClinet 專案內用同樣流程,檢測 Server端... 會整個卡住

# by kree

我照大大的方式寫 但斷線還是一樣偵測不太到(有時候久一點會成功) 我發現是這行出問題clnt.Client.Poll(0, SelectMode.SelectRead) 一直都是呈現false的狀態 請問這是哪裡出問題

Post a comment