目录
前言
一、TCP报头格式
1.首部长度
2.窗口大小
3.序号与确认序号
4.标志位
4.1 PSH
4.2 RST
5.紧急指针
6.TCP检验和
二、超时重传
三、连接管理机制
四、滑动窗口
五、拥塞控制
六、延迟应答
七、为什么TCP这么复杂?
前言
前面我们学习了TCP协议套接字的使用,了解了使用TCP的操作。但当时是在应用层,我们仅仅是浮于表面进行了初步的使用,今天我们来深入的理解TCP协议是如何实现的。为何面向字节流?为何是可靠的?
一、TCP报头格式
我们先来看看TCP报头字段,跟UDP一样,有源端口和目的端口,都占16位。
TCP协议本身就是一个结构化的字段,操作系统将这些字段描述起来,以便后续处理。
无论什么协议,都必须要解决报头如何和有效载荷分离的问题
带着这个问题,我们先来看一下4位首部长度
1.首部长度
不带选项的TCP报头有20字节,但是带上了可选的选项后,长度就不确定了,因此TCP报头里也得有描述报头长度的字段,这个字段就是4位首部长度。
但是4位最多只能表示0-15。因此约定好 首部长度*4字节 = 真实长度 。因此最多TCP报头可以有60字节。也就是选项最多有40字节。
那么现在我就能计算出报头的长度,让报头和有效载荷分离也就很简单了。
2.窗口大小
我们先来理解一下TCP通信的过程
- TCP有发送缓冲区,也有接受缓冲区,他们互不干扰,因此数据发生与接受是全双工的。
- 但是如果对方的接收缓冲区满了,那么我的数据在send之后,也不能直接发送到对方,而是在我自己的发送缓冲区的进行暂存。
- 如果自己的发送缓冲区也满了,那么该进程就会被阻塞。(等待条件满足再继续执行)
那么我们怎么知道对方的缓冲区还能存放多大的数据呢?
这就要通过TCP报头字段——窗口大小来确定。
窗口大小是用来流量控制的字段,我们都知道TCP三次握手,客户端向服务端发送SYN,服务器回给客户端SYN与ACK,客户端再给服务器发送ACK。
但是这个过程中每一次握手都要进行完整的报文发送,发送的内容就必须得有窗口大小,在握手时进行协商,商量窗口大小是多大。
比如我发送给对方的窗口大小为1000,代表我最大能接受1000字节的数据。
3.序号与确认序号
在理解序号之前,我们得先来学习TCP的确认应答机制与数据发送模式。
TCP链接的双方是对等的,为保证可靠性,当一端向另一端发送数据时,另一端必须向当前端回复ACK。
TCP做不到百分百的可靠通信,因为最后一条应答消息ACK不会有应答。
但是如果发送方收到了应答,那证明上一条消息对面一定能收到,这样就保证了可靠性。
但是这种发送方式虽然能保证每一条数据对方一定能收到(发送方一定时间内没收到ACK就重发),但是效率很低下,因为每次都要对面ACK后我再发送数据,是串行的。
因此,发送方可以一次性的发送批量报文,接收方每一次收到报文都向对方发送ACK,这样能保证数据成功发送的同时,还能保证效率。
但是随机也引发了一些问题,比如TCP是可靠的,有序就是可靠的一环,每一个报文在网络中传输速度可能都不一样,你无法保证你的接受顺序也是发送顺序。因此报文中的字段——序号和确认序号就能发挥作用了。
序号的真实值会在TCP三次握手时进行协商,双方确定一个随机序号,从这个随机序号+1开始进行发送!!!
- 发送方依次发送的报文,TCP会帮你按顺序定好,接收方接受到的报文,按照序号进行排序,就能保证接受顺序是有序的了。
- 接收方收到报文后,也需要告知发送方,我收到了多少多少号报文,因此在确认序号里填写收到的序号,发送方就知道你收到了,保证了可靠。
- 但是确认序号设计的是历史上我已经全部收到了发送方的哪些报文,比如你发送的是100,200,300,400报文,我收到了100,我确认序号就填写101(向发送方表面。直接从这个序号开始发,你不用再计算了),收到200确认序号填201,依次类推。如果先收到200,确定序号就填1(表示我收到了一个报文,但100还没收到),再收到了100,确认序号就需要填201。发送方也就知道,你200之前的报文全部接收到。
- 这样设计有什么好处呢?我们暂时不考虑发送方发送失败的问题,假如都发送成功,接收方也都返回了ACK,ACK却丢失了101,201,只接收到了301,401。那我发送方也懂了,你400之前的全收到了。也就是允许少量应答丢失(只要最后一个应答不丢失就行),不需要再补发了。
刚才我们提到的情况,似乎接受方应答时并没有用到序号,只用到了确认序号,但是TCP是全双工的,你可以给我发,我也可以给你发,我在应答你的时候,也可以像你发送数据,这样一个报文就可以干两份活了,这也就是捎带应答,因此序号和确认序号要分开来。
小总结:TCP保证可靠性,但与此同时也会进行各种提高效率的设定。
同时,发送缓冲区本质上就是一个比较大的字符数组。
比如这个我要发0,1,2,3,那么我在发送TCP的序号字段中就填3(单位是字节,这里不严谨,忽略char只是1位)。那我接收方接收到报文,发现是3,就发送确认序号里面是4,代表让发送方不用再计算了,从4开始发就好。
TCP报文中,并没有指定数据有多长,接收方再接受时,只用判断4位首部长度,将TCP报头与有效载荷分离,并将有效载荷无脑的放到自己的接受缓冲区中。上层就在接收缓冲区中读取数据就好,因此接受缓冲区被占用的空间是不确定的,因为下层读取会一直放数据,上层处理会一直出数据。这样就能直观的理解面向字节流。
4.标志位
TCP报头字段还有很多标志位,由于服务器会为很多客户端服务,因此服务器一定会同时接受到各种各样的TCP报头数据,那么服务器如何知道这条报文是建立链接,另一条报文是要断开链接,又一个报文需要我分析数据是否丢失,是否需要重发。
因此通过标志位可以判断报文是什么类型的,如下就是报文的标志位,在三次握手中发送的是SYN与ACK,在四次挥手时发送的是FIN与ACK。
4.1 PSH
而PSH代表PUSH的意思,比如在进行流量控制的时候,发送方在发送数据,接收方压力很大,应用层运行速度慢,接受缓冲区中的的数据一直无法上传到应用层。导致接受缓冲区被占满,于是给发送方发送的TCP报文中窗口大小为0。
那发送方也不能就这样干等啊,我咋知道你什么时候窗口大小不为0,虽说按道理你窗口大小不为0了会通知我,但是我也担心你嗝屁了,无法通知我了,于是我就定期发送询问报文,问问你状态还好嘛,按照TCP的特性,我给你发报文,你也得给我响应报文。这样我就可以知道你的状态了。
其中,询问报文我就可以加上PSH,代表发送方希望接收方尽快处理报文中的数据。
4.2 RST
TCP是保证可靠性的,但这并不代表建立链接时三次握手必须成功,他是有可能失败的。你给我应答了,我就知道我发送成功了,你不给我应答,我就得等待并开始怀疑是不是发送出问题了。
如果客户端发送了SYN,却一直没收到服务器发来的SYN+ACK,那么客户端就知道了,可能我发送的SYN失败了,也可能是服务器发送的SYN+ACK失败,但是无论那种失败,我都会再重新发送SYN,直到收到了SYN+ACK。收到之后,我再给客户端发送ACK,此时客户端认为连接已经完成,之后就要正式发送数据了。
- 但是,如果最后一个ACK发送失败,服务端一直没收到ACK,但后来客户端发送了正式的报文数据。
- 服务器就疑惑了,不是哥们,ACK都没发呢,咋就来正式报文了呢?
- 于是服务器给客户端发送RST报文,告诉客户端,链接失败啦,重新链接一下,此时客户端收到了,也就知道原来我发送的ACK错误,重新再来一次三次握手吧。
如下,就是重置了连接。
发错了...................................... 是这个
URG跟着下面的紧急指针一起讲解。
5.紧急指针
正确情况下,TCP双方进行通信,你发一些,我也响应你一些,将你的数据接受并按序号排好序进行下一步处理。但是可能客户端会有一些紧急任务需要尽快发送给服务器,如果还要按序号排序就不好了。也就是要让紧急数据进行插队,因此紧急任务需要再标志位中将URG置为1,同时紧急指针指示了紧急数据在 TCP 数据流中的位置,赶紧先将这个数据处理了来。
紧急指针只是一个位置,因为TCP规定,紧急数据一共只有一个字节,TCP只允许少量的数据进行插队,因为插队需要额外处理,也是一件耗时的事情。
比如客户端发送的数据发错了,想要终止前面的所有数据发送,或者是检测服务器状态是否还好,就可以利用上紧急指针。
6.TCP检验和
TCP检验和是TCP协议中用于检测数据传输过程中是否出现错误的一种校验机制。TCP检验和的计算通常采用的是16位的校验和算法。
其具体计算方式是将TCP报文中的各个字段以16位为单位进行划分,然后将每个16位的字段相加,最后对结果进行取反操作得到校验和值。这样计算出的校验和值将被添加到TCP报文头部的校验和字段中,以便接收方进行验证。
二、超时重传
- 主机A发送数据给B之后, 可能因为网络拥堵等原因, 数据无法到达主机B。
- 如果主机A在一个特定时间间隔内没有收到B发来的确认应答, 就会进行重发。
- 但是, 主机A未收到B发来的确认应答, 也可能是因为ACK丢失了,但这对主机A来讲,效果是一样的,他照样收不到应答,于是又会发送之前的数据
- 但这会导致主机B收到了两次同一份数据,但这也不用担心,主机B通过序号就明白这是同一个报文,会自己去去重
因此,这里也要求,发送方在将数据发送出去后,并不能立刻将数据丢弃,而是应该暂存起来,防止发送失败还有容错,可以重传。
但网络是波动的,并不能准确的计算出应该间隔多久进行超时重传
- 如果超时时间设的太长,,会影响整体的重传效率。
- 如果超时时间设的太短,有可能会频繁发送重复的包。
TCP为了保证无论在任何环境下都能比较高性能的通信,,因此会动态计算这个最大超时时间。
- Linux中(BSD Unix和Windows也是如此),超时以500ms为一个单位进行控制,每次判定超时重发的超时
- 时间都是500ms的整数倍。如果重发一次之后,仍然得不到应答,等待 2*500ms 后再进行重传。
- 如果仍然得不到应答,等待 4*500ms 进行重传,依次类推, 以指数形式递增。
- 累计到一定的重传次数,TCP认为网络或者对端主机出现异常,强制关闭连接。
三、连接管理机制
在TCP套接字编程中,客户端使用connect发起链接请求,服务器bind制定端口,listen设置监听状态,accept等待客户端建立连接。
服务器可能会收到来自各种客户端的很多连接,那么他必须将这些链接管理起来。管理的本质是先描述再组织,因此需要存在管理连接的结构体。该结构体里面有原ip和目的ip、源端口和目的端口、序号、连接状态、紧急指针等等信息。再用链表将他们组织起来。
建立链接是操作系统帮我们做的,但依然是有成本的,也要不断的修改连接的状态(比如SYN_SENT:请求连接状态、SYN_RCVD:收到请求链接并等待客户端发送最后的确认状态、ESTABLISHED:连接建立状态)。有了状态描述字段,才能更好的管理链接。
那为什么要进行三次握手呢?
1.因为通信双方要以最小的代价去验证全双工
- 如果只有一次握手,客户端不清楚服务器是否能收到,服务器也不知道自己是否能发送,肯定不行!
- 如果只有两次握手,服务器确认能收到客户端发送的信息,但是不能确认客户端是否能收到自己发送的信息,这也肯定不行。
- 三次握手,双方都能证明前面两次发送必定是成功的(最后一次数据发送成功与否不能确定,但是能确定前面的一定发送成功)
2.同时三次握手的本质是四次握手加一次捎带应答,服务器将SYN和ACK一起发送
而四次挥手则是在双方进行close关闭连接的时候进行的。断开连接仍然需要确定性。因此断开链接也需要确认,表示我知道你要断开了。
四次挥手也是断开链接的最小代价方案。
但是四次挥手一般情况下不会出现三次回收+捎带应答的情况,因为你想跟我断开了连接,代表你不会向我发送消息了,但是我可能还有重要的话没有说完,我还想跟你发送消息,因此在我发送完之后,我再跟你断开链接。
只有极少情况,我们两个同时想断开链接,你给我发了断开,我也忍你很久了,也想断开,此时就可以捎带应答,将四次挥手变成三次。
小总结
- 三次握手服务器是渣男,你请求连接,我直接答应并也请求连接你。
- 四次挥手服务器像舔狗,你说断开链接,我还不舍得,话说完才心灰意冷断开链接。
但是我们close()关闭的时候,是将文件描述符的读写都关闭了呀,因此,操作系统为我们提供了shutdown()接口,可以只关闭写端或者读端。
但是,根据这个图片,我们还可以看到链接双方的状态变化
当客户端关闭连接后,服务器一直不调用close()关闭连接,就会陷入CLOSE_WAIT状态,这样会一直占用服务器资源,因此可能会越用越卡。
同时客户端的链接也关闭不了,因为一直收不到服务器的FIN,因此也会陷入FIN_WAIT2
这里我们客户端调用close()关闭链接,并且退出来,仍然能用netstat -n 查询到链接,因为这是操作系统帮我们维护的,上层用户基本感知不到。
而当服务器主动断开链接,想要重新上线并绑定原始IP就会失败,因为操作系统帮我们维护的链接还存在。
因此我们可以用如下写法,允许在地址仍然占用状态下绑定套接字,允许多个套接字绑定到同一个端口。而客户端遇不到这些问题,因为客户端是随机绑定的,客户端IP并不固定。
那么为什么要有TIME_WAIT呢?
因为担心网络中还会存在尚未到达对方的报文,如果不进行等待,就会受到这些历史报文的干扰,影响新链接的建立,进行等待,让历史的报文在网络中消散。
TCP协议规定,主动关闭连接的一方要处于TIME_ WAIT状态,等待两个MSL(maximum segment lifetime最大生存时间) 的时间后才能回到CLOSED状态。两个MSL是为了保证发送报文与接受报文最差情况出现。
如下指令能查询MSL时间,我的是centos系统,不同系统不同版本可能会有些许不同
因为有了这些设计, 历史报文对新链接的影响已经很小了。并且,tcp双方在三次握手时,会商量随机起始序号,并不是说刚开始发送的时候一定从0开始,而是从随机起始序号+1开始。这样一旦发现历史报文跟新链接的起始序号不匹配,也就会忽略该报文,想对新链接有影响就更困难了。
四、滑动窗口
滑动窗口是TCP协议中用来实现流量控制和可靠数据传输的关键机制。之前我们提到,客户端会一次性的向服务器发送多条数据,服务器收到数据给客户端进行带确认序号的ACK,发现确认序号存在问题时,需要重新发送保证可靠性。因此客户端就不能将发送的报文立刻丢弃,而是应该暂时保存,提高容错率。
比如通信双方三次握手时,服务器告诉客户端他的窗口大小,7,于是客户端一次发送了1,2,3,4,5,6,7,8,但是只收到了对方相应的确认序号为5,窗口仍是7,于是客户端会将自己的发送窗口的左指针只能向右滑动到5,右指针为左指针+8,因为服务器并没有对后面的数据进行ACK,也就是可能没有收到客户端发送的5号数据,于是客户端不能丢弃窗口内的报文,因为可能会进行重发。
- win_start = 确认序号
- win_end = win_start + win 。
我们再来分析一下报文的丢失
- 最左边的报文丢失了,重新补发最左边就好。
- 中间的报文丢失了,滑动窗口会向右滑动,变为第一种情况,最左边的丢失。
- 最右边的报文丢失了,滑动窗口也会向右滑动,变成第一张情况,最左边的丢失。
于是,滑动窗口根本就不担心报文的丢失,他有足够的容错去重新发送报文,同时,TCP还有快重传机制,如果连续收到3次同样的确认序号,证明这个数据可能丢失了,发送方会立刻将对应的报文进行补发。这就就这样,滑动窗口一步一步的进行滑动,实现了流量控制与可靠传输。
从滑动窗口的快重传我们可以也可以看出,确认序号设置为已收到序号之前的所有报文是很巧妙的,只要不是最后一个报文(序号最大的),其他的丢失了也不担心,因为当客户端收到最后一个报文的确认序号时,就知道你前面的全部收到了,其他的到没到不重要。
目前,我们学习了超时重传与快重传,超时重传是保证下线,快重传能提高上线。
我们的TCP首部中,有一个16位窗口字段,就是存放了窗口大小信息。那么问题来了,16位数字最大表示65535,那么TCP窗口最大就是65535字节么?实际上,TCP首部40字节选项中还包含了一个窗口扩大因子M,实际窗口大小是窗口字段的值左移 M 位。也就是说能发送更多的数据。
五、拥塞控制
我们之前学习了很多保证TCP可靠与尽可能提高性能的方案,但都是在两台主机上做出的考虑,一个两个报文出现了问题,无所谓,重发就好,如果是网络出现了问题呢?
如果这段网络发生了一些问题,导致数据传输出现大面积错误,应该如何处理呢?
- 此时需要进行拥塞控制,让主机不要进行频繁重传,不要再去捣乱了,让网络缓一缓。
- TCP引入慢启动机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据
此处引入一个概念称作拥塞窗口
发送开始的时候,定义拥塞窗口大小为1;
每次收到一个ACK应答,拥塞窗口加1;
拥塞窗口也就是一个根据经验得来的数字,发送数据量超过拥塞窗口时,有很大概率会引起网络拥塞,并不一定引起。
因此窗口大小不能仅仅只看对面的接受能力,还要看网络的接受能力。
滑动窗口大小 = min (拥塞窗口大小,对方窗口大小) 。这样进行动态调节。
这样的拥塞窗口增长速度,是指数级别的,"慢启动" 只是指初使时慢,增长速度非常快。
- 为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍。
- 此处引入一个叫做慢启动的阈值,当TCP开始启动的时候, 慢启动阈值的初始值通常设置为一个默认值或基于网络的初始估计值
- 当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长
- 线性增长直到发生网络拥塞,阈值变为原来的一半,同时将拥塞窗口置为1,重新开始
拥塞窗口的计算默认是对方的接受能力无限大,因此一步一步往上增长,去探测网络的状态,拥塞窗口一直在变化,因为网络的状态会有波动,因此需要一直探测。
拥塞控制,归根结底是TCP协议想尽可能快的把数据传输给对方,但是又要避免给网络造成太大压力的折中方案。
六、延迟应答
主机A给主机B发送了四次,有四个报文序号为100,200,300,400,当主机B收到了报文100,按道理此时应该立刻响应,但是主机B上层的任务可能要做完了,马上又要从缓冲区里面拿数据了,主机B就稍微等一会,等到窗口变大一点,或者收到200的报文,再给你应答确认序号201,窗口大小xxx就行。这也展示了确认序号是收到的历史所有报文的含金量。
延迟应答是一种提高TCP传输效率的策略。
那么所有的包都可以延迟应答么? 肯定也不是
- 数量限制:每隔N个包就应答一次
- 时间限制:超过最大延迟时间就应答一次。
具体的数量和超时时间,依操作系统不同也有差异。 一般N取2, 超时时间取200ms
七、为什么TCP这么复杂?
因为要保证可靠性,同时又尽可能的提高性能。
可靠性:
- 校验和
- 序列号(按序到达)
- 确认应答
- 超时重发
- 连接管理
- 流量控制
- 拥塞控制
提高性能:
- 滑动窗口
- 快速重传
- 延迟应答
- 捎带应答
谢谢大家观看!!!