1. 程式人生 > 實用技巧 >C# Stream篇(七) -- NetworkStream

C# Stream篇(七) -- NetworkStream

NetworkStream

目錄:

1.NetworkStream的作用

和先前的流有所不同,NetworkStream 的特殊性可以在它的名稱空間中得以瞭解(System.Net.Sockets),聰明的你馬上會反應過來:

既然是在網路中傳輸的流,那必然有某種協議或者規則約束它,不錯,這種協議便是Tcp/IP協議,這個是什麼東東?別急,我先讓大家了

解下NetworkStream的作用:如果伺服器和客戶端之間基於TCP連線的,他們之間能夠依靠一個穩定的位元組流進行相互傳輸資訊,這也是

NetworkStream的最關鍵的作用,有了這個神奇的協議,NetWorkStream便能向其他流一樣在網路中(進行點對點的傳輸),這種傳輸的

效率和速度是非常高的(UDP也很快,稍後再介紹)

如果大家對這個概念還不是很清晰的話,別怕,後文中我會更詳細的說明

這裡有5點大家先理解就行

  1. NetworkStream只能用在具有Tcp/IP協議之中,如果用在UDP中編譯不報錯,會報異常
  2. NetworkStream 是面向連線的
  3. 在網路中利用流的形式傳遞資訊
  4. 必須藉助Socket (也稱之為流式socket),或使用一些返回的返回值,例如TcpClient類的GetStream方法
  5. 用法和普通流方法幾乎一模一樣,但具有特殊性

2.簡單介紹下TCP/IP 協議和相關層次

提到協議相信許多初學者或者沒搞過這塊的朋友會一頭霧水,

不過別怕,協議也是人定的,肯定能搞懂:

其實協議可以這麼理解,是人為定製的為某個活動定義的一些列規則和約束,

就好比足球賽上的紅黃牌,這是由世界足聯定製的協議或者規範,一旦不按照這個協議

足球賽肯定會一片混亂

進入正題:

TCP/IP
全稱:Transmission Control Protocol/Internet Protocol (傳輸控制協議/因特網互聯協議,又名網路通訊協議)

這個便是網際網路中的最基本的協議,Tcp/IP 定義了電子裝置如何進入到網際網路,以及資料如何在網路中傳遞。既然有了協議但是空頭支票

還是不行地,就好比足聯定製了這些規則,但是沒有裁判在球場上來實施這些規則一樣,Tcp/IP協議也有它自己的層次結構,關於它的層次

結構,大家看圖就能明白

傳送資料:

大家不用刻板的去理解這個協議,我還是用我們最普通的瀏覽網頁來讓大家理解下,首先開啟瀏覽器輸入一個url,這時候應用層會判斷這個要求是否是http的

,然後http會將請求資訊交給傳輸層來執行,傳輸層主要負責資訊流的格式化並且提供一個可靠地傳輸,這時候,TCP和UDP這兩個協議在這裡起作用了,

TCP協議規定:接收端必須發回確認,並且假如分組丟失,必須重新發送,接著網路層得到了這些需要傳送的資料,(網路中的IP協議非常重要,不僅是IP協議,

還有ARP協議(查詢遠端主機MAC地址)),這時候網路層會命令網路介面層去傳送這些資訊(IP層主要負責的是在節點之間(End to End)的資料包傳送,

這裡的節點是一臺網路裝置,比如計算機,大家便可理解為網路介面層的裝置),最終將請求資料傳送至遠端網站主機後等待遠端主機發送來資訊

接收資料:

好了,遠端網站主機會根據請求資訊(Ip,資料報等等)傳送一些列的網頁資料通過網線或者無線路由,回到網路介面層,然後逐級上報,通過網路層的ip然後通過

傳輸層的一些列格式化,最終通過http返回至瀏覽器顯示網頁了

基於篇幅的關係,還有其他的協議大家可以自行去學習瞭解學習

喜歡足球的朋友的朋友也許會反應過來:這不是2-4-5陣型麼?其實不然,很多協議我還沒畫上去,其實大致含義就是每個層次上的協議(足球隊員有他各自的職責),

這些才能構成計算機與計算機之間的傳輸資訊的橋樑。相信園子裡很多大牛都寫過http 協議,大家也可以去學習下

3.簡單說明下 TCP和UDP的區別

TCP:

1 TCP是面向連線的通訊協議,通過三次握手建立連線

2 TCP提供的是一種可靠的資料流服務,採用“帶重傳的肯定確認”技術來實現傳輸的可靠性

UDP:

1 UDP是面向無連線的通訊協議,UDP資料包括目的埠號和源埠號資訊,由於通訊不需要連線,所以可以實現廣播發送

2 UDP通訊時不需要接收方確認,屬於不可靠的傳輸,可能會出丟包現象,實際應用中要求在程式設計師程式設計驗證

