CocoaAsyncSocket UDP收发数据包大小限制
之前因为要解决项目的IPv6问题,去CocoaAsyncSocket逛了一下,看到一个比较有意思的issue —— GCDAsyncUDPSocket can not send data when data is greater than 9K? #535。问题很简单,使用UDP传输图片,可是当图片大小超过9K的时候无法发送。
开始我想的很简单。因为MTU的限制,所以9K大小的数据报肯定被分片了。UDP本身是不可靠的传输,任何一片数据的遗失都会丢弃该数据包。一般来说,MTU的大小是1500,那么9K大概分为6-7个包。所以还是有很大可能是数据报丢失的问题的。
MTU
但是这个哥们回复我了,他说查了一些资料,在stackoverflow上找到了一篇回答。set max packet size for GCDAsyncUdpSocket。这篇回答并没有提到iPhone只是说了在Mac上的操作,通过终端命令修改mac的最大缓冲区大小。他修改以后在模拟器上可以收发了,但是iPhone上仍然不知道怎么办。
为此我在源码里搜了一下关键字max,确实搜到了一个地方提到了maxSize这样一个东西。
max4ReceiveSize = 9216;
max6ReceiveSize = 9216;
相关的issue是[CRITICAL] Don't trust GCD to give accurate UDP packet sizes.#222。作者认为dispatch_source_get_data()
返回的数据是不可靠的,如果数据过大它会默不作声的对你的数据做一个截断,大小是9216。所以他pr上去我发现的maxSize那一段的代码。我去Apple那里查了一下dispatch_source_get_data()
的文档,人没说有这么一茬啊。
我自己做了一个简单的测试。发送一段小于9216的数据,没有问题正常发送;如果数据超过9216,我用wireshark抓包发现是没有UDP的包发出的。(后来发现抛出了Message too large的错误)。
图文无关对比stackoverflow的回答,我觉得问题应该就是出在iPhone的缓冲区大小的设置了。一般来说UDP的最大数据报大小是65535(IPv4环境下,因为在UDP数据包的首部里,使用16bit的字节标示数据报的长度。所以最大长度就是2^16 - 1 = 65535),但是因为iPhone设置了收发缓冲区的大小9216,导致数据收发出现问题了。(发送数据包太大就是Message too large,接受数据包太大就会对数据截断)。
我尝试找了很多地方,google和stackoverflow,去找设置的代码,在找到结果之前我还尝试联系了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属性来允许使用者自行设定最大发送数据包大小。(因为IPv4和IPv6对于UDP数据包的最大大小是不同的,但是创建socket之前我们无法判断当前的网络环境,所以按照IPv4的标准设置)之前他们对dispatch_source_get_data()
的误解我也没没有修改他们的代码了。之前你即使设置了```
max4ReceiveSize = 9216;max6ReceiveSize = 9216;
``的大小超过9216,还是只能收到9216,添加了上面的代码以后这个属性才算是真正的最大接受数据包大小了。
但是之后我得到了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.
其实这也是我之前想说的。虽然我们修改了缓存区的大小,但是UDP本身作为一个不可靠的传输,分片后的数据很容易因为其中一片的遗失而全部丢弃。虽然从及时性的考虑上很多时候UDP确实是一个比TCP好的选择,但是过大的数据选择UDP还不是一个很好的选择。分包太多太容易丢失了。我想这大概是默认最大收发数据大小是9216而不是65535的原因。
这段话我写入了注释,在帮忙写完了和我添加的代码相关的单元测试以后,pr终于被merge进主支了。
问题告一段落~