1,udp丟包
困擾幾天的udp內網傳輸部分終於做通了,解決的關鍵就在於setsockopt的調用,設置接收緩沖。
遇到的問題是這樣的,主機端發送udp數據包:
應用層的包大小為1452byte大小,這樣拆包是根據以太網的MTU為1500字節而考慮的(當然外網狀態下並不一定就是以太網網絡,路由MTU可能更加小),因為在網絡層和傳輸層還有8byte的udp包頭和20byte的ip包頭,所以以太網幀大小為1452+8+20 = 1480byte。
主機端(linux)現在接了11路視頻數據,發送的數據量還是很大的,但經過測試,數據是可以發送出去的,發送端沒有問題。我在客戶端用一個線程專門接包,然后進行處理,可總是處理不過來,當連接路數比較多的時候,即碼流增大時,出現接收不過來的情況。開始以為是主機端問題,進行寫文件測試發現主機端完全可以承受11路數據的發送(udp數據包),而接收端對每個部分都進行了詳細測試,都沒有效率問題而影響接包的處理,最后將目光放在了接收緩沖的問題上,經過查證,windows程序默認的udp socket的接收和發送緩沖都是8kB, 而將接收緩沖調大后,馬上解決了丟包現象:
int n = 512*1024; setsockopt(m_hRcvSock, SOL_SOCKET, SO_RCVBUF, (const char*)&n, sizeof(n));
可見對於一般而言,8kB是足夠了,但是對於要接收大量數據時,默認的接收緩沖(udp)是不夠的。需要進行手工設置,否則會造成包的丟失從而數據錯誤。之所以一開始沒有想到這上面是因為我們原來的網絡是用tcp進行的視頻傳輸(暫時沒有對tcp的接收和發送緩沖進行查證),而tcp狀態下我們可以很好的在內網傳輸16路的實時視頻數據,故以為在發送和接收上udp和tcp一樣不存在瓶頸問題。后查得tcp是會進行流量控制的,下面是一段摘抄:
說到流量控制,不得不提到TCP的另一個重要概念-—窗口。窗口表示了接收主機能接收的最大數據量,並且,窗口大小是隨着主機資源和主機當前正在接收多少個傳輸數量而變化的。主機將窗口字段用於流量控制,也就是說,流量控制是TCP窗口的一個功能。TCP采用流量控制管理進入接收主機緩沖區的數據流量。如果發送主機傳輸數據的速度比接收主機處理數據的速度更快以至接收主機緩沖區已滿不能處理更多的數據時,則接收主機就會請求發送主機降低數據發送速度直到接收主機可以接收更多的數據為止;相反,如果接收主機能夠處理更多的數據,則會請求發送主機加快數據的發送速度,這就是流量控制的用途,它保證了數據在傳輸的過程中完整的傳送到接收主機。
可以看出由於tcp是基於連接的,所以其在傳輸過程中會犧牲很多來進行傳輸的保證,故即使速度下降也會保證接收端的有序和正確接收。而udp是非連接的,其發送后不進行任何處理在保證數據的傳輸,高效但無保障,一切檢驗和有序以及完整處理均需要應用層來完成(這讓人想起了RTCP)。
2,綁定失敗
還有一個setsockopt的選相是SO_REUSEADDR, 今天在綁定一個地址來進行偵聽的時候,處理上是每來一個連接,便偵聽其地址發送來的udp端口,由於要多次綁定,而開始時總遇到地址重復的錯誤,后來一查發現,同一個地址進行端口綁定,好像有一個時間間隔限制,該限制以內不能重復綁定,故我進行了以上的設置,就可以多次重復綁定了~
經過查證,我遇到的是地址使用錯誤的問題:
使用 bind API 函數來綁定一個地址(一個接口和一個端口)到一個套接字端點。可以在服務器設置中使用這個函數,以便限制可能有連接到來的接口。也可以在客戶端設置中使用這個函數,以便限制應當供出去的連接所使用的接口。bind最常見的用法是關聯端口號和服務器,並使用通配符地址(INADDR_ANY),它允許任何接口為到來的連接所使用。bind 普遍遭遇的問題是試圖綁定一個已經在使用的端口。該陷阱是也許沒有活動的套接字存在,但仍然禁止綁定端口(bind 返回EADDRINUSE),它由 TCP 套接字狀態 TIME_WAIT 引起。該狀態在套接字關閉后約保留 2 到 4 分鍾。在 TIME_WAIT 狀態退出之后,套接字被刪除,該地址才能被重新綁定而不出問題。
等待 TIME_WAIT 結束可能是令人惱火的一件事,特別是如果您正在開發一個套接字服務器,就需要停止服務器來做一些改動,然后重啟。幸運的是,有方法可以避開 TIME_WAIT 狀態。可以給套接字應用 SO_REUSEADDR 套接字選項,以便端口可以馬上重用。
同樣,我在每次UDP偵聽socket使用完畢后,使用closesocket將使用的socket清除,這樣手動釋放后,不需要再進行setsockopt的設置也可以重復綁定了。從此可看出,原本我創建的socket為局部的,但其釋放好像並不同與普通的c變量的釋放方式,故在函數下次調用時候,出現地址重復的錯誤,而手動清除是保險的。該錯誤在linux和windows平台均有,但好像windows再調用setsockopt后,socket接收有異常,偶爾接收不了UDP報文,而linux下沒有此種現象。
什么會導致udp丟包呢,我這里列舉了如下幾點原因:
1.調用recv方法接收端收到數據后,處理數據花了一些時間,處理完后再次調用recv方法,在這二次調用間隔里,發過來的包可能丟失。對於這種情況可以修改接收端,將包接收后存入一個緩沖區,然后迅速返回繼續recv。
2.發送的包巨大丟包。雖然send方法會幫你做大包切割成小包發送的事情,但包太大也不行。例如超過30K的一個udp包,不切割直接通過send方法發送也會導致這個包丟失。這種情況需要切割成小包再逐個send。
3.發送的包較大,超過mtu size數倍,幾個大的udp包可能會超過接收者的緩沖,導致丟包。這種情況可以設置socket接收緩沖。以前遇到過這種問題,我把接收緩沖設置成64K就解決了。
int nRecvBuf=32*1024;//設置為32K
setsockopt(s,SOL_SOCKET,SO_RCVBUF,(const char*)&nRecvBuf,sizeof(int));
4.發送的包頻率太快,雖然每個包的大小都小於mtu size 但是頻率太快,例如40多個mut size的包連續發送中間不sleep,也有可能導致丟包。這種情況也有時可以通過設置socket接收緩沖解決,但有時解決不了。
5.發送的廣播包或組播包在windws和linux下都接收正常,而arm上接收出現丟包。這個還不好解決,我的解決方法是大包切割成大小為1448的小包發送,每個包之間sleep 1毫秒,雖然笨,但有效。我這里mtu size為1500字節,減去udp包頭8個字節,減去傳輸層幾十個字節,實際數據位1448字節。
除此之外還可以試試設置arm操作系統緩沖:
//設置mtu size 1500最大
ifconfig eth0 mtu 1500
//查看接收緩沖最大和默認大小。
sysctl -A | grep rmem
//設置接收緩沖的最大大小
sysctl -w net.core.rmem_max=1048576
sysctl -w net.core.rmem_default=1048576
sysctl -w net.ipv4.udp_mem=1048576
sysctl -w net.ipv4.udp_rmem_min=1048576
6,局域網內不丟包,公網上丟包。這個問題我也是通過切割小包並sleep發送解決的。如果流量太大,這個辦法也不靈了。
總之udp丟包總是會有的,如果出現了用我的方法解決不了,還有這個幾個方法: 要么減小流量,要么換tcp協議傳輸,要么做丟包重傳的工作