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


目錄

  • 說明
  • 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.。。。

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

 

源碼下載

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

Win7+VS2010

希望有幫助!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM