书到用时方恨少,绝知此事要躬行--谈TCP/UDP编程
文章出處:http://blog.csdn.net/jkler_doyourself/article/details/2715672
?
??????? 原來以為自己對tcp(udp)/ip編程還算比較了解,因為自己也親自拜讀過《Unix環境高級編程》、《TCP/IP協議詳解第一卷》這些計算機界圣經一般的數據。近來經歷過一次和高中同學,現在已經是一個部門的同事,在一起解決他模塊中的一個錯誤,才知道自己其實對TCP/UDP編程所知還不算深,竟然在一個不錯的公司里面工作三年后,還是回答不了他的提問"在tcp連接的recv操作是否會出現一次返還多個物理的tcp package?!"因為TCP協議的基礎協議是IP協議,我們在TCP上發送數據,特別在發送大量、大批數據的時候,就體現為多個IP數據報在這個連接上跑來跑去。
他說TCP是否具有這樣特性對他的應用有點影響,而他的程序也是由多個前輩在開發幾年后遺留下來的。他在有一次大數據量測試的時間發現,自己的服務器程序竟然在有一次收包recv函數調用過程中會收到客戶端發送過來的兩次請求應用協議數據,前一個請求數據是完整的,后一個請求數據只有一部分的數據。他的程序原來設定recv緩沖區大于任何客戶端發送應用協議數據的大小,假設一次recv操作就返還一次客戶端信令請求發過來的數據。
在原來客戶端請求量不是很大的時間,每次客戶端發送數據包過來,間隔性比較大,recv操作都返還一個完整的客戶端請求數據出去,而不攜帶后續請求的數據。這樣的現象具體原因可解釋為,由于recv緩沖區大于任何一個客戶端請求應用協議數據的大小,而且在網絡條件比較好的情況下,通??蛻舳苏埱髤f議數據難于大于物理網絡上要求IP數據報的最大值;同時,不要忘了前面的間歇性的特征,兩次recv操作之間可能沒有任何客戶端請求數據上來!
這樣,recv操作會完整地返還一個客戶端請求協議數據出去。但是,在我同學的模擬測試的情況下,客戶端和服務器端都跑在本地機器上,模擬程序可以模擬客戶端在短時間內發送多個包上來。這樣,在客戶端請求比較多的情況下,在上層業務兩次調用recv操作之間時,可能tcp的操作系統緩沖區里面,已經有多于一條的客戶端請求協議數據過來。發現由于tcp的recv機制是如果操作系統緩沖區內有數據或每次客戶端數據過來即解除阻塞狀態返還上層業務,而且數據的最大返還數據量為上層業務設定recv的最大緩沖區大小,也就是在某些情況下返還的數據可能小于應用程序指定的長度。由于TCP這樣的機制,在我們前面所描述的場景中,如果在兩次recv操作之間來了多于兩個的客戶端請求數據上來,就會導致一次recv返還多于一個的客戶端請求協議數據出去,但是怎么解決這個問題呢?為什么tcp是這樣的特性呢?后來也曾經迷茫很長時間!
??????? 在后來,為了保持住自己自詡為高手的臉面,就耐下心來,詳細研讀了tcp/ip詳解,unix網絡編程和msdn,以及一些開源軟件怎么處理客戶端tcp請求應用協議數據。有時一點虛榮心,如果處理的得當,也會轉化為強烈的求知欲的,呵呵!當初定位研究那些開源軟件就選定那些客戶端請求應用協議會在同一條tcp連接上一直發送請求數據,例如ftp 21端口上命令數據和短信的smpp協議。研究后,才發現自己對于tcp(udp)/ip的網絡編程還了解的很少,有時還相當地無知,可憐自己一直還以為很了解這一領域,遂就聯想到一句古語“書到用時方恨少,絕知此事要躬行”!
??????? 下面簡單總結一下研究的精華內容,呵呵,文章寫的臭長臭長估計看的人的數量就會下降。就象霍金在自己的科普讀物《時間簡史》中說到,科普書中出現數學公式會降低書的銷售量一樣。
tcp與udp一點簡單區別:
1.首先tcp可以看成是一種流協議,沒有報文大小,這也可以體現在tcp報文頭的格式上,tcp報文頭沒有數據大小的定義;
2.與tcp通訊相對應的udp通訊,udp報文頭具有數據大小的說法,具有報文邊界。
??????? 所以,tcp通訊一次recv只能是有多少數據取多少數據,通常意義上取數據的大小要由上層業務決定,但是udp通訊一次recv操作,一般來說也只能返還一個完整的udp數據包,而且最大也只能返還一個完整的udp數據包,不可能將recv操作返還數據跨越到下一個完整的udp包來。
tcp與udp recv操作的一點簡單區別:
由于tcp是可靠的數據鏈接,而且可以看成一種流協議,在現在Java的IO框架里面,就將socket體現為一種stream。如果在此流上連續發送多條客戶端請求,而且客戶端每個請求具有一定的獨立性,那么分割每個客戶端請求的就需要一定意義上的應用協議。通過前面的介紹,我們知道實際上我朋友的應用程序使用了近乎于簡單和幼稚的的通許協議,它設定recv緩沖區為最大客戶端請求信令緩沖區大小,認為每次recv操作取到的數據都是一次完整的客戶端請求數據。雖然在理論上這個協議是錯的,但是,我們會看到有時錯的東西不一定在實際中是錯誤的,可能因為多種因素的誤打誤撞,非常巧合地成功了!對于這種程序取得的成功,我只能說他們是在利用某種巧合進行編程,而這種巧合就造成他們程序的依賴環境必定不是足夠的簡單、足夠的通用的,而在應用到一個新的環境中時就會出現水土不服。我們經常有些程序員抱怨為什么程序在這里是對的,在那里就是錯的,他們可能根本就沒有想到,所有的錯可能都是自己的錯,因為計算機中沒有靈異事件,所有的錯幾乎都可以看成是一種程序員的錯誤的累積和放大造成的!
這個協議,我再次強調一下會在某些場景下表現的非常正常,例如客戶端請求具有間歇性,且網絡條件比較好,一次客戶端請求很有可能就在一個IP包中就完整發送過來了,這樣他這個工作邏輯就運行的毫發無錯,每次讀到的數據就是一個完整的包,他程序對自己運行環境的假定,就在這么多因素的扭轉下變得滿足了。但是,在我同學的本地機器測試中,由于利用模擬器在本機進行發包,這樣就破壞了他的工作協議,即有很大可能性,在一次recv操作間歇有多于一條的客戶端請求聚集到tcp緩沖區中;同時,客戶端和服務器端跑在同一臺機器上,在兩次Server端recv操作間也會比在不同機器上運行時要大。在這種情況下,每次recv操作返還的數據就出現跨客戶端請求協議數據的情況,就造成了它原來工作協議的不滿足。
??????? 對于利用TCP這樣的應用場景,我們如何來解決這個問題呢?為了快速地找到答案,我就選擇ftp FileZilla和smpp API兩種開源代碼,發現他們對于tcp數據通訊的處理,這兩種軟件采取了截然不同、而又相對比較典型的處理策略?,F總結如下:
1.FileZilla ftp采用每次如果出現多的數據就進行緩存,緩存數據和下一次recv獲得的數據再進行拼裝,每次從這些可能拼裝出來的數據中,取出客戶端請求的ftp命令。
2.SMPP API采用按照標準的協議,每次讀取一個固定長度的報文頭。它是通過封裝recv操作,達到每次應用application_recv函數返還時就是返還應用程序指定大小的數據長度出來。讀取出客戶端請求報文頭后,就按照報文頭中指定的協議數據長度進行下次讀取,依然是必須返還應用程序指定數據后才能返還。就象在一條不間斷的流上進行滑行一樣!
??????? 由于朋友的程序具有SMPP API工作特點,當時就簡單地采用了SMPP API的tcp處理策略,畢竟每次判斷緩存以及和緩存數據進行拼接,在我認為可能是一個稍微復雜一點的算法,呵呵!
其實寫到這里估計你還不意味寫這篇文章有什么價值,有可能你已經在以前就了解到了tcp和upd通訊recv的處理策略。我最初其實也覺得如果只是自己再深層次地認識了一下tcp/udp編程的特性,也最多只能算作一次經驗總結罷了!但是,真正觸發我要把它寫成一篇博客是因為后來隨之發生的事情。就在我準備固定把TCP當作一種流概念去處理,當作一次知識儲藏的時間,同事朋友發現我們現在的處理策略不能戰勝一次數據錯誤,也就是如果出現一次客戶端請求數據錯誤,即有可能在以后導致一步錯步步錯。因為根據流的特性,如果前面分割都已經錯誤的情況下,后面依據分割點的解釋也就可能不對了。但是,我的朋友發現他以前的程序卻有可能從這樣的數據錯誤的困境中恢復過來,而相反,我們后來的處理策略卻很難在災難中重生。
當時,我也很迷惑,就建議我同事朋友可能只有告訴客戶端重新建立一次連接,就可以讓數據通訊正常起來。但是,后來,在同事朋友研究了他以前的代碼后發現,在TCP連接可以被當作一種流來處理的同時,還具有另外一個比較微妙的特性。他覺得在我們把TCP當作一種流來處理的時候,如果在這個流上數據具有間歇性,也就是這條流上不總是有數據,數據可能是時斷時續、時有時無的,數據和數據間又具有某種獨立性。在這種場景下,是雖然TCP可以邏輯可以看做一種流,但是卻是一種非致密性、并不十分“連續”的流。這樣,我們就在Server端接受客戶端請求數據發現出錯的情況下,可以快速地多“吞”幾次客戶端數據,這時必利用原始recv,只要有數據就返還,不一定必須返還應用程序指定長度大小,也就是讀一次recv操作后,就進行拋棄,不管其是正確的、還是錯誤的,以達到清空操作系統TCP緩沖區的目的。進行這樣的嘗試行為后,我們后來改造的TCP Server端就有可能象以前的Server端實現,在數據間歇性的場景下獲得自救。Server端很有可能在下一次執行recv的時候,就很有可能“幸運”地遇到一個客戶端請求的開頭數據,這樣秩序就重新開始了,呵呵!
??????? 正是這種TCP連接在數據間歇性場景下“自救”的發現,撬動了我去嘗試寫這篇博客!由此,感謝我那位朋友I_Will_Go!!!
??????? 從這次經驗獲得中,我覺得世上卻是很多事情具有很大的相像性。例如在人類歷史上,一次困局和亂世的重生,大都經過一次比較有自傷性的行動,就象上面的舉的TCP通訊例子,靠幾次“猛吞”客戶端數據,拋棄一切,無視良莠,從而成功獲得自救!
另外,從這里面我們也可以看到,如果一個模塊運行多年依然還能夠運行的話,一定是有它成功的地方。我們作為后來的維護者,千萬不能隨便都對那些代碼嗤之以鼻,不愿意去研究研究,在以前曹雪芹就講過世事洞明皆學問,不管好的模塊和差的模塊,去研究它,可能都有它作的比較好的地方,三人行必有我師。
?
附:
?
IP頭結構
IP首部中包含了首部長度和總長度;?
?
TCP頭結構
TCP首部中只包含了首部長度(因為具有選項),但不包含用戶數據長度,不提供報文邊界的概念;?
?
UDP頭結構
?UDP首部中只包含了用戶數據報長度(UDP首部中沒有選項,長度固定),這個用戶數據報長度就是UDP報文的邊界了。
?
?
?
總結
以上是生活随笔為你收集整理的书到用时方恨少,绝知此事要躬行--谈TCP/UDP编程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 我们开源了一个轻量的 Web IDE U
- 下一篇: 智能网关与服务器连接简介