清晰易懂TCP通訊原理解析(附demo、簡易TCP通訊庫原始碼、解決沾包問題等)C#版

周見智發表於2015-01-23

目錄

  • 說明
  • TCP與UDP通訊的特點
  • TCP中的沾包現象
  • 自定義應用層協議
  • TCPLibrary通訊庫介紹
  • Demo演示
  • 未完成功能
  • 原始碼下載

說明

我前面部落格中有多篇文章講到了.NET中的網路程式設計,與TCP和UDP相關的有:

1.http://www.cnblogs.com/xiaozhi_5638/p/3167794.html

2.http://www.cnblogs.com/xiaozhi_5638/p/3169641.html

3.http://www.cnblogs.com/xiaozhi_5638/p/3290283.html

4.http://www.cnblogs.com/xiaozhi_5638/p/3313959.html

另外也有一些講的是透過Socket模擬瀏覽器訪問Web伺服器,或者模擬Web伺服器接收瀏覽器的請求:

1.http://www.cnblogs.com/xiaozhi_5638/p/3912668.html

2.http://www.cnblogs.com/xiaozhi_5638/p/3917943.html

(之前文章的排版不太好,不好意思!)

之所有對.NET中網路程式設計寫得比較多,主要原因有兩個,一是我公司做的專案多數跟通訊這個有關;二是研究Socket通訊工作模式有益於對軟體架構設計的理解,因為它裡面到處都使用到了“泵”結構,而這個結構幾乎是所有框架、大型模組所必需具備的。另外,工作之餘寫的一本書(即將要出版)中有一章專門講到了“泵”結構在軟體系統中的作用。

這次寫這篇文章主要是看了網上一個人提的有關TCP程式設計的問題,所以就再次整理了一下這方面的知識,並且做了一個“簡易通訊庫”發出來給大家看看,程式碼很簡單,功能也不是特別全,但是具備很好的擴充套件性,基本上可以用來說明.NET中TCP通訊的工作模式。

 

TCPUDP通訊的特點

關於對這兩者的比較,網上一搜一大片,講得也比較清楚。TCP通訊就像打電話,雙方通訊之前需要建立連線、雙方就位後方可開始會話;而UDP通訊就像發簡訊,一方給另一方傳送資料前,並不需要對方就位。

上面兩幅圖顯示了TCP與UDP通訊過程建立的區別。

除了它們通訊過程建立的不同之外,兩者還有以下區別:

  • TCP通訊特點

1)可靠性

   通訊雙方均就位,一方傳送資料,另一方收到後會做出回應,如果超時未傳送成功,會自動重發,資料不會丟失。

2)順序性;

   既然資料是按順序走在建立的一條隧道中,那麼資料遵循“先走先達到”的規則,並且隧道中的資料以“流”的形式傳輸,傳送方傳送的前後兩次資料之間沒有邊界,需要接收方自己根據事先規定好的“協議”去判斷資料邊界。

3)高損耗。

   “高損耗”包括機器效能損耗高、寬頻流量損耗高。因為通訊雙方時刻需要維持著連線的存在,這必然會損耗通訊雙方主機效能,要想維持隧道的通暢,通訊雙方必須不斷地傳送檢測包和應答包,同時,它還支援資料重發等資料糾錯功能,這些都將導致網路流量的增加。

 

  • UDP通訊特點

1)不可靠性;

   既然無連線,傳送方只管傳送資料,而不管對方是否能夠正確地接收到資料,更不負責資料超時重發等功能。

2)無序性;

   資料以“資料包”的形式傳送,可以把“資料包”看成是一個“包”。如果把TCP傳輸資料比如成“河裡的流水”,那麼UDP傳輸資料就是‘郵局寄信’。傳送方先傳送的資料可能後到達,後傳送的資料可能先到達,這個跟短訊息類似。

3)低損耗。

   “低損耗”包括機器效能損耗低、寬頻流量損耗低。UDP通訊不需要維持一個連線的存在,所以它不需要消耗額外的機器效能。同時它也沒有像TCP通訊那樣為了保持隧道的通暢,而必須不停地傳送檢測包和應答包,更不會進行一些資料檢測糾錯、重發等行為。

這次我們只討論TCP通訊。

 

TCP通訊中的“沾包”現象

