TCP协议原理(二)TCP数据传输

TCP数据传输

TCP是字节流传输

之前的文章我们讨论过,TCP是基于字节流的传输协议,无论是发送还是接受,内核都为TCP连接维护了缓冲区。应用程序使用系统调用send() recv()进行读写,实际上都是对于tcp连接的缓冲区进行操作,而数据实际接受与发送是内核来处理。
tcp缓冲区
我们使用tcpdump来观察一个echo服务器的例子来理解缓冲区。
测试代码是之前介绍reactor的样例,这个echo服务器的功能是处理tcp连接,读取客户端消息并返回客户端发送的内容。
reactor实现的echo服务器样例

#三次握手
client > server.distinct: Flags [S], seq 192037957, win 64240, options [mss 1460,nop,wscale 8,nop,nop,sackOK], length 0
server.distinct > 10.1.45.14.63106: Flags [S.], seq 4187958664, ack 192037958, win 14600, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
client > server.distinct: Flags [.], ack 1, win 8212, length 0
# 数据交互
client > server.distinct: Flags [P.], seq 1:1921, ack 1, win 8212, length 1920
server.distinct > 10.1.45.14.63106: Flags [.], ack 1921, win 137, length 0
server.distinct > 10.1.45.14.63106: Flags [P.], seq 1:1025, ack 1921, win 137, length 1024
client > server.distinct: Flags [.], ack 1025, win 8208, length 0
server.distinct > 10.1.45.14.63106: Flags [P.], seq 1025:1921, ack 1921, win 137, length 896
client > server.distinct: Flags [.], ack 1921, win 8212, length 0
# 挥手断开
client > server.distinct: Flags [F.], seq 1921, ack 1921, win 8212, length 0
server.distinct > 10.1.45.14.63106: Flags [F.], seq 1921, ack 1922, win 137, length 0
client > server.distinct: Flags [.], ack 1922, win 8212, length 0
  • 报文1-3:是三次握手建立连接的过程,上一篇文章中讨论过不再介绍。
  • 报文4:报文,客户端向服务器发送了长度为1920的数据报,seq为 1:1921
  • 报文5:是服务器向客户端确认收到 1921之前的报文。这个时候,数据在写缓冲去中,epoll通知应用层有数据来了。
  • 报文6-7: 应用层通过recv来读。测试样例中,应用层的缓冲区大小有限制不是全部读完,所以只读取了1024的数据,然后发送报文6。 报文7是客户端确认收到。
#define BUFF_LENGTH 1024
int len = recv(fd_, buffer_, BUFF_LENGTH, 0);
  • 报文8-9:第二次读缓冲区剩余896部分,然后发送。客户端确认收到。
  • 报文10-12: 客户端主动断开连接。挥手的过程,其中服务器的取人断开与FIN报合并发送所以只有"三次挥手"

TCP连接的交换数据

TCP 报文按照长度可分为两种,交互数据和成块数据。

TCP交互数据流

交互数据通常包含很少字节,要求实时性很高,例如ssh服务。下面使用tcpdump监听使用ssh协议实行ls命令抓取的报文。

tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on bond0, link-type EN10MB (Ethernet), capture size 65535 bytes
1. client > server.ssh: Flags [P.], seq 2254020529:2254020569, ack 3185988183, win 8208, length 40
2. server.ssh > client: Flags [P.], seq 1:41, ack 40, win 223, length 40
3. client > server.ssh: Flags [.], ack 41, win 8208, length 0
4. client > server.ssh: Flags [P.], seq 40:80, ack 41, win 8208, length 40
5. server.ssh > client: Flags [P.], seq 41:81, ack 80, win 223, length 40
6. client > server.ssh: Flags [.], ack 81, win 8208, length 0
7. client > server.ssh: Flags [P.], seq 80:120, ack 81, win 8208, length 40
8. server.ssh > client: Flags [P.], seq 81:121, ack 120, win 223, length 40
9. server.ssh > client: Flags [P.], seq 121:625, ack 120, win 223, length 504
10. client > server.ssh: Flags [.], ack 625, win 8212, length 0
  • 报文1 是客户端发给服务器,内容是一个字节的"l",由于ssh是加密协议,还有其他内容所以长度是40
  • 报文2 是服务器对报文1的确认,同时回显字母"l"
  • 报文3 是客户端对于报文2的确认
  • 报文4-6 是字母"s”通用的处理过程
  • 报文7 是客户端输出回车符
  • 报文8 是服务器确认收到回车符,同时回显
  • 报文9 是服务器返回的客户端查询的目录内容
  • 报文10 是客户端对报文8,9的确认