3 由於上述2點的關係,UDP傳輸速度更快,但是安全性比較差,很容易發生未知的錯誤,所以本章的NetworkStream無法使用在UDP的功能上

4.簡單介紹下套接字(Socket)的概念

關於Socket的概念和功能可能可以寫很長一篇博文來介紹,這裡大家把Socket理解Tcp/IP協議的抽象,並且能夠實現Tcp/IP協議棧的工具就行,換句話說,我們可以

利用Socket實現客戶端和服務端雙向通訊,同樣,對於Socket最關鍵的理解還沒到位,很多新人或者不常用的朋友會問:Socket到底功能是什麼?怎麼工作的?

再次舉個例子,女友打電話給我,我可以選擇連線,或者拒絕,如果我接了她的電話,也就是說,我和她通過電話連線(Connect),那電話就是“Socket”,女友和我

都可以是客戶端或服務端,只要點對點就行,我們的聲音通過電話傳遞,但是具體傳輸內容不歸Socket管轄範圍,Socket的直接任務可以歸納為以下幾點:

  1. 建立客戶端或服務端
  2. 服務端或客戶端監聽是否有服務端或客戶端傳來的連線資訊(Listening)
  3. 建立點對點的連線(Connect)
  4. 傳送accept 資訊給對方,表示兩者已經建立連線,並且可以互相傳遞資訊了(Send)
  5. 具體傳送什麼資訊內容不是Socket管轄的範圍,但是必須是Socket進行傳送的動作
  6. 同理可以通過Socket去接受對方發來的資訊,並加以處理

簡單的Socket示例程式碼:

點選這裡

5.簡單介紹下TcpClient,TcpListener,IPEndPoint類的作用

1: TcpClient

此類是微軟基於Tcp封裝類,用於簡化Tcp客戶端的開發,主要通過構造帶入主機地址或者IPEndPonint物件,然後呼叫Connect進行和伺服器點對點的連線,連線成功後通

過GetStream方法返回NetworkStream物件

2: TcpListener

此類也是微軟基於Tcp封裝類,用於監聽服務端或客戶端的連線請求,一旦有連線請求資訊,立刻交給TcpClient的AcceptTcpClient方法捕獲,Start方法用於開始監聽

3: IPEndPonint

處理IP地址和埠的封裝類

4:IPAddress

提供包含計算機在 IP 網路上的地址的工具類

6.使用NetworkStream的注意事項和侷限性

抱歉到目前為止才開始介紹NetworkStream,我相信大家到這裡在回過頭去看第一節的作用時能夠更多的領悟。前五節意在說明下NetworkStream背後那個必須掌握的知識點,

這樣才能在實際程式設計過程中很快上手,畢竟NetworkStream的工作環境和其他流有著很大的差別,

再回到第一節關於NetworkStream的知識點,在使用時有幾點必須注意

首先

1 再次強調NetworkStream是穩定的,面向連線的,所以它只適合TCP協議的環境下工作

所以一旦在UDP環境中,雖然編譯不會報錯,但是會跳出異常

2 我們可以通過NetworkStream簡化Socket開發

3 如果要建立NetworkStream一個新的例項,則必須使用已經連線的Socket

4 NetworkStream 使用後不會自動關閉提供的socket,必須使用NetworkStream建構函式時指定Socket所有權(NetworkStream 的建構函式中設定)。

6 NetworkStream支援非同步讀寫操作

NetworkStream的侷限性

  1. 可惜的是NetworkStream基於安全上的考慮不支援 Posion屬性或Seek方法,尋找或改變流的位置,如果試圖強行使用會報出NotSupport的異常
  2. 支援傳遞資料的種類沒有直接使用Socket來的多

7.NetworkStream的構造

1.NetworkStream (Socket) 為指定的 Socket 建立 NetworkStream 類的新例項

2.NetworkStream (Socket, Boolean ownsSocket) 用指定的 Socket 所屬權為指定的 Socket

ownsSocket表示指示NetworkStream是否擁有該Socket

3.NetworkStream (Socket, FileAccess) 用指定的訪問許可權為指定的 Socket 建立

FileAccess 值的按位組合,這些值指定授予所提供的 Socket 上的 NetworkStream 的訪問型別

4.NetworkStream (Socket, FileAccess, Boolean ownsSocket) 。

對於NetworkStream建構函式的理解相信大家經過前文的解釋也能夠掌握了,但是有幾點

必須強調下

1如果用構造產生NetworkStream的例項,則必須使用連線的Socket

2 如果該NetworkStream擁有對Socket的所有權,則在使用NetworkStream的Close方法時會同時關閉Socket,

否則關閉NetworkStream時不會關閉Socket

3, 能夠建立對指定Socket帶有讀寫許可權的NetworkStream

