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