以上过程中客户端对于服务器返回数据发送的确认报文 3,6,10都不携带任何数据长度,服务器返回的每次发送的确认报文都包含他需要发送的数据。这种机制叫延迟确认,既服务器收到报文后不立即确认,而是等一下看是否有数据需要发送,有的话一起发送,因为服务器处理速度快,这样可以减少报文数量。而客户端输入速度明显慢于处理速度,所以客户端总是收到消息立刻确认。之前断开连接"三次挥手"也是延迟确认。
在广域网中交互数据流可能有很大延迟,且交互数据的小tcp报文一般会有很多可能导致拥塞。解决这个问题的一个办法是使用Nagle算法。Nagle算法要求一个连接通信两端在任意时刻最多只能有一个未确认的报文,在该报文未确认之前,不能发送其他报文。在等待确认的过程中收集本端需要发送的微量数据,确认到来时用一个报文全部发送。这样可以减少微小报文的数量,同时具备自适应性,确认越快数据发送也越快。

TCP块数据流

下面的案例是用vsftp传输一个大文件。在客户端上使用tcpdum观察现在流程。vsftp使用21端口监听连接,每一个下载任务会新建一个socket进行传输,以下节选了其中一段。

0. client > server: Flags [.], ack 33017, win 271, options [nop,nop,TS val 2947959508 ecr 195320532], length 0
1. server > client: Flags [.], seq 33017:35913, ack 1, win 114, options [nop,nop,TS val 195320532 ecr 2947959508], length 2896
2. server > client: Flags [.], seq 35913:37361, ack 1, win 114, options [nop,nop,TS val 195320532 ecr 2947959508], length 1448
3. server > client: Flags [.], seq 37361:38809, ack 1, win 114, options [nop,nop,TS val 195320532 ecr 2947959508], length 1448
4. server > client: Flags [.], seq 44601:46049, ack 1, win 114, options [nop,nop,TS val 195320532 ecr 2947959508], length 1448
5. client > server: Flags [.], ack 38809, win 226, options [nop,nop,TS val 2947959509 ecr 195320532,nop,nop,sack 1 {44601:46049}], length 0
6. server > client: Flags [.], seq 38809:43153, ack 1, win 114, options [nop,nop,TS val 195320532 ecr 2947959508], length 4344
7. client > server: Flags [.], ack 43153, win 193, options [nop,nop,TS val 2947959509 ecr 195320532,nop,nop,sack 1 {44601:46049}], length 0
8. server > client: Flags [.], seq 43153:44601, ack 1, win 114, options [nop,nop,TS val 195320532 ecr 2947959508], length 1448
9. server > client: Flags [.], seq 46049:47497, ack 1, win 114, options [nop,nop,TS val 195320532 ecr 2947959508], length 1448
10. server > client: Flags [.], seq 51841:53289, ack 1, win 114, options [nop,nop,TS val 195320532 ecr 2947959508], length 1448
11. client > server: Flags [.], ack 47497, win 160, options [nop,nop,TS val 2947959509 ecr 195320532,nop,nop,sack 1 {51841:53289}], length 0
  • 报文0是前一次传输的确认
  • 报文1-4位服务器向客户端发送数据报,传输文件,其中报文3-4并不连续。其中38809-44601未收到,可能是丢包后者是先后顺序不同还没收到。
  • 报文5是对报文1-4的确认,其中sack 1 {44601:46049}代表开启了选择重传功能,表示这一段已经收到但是未确认。这样服务器如果需要重传的时候就可以跳过这一段。
  • 报文6,7是收到了38809到43153的报文,同时发送确认
  • 报文8 是同步43153到44601报文,到此为止前边选择重传的内容补充完整
  • 报文9和10是新的一段数据同步
  • 报文11是截止报文9的确认,同时又发生了报文缺失的情况,需要选择重传同步。

