目錄
- 說明
- 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通訊的工作模式。
TCP與UDP通訊的特點
關於對這兩者的比較,網上一搜一大片,講得也比較清楚。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 }
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 }
可以看到,上面一個按照協議格式化資料,而另一個按照協議解析資料。它們兩個完全遵守同一個協議。
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 }
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 }
可以看到,使用起來很簡單。註冊事件後,既可以開始執行了。
下面可以看一下Demo截圖:
注意,這個Demo只是利用庫中的預設實現類來完成的。你完全可以自己定義一個協議,按照你自己的方式傳送資料,比如“頭(4Byte)+是否加密(1Byte)+傳送方程式版本(8Byte)+訊息長度(4Byte)+訊息內容(NByte)+附加資訊(8Byte)”這種方式傳送資料/接收資料。只要你正確的實現了上面強調的方法和屬性。
未完成功能
剛開始就說過,TCPLibrary功能不足,很多功能都沒有。列舉幾個如下
1.執行緒安全
2.心跳檢測
3.都只有開始,沒有結束的功能
4.。。。
可以把原始碼下下來,自己嘗試補充這些功能。
原始碼下載
下載地址:https://files.cnblogs.com/xiaozhi_5638/TCPDemo.rar
Win7+VS2010
希望有幫助!