上面提到過,TCP通訊中,資料是以“流”的形式傳輸的。前一次傳送的資料和後一次傳送的資料之間並沒有明顯的界限,這就會出現一個問題:當你收到一部分資料時,你無法判斷接收到的資料是否是完整的?

如上圖,傳送方傳送三次資料,而接收方可能一共分四次接收。並且每次接收到的資料量不確定(雖然每次收到的資料不確定,但是將四次接收到的資料拼接起來,與傳送時的一致)。這樣以來,當我們每次收到一份資料時,我們無法輕易判斷(幾乎不能)收到的資料是否完整(是否可以正確地被處理)。

以上現象我們稱之為“沾包”。TCP通訊過程中,要想解決“沾包”問題,我們必須人工採取一些措施,比如在傳送資料時遵循一些“規則”,在接收到資料時,再按照相同的“規則”去解析資料,最終得到一份完整的資料,並進行正確的處理。沒錯,這裡說的“規則”便是我們通常聽到的“協議”。

關於協議,講到的地方也很多。簡單的說,協議就是一種“資料結構”,合作雙方必須同時按照相同的資料結構傳送/接收資料,比如傳輸層的TCP/UDP協議,又比如應用層的HTTP/FTP等協議。B/S結構系統使用到的協議見下圖:

在TCP通訊中,在傳送和接收資料的時候,如果我們遵循事先定義的一種“協議”(屬於一種應用層協議)。比如,在傳送資料時,按照“資料頭(4Byte)+內容長度(4Byte)+內容正文(NByte)+附加資訊(8Byte)”這種形式去“格式化”需要傳送的資料;同理,在接收到資料後,按照這種形式去“反格式化”資料,這樣我們便可以判斷資料邊界,輕鬆得到一條完整資料。

 

自定義應用層協議

是的。我們自己完全可以定義一個類似HTTP這樣的應用層協議,只要你能力足夠強,系統足夠大。今天在這裡,我只舉個簡單的例子,假設一個TCP通訊系統中,客戶端連線上伺服器後,客戶端向伺服器傳送一個字串,併傳送一個字串轉換指令(比如大小寫轉換、除去特殊字元等指令),伺服器接收到資料後,按照對應的指令,將字串轉換後傳送回給客戶端。那麼這裡的應用層協議可以這樣設計:

 字串轉換指令

序號

指令值(byte)

說明

1

0x01

將字串中小寫字元轉換成大寫

2

0x02

將字串中大寫字元轉換成小寫

3

0x03

去掉字串中的百分號(%)字元

4

0x04

將字串中的百分號(%)替換為空格

如上表所示,假設一共有四種字串轉換請求,那麼我們可以按下面圖設計應用層協議的資料結構:

如上圖所示,開頭一個位元組代表字串轉換指令型別,後續四個位元組存放一個Int32的整型資料,表示字串的長度(字串採用Unicode編碼),最後N個位元組表示字串內容。資料傳送方必須按照此協議格式傳送資料,資料接收方必須按照此協議格式接收資料。

傳送資料時按照協議格式化資料很簡單,但是,接收資料後,按照協議去解析資料該怎樣呢?事實上,這個相對來講稍微複雜一點。我們可以將每次接收到的資料(位元組流)寫入一個緩衝區,然後判斷緩衝區中是否存在一條完整的資料,如果存在,則處理這條完整的資料;否則,繼續接收資料,將接收到的資料再次寫入緩衝區...以此迴圈。

 

TCPLibrary通訊庫介紹

其實我只是將一些程式碼單獨拿出來生成了一個dll,這部分程式碼可以為我們搭建起TCP通訊的框架,包括服務端偵聽、(服務端/客戶端)接收資料、上下線、訊息處理並通知Application以及“沾包”問題處理等等。功能並不全面,如果要拿去實際專案中使用還需要自己完善,文章末會列出未完成的功能。

TCP通訊過程建立之後,大概結構如下:

整個通訊庫中,只包含5個抽象類,以及5個預設實現類(所以說簡易):

使用該通訊庫的前提是要定義好程式使用到的“協議”,然後重點實現ZMessage.RawData屬性和ZDataBuffer.TryReadMessage方法,前者可以按照協議格式化需要傳送的資料,後者可以按照協議解析一條完整的訊息。庫中包含5個預設實現類(以Base開頭的),它預設使用以下的協議進行通訊:

其中,BaseDataBuffer.TryReadMessage方法具體實現為:

 1         /// <summary>
 2         /// 按照規定協議,重寫TryReadMessage方法
 3         /// </summary>
 4         /// <returns></returns>
 5         internal override ZMessage TryReadMessage()
 6         {
 7             if (_length >= 8)   //  4 + 4 + N
 8             {
 9                 using (MemoryStream ms = new MemoryStream(_buffer))
10                 {
11                     BinaryReader br = new BinaryReader(ms);
12                     int msgtype = br.ReadInt32();  //讀取訊息型別
13                     int msglength = br.ReadInt32();  //讀取訊息長度
14                     if (_length - 8 >= msglength)  //如果緩衝區中存在一條完整訊息,則讀取
15                     {
16                         byte[] msgcontent = br.ReadBytes(msglength);  //讀取訊息內容
17                         BaseMessage bm = new BaseMessage(msgtype, msgcontent); //還原成一條完整的訊息
18                         Remove(8 + msglength);  //注意! 移除已讀資料
19 
20                         return bm;  //返回讀取到的訊息
21                     }
22                     else
23                     {
24                         return null;
25                     }
26                 }
27             }
28             else
29             {
30                 return null;
31             }
32         }
View Code

BaseMessage.RawData屬性具體的實現為:

 1         /// <summary>
 2         /// 按照規定協議,重寫RawData屬性
 3         /// </summary>
 4         public override byte[] RawData
 5         {
 6             get
 7             {
 8                 byte[] rawdata = new byte[4 + 4 + MsgContent.Length];  //訊息型別 + 訊息長度 + 訊息內容
 9                 using (MemoryStream ms = new MemoryStream(rawdata))
10                 {
11                     BinaryWriter bw = new BinaryWriter(ms);
12                     bw.Write(MsgType);  //先寫入MsgType
13                     bw.Write(MsgContent.Length);  //再寫入MsgContent的長度
14                     bw.Write(MsgContent); //最後寫入訊息內容
15                     return rawdata;
16                 }
17             }
18         }
View Code

可以看到,上面一個按照協議格式化資料,而另一個按照協議解析資料。它們兩個完全遵守同一個協議。

 

Demo演示

使用TCPLibrary中的預設實現類(以Base開頭的型別),我做了一個簡單的Demo。該Demo可以完成字串、可序列化物件(圖片)的傳送與接收。Demo原始碼很簡單:

l  Server端初始化:

1         private void Form1_Load(object sender, EventArgs e)
2         {
3             _server = new BaseServerSocket();
4             _server.Connected += new ConnectedEventHandler(_server_Connected);
5             _server.DisConnected += new DisConnectedEventHandler(_server_DisConnected);
6             _server.MessageReceived += new MessageReceivedEventHandler(_server_MessageReceived);
7             _server.StartAccept(9100);
8             textBox1.AppendText("服¤t務?器¡Â啟?動¡¥,ê?監¨¤聽¬y端?口¨² " + 9000 + "...\r\n");
9         }
View Code

l  Client端的初始化:

1         private void Form1_Load(object sender, EventArgs e)
2         {
3             _client = new BaseClientSocket();
4             _client.Connected += new ConnectedEventHandler(_client_Connected);
5             _client.DisConnected += new DisConnectedEventHandler(_client_DisConnected);
6             _client.MessageReceived += new MessageReceivedEventHandler(_client_MessageReceived);
7             _client.Connect("127.0.0.1",9100);
8         }
View Code

可以看到,使用起來很簡單。註冊事件後,既可以開始執行了。

下面可以看一下Demo截圖:

注意,這個Demo只是利用庫中的預設實現類來完成的。你完全可以自己定義一個協議,按照你自己的方式傳送資料,比如“頭(4Byte)+是否加密(1Byte)+傳送方程式版本(8Byte)+訊息長度(4Byte)+訊息內容(NByte)+附加資訊(8Byte)”這種方式傳送資料/接收資料。只要你正確的實現了上面強調的方法和屬性。

 

未完成功能

剛開始就說過,TCPLibrary功能不足,很多功能都沒有。列舉幾個如下

1.執行緒安全

2.心跳檢測

3.都只有開始,沒有結束的功能

4.。。。

可以把原始碼下下來,自己嘗試補充這些功能。

 

原始碼下載

下載地址:https://files.cnblogs.com/xiaozhi_5638/TCPDemo.rar

Win7+VS2010

希望有幫助!

相關文章