TCP編程問題總結!
先來復習一下TCP/IP五層模型:從上到下依次是應用層、傳輸層、網(wǎng)絡層、數(shù)據(jù)鏈路層、物理層;我們會接觸到的是應用層、傳輸層、網(wǎng)絡層。
這三層是干啥的?以下是來自《計算機網(wǎng)絡——自頂向下方法》這本書的筆記(是一本好書,深入淺出,把復雜的概念講的很容易懂,不同于大學時那些味同嚼蠟的課本)。
網(wǎng)絡層
網(wǎng)絡層提供主機到主機的通信服務,即從一個IP地址到另一個IP地址的數(shù)據(jù)傳輸,網(wǎng)絡層分為數(shù)據(jù)平面和控制平面。
數(shù)據(jù)平面主要作用是從其輸入鏈路向其輸出鏈路轉發(fā)數(shù)據(jù);
控制平面作用是協(xié)調(diào)這些本地的每個路由器轉發(fā)動作,使得數(shù)據(jù)報沿著源和目的地主機之間的路由器路徑最終進行端到端傳送,簡單說,控制平面的作用就是路由選擇。計算轉發(fā)路徑的算法稱為路由選擇算法。
作者舉了個例子來說明數(shù)據(jù)平面和控制平面,一個人駕車從賓夕法尼亞州到佛羅里達州,轉發(fā)就是經(jīng)過立交橋的過程,從立交橋的一個入口進入立交橋然后從一個出口離開立交橋走上了另一條路;而控制平面路由選擇就是著手行程之前規(guī)劃路線的過程,查閱地圖,從許多可能路線中選擇一條。
傳輸層(又叫運輸層)
為運行在不同主機的應用進程提供邏輯通信,網(wǎng)絡層為主機之間提供了邏輯通信,報文到達主機后,是傳輸層協(xié)議將報文定向到不同進程的。
作者舉了一個很有意思的例子:有兩個家庭A和B,每家有12個孩子,他們每個人每星期都要給對方家庭12個小孩寫信,那每周有144封信件往來,每星期A家庭由Ann來為大家收集信件并投遞到郵車上,信件到時,Ann再把信件一封封分給兄弟姐妹,B家庭由Bill來做這個工作。Ann和Bill做的事情就是傳輸層做的事。這個例子中:
家庭=主機
兄弟姐妹 =進程
運輸層協(xié)議 = Ann或者Bill
網(wǎng)絡層協(xié)議= 郵政服務(包括郵車)
信封上的字符 = 應用層報文
舉個例子電腦上跑著好幾個網(wǎng)絡應用,瀏覽器、網(wǎng)易云音樂、迅雷下載等,到達電腦的數(shù)據(jù)怎么知道是給哪個應用進程的?這就是傳輸層要做的事。
傳輸層通過什么來區(qū)分不同進程?socket端口號。
上面的兩個例子都很妙對不對?
傳輸層的協(xié)議分為TCP和UDP,TCP是面向連接的保證數(shù)據(jù)可靠到達的通信協(xié)議(是一個負責任的信件分發(fā)員,除了把到達主機的數(shù)據(jù)分給不同進程還有很多附加服務),UDP是面向無連接的不可靠的數(shù)據(jù)報文協(xié)議(只提供最基本分發(fā)服務,只管把到達主機的數(shù)據(jù)分發(fā)一下,收沒收到不管)。
那TCP是怎么保證數(shù)據(jù)可靠到達的?簡單說他把要傳輸?shù)臄?shù)據(jù),分成一個一個包裹,每個包裹編號,按順序發(fā),不斷傳輸?shù)臄?shù)據(jù)包形成數(shù)據(jù)流,每個編號的數(shù)據(jù)包到達后對方發(fā)送ACK,發(fā)送方收到對方的ACK才認為這包數(shù)據(jù)成功發(fā)出去了,數(shù)據(jù)發(fā)送的數(shù)據(jù)包序號窗體往后滑。實際并不是這么簡單還有很多內(nèi)容。
應用層就不說了。
下面總結幾個TCP通信遇到過的問題。
1、??數(shù)據(jù)到達的次序不是預期
遇到過一個這樣的問題,有一款洗衣機,一定要先開機,才能再設置洗衣的模式,調(diào)試時發(fā)現(xiàn)APP明明是先發(fā)開機指令,再發(fā)設置模式,但家電端wifi板總是先收到設置模式,再收到開機指令。
分析發(fā)現(xiàn),APP是把數(shù)據(jù)按順序發(fā)給了服務器,但是服務器會按順序一條一條發(fā)給wifi板嗎?并不是,服務器是一對多同時與成千上萬的設備通信的,有手機APP用戶,有家電wifi,所以他通常是并發(fā)的,也就是會把消息分給很多個線程同時處理,比如這里APP發(fā)來的控制消息應該是分給了兩個線程同時處理,應用層2個線程同時向傳輸層丟數(shù)據(jù),這兩包數(shù)據(jù)指向同一個IP同一個端口(即家電wifi板),傳輸層只有一個,那不同線程的數(shù)據(jù)誰先傳下來誰就排在前面了,所以造成了先發(fā)的不一定先到的現(xiàn)象。
那這個問題后來怎么解決的?APP把開關機和設模式一起發(fā)下來由wifi板端來處理,首先檢索有沒有開關機消息,有的話去執(zhí)行開關機,再檢索有沒有設模式。
2、??數(shù)據(jù)分多次連著到達
這是TCP編程要考慮的基本問題,理想的情況是APP發(fā)一條完整消息,wifi板收到一條完整消息。但在測試時發(fā)現(xiàn),對著APP點擊空調(diào)開關機鍵,不斷的開關開關,點個20次左右,APP上最后顯示是開空調(diào)最后是關,或者APP上最后是關空調(diào)最后是開(這個測試是不是很變態(tài)?)
分析log發(fā)現(xiàn)是最后一條控制消息分兩次到達了,tcp傳輸層是數(shù)據(jù)流傳輸,發(fā)送方的傳輸層不會管應用層傳下來是什么數(shù)據(jù),他只會把數(shù)據(jù)分裝成一個一個小包裹往外發(fā),接收方的傳輸層也不會管對方發(fā)來的數(shù)據(jù)里面具體是什么,收到多少就傳多少給應用層。
我們當時的TCP數(shù)據(jù)處理程序不完善,無法處理一條消息分兩次到達的情況,如果消息分兩次到達,第一次收到的片段不符合應用層協(xié)議格式就被丟棄掉了,第二次到達的也被丟棄掉了。
一個完善的tcp數(shù)據(jù)處理程序應該:
(1)最基本能處理一條完整到達的消息;
(2)好幾條消息一起收到,能一條一條處理完;
(3)好幾條完整消息+一段殘缺的消息片段,能把前面完整的一條一條處理完,再把殘缺的消息片段存入緩存,等著下次或下幾次收到剩下的消息片段組成一條完整消息再處理;
(4)只有一段殘缺的消息片段,存入緩存,與下次或者下幾次收到的消息組成完整消息再處理。
后來對程序做了完善,消息存在一個循環(huán)隊列里處理,bug解除了。
3、快速點擊APP時發(fā)現(xiàn)后面執(zhí)行變慢
還是上面那個變態(tài)的測試,不斷的點APP上的開關按鈕,測試的人反應說一開始空調(diào)反應很快,為什么點了10次以上之后,反應這么慢,也就是APP上已經(jīng)點完了,空調(diào)慢半拍還在那里開關開關好久,好像是在自動開關一樣。
檢查TCP線程,while主循環(huán)里面用了select,當select檢測到有數(shù)據(jù)到達這個socket時,去收數(shù)據(jù)。而select設置的超時時間是1s,也就是沒數(shù)據(jù)時,最多等待1s才往下走,有數(shù)據(jù)時馬上去收,后來把1s改成了500ms,明顯就快多了!
但我還是有疑問,按理說select函數(shù)并不會耽誤數(shù)據(jù)的處理和收發(fā)啊,因為他是有數(shù)據(jù)馬上返回告訴你有數(shù)據(jù),此時馬上去收,無數(shù)據(jù)等待一個timeout的時間返回,那就應該不管timeout時間設多長都沒關系才對啊?
4、??數(shù)據(jù)分兩次中間隔了一段時間才到
這是最近在wifi+zigbee網(wǎng)關上出現(xiàn)的一個bug,網(wǎng)關一頭是wifi連接服務器,一頭是zigbee接著很多子設備,比如開關、水浸、氣感等等,bug的現(xiàn)象是概率性出現(xiàn)場景命令無法執(zhí)行,比如開所有燈或者關所有燈這樣的場景命令,用戶在APP上點一個場景按鈕,消息下發(fā)到網(wǎng)關上。
分析log發(fā)現(xiàn),這條消息是分兩次到達了,兩次到達中間還隔了2秒,奇怪的是第二段數(shù)據(jù)到達時前面那一段數(shù)據(jù)被清除掉了,沒有存下來,但是大多數(shù)時候分兩次到達的消息都能正確處理,為什么單單這一次沒有處理好?這一次跟其他次有什么區(qū)別?
這一次的區(qū)別是,看到收到第一段消息后,發(fā)ping消息的時間到了,給服務器發(fā)了ping消息(這條消息是應用層的心跳消息,30s發(fā)一次,為了保持心跳以及偵測掉線,當發(fā)給服務器的ping消息5s沒收到服務器回復認為掉線了)然后過了2s才收到第二段消息。
搜索所有清除緩存的地方,發(fā)現(xiàn)就在發(fā)ping消息的地方把緩存清除了!所以造成了消息起那一段丟掉了沒有被正確處理,去掉這個清除動作多次測試沒有再出現(xiàn)這種情況。
這里發(fā)現(xiàn)另外一個問題是:這一次的ping消息沒有收到服務器回包,因此網(wǎng)關這邊判斷掉線了,收到的控制消息也沒有再去處理,應該怎么設計掉線的邏輯?
僅僅沒有收到心跳消息回包就認為掉線合理不? 此時我們關注的有用的控制消息是能正常收到的??!
所以應該對判斷掉線的邏輯做一些優(yōu)化:
(1)當沒收到ping消息回包但控制消息仍然能收到時不應該判斷成掉線,只要還能收到數(shù)據(jù)就不認為掉線;
(2)消抖處理,當連續(xù)幾次沒有收到ping消息(ping消息30s發(fā)一次)回包時才認為掉線。
5、??數(shù)據(jù)被意外清除
問題4改了后,提測后結果又出現(xiàn)了一次場景消息不執(zhí)行,前前后后測了兩百次出現(xiàn)一次,崩潰!憑直覺我覺得這是一個新bug!
分析log,發(fā)現(xiàn)這也是一個分兩次到達的消息沒有正確處理,第一次到達的數(shù)據(jù)總共有n條完整的消息+控制消息的前半段,看到最后有去把殘缺消息片段拷貝到緩存中的操作,但是當后半段消息收到時,緩存里打印出來卻沒有前半段消息!
拷貝字符串用的是memcpy這個庫函數(shù),要拷貝的字符串長度用的是strlen這個函數(shù)。
把這條出問題的消息再次用測試代碼運行起來,增加log,看代碼怎么跑的,看到確實有去處理前面那些一條一條的完整消息,問題在于,處理的時候直接把主循環(huán)用于接收socket數(shù)據(jù)的緩存指針傳進去了,有個地方要計算消息的MD5摘要值與消息中帶下來的MD5摘要值去比較,把字符串中某個位置的字符賦值成了0。
file:///C:\Users\Administrator.WIN-STED6B9V5UI\AppData\Local\Temp\ksohtml18176\wps7.png
這個操作是很危險的!直接導致了后面拷貝殘缺消息片段時strlen計算出來的長度是0,strlen、strstr、strcpy這類字符串處理函數(shù)都是遇到0就停止的。
直接把接收緩存指針傳進去,這種操作也不規(guī)范。
修改方法是,對賦值0那個函數(shù)傳進去的指針不再是用于接收socket數(shù)據(jù)的緩存指針,而是而是另外開辟緩存,把要處理的數(shù)據(jù)拷貝過去,再把新開辟的緩存指針傳進去。
6 、socket端口號問題
寫到這好累了,長話短說,與服務器連接過程如下:
(1)調(diào)用socket 函數(shù),創(chuàng)建一個tcp類型的socket;
(2)初始化自己的地址my_addr,類型為sockaddr_in,內(nèi)容包括端口號、類型、IP地址(如下圖);
file:///C:\Users\Administrator.WIN-STED6B9V5UI\AppData\Local\Temp\ksohtml18176\wps8.jpg
(3)調(diào)用bind,把socket 和my_addr綁定起來;
(4)初始化要連接的服務器地址svr_addr;
file:///C:\Users\Administrator.WIN-STED6B9V5UI\AppData\Local\Temp\ksohtml18176\wps9.jpg
(5)調(diào)用connect,連接服務器
第2步有一個特別要注意的是自己地址的端口號必須是一個不重數(shù),也就是說這次用的是2000,那么下次wifi板再次connect時(比如斷電上電再次connect)不能再用2000,可以是2001依次遞增或者其他。
那么為什么要這樣?因為服務器偵測wifi板掉線一般是沒有wifi板自身快的,wifi板去重連服務器時如果用的是原來的端口號,而服務器那邊還沒偵測出wifi板掉線,原來的那個端口號的tcp鏈接還在,資源沒有釋放,再用原來那個端口號建立新的鏈接肯定不會成功;另一種情況,wifi板斷電上電重新去連服務器,服務器肯定是不知道wifi板重啟了,還用原來的端口號去連也是連不上的,除非斷電很久等服務器偵測出wifi掉線再上電。
所以正確的做法是把端口號存入Flash里,每次用時從flash里取,用完更新這個值。
單片機方案都不會自己產(chǎn)生不重數(shù),因此需要自己操心存起來, 有些linux系統(tǒng)方案是底層自己會產(chǎn)生不重數(shù)不用自己操心。
7、??沒有 keep alive機制引發(fā)的問題
好累了,這個有點不記得了,改天完全記起來再補充。今天的分享就到這里 有疑問3250395686
評論
查看更多