首先分析一下,局域网两台机器,A 发一个 TCP 包到 B 需要多久,如图:

从上图可以看出, SYN 从 0 秒开始发出,到 B 收到这个 SYN ,B 再立即发 SYN+ACK ,B不会做额外的等待工作,TCP 实现里面 ,B收到 SYN 就会尽可能快地发 SYN + ACK。
再到 A 收到 这个 SYN+ACK 过了 0.0008 秒,也就是 0.8 毫秒,非常快,也就是局域网 一个 RTT (round trip time)时间是 0.8毫秒,也就说 A 发数据到 B,只需要 0.4 毫秒,0.4毫秒 B就能收到数据。
再分析一下 TCP 的重试机制。3次握手成功之后,调用 TCP 的 socket api 函数 send()
发送数据,send()
会立即返回成功,不需要等服务器发回来确认 ACK,TCP 协议栈的数据发送实际上是异步的。
如果 send()
发送的数据一直没收到 ack ,会出现什么情况,咱们演示一遍。先说下操作流程,如下:
TCP client 跟 服务器 三次握手成功之后,开始发送数据,发 data-1 ,服务器返回 ack-1。但是当 发 data-2 的时候,把中间的路由器网线拔开了。这时候 服务器收不到 data-2 ,也不会发 ack-2。这时候 client 就会不断重发 data-2。
本次演示使用 PHP 写 TCP client,代码如下:
<?php
$fp = fsockopen("192.168.0.123", 5188, $errno, $errstr, 30);
if (!$fp) {
echo "$errstr ($errno)<br />\n";
} else {
sleep(3);
fwrite($fp, "data-1");
sleep(30);
fwrite($fp, "data-2");
printf("发送 data-2 时间 %s \r\n",time());
while (!feof($fp)) {
echo fgets($fp, 128);
}
printf("收到链接断开 时间 %s \r\n",time());
echo("end \r\n");
fclose($fp);
}
?>
通信过程如图:


上图的流程是这样的,从 第 8个 数据包 开始 发 data-2
,由于网线已经断开,所以一直没收到 ack-2 ,所以 TCP 内部 从 33.3 秒后进行第一次重试,一共进行了 4 次 重试,然后到 ARP 数据包发送,TCP 客户端从 重试 到 确认链接断开用了 19 秒。
演示完毕,项目代码下载:百度网盘,提取码:wlus
上面的重试,其实就是 TCP 协议本身的可靠性之一。假如网络波动,中间路由断开 1~2 秒,是不会影响 TCP 数据传输,因为 TCP 内部会重试 4 秒左右。这个 RTO (重试时间)是动态计算的。
但是这个机制,也给 需要非常实时的数据传输 带来了局限性,例如视频传输,要做到 50 毫秒以下,中间路由断开 1~2 秒,TCP client 内部缓存的重试数据其实是需要快速丢弃,不要再重试。而且 TCP 里面的重试机制, RTO,在调用层是没法控制的。
另一个方面需要考量,数据包的顺序到达,以及乱序到达。我们知道 IP 层数据包是乱序到达,而且 IP 层 的数据包如果 大于 MTU 会进行 数据包分片。但是 由于 TCP 的socket 跟 UDP 的 socket 都对 IP 层做了封装,使用 TCP 或者 UDP 不需要考虑 IP 数据包分片。
但是 由于 UDP 没有对数据包 做 顺序控制,也就是说 client 发送 data-1 , 随后 client 马上发送 data-2 ,这种情况下,如果是使用 TCP 的socket,server 从 recv()
函数第一个拿到的肯定是 data-1
数据包,第二个是 data-2
。
但是如果使用的是 UDP 的 socket,server 从 recv()
函数第一个拿到的可能是 data-1
也可能是 data-2
。因为 UDP 没对顺序进行控制传递给上一层。
但是对于 实时数据传输来说 ,这种顺序控制,是否有必要,我个人觉得没必要。画个流程图讲解一下:

上面的流程协议是基于 UDP 实现的,自己实现 ACK。
从上图可以看出,播放器端的机器已经 收到了 I-1 ,P-2,P-3, I-2帧, 一共4帧。
I-1 ,P-2,P-3 这3 帧已经通过 udp 发送ack 告诉客户端收到了,P-4 帧由于网络波动服务器端没收到,所以也没发ack,但是 I-2 帧已经到了服务器端。
这时候可以这样设计,如果 到了要播放 P-4 帧的时候,P-4帧还没到,就直接跳过 P-4 直接拿 I-2 帧播放。同时发 I-2 的ack包,告诉 对端,I-2 以前的数据包不用重试了,已经过了播放时间。
这个方法,在TCP 里没法实现,因为 TCP 内部会做 数据包排序,用 recv()
函数 必须先拿到 P-4 再拿 I-2帧,虽然 I-2 帧已经在TCP 协议栈内内存里,但是由于他强制控制顺序,他就是不传递给调用层。
总结,用 UDP 开发实时通信的优势。
1,UDP 没有重试机制,所以可以根据实时性要求,自己实现灵活的重试机制。
2,UDP 没有内部没有强制 对数据包进行排序,可以自己实现数据包排序,灵活跳过某些数据帧。
TODO:写一个学习用的传输协议。
RVCP real-time video communicate protocol 实时视频传输协议 RACP real-time audio communicate protocol 实时音频传输协议
©版权所属:知识星球:弦外之音,QQ:2338195090。
由于笔者的水平有限, 加之编写的同时还要参与开发工作,文中难免会出现一些错误或者不准确的地方,恳请读者批评指正。如果读者有任何宝贵意见,可以加我微信 Loken1。