一、TCP协议报文

1.如何交付?
根据源端口和目的端口进行交付
2.报头和有效载荷如何分离?
TCP报头=标准报头+选项
标准报头=20字节 选项的长度是可变化的
在报头中包含一个4位首部长度(数据偏移),这个首部长度是包含选项的
4位,0000 1111,那么可以说我们的报头的范围是[0,15]吗?
不可以!!!因为我们的标准报头都20字节,怎么可能报头范围小于20。
所以我们约定基本首部单位是4字节。
所以我们的报文范围应该是[0*4,15*4]->[0,60]字节,对吗?
还是不对!因为我们的标准报头是必须存在且长度固定位20字节,所以报文的范围是[20,60]字节。
所以4位首部长度是从0101开始到1111结束。
换句话说,选项范围是[0,40]字节
直接回答问题,无脑读取20字节,读取之后提取4位首部长度,4位首部长度✖4减去标准报头长度(20字节),剩下的就是选项长度,再减去选项,剩下的就是有效载荷。
3.在UDP中存在报文大小,在TCP中为什么没有?
UDP是面向数据报,去掉报头就是完整的数据;而TCP是面向字节流,它所携带的数据不一定是完整的,所以TCP就不管了,由上层自己分析。
二、可靠性
1.什么是可靠性
直接回答,具有应答,就可以保证历史消息具有可靠性。
举个例子,同学A给同学B发送消息①:你吃饭了吗,同学B给同学A回消息②:吃了。
在这个例子中消息①具有应答,它就是可靠的,而因为同学A并没有回复消息②,所以同学B不确定同学A是否收到消息②,所以消息②就是不可靠的。
2.TCP的一般通信过程

如上图所示报文①由客户端发给服务器,服务器回送应答①,所以报文①就是可靠的;服务器在收到报文②后进行应答,而应答②在发送过程中丢失了,所以报文②就是不可靠的,客户端没有收到应答②,它就会重新发送一次报文②。
注意:TCP不对应答做应答,收到就可靠,没收到就重发,不然就会陷入无限循环。
3.TCP的通用通信过程
在上边的过程中,我们发现这样一应一答的方式有点慢,在现实TCP传输过程中,是可以连续发送多条报文的。
规定:每一条报文都要有应答。

