20170821-20170827C#工作學習周總結

Data:2017-08-24

Tips:

indexof() :string對象的一個方法,在字符串中從前向後定位字符和字符串;所有的返回值都是指在字符串的絕對位置,如爲空則爲- 1 ;


網絡文件傳輸

本週主要做了一個仿服務器客戶端文件傳輸的實例,以下以該實例作爲總結:

需求:

分別實現客戶端和服務器的功能(兩個程序),之間可實現文件傳輸,考慮斷點續傳和文件完整性校驗。

解決步驟及要考慮的問題:

不同主機之間信息是如何傳遞的?(網絡、識別獨一無二的機器)

怎麼讀寫文件?怎麼發文件?

怎麼校驗已接收文檔的正確性?

C# 中怎麼實現?

詳細描述

1、不同主機之間信息是如何傳遞的?

毫無疑問是網絡,但是這個概念就大了去了,把什麼事都拋給網絡算什麼事?所以得解釋,網絡到底是什麼。經典的ISO七層模型將網絡劃分爲七層,從底向上分別是:物理層、數據鏈路層、網絡層、傳輸層、會話層、表示層和應用層,而TCP/IP參考模型分爲四個層次:主機到網絡層、網絡互連層、傳輸層和應用層,而我們常說的以TCP還是UDP的方式傳送,就是在傳輸層定義的兩種服務質量不同的協議,所以這裏引出了:傳輸協議的概念。

1.1 什麼是傳輸協議呢?

顧名思義,協議就是雙方達成一致的文件或軍要求執行實現的標準。比如,A和B商量好,見面的時候手裏拿着紅色玫瑰花,認物不認人,這就是協議。而傳輸協議,即指有信息交互關係發生的雙方,以某一個都認同的數據處理方式去收發數據,這便是傳輸協議。目前在傳輸層上的兩種服務質量不同的協議,分別是TCP(傳輸控制協議TCP(transmission control protocol))和UDP(用戶數據報協議UDP(user datagram protocol)),自定義協議都是基於這兩種協議某一種建立起來的。

TCP協議是一個面向連接的、可靠的協議。它將一臺主機發出的字節流無差錯地發往互聯網上的其他主機。在發送端,它負責把上層傳送下來的字節流分成報文段並傳遞給下層。在接收端,它負責把收到的報文進行重組後遞交給上層。TCP協議還要處理端到端的流量控制,以避免緩慢接收的接收方沒有足夠的緩衝區接收發送方發送的大量數據。  
  UDP協議是一個不可靠的、無連接協議,主要適用於不需要對報文進行排序和流量控制的場合。

文件,說白了也是一種消息,所以傳文件和傳消息的本質是一樣的。

現在協議的問題搞清楚了,那怎麼建立連接呢?如何讓物理上獨立的兩臺主機發生數據關聯?——套接字。

1.2 什麼是套接字?

剛纔我們有講到,主機之間要建立聯繫,茫茫人海中,怎麼找到你呢?肯定是有獨一無二的標識來標記每一個獨立的個體,比如,我們合法公民都有身份證號,唯一識別的。計算機,或者說,其他需要連入網絡的機器要怎麼識別呢?IP地址。

什麼是IP地址?

Internet Protocol Address,又譯爲網際協議地址),縮寫爲IP地址(英語:IP Address),是分配給網絡上使用網際協議(Internet Protocol, IP)的設備的數字標籤——維基百科

什麼是端口(虛擬)?

如果把IP地址比作一間房子 ,端口就是出入這間房子的門。真正的房子只有幾個門,但是一個IP地址的端口可以有65536(即:2^16)個之多!端口是通過端口號來標記的,端口號只有整數,範圍是從0 到65535(2^16-1)。——百度百科

而通過IP地址和端口號,便能連入一臺設備。套接字——socket,便是IP + port,一個IP地址和一個端口號合稱爲一個套接字(Socket)。每個TCP、UDP數據段中都包含源端口和目標端口字段。一個套接字對(Socket pair)可以唯一地確定互連網絡中每個TCP連接的雙方(客戶IP地址、客戶端口號、服務器IP地址、服務器端口號)。 平常所說的FTP、TELNET、SMTP、DNS、TFTP、SNMP、RIP等都是指建立在TCP/IP基礎上的應用層協議。

2、怎麼讀寫文件?怎麼發文件?

