目錄html
說明編程
我前面博客中有多篇文章講到了.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通訊過程創建的區別。
除了它們通訊過程創建的不一樣以外,二者還有如下區別:
1)可靠性;
通訊雙方均就位,一方發送數據,另外一方收到後會作出迴應,若是超時未發送成功,會自動重發,數據不會丟失。
2)順序性;
既然數據是按順序走在創建的一條隧道中,那麼數據遵循「先走先達到」的規則,而且隧道中的數據以「流」的形式傳輸,發送方發送的先後兩次數據之間沒有邊界,須要接收方本身根據事先規定好的「協議」去判斷數據邊界。
3)高損耗。
「高損耗」包括機器性能損耗高、寬帶流量損耗高。由於通訊雙方時刻須要維持着鏈接的存在,這必然會損耗通訊雙方主機性能,要想維持隧道的通暢,通訊雙方必須不斷地發送檢測包和應答包,同時,它還支持數據重發等數據糾錯功能,這些都將致使網絡流量的增長。
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.。。。
能夠把源碼下下來,本身嘗試補充這些功能。
源碼下載
下載地址:http://files.cnblogs.com/xiaozhi_5638/TCPDemo.rar
Win7+VS2010
但願有幫助!