8.NetworkStream的屬性

1. CanSeek :用於指示流是否支援查詢,它的值始終為 false

2. DataAvailable 指示在要讀取的 NetworkStream 上是否有可用的資料。一般來說通過判斷這個屬性來判斷NetworkStream中是否有資料

3. Length:NetworkStream不支援使用Length屬性,強行使用會發生NotSupportedException異常

4.Position: NetworkStream不支援使用Position屬性,強行使用會發生NotSupportedException異常

9.NetworkStream的方法

同樣,NetworkStream的方法大致重寫或繼承了Stream的方法

但是以下方法必須注意:

1 int Read(byte[] buffer,int offset,int size)

該方法將資料讀入 buffer 引數並返回成功讀取的位元組數。如果沒有可以讀取的資料,則 Read 方法返回 0。Read 操作將讀取儘可能多的可用資料,

直至達到由 size 引數指定的位元組數為止。如果遠端主機關閉了連線並且已接收到所有可用資料,Read 方法將立即完成並返回零位元組。

2 long Seek(long offset, SeekOrigin origin)

將流的當前位置設定為給定值。此方法當前不受支援,總是引發 NotSupportedException。

3 void Write(byte[] buffer, int offset,int size)

Write方法在指定的 offset 處啟動,並將 buffer 內容中的 size 位元組傳送到網路。Write 方法將一直處於阻止狀態(可以用非同步解決),直到傳送了請求

的位元組數或引發 SocketException 為止。如果收到 SocketException,可以使用 SocketException.ErrorCode 屬性獲取特定的錯誤程式碼。

10.NetworkStream的簡單示例

建立一個客戶端向服務端傳輸圖片的小示例

服務端一直監聽客戶端傳來的圖片資訊

   /// <summary>
   /// 服務端監聽客戶端資訊,一旦有傳送過來的資訊,便立即處理
   /// </summary>
    class Program
    {
        //全域性TcpClient
       static TcpClient client;
        //檔案流建立到磁碟上的讀寫流
       static FileStream fs = new FileStream("E:\\abc.jpg", FileMode.Create);
        //buffer
       static int bufferlength = 200;
       static byte[] buffer = new byte[bufferlength];
        //網路流
       static NetworkStream ns;

        static void Main(string[] args)
        {
            ConnectAndListen();
        }

       static void ConnectAndListen() 
        {
           //服務端監聽任何IP 但是埠號是80的連線
            TcpListener listener = new TcpListener(IPAddress.Any,80);
           //監聽物件開始監聽
            listener.Start();
            while(true)
            {
                Console.WriteLine("等待連線");
                //執行緒會掛在這裡,直到客戶端發來連線請求
                client = listener.AcceptTcpClient();
                Console.WriteLine("已經連線");
                //得到從客戶端傳來的網路流
                ns = client.GetStream();
                //如果網路流中有資料
                    if (ns.DataAvailable)
                    {
                        //同步讀取網路流中的byte資訊
                       // do
                      //  {
                      //  ns.Read(buffer, 0, bufferlength);
                      //} while (readLength > 0);

                        //非同步讀取網路流中的byte資訊
                        ns.BeginRead(buffer, 0, bufferlength, ReadAsyncCallBack, null);
                    }
            }
        }

       /// <summary>
       /// 非同步讀取
       /// </summary>
       /// <param name="result"></param>
       static void ReadAsyncCallBack(IAsyncResult result) 
       {
           int readCount;
           //獲得每次非同步讀取數量
           readCount = client.GetStream().EndRead(result);
           //如果全部讀完退出,垃圾回收
           if (readCount < 1) 
           {
               client.Close();
               ns.Dispose();
               fs.Dispose();
               return; 
           }
          //將網路流中的圖片資料片段順序寫入本地
           fs.Write(buffer, 0, 200);
           //再次非同步讀取
           ns.BeginRead(buffer, 0, 200, ReadAsyncCallBack, null);
       }
    }

客戶端先連線上服務端後在傳送圖片,注意如果是雙向通訊的話最好將客戶端和服務端的專案設定為多個啟動項便於除錯

    class Program
    {
       /// <summary>
       /// 客戶端
       /// </summary>
       /// <param name="args"></param>
        static void Main(string[] args)
        {
            SendImageToServer("xxx.jpg");
        }   

        static void SendImageToServer(string imgURl)
        {
            if (!File.Exists(imgURl)) return;
             //建立一個檔案流開啟圖片
            FileStream fs = File.Open(imgURl, FileMode.Open);
            //宣告一個byte陣列接受圖片byte資訊
            byte[] fileBytes = new byte[fs.Length];
            using (fs)
            {
                //將圖片byte資訊讀入byte陣列中
                fs.Read(fileBytes, 0, fileBytes.Length);
                fs.Close();
            }
            //找到伺服器的IP地址
            IPAddress address = IPAddress.Parse("127.0.0.1");
            //建立TcpClient物件實現與伺服器的連線
            TcpClient client = new TcpClient();
            //連線伺服器
            client.Connect(address, 80);
            using (client)
            {
                //連線完伺服器後便在客戶端和服務端之間產生一個流的通道
                NetworkStream ns = client.GetStream();
                using (ns)
                {
                    //通過此通道將圖片資料寫入網路流,傳向伺服器端接收
                   ns.Write(fileBytes, 0, fileBytes.Length);
                }
            }
        }
    }