什麼是文件?個人理解就是按照一定格式存取的固定大小的存儲單元。怎麼理解這句話呢?首先,文本文件其實是一種無格式文件,那什麼是格式?就是存儲和解析的方式。放在計算機最底層來講,都是以01機器碼存儲的,那麼怎麼識別文件原來的樣子呢?自然是解析方式的不同了——爲什麼.xlxs不能以.docx的格式打開?.mp4的格式爲什麼不能以.txt格式打開?就是解析方式的問題,比如說,本來應該8-2-4字節的方式讀取,如果用了4-2-8的格式讀取,自然是亂碼錯誤的。指定存儲格式,便是加密的過程,跟摩斯碼差不多的,看你怎麼選~

怎麼讀文件?

這裏要引入流Stream的概念。什麼是流呢?這其實是一個形象的叫法。流的意思就是像水流一樣長長的一串沒空隙的東西。Stream is actually an object from which we can read or write a sequence of bytes.

讀文件也即將文件以字節數組的方式讀出。

怎麼發文件?

文件既然已經都出來並且以字節數組的方式存儲起來了,那麼問題變成了:“如何將這個字節數組傳出去”了。

3、怎麼校驗已接收文檔的正確性?——MD5、SHA1、CRC

MD5

MD5 Message-Digest Algorithm,一種被廣泛使用的密碼散列函數,可以產生出一個128位16字節的散列值(hash value),用於確保信息傳輸完整一致。服務器預先提供一個MD5校驗和,用戶下載完文件以後,用MD5算法計算下載文件的MD5校驗和,然後通過檢查這兩個校驗和是否一致,就能判斷下載的文件是否出錯。

SHA1

