1.1 基本概念
.NET中的System.Net.Socktes命名空間提供了大量的對網絡編程的支持類,這些類對Socket編程提供了良好的封裝和支持,涵蓋了TCP、UDP等連接和無連接的通信。應用程序可以通過 TCPClient、TCPListener 和 UDPClient 類使用傳輸控制協議 (TCP) 和用戶數據文報協議 (UDP) 服務。
這些協議類建立在 System.Net.Sockets.Socket 類的基礎之上,負責數據傳送的細節。TCPListener、TCPClient、UDPClient類在底層使用 Socket 類的同步方法提供對網絡服務的簡單直接的訪問,沒有維護狀態信息的系統開銷,使用他們可以不需要了解協議特定的套接字的設置細節。
另一方面,上述包裝類都是封裝Socket的同步處理過程,如果要使用異步 Socket 方法,可以使用 NetworkStream 類提供的異步方法。
這三個協議類的主要功能描述如下:
TCPListener | 從TCP的客戶端監聽連接。 1、TcpListener 類提供一些簡單方法,用於在阻塞同步模式下偵聽和接受傳入連接請求。可使用 TcpClient 或 Socket 來連接 TcpListener。可使用 IPEndPoint、本地 IP 地址及端口號或者僅使用端口號,來創建 TcpListener。可以將本地 IP 地址指定爲 Any,將本地端口號指定爲 0(如果希望基礎服務提供程序爲您分配這些值)。如果您選擇這樣做,可在連接套接字後使用 LocalEndpoint 屬性來標識已指定的信息。 2、Start 方法用來開始偵聽傳入的連接請求。Start 將對傳入連接進行排隊,直至您調用 Stop 方法或它已經完成 MaxConnections 排隊爲止。可使用 AcceptSocket 或 AcceptTcpClient 從傳入連接請求隊列提取連接。這兩種方法將阻塞線程。如果要避免阻塞,可首先使用 Pending 方法來確定隊列中是否有可用的連接請求。 3、調用 Stop 方法來關閉 TcpListener。 |
TCPClient | 爲TCP網絡服務提供客戶端連接。 1、TcpClient 類提供了一些簡單的方法,用於在同步阻塞模式下通過網絡來連接、發送和接收流數據。 2、爲使 TcpClient 連接並交換數據,使用 TCP ProtocolType 創建的 TcpListener 或 Socket 必須偵聽是否有傳入的連接請求。可以使用下面兩種方法之一連接到該偵聽器:
3、要發送和接收數據,請使用 GetStream 方法來獲取一個 NetworkStream。調用 NetworkStream 的 Write 和 Read 方法與遠程主機之間發送和接收數據。使用 Close 方法釋放與 TcpClient 關聯的所有資源。 |
UDPClient | 提供用戶數據包(UDP)網絡服務。 1、UdpClient 類提供了一些簡單的方法,用於在阻塞同步模式下發送和接收無連接 UDP 數據報。因爲 UDP 是無連接傳輸協議,所以不需要在發送和接收數據前建立遠程主機連接。但您可以選擇使用下面兩種方法之一來建立默認遠程主機:
2、可以使用在 UdpClient 中提供的任何一種發送方法將數據發送到遠程設備。使用 Receive 方法可以從遠程主機接收數據。 3、UdpClient 方法還允許發送和接收多路廣播數據報。使用 JoinMulticastGroup 方法可以將 UdpClient 預訂給多路廣播組。使用 DropMulticastGroup 方法可以從多路廣播組中取消對 UdpClient 的預訂。 |
此外,TcpClient 和 TcpListener類創建在Socket之上,在Tcp服務方面提供了更高層次的抽象,體現在網絡數據的發送和接受方面,是TcpClient使用標準的Stream流處理技術——使用 NetworkStream 類表示網絡——使用 TcpClient的GetStream 方法返回網絡流,然後調用該流的 Read 和 Write 方法實現網絡數據的讀取和寫入(接收和發送)。值得說明的是:NetworkStream 不擁有協議類的基礎套接字,因此關閉它並不影響套接字。
採用流技術,使得網絡數據的讀寫數據更加方便直觀,同時,.Net框架負責提供更豐富的結構來處理流,貫穿於整個.Net框架中的流具有更廣泛的兼容性,構建在更一般化的流操作上的通用方法使我們不再需要困惑於文件的實際內容(HTML、XML 或其他任何內容),應用程序都將使用一致的方法(Stream.Write、Stream.Read) 發送和接收數據。另外,流在數據從 Internet 下載的過程中提供對數據的即時訪問,可以在部分數據到達時立即開始處理,而不需要等待應用程序下載完整個數據集。.Net中通過NetworkStream類實現了這些處理技術。
NetworkStream 類包含在.Net框架的System.Net.Sockets 命名空間裏,該類專門提供用於網絡訪問的基礎數據流。NetworkStream 實現通過網絡套接字發送和接收數據的標準.Net 框架流機制。NetworkStream 支持對網絡數據流的同步和異步訪問。NetworkStream 從 Stream 繼承,後者提供了一組豐富的用於方便網絡通訊的方法和屬性。
同其它繼承自抽象基類Stream的所有流一樣,NetworkStream網絡流也可以被視爲一個數據通道,架設在數據來源端(客戶Client)和接收端(服務Server)之間,而後的數據讀取及寫入均針對這個通道來進行。
.Net框架中,NetworkStream流支持兩方面的操作:
1、 寫入流。寫入是從數據結構到流的數據傳輸。
2、讀取流。讀取是從流到數據結構(如字節數組)的數據傳輸。
與普通流Stream不同的是,網絡流沒有當前位置的統一概念,因此不支持查找和對數據流的隨機訪問。相應屬性CanSeek 始終返回 false,而 Seek 和 Position 方法也將引發 NotSupportedException。
1.2 應用示例
1、使用TcpListener建立TCP網絡偵聽
使用TcpListener建立網絡偵聽,實際上是很簡單的過程,示例如下:
//在本機的1300端口建立偵聽 TcpListener server=null; try { // Set the TcpListener on port 13000. Int32 port = 13000; IPAddress localAddr = IPAddress.Parse("127.0.0.1"); server = new TcpListener(localAddr, port);
// Start listening for client requests. server.Start(); //省去了對數據接收、發送的處理代碼 …… } catch(SocketException e) { Console.WriteLine(e.Message); } finally { server.Stop(); } |
2、使用TcpClient建立和服務器的連接
方法一: String server = “192.168.0.18”; //服務器地址 Int32 port = 13000; //端口號 TcpClient client = new TcpClient(server, port); //構造客戶端並嘗試連接到服務器 方法二: TcpClient tcpClient = new TcpClient (); tcpClient.Connect ("www.contoso.com", 11002); |
3、讀寫數據
// Buffer for reading data Byte[] bytes = new Byte[256]; String data = null;
//這裏將阻塞線程,直到有客戶端連接建立起來 TcpClient client = server.AcceptTcpClient();
// Get a stream object for reading and writing NetworkStream stream = client.GetStream();
int i; // Loop to receive all the data sent by the client. while((i = stream.Read(bytes, 0, bytes.Length))!=0) { // Translate data bytes to a ASCII string. data = System.Text.Encoding.ASCII.GetString(bytes, 0, i); Console.WriteLine("Received: {0}", data);
// Process the data sent by the client. data = data.ToUpper();
byte[] msg = System.Text.Encoding.ASCII.GetBytes(data);
// Send back a response. stream.Write(msg, 0, msg.Length); Console.WriteLine("Sent: {0}", data); }
// Shutdown and end connection client.Close(); |
1.3 應用感觸
1、ReadToEnd阻塞線程
在我們最開始的代碼中,爲了省事兒,在獲得了NetworkStream對象後,又使用他構造了一個StreamReader對象,然後使用這個對象的ReadToEnd方法,來一次讀完數據,代碼如下:
/*建立起的連接*/ _StateDataTcpClient = _StateDataTcpListener.AcceptTcpClient();
/*讀取發送到此監聽端口上的數據*/ StreamReader streamReader = new StreamReader(_StateDataTcpClient.GetStream(), System.Text.Encoding.Default); string str_result = streamReader.ReadToEnd(); |
這段代碼,對讀取數據倒是非常簡潔,而且程序在剛開始運行的時候一切正常,然而,當系統開發完畢,進行系統測試時,在網絡上添加了網絡視頻服務,它採用UDP廣播的形勢在局域網內發送視頻數據,這樣以來,我們的系統問題就來了,症狀如下:系統啓動後,運行正常,可以正常接收數據,但是在接收到數十條不等的數據後,就再也不接收數據了,但系統其它部分可正常工作。
由於對網絡編程的經驗欠缺,解決這個問題就花費了很長的時間,幾經調試和周折,才最終定位了問題之所在。原來StreamReader的ReadToEnd方法是要阻塞線程的,MSDN給的解釋是“ReadToEnd 假定流在到達末尾時會知道已到達末尾。對於交互式協議(服務器僅當被請求時才發送數據而且不關閉連接),ReadToEnd 可能被無限期阻塞,應避免出現這種情況。”對於我們的系統來說,發送數據的客戶端是集成的其它系統,至於其數據發送方式,無法準確掌握,但事實是,在這個函數這裏,的確阻塞了線程。
在掌握了上述情況後,我們調整了數據接收部分的代碼,採用了NetworkStream的Read方法,這樣以來,問題就得以解決,經測試,數據接收部分非常正常了,也不再收網絡上的UDP數據報的影響了。目前,這個系統已經連續運行了15天,沒有出現任何問題。
2、數據要循環讀取
在使用NetworkStream讀取數據的時候,我採取了大緩衝區,一次讀取的方式。也就是說,明知道數據量很小,我構建了一個明顯大得多的數據緩衝區,然後使用Read方法讀取數據,但是從試驗結果來看,一次Read是不能讀取完所有數據的。
參考MSDN,解釋說:該方法將數據讀入 buffer 參數並返回成功讀取的字節數。如果沒有可以讀取的數據,則 Read 方法返回 0。Read 操作將讀取儘可能多的可用數據,直至達到由 size 參數指定的字節數爲止。如果遠程主機關閉了連接並且已接收到所有可用數據,Read 方法將立即完成並返回零字節。
這裏說的是,該方法一次讀取儘可能多的數據,然而這儘可能多,到底是多少了,我查了網絡,也沒有獲得深入的解釋。最後不得不使用MSDN上的示例代碼,採用一個循環,來讀取數據,直到數據讀取爲0才停止,如此以來,數據讀取部分才完全正常。
1.4 完整示例
下面,是一個使用TcpListener、TcpClient、NetworkStream來讀取數據的完整示例。
/*獲得本機地址、端口,建立偵聽*/ IPAddress localIP = GetLocalIPAddress(); _AlarmDataTcpListener = new TcpListener(localIP, _AlarmDataPort); _AlarmDataTcpListener.Start(int.MaxValue);
//數據接收緩衝區 byte[] buffer = new byte[1024];
//循環偵聽並處理數據 while (_IsWorking) { //基礎網絡流 NetworkStream ns = null;
try { //等待客戶端連接 _AlarmDataTcpClient = _AlarmDataTcpListener.AcceptTcpClient();
//已經建立起與客戶端的連接,準備接受數據 ns = _AlarmDataTcpClient.GetStream(); ns.ReadTimeout = 3000;
StringBuilder strBuilder = new StringBuilder();
int readLength = 0; //循環接收數據,直到超時 do { try { readLength = ns.Read(buffer, 0, buffer.Length); if (readLength > 0) { strBuilder.AppendFormat("{0}", Encoding.Default.GetString(buffer, 0, readLength)); ns.ReadTimeout = 1000; } } catch (IOException e) { Console.WriteLine(e.Message); Console.WriteLine(e.StackTrace);
readLength = 0; } } while (readLength > 0);
string str_result = strBuilder.ToString();
//清理使用的資源 ns.Close(); ns = null; _AlarmDataTcpClient.Close(); _AlarmDataTcpClient = null;
//顯示接收到的數據 Console.WriteLine(str_result);
//使用線程池,啓動一個單獨的線程來處理此次接收到數據 if (str_result != string.Empty) { ThreadPool.QueueUserWorkItem(new WaitCallback(ProcessReceivedAlarmData), str_result); } }//end try catch (Exception exp) { if (ns != null) { ns.Close(); ns = null; }
if (_AlarmDataTcpClient != null) { _AlarmDataTcpClient.Close(); _AlarmDataTcpClient = null; }
Console.WriteLine(exp.Message); Console.WriteLine(exp.StackTrace); }//end catch }//end while (_IsWorking) |