通过上面的观察我们可以发现,传输大量数据块的时候,发送方会连续发送多个报文段,接收方可以一次性确认。同时tcp报文到达可能是乱序可能是丢失的,接受端可以使用选择重传功能来通知发送端当前已经接受但是为确认的报文段。来帮助发送端确认需要重传的部分。
既然发送端可以连续发送,那么发送端收到一次确认后还可以发送多少数据如何同步。这个根据接收通告窗口决定。报文0是通知发送端还能接受271*128(本案例窗口扩大因子为7)字节数据, 而在报文5中窗口大小变为226,可以接受的数据量变少了。说明接受方的缓冲区有数据未被客户端读取停留。

TCP 超时重传

TCP为每个报文段委会一个重传定时器,在第一次发送时启动。超时未收到应答将重传并重置定时器。每次重传超时时间如何选择,重传次数就是重传的策略。
一种常见的策略是和重连策略类似,每次重传时间加倍,每次均失败的情况下底层的IP与ARP开始接管。ip接管前重传次数以及放弃连接前重传次数可以通过内核配置修改。

拥塞控制

TCP模块还有一个任务,就是提高网络利用率,降低丢包率,保证网络资源对于每条数据流的公平性,这就是拥塞控制。拥塞控制最终控制的是发送端向网络中一次性写入的数据量,我们称为 SWND (SendWindow)。SWND 决定发送端连续发送TCP报文的数量,在讨论时他的单位是发送端报文的最大长度一般成为SMSS(Sender Maximum Segment Size),大多情况下等于MSS。

拥塞控制就是调节 SWND 的过程,SWND 过大写入网络数据过多,接收端来不及接受就会拥塞,SWND过小则无法充分利用网络资源,一个显著的体验就是延迟大。
SWND 的大小可以通过接受通过窗口 RWND 来控制,但显然不足以解决问题,所以引入了一个成为拥塞窗口CWND(Congestion Window)的状态变量。

SWND 是实际发送到互联网中的数量,是RWNDCWND 当中较小的一个值。RWND大小是发送端决定, CWND大小是根据上一次SWND大小以及当前网络状况例如RTT是否丢包等计算得出。所以 SWND 大小是通过一个闭环的反馈机制调整控制

下面介绍拥塞控制中的四个机制

慢启动与拥塞避免

慢启动

TCP连接建立好之后,CWND 被设置为初始值 IW, 大小为2-4个SMSS。之后每收到接收端一个确认,CWND按照 CWND+= min(N, SMSS) 的规则, 其中N是此次确认中包含的之前未被确认的字节数,也就是本次新增确认的字节数。
方便演示,我们假设 IW为1SMSS, 所有确认报都正常收发。

  • 建立连接后 CWND值为1, 发送端在第一个 RTT发送一个报文同时也会收到一个ack确认报时。第一个 RTT结束 , CWND值为2
  • 在第二个 RTT 内,此时CWND为2, 发送端将发送两个报文。正常情况下也会收到2个确认,CWND增加两次,此时为4
  • 同理第三个 RTT 过后 CWND增长为8
    在这里插入图片描述
    总结:
  • 在单个 RTT 内,没收到一个确认报 CWND增加1, 线性增长。
  • 每经过一个 RTTCWND翻倍,指数增长。

慢启动的目的是为了在启动时不知道网络状况如何,使用一种迅速且平滑的方式增加 CWND

拥塞避免

