(轉)C# 溫故而知新:Stream篇(七)

(本文轉自)http://www.cnblogs.com/JimmyZheng/archive/2012/05/17/2502727.html

C# 溫故而知新:Stream篇(七)

NetworkStream

目錄:

  • NetworkStream的作用
  • 簡單介紹下TCP/IP 協議和相關層次
  • 簡單說明下 TCP和UDP的區別
  • 簡單介紹下套接字(Socket)的概念
  • 簡單介紹下TcpClient,TcpListener,IPEndPoint類的作用
  • 使用NetworkStream的注意事項和侷限性
  • NetworkStream的構造
  • NetworkStream的屬性
  • NetworkStream的方法
  • NetwrokStream的簡單示例
  •            創建一個客戶端向服務端傳輸圖片的小示例
  • 本章總結

 

 

 

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的知識點,在使用時有幾點必須注意

首先

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

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

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

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

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

NetworkStream支持異步讀寫操作

NetworkStream的侷限性

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

 

 

7.NetworkStream的構造

1.NetworkStream (Socket)  爲指定的 Socket 創建 NetworkStream 類的新實例

2.NetworkStream (SocketBoolean ownsSocket)  用指定的 Socket 所屬權爲指定的 Socket

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

3.NetworkStream (SocketFileAccess)  用指定的訪問權限爲指定的 Socket 創建

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

4.NetworkStream (SocketFileAccessBoolean 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篇快接近於尾聲了

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

發佈了4 篇原創文章 · 獲贊 14 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章