确认序号=序号+1
核心:指定报文序号之前的所以信息,已经全部收到了
如果只收到401,表示401之前的报文全部收到了,这样的设计可以允许少量应答丢失,后边再详细解释原理。
传输传的是最开始的图片(报头+数据),应答只传报头。
三、16位窗口大小
当接收端的接收缓冲区满了的时候,发送端继续发送数据,因为接受缓冲区满了,就将数据丢弃,这有问题吗,这没有问题,但是这不合理!这样会导致大量的资源浪费,效率低。
我们要按量按需发送,就必须知道对方的接收缓冲区中剩余空间的大小。
那么发送端如何尽早得知对方的接受能力呢?
在TCP报文中,存在一个16位窗口大小。
这个16位窗口大小就是缓冲区剩余空间的大小。
那么这个剩余缓冲区空间大小是自己的还是对方的呢?
答案是自己的,这时候你可能就要问了,不是说要知道对方的缓冲区大小吗,为什么是自己的。
现在就告诉你,因为我们传送的所有报文都是给对方看的,传过去对方不就知道剩余空间有多大了吗。
根据对方的缓冲区大小来控制自己发送数据的速度,这就叫做流量控制!
四、标志位
在Linux内核中,TCP报文依旧是一个结构体
cpp
struct tcphdr {
__u16 source;//源端口
__u16 dest;//目的端口
__u32 seq;//序号
__u32 ack_seq;//确认序号
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u16 res1:4,//小端标志位
doff:4,
fin:1,
syn:1,
rst:1,
psh:1,
ack:1,
urg:1,
ece:1,
cwr:1;
#elif defined(__BIG_ENDIAN_BITFIELD)
__u16 doff:4,//大端标志位
res1:4,
cwr:1,
ece:1,
urg:1,
ack:1,
psh:1,
rst:1,
syn:1,
fin:1;
#else
#error "Adjust your <asm/byteorder.h> defines"
#endif
__u16 window;//16位窗口
__u16 check;//校验和
__u16 urg_ptr;//紧急指针
};
所谓标志位,就是报头中的一个比特位!
1.SYN
全称:Synchronize Sequence Numbers 同步序列号
功能:建立连接,握手过程中使用的标志位
使用场景:三次握手
2.ACK
全称:Acknowledgment 确认
功能:确认号是否有效,表明报文是一个应答报文
3.FIN
全称:Finish 完成
功能:通知对方,本端要关闭了,我们称携带FUN标识的为结束报文
使用场景:四次挥手
4.PSH
全称:Push 推送
功能:提示接收端应用程序立刻从TCP缓冲区把数据读走
理解:当接受方应用层太忙了,没时间从接收缓冲区读取数据,导致接收缓冲区满了,那我发送端就这么傻傻的等着它吗,这也太慢了吧,这时发送方携带PSH标志位,催促对方赶紧给我腾出空间,我要发送数据了(其实不一定要满,就是催一下)
缓冲区满了的两种处理办法
当接收缓冲区由满到腾出空间后,接收方会主动向发送方发出一个报文,提示我有空间了。
当接收区满了的时候,发送放会定时的向接受去发送不携带数据的报文询问它好了没有
5.RST
全称:Reset 重置
功能:对方要求重新建立连接,我们把携带RST标识的称为复位报文段
理解:我们都知道通信之前要进行三次握手,但是三次握手一定会成功吗?即便握手成功就不会断了吗?
这种情况都要进行重新连接,重新连接的请求就需要携带RST标志位。
情况:在三次握手的过程中,我们最害怕第三次握手报文丢失。因为它是没有应答的,没有人能保证它的可靠性。站在发送端的角度,它只要发出携带ACK的报文就算握手成功;而站在接收端的角度下,只有收到携带ACK的报文才算三次握手成功。当发送端发出ACK后,他认为三次握手成功了,但是报文在传输过程中丢失了,所以三次握手失败了,但是发送端认为成功了,它照常发送数据,接收端都懵了,说你给我发啥呢,直接发送一个RST给发送端,要求链接重置,发送端收到后就重新进行三次握手。
6.URG
全称:Urgent 紧急
功能:紧急指针是否有效,如果我们的数据想要被优先处理,这时就需要使用紧急指针。
情况:接收缓冲区中的报文是一个队列,相当于排队,当发送端发现发送的数据错误了,要取消上传,这时发送紧急数据进行暂停,相当于插队。
补充:紧急数据,不属于常规数据,属于带外数据。紧急指针的本质是偏移量,在数据中的偏移量,紧急数据只有一个字节!
五、具体的机制
0.序号
如图,第一段中由一千个字节的数据,他的序号就是1000,确认号就是1001;当第二段数据发送时,他的序号就是1001,确认号是2001;这样连贯的数字保证了,应答的确认序号之前的数据肯定已经被接受了。
举个例子:
发送方发送:
0~100、101~200(丢失)、201~300(正常收到)接收方的处理流程:
- 收到
0~100:字节流连续,返回Ack=101(表示 0~100 已收全,期望下一个是 101);101~200丢失:无任何报文到达接收方,接收方的连续字节流终止在 100;- 收到
201~300:序列号与当前连续流的末尾(100)不连续,属于乱序报文段 ;
- 接收方会将
201~300暂存到接收缓冲区的乱序队列 中,不递交给应用层;- 不更新确认序号 ,仍返回Ack=101(重复确认)。
1.确认应答机制
前面介绍过了,简单来说,就是收到应答就能百分百确认对方收到了数据,没有收到应答,不管是丢失了还是怎么样,就当作没收到,重新发
2.超时重传机制
丢包只有两种情况
发送方没有收到应答,意味着什么?
意味着丢包吗?错!只能意味着数据可能丢失,对方可能没收到。
收不到应答+超时 意味着丢包了!
这时候我们可以利⽤前⾯提到的序列号, 就可以很容易做到去重的效果.
那么, 如果超时的时间如何确定?
• 最理想的情况下, 找到⼀个最⼩的时间, 保证 "确认应答⼀定能在这个时间内返回".
• 但是这个时间的⻓短, 随着⽹络环境的不同, 是有差异的.
• 如果超时时间设的太⻓, 会影响整体的重传效率;
• 如果超时时间设的太短, 有可能会频繁发送重复的包;
TCP为了保证⽆论在任何环境下都能⽐较⾼性能的通信, 因此会动态计算这个最⼤超时时间.
• Linux中(BSD Unix和Windows也是如此), 超时以500ms为⼀个单位进⾏控制, 每次判定超时重发的超时时间都是500ms的整数倍.
• 如果重发⼀次之后, 仍然得不到应答, 等待 2*500ms 后再进⾏重传.
• 如果仍然得不到应答, 等待 4*500ms 进⾏重传. 依次类推, 以指数形式递增.
• 累计到⼀定的重传次数, TCP认为⽹络或者对端主机出现异常, 强制关闭连接.
3.连接管理机制

