之前因為要解決項目的IPv6問題,去CocoaAsyncSocket逛了一下,看到一個比較有意思的issue —— GCDAsyncUDPSocket can not send data when data is greater than 9K? #535。問題很簡單,使用UDP傳輸圖片,可是當圖片大小超過9K的時候無法發(fā)送。
開始我想的很簡單。因為MTU的限制,所以9K大小的數(shù)據(jù)報肯定被分片了。UDP本身是不可靠的傳輸,任何一片數(shù)據(jù)的遺失都會丟棄該數(shù)據(jù)包。一般來說,MTU的大小是1500,那么9K大概分為6-7個包。所以還是有很大可能是數(shù)據(jù)報丟失的問題的。
但是這個哥們回復我了,他說查了一些資料,在stackoverflow上找到了一篇回答。set max packet size for GCDAsyncUdpSocket。這篇回答并沒有提到iPhone只是說了在Mac上的操作,通過終端命令修改mac的最大緩沖區(qū)大小。他修改以后在模擬器上可以收發(fā)了,但是iPhone上仍然不知道怎么辦。
為此我在源碼里搜了一下關(guān)鍵字max,確實搜到了一個地方提到了maxSize這樣一個東西。
max4ReceiveSize = 9216;
max6ReceiveSize = 9216;
相關(guān)的issue是[CRITICAL] Don't trust GCD to give accurate UDP packet sizes.#222。作者認為dispatch_source_get_data()
返回的數(shù)據(jù)是不可靠的,如果數(shù)據(jù)過大它會默不作聲的對你的數(shù)據(jù)做一個截斷,大小是9216。所以他pr上去我發(fā)現(xiàn)的maxSize那一段的代碼。我去Apple那里查了一下dispatch_source_get_data()
的文檔,人沒說有這么一茬啊。
我自己做了一個簡單的測試。發(fā)送一段小于9216的數(shù)據(jù),沒有問題正常發(fā)送;如果數(shù)據(jù)超過9216,我用wireshark抓包發(fā)現(xiàn)是沒有UDP的包發(fā)出的。(后來發(fā)現(xiàn)拋出了Message too large的錯誤)。
對比stackoverflow的回答,我覺得問題應該就是出在iPhone的緩沖區(qū)大小的設置了。一般來說UDP的最大數(shù)據(jù)報大小是65535(IPv4環(huán)境下,因為在UDP數(shù)據(jù)包的首部里,使用16bit的字節(jié)標示數(shù)據(jù)報的長度。所以最大長度就是2^16 - 1 = 65535),但是因為iPhone設置了收發(fā)緩沖區(qū)的大小9216,導致數(shù)據(jù)收發(fā)出現(xiàn)問題了。(發(fā)送數(shù)據(jù)包太大就是Message too large,接受數(shù)據(jù)包太大就會對數(shù)據(jù)截斷)。
我嘗試找了很多地方,google和stackoverflow,去找設置的代碼,在找到結(jié)果之前我還嘗試聯(lián)系了Apple的工程師。在他們回復我之前,我終于找到了解決方案
/**
* The theoretical maximum size of any IPv4 UDP packet is UINT16_MAX = 65535.
* The theoretical maximum size of any IPv6 UDP packet is UINT32_MAX = 4294967295.
*
* The default maximum size of the UDP buffer in iOS is 9216 bytes.
*
* This is the reason of #222(GCD does not necessarily return the size of an entire UDP packet) and
* #535(GCDAsyncUDPSocket can not send data when data is greater than 9K)
*
*
* Enlarge the maximum size of UDP packet.
* I can not ensure the protocol type now so that the max size is set to 65535 :)
**/
int maximumBufferSize = 65535;
status = setsockopt(socketFD, SOL_SOCKET, SO_SNDBUF, (const char*)&maximumBufferSize, sizeof(int));
if (status == -1)
{
if (errPtr)
*errPtr = [self errnoErrorWithReason:@"Error setting send buffer size (setsockopt)"];
close(socketFD);
return SOCKET_NULL;
}
status = setsockopt(socketFD, SOL_SOCKET, SO_RCVBUF, (const char*)&maximumBufferSize, sizeof(int));
if (status == -1)
{
if (errPtr)
*errPtr = [self errnoErrorWithReason:@"Error setting receive buffer size (setsockopt)"];
close(socketFD);
return SOCKET_NULL;
}
在1988行處添加這段代碼即可。
在后來的pr里,我參考了
max4ReceiveSize = 9216;max6ReceiveSize = 9216;
的方案,添加了maxSendSize屬性來允許使用者自行設定最大發(fā)送數(shù)據(jù)包大小。(因為IPv4和IPv6對于UDP數(shù)據(jù)包的最大大小是不同的,但是創(chuàng)建socket之前我們無法判斷當前的網(wǎng)絡環(huán)境,所以按照IPv4的標準設置)之前他們對dispatch_source_get_data()
的誤解我也沒沒有修改他們的代碼了。之前你即使設置了```
max4ReceiveSize = 9216;max6ReceiveSize = 9216;
``的大小超過9216,還是只能收到9216,添加了上面的代碼以后這個屬性才算是真正的最大接受數(shù)據(jù)包大小了。
但是之后我得到了Apple工程師的回復:
You’re approach this the wrong way. Given that a typical link MTU is 1500 bytes, a large UDP datagram will have to be fragmented, and that’s both expensive and risky (if one fragment goes missing, the entire datagram is lost). You are much better off sending a large number of smaller UDP datagrams, preferably using a path MTU algorithm to avoid fragmentation.
其實這也是我之前想說的。雖然我們修改了緩存區(qū)的大小,但是UDP本身作為一個不可靠的傳輸,分片后的數(shù)據(jù)很容易因為其中一片的遺失而全部丟棄。雖然從及時性的考慮上很多時候UDP確實是一個比TCP好的選擇,但是過大的數(shù)據(jù)選擇UDP還不是一個很好的選擇。分包太多太容易丟失了。我想這大概是默認最大收發(fā)數(shù)據(jù)大小是9216而不是65535的原因。
這段話我寫入了注釋,在幫忙寫完了和我添加的代碼相關(guān)的單元測試以后,pr終于被merge進主支了。
問題告一段落~