C#版清晰易懂TCP通信原理解析(附demo)


【轉】 C#版清晰易懂TCP通信原理解析(附demo)

 

(點擊上方藍字,可快速關注我們)

 

來源:周見智

cnblogs.com/xiaozhi_5638/p/4244797.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通信系統中,客戶端連接上服務器后,客戶端向服務器發送一個字符串,並發送一個字符串轉換指令(比如大小寫轉換、除去特殊字符等指令),服務器接收到數據后,按照對應的指令,將字符串轉換后發送回給客戶端。那么這里的應用層協議可以這樣設計:

 

 

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

 

 

如上圖所示,開頭一個字節代表字符串轉換指令類型,后續四個字節存放一個Int32的整型數據,表示字符串的長度(字符串采用Unicode編碼),最后N個字節表示字符串內容。數據發送方必須按照此協議格式發送數據,數據接收方必須按照此協議格式接收數據。

 

發送數據時按照協議格式化數據很簡單,但是,接收數據后,按照協議去解析數據該怎樣呢?事實上,這個相對來講稍微復雜一點。我們可以將每次接收到的數據(字節流)寫入一個緩沖區,然后判斷緩沖區中是否存在一條完整的數據,如果存在,則處理這條完整的數據;否則,繼續接收數據,將接收到的數據再次寫入緩沖區...以此循環。

 

 

TCPLibrary通信庫介紹

 

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

 

TCP通信過程建立之后,大概結構如下:

 

 

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

 

 

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

 

 

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

 

/// <summary>

/// 按照規定協議,重寫TryReadMessage方法

/// </summary>

/// <returns></returns>

internal override ZMessage TryReadMessage()

{

    if (_length >= 8)   //  4 + 4 + N

    {

        using (MemoryStream ms = new MemoryStream(_buffer))

        {

            BinaryReader br = new BinaryReader(ms);

            int msgtype = br.ReadInt32();  //讀取消息類型

            int msglength = br.ReadInt32();  //讀取消息長度

            if (_length - 8 >= msglength)  //如果緩沖區中存在一條完整消息,則讀取

            {

                byte[] msgcontent = br.ReadBytes(msglength);  //讀取消息內容

                BaseMessage bm = new BaseMessage(msgtype, msgcontent); //還原成一條完整的消息

                Remove(8 + msglength);  //注意! 移除已讀數據

 

                return bm;  //返回讀取到的消息

            }

            else

            {

                return null;

            }

        }

    }

    else

    {

        return null;

    }

}

 

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

 

 

/// <summary>

/// 按照規定協議,重寫RawData屬性

/// </summary>

public override byte[] RawData

{

    get

    {

        byte[] rawdata = new byte[4 + 4 + MsgContent.Length];  //消息類型 + 消息長度 + 消息內容

        using (MemoryStream ms = new MemoryStream(rawdata))

        {

            BinaryWriter bw = new BinaryWriter(ms);

            bw.Write(MsgType);  //先寫入MsgType

            bw.Write(MsgContent.Length);  //再寫入MsgContent的長度

            bw.Write(MsgContent); //最后寫入消息內容

            return rawdata;

        }

    }

}

 

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

 

Demo演示

 

使用TCPLibrary中的默認實現類(以Base開頭的類型),我做了一個簡單的Demo。該Demo可以完成字符串、可序列化對象(圖片)的發送與接收。Demo源碼很簡單:

 

Server端初始化:

 

private void Form1_Load(object sender, EventArgs e)

{

    _server = new BaseServerSocket();

    _server.Connected += new ConnectedEventHandler(_server_Connected);

    _server.DisConnected += new DisConnectedEventHandler(_server_DisConnected);

    _server.MessageReceived += new MessageReceivedEventHandler(_server_MessageReceived);

    _server.StartAccept(9100);

    textBox1.AppendText("服¤t務?器¡Â啟?動¡¥,ê?監¨¤聽¬y端?口¨² " + 9000 + "...\r\n");

}

 

Client端的初始化:

 

private void Form1_Load(object sender, EventArgs e)

{

    _client = new BaseClientSocket();

    _client.Connected += new ConnectedEventHandler(_client_Connected);

    _client.DisConnected += new DisConnectedEventHandler(_client_DisConnected);

    _client.MessageReceived += new MessageReceivedEventHandler(_client_MessageReceived);

    _client.Connect("127.0.0.1",9100);

}

 

可以看到,使用起來很簡單。注冊事件后,既可以開始運行了。

 

下面可以看一下Demo截圖:

 

 

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

 

源碼下載

 

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

 

看完本文有收獲?請轉發分享給更多人

關注「DotNet」,提升.Net技能 

 


免責聲明!

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



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