是一種密碼散列函數,美國國家安全局設計,並由美國國家標準技術研究所(NIST)發佈爲聯邦數據處理標準。SHA-1可以生成一個被稱爲消息摘要的160[字節散列值,散列值通常的呈現形式爲40個數。(2017年2月23日,Google公司公告宣稱他們與CWI Amsterdam合作共同創建了兩個有着相同的SHA-1值但內容不同的PDF文件,這代表SHA-1算法已被正式攻破。)——維基百科

CRC

Cyclic redundancy check,通稱CRC是一種根據網絡數據包或電腦文件等數據產生簡短固定位數校驗碼的一種散列函數,主要用來檢測或校驗數據傳輸或者保存後可能出現的錯誤。生成的數字在傳輸或者存儲之前計算出來並且附加到數據後面,然後接收方進行檢驗確定數據是否發生變化。一般來說,循環冗餘校驗的值都是32位的整數。由於本函數易於用二進制的電腦硬件使用、容易進行數學分析並且尤其善於檢測傳輸通道干擾引起的錯誤,因此獲得廣泛應用。——維基百科

4、C#中要怎麼實現?

這便是C#網絡編程。做的時候很痛苦啊,根本沒有任何關於這方面書本資料,所有的知識都要靠MSDN官方文檔和前人們犯過的錯中總結。基本概念上文已經做了概述,接下來詳細談怎麼在C#.Net下實現。

IP地址的獲取

參見上一週的總結【用於IP地址的.Net類】

建立連接【TCP的實現】

這裏要清楚,在客戶端和服務器之間正式開始數據交換之前,它們還是有區別的,服務端要在某一個端口監聽,到底誰連入到了我接收消息的端口,而此處監聽,可以針對某一臺具體設備(其他設備即使連入也忽略掉),也可以是任意一臺接入設備,也因此有監聽IP範圍不同。

以MSDN TCPListener類爲例:

服務端建立監聽

//核心代碼
TcpListener server = null;
// Set the TcpListener on port 13000.
Int32 port = 13000;
IPAddress localAddr = IPAddress.Parse("127.0.0.1");

// TcpListener server = new TcpListener(port);
server = new TcpListener(localAddr, port);

// Start listening for client requests.
server.Start();
Console.Write("Waiting for a connection... ");

// Perform a blocking call to accept requests.
// You could also user server.AcceptSocket() here.
//下面這句話一旦執行,服務端便進入等待連接的狀態,這句之後的語句開始執行,說明連接成功
TcpClient client = server.AcceptTcpClient();

當然,既然是服務端和客戶端都需要自己完成,那麼寫程序的時候最好兩面同時開始寫,這樣才知道執行到哪一句該服務器響應了,再執行到哪一句該客戶端響應了等等。所以,剛纔實現好了服務端的監聽,接下來應該是客戶端的連接。

2010102414143479

//服務端主機IP
string hostIP = "127.0.0.1";

//先建立IPAddress物件,IP為欲連線主機之IP
IPAddress ipa = IPAddress.Parse(hostIP);

//建立IPEndPoint
IPEndPoint ipe = new IPEndPoint(ipa, 1234);

//先建立一個TcpClient;
TcpClient tcpClient = new TcpClient();

//開始連線
Console.WriteLine("主機IP=" + ipa.ToString());
Console.WriteLine("連線至主機中...\n");
tcpClient.Connect(ipe);
   if (tcpClient.Connected)
   {
       Console.WriteLine("連線成功!");
   }

截至目前爲止,客戶端和服務端之間的通道已經打開,之後就可以傳數據了!

傳數據的方式有很多,我選擇了NetworkStream【同步】,是它幫我成功傳輸了第一組數據,而後來的故事,卻痛不欲生,可能還是對這個類理解不到位吧,但對於現在來講,實現功能很重要!

首先我們從NetworkStream類講起,引用MSDN的描述:

The NetworkStream class provides methods for sending and receiving data over Stream sockets in blocking mode.

Use the Write and Read methods for simple single thread synchronous blocking I/O. If you want to process your I/O using separate threads, consider using the BeginWrite and EndWrite methods, or the BeginRead and EndRead methods for communication.

Read and write operations can be performed simultaneously on an instance of the NetworkStream class without the need for synchronization. As long as there is one unique thread for the write operations and one unique thread for the read operations, there will be no cross-interference between read and write threads and no synchronization is required.

這三段話是幸福和痛苦的來源。可以看到,NetworkStream以異步阻塞模式發送/接收數據,關於同步異步及阻塞非阻塞模式,可以參考怎樣理解阻塞非阻塞與同步異步的區別?

也就是說,一個線程可以一直寫,另外一個線程可以一直讀(當然,如果流裏面沒有數據自然不能讀了),它們沒有交叉打斷的功能,只有在讀的一方沒有數據可讀了,纔會等待寫的一方往流裏面寫數據。

而我們的數據幀是以幀頭+數據的方式傳送的,所以絕不能允許一直往流裏邊寫數據的情況,會造成取數據的紊亂,解析結果亂碼的錯誤。因此,應該手動加入控制,客戶端讀完一句後再讓服務端往裏邊寫,手動同步該過程。注意區分,同時和同步是針對不同的對象描述的,二者不等價。

可以在服務端/客戶端這麼寫:

//服務端
//還有數據可讀的循環
while (preData.Left > 0)
                {
                    clientStream.Read(structBytes, 0, client.ReceiveBufferSize);
                    cmdFromClient = (Mode.ClientCmd)Convertor.BytesToStruct(structBytes, clientCmd.GetType());

                    //客戶端準備好接收數據
                    if(cmdFromClient.IsReady)
                    {
                    }
                 }
     while (true)
            {
                //把當前收到的字節數組分割爲結構體和剩下的數據部分,而結構體的大小是知道的
                //覆蓋填寫,先測試不支持斷點的,即本地不記錄當前下載位置,在V0.2改進
                //收到的每一段均存在receiveBytes數組中
                byte[] receiveBytes = new byte[client.ReceiveBufferSize];

                //給服務器發通知,可以開始寫下一波(第一波)了
                clientCmd.IsReady = true;
                clientBytes = Convertor.StructToBytes(clientCmd);
                client.GetStream().Write(clientBytes, 0, clientBytes.Length);

                int receiveBytesLength = netStream.Read(receiveBytes, 0, client.ReceiveBufferSize);

                //下面這三行和上面的那三行實現與服務端同步
                clientCmd.IsReady = true;
                clientBytes = Convertor.StructToBytes(clientCmd);
                client.GetStream().Write(clientBytes, 0, clientBytes.Length);

                byte[] realReceiveBytes = receiveBytes.Skip(0).Take(receiveBytesLength).ToArray();

                //取前一部分轉換成結構體
                byte[] cmdStructureBytes = Extension.SubArrayDeepClone(realReceiveBytes, 0, 16);
                //將字節流轉換爲結構體
                Mode.PreData preData = new Mode.PreData();
                //DataCmdFromClient爲命令結構體
                Mode.PreData DataCmdFromServer = (Mode.PreData)Convertor.BytesToStruct(cmdStructureBytes, preData.GetType());
                //1 去後面的部分轉換爲字節流
                //byte[] dataBytes = realReceiveBytes.Skip(16).Take(DataCmdFromServer.Period + 16).ToArray();
                //byte[] dataBytes = new byte[1000];
                byte[] dataBytes = Extension.SubArrayDeepClone(realReceiveBytes, 16 , DataCmdFromServer.Period);
                //運行時校驗
                string tmp_2 = Encoding.UTF8.GetString(dataBytes);
                //2 字節流轉文件流
                fs.Write(dataBytes, 0, dataBytes.Length);

                //解析命令——Predata,包括本次傳送的字節數和本次傳輸完成之後剩餘的字節數
                //若果爲0,則break掉
                if (DataCmdFromServer.Left == 0)
                {
                    fs.Close();
                    break;
                }
            }

至此,單線程同步傳輸程序完結!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章