①三次握手

前两次握手,不能携带数据,因为三次握手没有完成
我们在发送数据的时候不是要进行流量控制吗,那第一次发送数据我怎么知道对方的剩余空间呢?
第一次发送数据不是第一次发送报文,在三次握手过程中就已经知道了对方的接收缓冲区的剩余空间了。
那第一次握手呢?
我第一次没带数据我管他剩余空间大小干嘛😕
为什么要三次握手?
理由一:以最短的方式,进行验证全双工。本质是验证网络环境是否支持通信。
一次握手只能验证接受方能收数据,其他的都不能验证;
二次握手不能验证接受方可以发数据。
理由二:三次握手本质是四次握手,但是由于客户端的连接请求服务器要无脑接收所以进行了捎带应答(ACK+SYN)。所以三次握手就是以最小的成本,验证双方的通信意愿
②四次挥手
挥手为什么要进行四次?
因为TCP通信是全双工的,我跟你断开是我不再给你发消息了,跟你还给不给我发消息没有关系。
模拟一下
C:我累了,我不会再给你发消息了(我要发的数据已经发完了,我要和你断开)
S:收到,但是我还要给你发
过了一段时间...
S:我也累了,我不会再给你发消息了/(ㄒoㄒ)/~~
C:收到ヾ( ̄▽ ̄)Bye~Bye~
二者彻底断了联系
在实际操作过程中,一方请求断开是使用close的,但是close会将读写端全部关闭,如何保证对方发送时能收到呢?
在Linux系统中,提供了接口,调用函数shutdown(int sockfd,int how),关闭读端how为SHUT_RD,关闭写端how为SHUT_WR;只关闭写端就好了,还能收到。
但是在实际操作的过程中都是直接使用close,很少使用shutdown
为什么是四次挥手呢?
因为断开连接时双方都要同意,一方要断,另一方也许还没发完。当一方请求另一方正好要断开时,FIN和ACK可以合并。
理解TIME_WAIT状态
• TCP协议规定,主动关闭连接的⼀⽅要处于TIME_ WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态.
• 我们使⽤Ctrl-C终⽌了server, 所以server是主动关闭连接的⼀⽅, 在TIME_WAIT期间仍然不能再次监听同样的server端⼝;
• MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同, 在Centos7/Ubuntu上默认配置的值是60s;
• 可以通过 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看msl的值
想⼀想, 为什么是 TIME_WAIT 的时间是 2MSL ?• MSL 是 TCP 报⽂的最⼤⽣存时间, 因此 TIME_WAIT 持续存在 2MSL 的话就能保证在两个传输⽅向上的尚未被接收或迟到的报⽂段都已经消失(否则服务器⽴刻重启, 可能会收到来⾃上⼀个进程的迟到的数据, 但是这种数据很可能是错误的);
• 同时也是在理论上保证最后⼀个报⽂可靠到达(假设最后⼀个ACK丢失, 那么服务器会再重发⼀个FIN. 这时虽然客⼾端的进程不在了, 但是TCP连接还在, 仍然可以重发LAST_ACK);
在我们做TCP实验的时候,我们发现关闭服务器后,立即使用相同的端口会bind失败,为什么?
因为服务器出于TIME_WAIT状态,端口还被占用着呢,不能被继续使用了,就这么简单。
那么如何解决这个问题呢?
使⽤ setsockopt ()设置 socket 描述符的 选项 SO_REUSEADDR 为 1 , 表⽰允许创建端⼝号相同但IP地址不同的多个 socket 描述符
六、滑动窗口
滑动窗口是什么?
窗口大小指的是无需等待确认应答而可以继续发送数据的最大值。也可以叫滑动窗口。
滑动窗口时发送缓冲区的一部分!
一张图解释
已发送已确认的数据就没有用了,在缓冲区内情况,可以继续接收新的数据;
序号在发送的轮转中,数字是依次增大的,也就意味着滑动窗口未来基本是向右滑动的;
滑动窗口的本质:start&&end下标增加。
那么滑动窗口的大小由谁决定?
对方的接受能力(收到的报头中的16位窗口的大小)
start=报文确认序号
end=start+win
滑动窗口会不会向左滑动?
不会!左侧的都是已发送已确认的,不能向左滑动
滑动窗口可以变大吗?
举个例子
在上图中16位窗口大小位64-50=14,滑动窗口的大小就是14,如果应用层突然把缓冲区里的数据全部取走,16位窗口的大小就变成了64,滑动窗口的大小就变成了64.
综上所述,滑动窗口的大小能变大。
滑动窗口可以变小吗?
同理,应用层不读数据或者读的没有收到的快,16位窗口就变小了,滑动窗口就变小了。
滑动窗口会不会不变?
同理,应用层读取速度与缓冲区接受速度相同,16位窗口大小不变,滑动窗口就不变。
滑动窗口可以变成0吗?
当然可以!应用层不读取数据,但是接收缓冲区满了,16位窗口就变成0了,滑动窗口大小就变成0了。
tcp发出,暂时没有应答的时候,必须让对应的报文暂时保存起来,以方便管理,那报文存在哪里呢?
滑动窗口内,没有收到应答的时候,滑动窗口就不动,这样报文暂存起来了吗!
快重传
• 当某⼀段报⽂段丢失之后, 发送端会⼀直收到 1001 这样的ACK, 就像是在提醒发送端 "我想要的是1001" ⼀样;
• 如果发送端主机连续三次收到了同样⼀个 "1001" 这样的应答, 就会将对应的数据 1001 - 2000 重新发送;
• 这个时候接收端收到了 1001 之后, 再次返回的ACK就是7001了(因为2001 - 7000)接收端其实之前就已经收到了, 被放到了接收端操作系统内核的接收缓冲区中;
如果丢包了怎么办?滑动窗口会不会跳过报文进行应答?
有三种情况
a.最左侧丢失
最左侧报文真的丢了,滑动窗口左端不变,因为如果后边的收到了,而第一段没有收到,所以返回值为第一段的确认值,滑动窗口左端不变
最左侧报文对应的应答丢了,滑动窗口正常工作,因为如果只是应答丢了,后边的应答序号会返回,接收端就知道前边的没有丢了,滑动窗口正常工作。
b.中间丢失
中间报文丢失,就意味着最左侧收到了,滑动窗口会先滑动,滑动到中间报文位置,原中间位置就变成了最左侧,然后按照最左侧的规则处理就行了。
c.最右侧丢失
与中间丢失一样,把最右侧变成最左侧,按照最左侧丢失处理。
滑动窗口不会跳过报文进行应答,由确认序号的定义决定的,确认消息一定是连续确认的!
滑动窗口会溢出吗?
不会!我们可以把缓冲区想象成一个环形区域,发送过并且得到应答后就会往后接上,继续使用。
七、拥塞控制
引入
当发送方发送一千个报文,只有两三个应答没有被收到,发送方会重新发送;但是当发送方发送了一千个报文,只收到了两三个应答,这时候发送方判定发生了网络拥塞;TCP当然不可能解决网络拥塞的问他,但是它还是能够处理一部分问题以保证效率。
当发送方判定是发生了网络拥塞的问题,要不要重发?
不能立即重发!!本来就拥堵了,还发会让网络更加拥堵!
TCP如何解决的?
因为⽹络上有很多的计算机, 可能当前的⽹络状态就已经⽐较拥堵. 在不清楚当前⽹络状态下, 贸然发送⼤量的数据, 是很有可能引起雪上加霜的.
TCP引⼊ 慢启动 机制, 先发少量的数据, 探探路, 摸清当前的⽹络拥堵状态, 再决定按照多⼤的速度传输数据;
第一次发1个报文
第二次发2个报文
......
第n次发2^n个报文
经过这样递增式的发送,网络的拥堵会大大缓解,慢慢的报文增多,都收到了应答,这时候就要尽快恢复网络通信的过程,这就是拥塞控制。
不是说好了,发送多少数据由滑动窗口决定吗,那你怎么控制你发送的数据呢?
为了支持拥塞控制算法,设计者设计了一个拥塞窗口。
那什么是拥塞窗口呢?
拥塞窗口本质上就是一个整数,在发送方和接受方都有一个计数器,这个计数器就是拥塞窗口,一台主机同时发送的报文个数超过拥塞窗口中的值,就可能造成网络拥堵。
而网络是变化的,这就决定了拥塞窗口一定要进行更新变化。
到了这里就要更正滑动窗口的大小了:
滑动窗口=min(对方的16位窗口大小,拥塞窗口)
如果网络不好,那么对方的缓冲区剩余部分再多也不好使。
那么拥塞窗口是一直指数增长吗
不是!增长到一定程度就会线性增长。
本质是为了探测网络的新的拥塞窗口的值。
ssthresh是一个阈值
一个例子包你理解网络拥塞
你有一个男/女朋友,有一天你惹ta生气了,你们处于冷战其间,你就颤颤巍巍的给ta发了一条消息,如果ta没回,就说明还拥塞着呢,如果ta回了,你就继续发两条消息、四条消息...最后你们关系恢复了;又过了一阵,又生气了,消息数直接变成了0,重复这个过程。
八、其他
1.延时应答
①当发送方给接受方发送数据的时候,一般情况下,接受方会立即计算出自己的缓冲区内的剩余部分大小并携带其做出应答;但是在延时应答时,接受方会在合理的时间内等待一会再给出应答,这样的目的是:在等待的过程中应用层可能会从缓冲区读取一部分数据,这样缓冲区的剩余部分就会大一些,这时候再做应答,发送方的滑动窗口就会大一些,能够同时发送的报文数量会变多,提高了效率。
②还有一种目的是为了少发应答
每个包都会延时应答吗?
不是的。
数量限制:每隔N个包就应答一次;
时间限制:超过最大延迟时间就应答一次;
具体的数量和超时时间,依操作系统不同也有差异;一般N取2,超时时间取200ms。
2.捎带应答
普通的应答就是一个报头,但是捎带应答就是报头+数据,提高效率。
3.面向字节流
创建⼀个TCP的socket, 同时在内核中创建⼀个 发送缓冲区 和⼀个 接收缓冲区;
• 调⽤write时, 数据会先写⼊发送缓冲区中;
• 如果发送的字节数太⻓, 会被拆分成多个TCP的数据包发出;
• 如果发送的字节数太短, 就会先在缓冲区⾥等待, 等到缓冲区⻓度差不多了, 或者其他合适的时机发送出去;
• 接收数据的时候, 数据也是从⽹卡驱动程序到达内核的接收缓冲区;
• 然后应⽤程序可以调⽤read从接收缓冲区拿数据;
• 另⼀⽅⾯, TCP的⼀个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这⼀个连接, 既可以读数据, 也可以写数据. 这个概念叫做 全双⼯
由于缓冲区的存在, TCP程序的读和写不需要⼀ 匹配, 例如:
• 写100个字节数据时, 可以调⽤⼀次write写100个字节, 也可以调⽤100次write, 每次写⼀个字节;
• 读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以⼀次read 100个字节, 也可以⼀次read⼀个字节, 重复100次;
4.粘包问题
为什么会存在粘包问题?
因为TCP是面向字节流的。
如何解决粘包问题?
自定义协议+序列和反序列化
• 对于定⻓的包, 保证每次都按固定⼤⼩读取即可; 例如上⾯的Request结构, 是固定⼤⼩的, 那么就从缓冲区从头开始按sizeof(Request)依次读取即可;
• 对于变⻓的包, 可以在包头的位置, 约定⼀个包总⻓度的字段, 从⽽就知道了包的结束位置;
• 对于变⻓的包, 还可以在包和包之间使⽤明确的分隔符(应⽤层协议, 是程序猿⾃⼰来定的, 只要保证分隔符不和正⽂冲突即可);
5.TCP异常情况
进程终⽌: 进程终⽌会释放⽂件描述符, 仍然可以发送FIN. 和正常关闭没有什么区别.
机器重启: 和进程终⽌的情况相同.
机器掉电/⽹线断开: 接收端认为连接还在, ⼀旦接收端有写⼊操作, 接收端发现连接已经不在了, 就会进⾏reset. 即使没有写⼊操作, TCP⾃⼰也内置了⼀个保活定时器, 会定期询问对⽅是否还在. 如果对⽅不在, 也会把连接释放.
另外, 应⽤层的某些协议, 也有⼀些这样的检测机制. 例如HTTP⻓连接中, 也会定期检测对⽅的状态. 例如QQ, 在QQ断线之后, 也会定期尝试重新连接.
九、TCP小结
TCP既要保证可靠性,又要尽可能的提高效率。
保证可靠性:
校验和
序列号
确认应答
超时重发
连接管理
流量控制
拥塞控制
提高性能:
滑动窗口
快速重传
延迟应答
捎带应答