附件:關於Socket的一個簡單示例

伺服器端建立服務並且迴圈監聽

        const int PORT = 80;
        static Socket clientSocket;
        static Socket client;
        static void Main(string[] args)
        {
          Thread thread=new Thread(new ThreadStart(SetUpBlockServer));
          thread.Start();
        }

        /// <summary>
        /// 伺服器端建立服務並且迴圈監聽
        /// </summary>
        static void SetUpBlockServer() 
        {
            //同樣建立TcpListener 監聽物件監聽客戶端傳來的資訊
            TcpListener lis = new TcpListener(IPAddress.Any, PORT);
            Console.WriteLine("正在監聽任何埠號為80的任意IP的連線");
            //啟動監聽
            lis.Start();
            while (true) 
            {
                //程序會掛起,知道客戶端的socket傳送連線請求
                clientSocket = lis.AcceptSocket();
                Console.WriteLine("{0}時刻接收到客戶端的連線請求",DateTime.Now.ToString("G"));
                //連線成功後傳送給客戶端資訊
                string testMessage = "連線成功";
                clientSocket.Send(Encoding.Default.GetBytes(testMessage));
            }
        }

客戶端連線服務端的請求,和迴圈監聽服務端傳來的資訊

 class Program
    {
        //埠
        const int PORT = 80;
        
        static void Main(string[] args)
        {
            Connect("127.0.0.1");
         
        }
        /// <summary>
        /// 建立與伺服器端的非同步連線
        /// </summary>
        /// <param name="server"></param>
        static void Connect(string server) 
        {
            //建立一個socket用來和服務的Socket進行通訊
            Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            //獲得伺服器IP地址
            IPAddress ipAddress = IPAddress.Parse(server);
            //獲得伺服器埠
            IPEndPoint point = new IPEndPoint(ipAddress, PORT);
            //開始非同步連線,注意將socket放入非同步方法的引數中,提供給回撥方法使用
            socket.BeginConnect(point, new AsyncCallback(ConnectCallBack), socket);
            Thread.Sleep(10000000);
        }

        /// <summary>
        ///非同步連線後的callback事件
        /// </summary>
        /// <param name="result"></param>
        static void ConnectCallBack(IAsyncResult result) 
        {
            try
            {
                //建立一個接受資訊的byte陣列
                byte[] receiveBuffer = new byte[4098];
                //從回撥引數中獲取上面Conntect方法中的socket物件
                Socket socket = result.AsyncState as Socket;
                //判斷是否和服務端的socket建立連線
                if (socket.Connected)
                {
                    //開始 非同步接收服務端傳來的資訊,同樣將socket傳入回撥方法的引數中
                    socket.BeginReceive(receiveBuffer, 0, receiveBuffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallBack), socket);
                }
                else { ConnectCallBack(result); }
            }
            catch 
            {
                Console.WriteLine("連接出錯");
            }
        }

        /// <summary>
        ///  一旦伺服器傳送資訊,則會觸發回撥方法
        /// </summary>
        /// <param name="result"></param>
        static void ReceiveCallBack(IAsyncResult result) 
        {
            byte[] receiveBuffer = new byte[4098];
            Socket socket = result.AsyncState as Socket;
            //讀取從伺服器端傳來的資料,EndReceive是關閉非同步接收方法,同時讀取資料
            int count = socket.EndReceive(result);
            if (count > 0) 
            {
                try
                {
                   //接受完服務端的資料後的邏輯
                }
                catch 
                {
                
                }
            }
            // 遞迴監聽伺服器端是否發來資訊,一旦伺服器再次傳送資訊,客戶端仍然可以接收到
            socket.BeginReceive(receiveBuffer, 0, receiveBuffer.Length, SocketFlags.None, ReceiveCallBack,socket);
        }


    }

本章總結

本章簡單介紹了關於NetworkStream以及其周邊的一些衍生知識,這些知識的重要性不言而喻,從Tcp/IP協議到期分層結構,

Socket和NetworkStream 的關係和注意事項,以及Socket在Tcp/IP協議中的角色等等,不知不覺Stream篇快接近於尾聲了

期間感謝大家的關注,今後我會寫新的系列,請大家多多鼓勵