指数型的增长必将导致CWND快速膨胀,最最终导致拥塞。因此定义了另一个状态变量,慢启动门限(slow start threshlod size, ssthresh)。当 CWND超过这个值, 进入拥塞避免阶段。
拥塞避免算法是的 CWND 按照线性的方式增加,减缓扩大趋势。下面是两种实现方法

  • 每个RTT内不论收到多少确认,只进行一次CWND+= min(N, SMSS)
  • 每次收到一次确认报文按照 CWND+= SMSS*SMSS/CWND

对于方法2, 假设当前 CWND 是 k个SMSS, 单个RTT 内每收到一次确认 CWND 增加 1/k个SMSS。 一个 RTT 结束收到k个确认,CWND增加1。

超时导致的拥塞

当发生超时的情况,触发重传定时器,认为是发生了拥塞。这个使用将使用慢启动的方式进行拥塞控制。

无论是慢启动还是拥塞避免阶段,在发生超时重传时将会对 ssthresh 进行调整。

  • 调整慢启动到拥塞控制的切换阈值: ssthresh = max(FlightSize / 2, 2*SMSS)
  • 调整当前CWMD使其小等于SMSSCWMD <= SMSS

其中FlightSize是已发送但未确认字节数,一般等于发生拥塞时的CWMD大小,一般情况下远大于2倍SMSS, 所以一般可以认为调整为1/2的拥塞时 CWMD大小。之后CWMD一般重置为1个SMSS大小,一定是小于2倍SMSS的,所以拥塞控制后会再次进入慢启动阶段。

慢启动与拥塞控制
上图演示了慢启动,拥塞避免,还有发生拥塞之后的调整过程。其中横坐标为RTT,纵坐标为SMSS
可以观察到 CWND在慢启动阶段指数型增长,在第一次达到ssthresh之后开始拥塞避免阶段,线性上升。在增长到110时,发生拥塞,开始调整, ** ssthresh**变为55,再次进入慢启动阶段。

重复ack导致的拥塞

有很多情况会导致发送端收到重复 ack 报,比如报文段丢失,或者接收端乱序重拍等。一般认为如果发送端收到3个重复的确认报文,就认为拥塞发生。如果说所有拥塞都用之前的慢启动方式来处理,对于BDP大的网络来说使得带宽利用率低。所以针对不同的丢包要有不同的处理方式,由于重复ack引起的重传,这个时候使用快重传和快速恢复的算法来处理。

快重传

快重传算法要求接收方每收到一个失序的报文就立刻发出重复的确认,让发送端尽早知道有报文没有到达,而不是等待自己发送数据时才捎带确认。而接收端在收到连续三次重复的 ack 报文就立刻重传缺失报文,不必等待重传计时器到期。
观察下图,接收端在m3丢失之后接收到m4,m5,m6在接受后都立刻发送重复的确认ack2,发送端收到三个重复的确认m2后立刻重发m3
tcp快重传

快恢复

快恢复算法是配合快重传使用,在发送端触发连续三次相同 ack报时。

  1. 重新计算ssthresh,计算公式同上。ssthresh = max(FlightSize / 2, 2*SMSS) 基本可以认为 ssthresh是当前CWND的一半。
  2. 不同于慢启动,重置CWND为初始值,而是使用新的ssthres设置。其中3表示有收到的三个数据报 CWND=ssthresh + 3*SMSS
  3. 重传丢失的报文
  4. 之后每收到一个重复的ack确认, CWND+=SMSS
  5. 收到新的数据的ack时,设置 CWND=ssthresh
  6. 进入拥塞避免阶段
    在这里插入图片描述
    根据对比图可以看出,相对于慢启动的重头再来,快速恢复的主要思想就是还能收到确认报说明网络状况没有出现超时这么糟糕,调整ssthresh后然再次进入拥塞控制阶段。

总结

本文归纳介绍TCP协议在数据传输方面的处理方案。首先明确了TCP协议是基于字节流的传输,发送接受端都有缓冲池。之后介绍了TCP协议传输两种数据,一种是要及时响应的交互数据流,一种是延迟响应的大文件传输。最后介绍TCP协议的异常处理机制,以及进行拥塞控制的四种算法。

参考书籍

《Linux高性能服务器编程》

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值