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