目录
一、协议段格式及其策略
1、报头和有效载荷如何分离
a、提取报文前20,提取首部长度字段
b、首部长度字段*4 - 20 == 0?报头完毕:有几个字节的选项
c、剩下的就是有效载荷
tcp选项部分是可变的
那些是不可靠的:丢包(少量、大量)、乱序、重复、效验失败、发送太快/太慢、网络出问题
为什么会出现这么多不可靠的问题呢?
单纯就是因为通信双方,距离变长了。
发送和应答最终的呈现的是什么样子?
client和server是TCP客户端和TCP服务端,无论是请求还是响应,双方在进行交互的时候,通信发送的都是,TCP报头+有效载荷(如果有的话)
确认应答(ACK)机制
你怎么知道这个报文丢包了??
就有了确认应答机制,C可以并发的向网络发多个报文,S也可以并发的响应回来,发送各个报文的时间上就被重叠了,提高发送效率
1、多个报文经过网络,发送顺序,不一定是收到的顺序
2、当client收到多个确认的时候,client如何得知哪一个应答,对应哪一个报文?注定了,我们想要办法给报文带上编号?
每个报文都有自己的编号,应答的时候也要告诉我们对应的发送方,所以有确认序号。
TCP报头中,必定包含序号
32位序号:是client自身对报文的编号
确认序号X:X-1之前的报文已经全部收到了,下次发请从X编号开始发送
server如果11没收到,但是12收到了,代表了12之前的都收到了,包括10也收到了,所以11丢了,并不是数据丢了,而是确认应答丢了
所以可以允许少量的ACK丢失,更细粒的确认丢包原因
双方交互的都是TCP报文,为什么序号和确认序号不搞成一个字段,为什么要搞成2个字段呢?响应的时候在序号里填写11不就行了吗?
只研究了C到S的方向,S向C发消息的时候,也可以用一个序号,但是RCP是全双工的,C在向S发消息,C也有可能在接受S的响应,S也一样,未来当S收到报文之后,可能即想对报文做确认,又想把自己发送的数据发给C,在TCP大部分在收发的时候,可能存在一种情况:
以S为例,在响应里面,即可能包含C对S发的报文的确认,也有S对C发送数据,把这两个压缩成一个应答/请求,这就是捎带应答
32位序号:S->C数据序号
32位确认序号:S->C历史数据的确认
同时存在,所以必须是不同的字段
比如,两个人,吃了吗。吃了。你吃了吗。吃了,我吃的是饺子。
6个标志位
6个标志位:在报头里只占一个比特位,要么是0要么是1
标志位本质是什么?
未来有很多的客户端,会同时给S端发消息,有的是请求连接,有的是发送消息,有的是断开连接,所以服务器会在同一个时刻/时间段会收到各种各样的报文请求,各种各样的报文请求,都是不同的:
如我是一张餐厅老板,入住美团,有的是来吃饭的,有的是来取外卖的,有的是来找东西的等待,作为老板,来吃饭的我就要点餐,来找东西的,我就要问问找什么,取外卖我就问取餐号,我就会根据不同的人提供不同的处理动作,同样的,报文是有类型的。
那么怎么标识不同的报文呢?所以标志位本质是标识不同类型的报文
为什么有标志位?
因为标志位本质是标识不同类型的报文,就是为了标志位不同的报文,反应出报文的不同。
具体标志位,怎么设计的?
ACK: 该报文是一个确认应答报文,也可能会携带数据(捎带应答),是一个确认应答报文,就多多关注确认序号
SYN: 是一个链接请求的报文,(1、SYN 2、SYN+ACK 3、ACK(三次握手))只要SYN被置为1了,就是一个链接请求的报文
FIN: 是一个链接断开的报文,(4次挥手)
PSH: 催对方,让对方应用层赶紧把数据拿走,
比如: 现实生活中,你在写作业,有人问你,你作业写完了,我说,正在做,有的人说加油做,有的人说赶急做,明天要交,所以一定情况下,一些别的原因,有人推你,尽快做完一件事
同理,通信时,对方因为应用层的原因,接收缓冲区的数据迟迟不拿走,这时就可以把PSH置为1,发过去,催对方
RST: TCP是为了保证可靠性,TCP在进行三次握手的时候,是不是一定能握手成功呢?不一定,成功了,就一定是经过了三次握手,双方链接成功,双方也以为连接上了,S端因为一些原因,把连接断了,C端不知道,也有可能发送,
比如,S端网线断了,再插上,C端就又向S端发请求,因为C端不知道S端断网了,C端就向S端发送一个置为1的RST,S端就又向C端发送三次握手,要重置连接,重新发起三次握手
URG: 紧急指针标志位,比如,TCP必须是按序到达的,我们有人向缓冲区放数据,有人向缓冲区的拿数据,有人放,有人拿,这就是生产消费模型
如果我们想插队呢?我们想让特殊的数据尽快的处理,就有了URG,告诉接收方,我们报文里有紧急数据,这16位紧急指针是有效的,因为16位紧急指针大部分是无效的
16位紧急指针: 本质是一个偏移量,紧急数据在有效载荷中的偏移量
这个数据有多大呢,报头里并没有标识处理?因为我们的紧急数据只有一个字节,所以并不用告诉我们紧急数据的大小,就需要告诉我们在那个位置
URG有什么用?
携带了URG的标志位,就不用排队了,要通过特殊的发送和接收才能收到URG,有时候我们在正常上传数据,我们突然不想上传了,向对方发送不想上传的数据时候,这个数据是要排队的,还有大量的数据会紧急叫停,这时候就可以带一个URG,不用排队了,
但是对方怎么知道我们是暂停上传还是取消上传呢?
这时候就可以用1个字节的数据表示状态码,1表示取消,2表示暂停,3表示继续
紧急指针也可以叫带外数据,不走主传输流,直接传输,就是带外数据
如何使用?recv中的flag参数,有很多对应标志位
比如:MSG_OOB,在接收的时候,不会从正常的流中接收,就可以读取到紧急数据,也可以使用MSG_OOB来发送
可靠性并不是对方把数据全收到就叫可靠性,我发出来数据,我作为发送方,我知道对方收到或没收到,要有回馈,这也叫做可靠性,我知道没发成功
TCP不关心,上层传下来的是什么,不关心发送缓冲区里的数据,反正都是二进制数据
char sendBuffer[N] 发送缓冲区宏观是看做为一个大数组,一格一格的,上层传下来数据,无论是结构体还是其他都是二进制,上层拷到发送缓冲区中的数据天然的就有编号,这个编号就是数组下标,一段数据,就是数组的开始和结束,所以数组里的数据有人拿,有人放,这就是TCP面向字节流的表现
超时重传
- 主机A发送数据给B之后, 可能因为网络拥堵等原因, 数据无法到达主机B;
- 如果主机A在一个特定时间间隔内没有收到B发来的确认应答, 就会进行重发
但是, 主机A未收到B发来的确认应答, 也可能是因为ACK丢失了
主机A向B超时重传,但是B已经收到过了,A再超时重传一次,因此主机B会收到很多重复数据。那么TCP协议需要能够识别出那些包是重复的包, 并且把重复的丢弃掉。
这时候我们可以利用前面提到的序列号, 就可以很容易做到去重的效果,收到重复报文,也是不可靠的体现,所以有去重的功能。
那么, 如果超时的时间如何确定?
- 最理想的情况下, 找到一个最小的时间, 保证 “确认应答一定能在这个时间内返回”.
- 但是这个时间的长短, 随着网络环境的不同, 是有差异的.
- 如果超时时间设的太长, 会影响整体的重传效率;
- 如果超时时间设的太短, 有可能会频繁发送重复的包;
TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间
- Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时时间都是500ms的整数倍.
- 如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传.
- 如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增.
- 累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接
主机A如果把数据已经发到了网络里了,可是主机A不能把已经发送的数据去掉,主机A一旦发送出去,会对数据暂时维护一段时间,会维护到还没反馈的时候
一个已经发送的数据,还没回应的时候,会暂时保存起来,以应对超时重传
流量控制
16位窗口大小:用来流量控制的,防止发送方发送太快或太慢
send/write read/recv这些IO接口本质是拷贝给了发送和接受缓冲区,并没有发送到网络,发送过程,就是把数据从C的发送缓冲区拷贝到S的接受缓冲区,所以发送缓冲区所对应的传输层(TCP),什么时候发,发多少,出错了怎么办,由TCP控制,所以应用层的作用就是把数据拷贝给了下一层,就返回了。
可是,这两台机器相隔千里之外,如果给对方发送数据太快,对方接受来不及,对方接受缓冲区满了,你再发,那么对端就来不及接受,就把报文直接被丢弃了,那么报文就丢失了,所以TCP有确认应答机制,可以超时重传,我不怕,但是站在这几个后续报文的角度,我在路上都没出问题,到了目的,你告诉我们要被丢弃了,就仅仅是你来不及接受,虽然可以,但是不合理,这时候已经消耗了很多网络资源,我没有出错,是你来不及接收,是因为你发的太快,这种做法是比较低效的表现,所以你来不及收了,就要早点说嘛,让发送方控制一下发送速度,这种策略就是流量控制,
那我们怎么样控制发送缓冲区不要发送太快呢?
接收方,对对方发过来的每一个数据都有一个应答,我们在应答的时候就可以告诉对方我的接受能力,我的发送速度是由对方接收能力来决定的,对方的接受能力取决于,接收缓冲区中剩余空间的大小,所以:
1、每个报文都有应答
2、自己是知道自己的接受能力的
所以有了16为窗口大小,这就是流量控制
对方就会把剩余空间大小填到16位窗口大小里,根据16位窗口决定发送方最多还可以发送多少数据
填谁的接受缓冲区的剩余空间的大小?
填自己的,凡是我要构建TCP报文,必定是我要给对方发信息,无论是发数据还是应答,还是数据加应答,填的都是自己的信息
所以叫做流量控制,我们双方就不用担心发送太快的问题,数据就不会丢失,既然我们发的太快了,那如果C端发的太慢了,流量控制也会给你加快点,提速。
比如S端还剩100kb的空间,C端给S端发了,S端上层立马取走了数据,空间变的很大了,S端立马就给C端同步的缓冲区变为4096了,变的很大了,C端就立马发快了
一旦校验失败了,该报头和数据立马丢弃,这就是真正的丢包,但是不用担心,可以超时重传
保留就是有时间就用
TCP在正常通信的时候,来不及接受,我可以给你补发,但不是最优的选择,是低效率的表现
当我们进行首次发送的时候,我怎么知道你的接受大小?怎么保证第一次发送就不会过快或过慢?
双方在进行TCP的时候,第一次并不是通信的时候,在通信之前早就发送报文交换了,在三次握手之前就至少发送一次报文交换了。
第一次交换的时候,并没有有效载荷,大小肯定是合适的,当接收方空间满了,上层还没取走,发送方会不断发送窗口探测,没有有效载荷,当接收方被上层收走了,接收方也会给发送方通知一下,窗口更新
滑动窗口
收发是串行的,收发效率就低下,
一般是Client给server塞大量报文,server给client大量响应,两种方法都可能会同时存在
TCP协议,可靠性是主要研究的问题,但是不是全部
效率问题,也是TCP考虑的问题
不管并发访问多少,但是对方是由接受能力上限的,所以发送方并发发送,一定要在对方能够接受的前提条件下,进行并发发送
所以我们发送方目前最大一次可以向对方发送多少的数据呢?由对方的窗口大小决定(目前这样认为)
1、先谈滑动窗口一般情况
滑动窗口,其实是发送缓冲区里的一部分,里面的数据暂时不用收到应答,可以直接发送,但是不能超过对方窗口大小
滑动窗口的左侧部分数据,已经发送已经确认,可以被覆盖,是无效数据,右侧是尚未发送的数据区域,在滑动窗口的数据,可以直接发送,但是尚未收到应答,有了应答,窗口就要滑动了
滑动窗口应该是多大的?
和对方的接受能力有关,应答报文中win大小(目前)
所以我们对应的谁向滑动窗口放数据?
用户,向尚未发送的数据区域放数据
1.1、如何理解滑动窗口
发送缓冲区当成一个char类型的数组,大小与系统有关,
上层拷贝下来的数据,数组就天然有了编号,滑动窗口就是一块数组区间,本质就是由两个数组下标维护的数组区间
所以滑动窗口未来是要扩大或移动,本质上就是对两个数组下标进行++,这就是滑动窗口
2、再谈特殊窗口
1)滑动窗口只能向右滑动吗?可以向左滑动吗?
不能向左,因为左边数据已经发送了,无意义了
2)滑动窗口能变大,变下吗?能变0吗,变0之后,表示什么意思?
能变大,取决于对方给我通告的win大小,本质就是winend++,能变小,我在给对方一直发报文,对方上层一直不接受,给我的win大小越来越小,所以能变小,滑动窗口的大小是浮动的,能变成0,上层一直不接受,表示对方不能在接受数据,就进入流量控制的窗口探测和窗口更新
3)能一直滑动吗?越界怎么办?
把发送缓冲区设计成环状结构,所以可以把历史数据覆盖,把环形区当成字节流
4)滑动窗口的大小,怎么更新的,依据是什么?
目前是根据对方的接受能力,响应报头中的win大小,
应答也是有序的,也是按序达到,应答的seq,winstart = seq,这就是起始位置,应答的win,winend = winstart + win,这就是更新
5)滑动窗口内部的报文既然可以直接发送多个报文,如果第一个丢失了呢?中间的丢失了呢,最后一个丢失呢?
比如:1000,2000,3000,4000,如果1000丢了,2000,3000,4000中任意一个收到了,有两种情况, 数据丢了,应答丢了
收到2001和3001时,所以1000也收到了,所以应答丢了,我们一点也不担心,
但是数据丢了,在2000和3000,4000给你的应答,接收方收了2000,3000,4000,但是接收方的ACK只能写1001
所以会收到很多重复的ACK序号,TCP就能确定数据数据丢了,进行补发,滑动窗口就不会动,一直等,等超时重传,补发后,ACK就会一下到6001,7001,8001
要支持重传,就必须被暂时保持起来,保存在滑动窗口中
中间丢失会传化成第一个丢失,所以滑动窗口中的丢失都会传化成第一个丢失
当中间某一个报文丢失了,连续收到三个同样的序号ACK后,就会对最近的报文做确认,进行重传,这就是快重传,决定重传的下限,即超时重传,一个决定重传的上限,即快重传,超时重传效率会低下
快重传是收到三个同样的ACK才是快重传
拥塞控制
所以client的发送数据都会经过网络,万一是网络出问题呢?
TCP不仅把c和s端考虑了,还考虑了网络,网络中有大量的接收方和发送方,都是遵守的TCP/IP协议
假设考操作系统,成绩出来后,有1到2个人挂科,是同学的问题,可是有30人,只有1到2个人过了,其他人都挂了,这就是题的问题,学校就出了教学事故,所以同样是挂科,挂少和挂多是不同的事情
同理,如果A主机给B主机发信息,只有1到2个报文丢失了,这是正常的,给你补上就是了,如果发10000报文,丢了9999报文,就是网络的问题
当大量丢包的时候,发送方的发送策略是什么?
超时重传/快重传还是等等再发呢,是等等再发,一旦我们谈网络,一定会有其他主机,C端向S发送数据,S端出现大面积丢包,3给4发,5给6发,4和6主机也可能大面积丢包
一旦网络出现拥塞,会覆盖这个网络的所有主机,如果一旦超时重传,所有主机也会超时重传,同一时间,出现大量补发,反而会加重网络拥塞
发送网络拥塞了,就减少发送量,如果少量报文,丢了就继续超时重传,
发生网络拥塞了,要保证:
1、保证网络拥塞不能加重
2、在网络拥塞有起色的情况下,尽快恢复网络通信
拥塞窗口
A向B发送1个2个3个4个报文,发送了网络拥塞,发送方要基本得知网络拥塞的严重程度,必须要进行网络状态的探测
需要对当前的网络情况进行衡量,就有了拥塞窗口,超过这个拥塞窗口的大小时,就可能会发生网络拥塞,没有超过,就小概率的发生网络拥塞
拥塞窗口就是发送方主机,衡量网络健康状态的指标
拥塞窗口:网络的状态是变化的,衡量网络健康状态,即拥塞窗口大小一定是变化的
但是作为主机的你,你怎么知道网络的健康状态是什么样子的呢?
尝试与探测出来的,所以发送方主机一直都要想办法得到当前网络的拥塞窗口,所以发送方:滑动窗口 = min(对端主机的接受能力win,网络的拥塞窗口)
win_start = seq;
win_end = min(seq_win,拥塞窗口)
A主机向B发送数据,一旦识别网络发生拥塞了,如何设置成1呢,把拥塞窗口设置成1就行了,滑动窗口就成1了,也不用担心滑动窗口一直在增长了
为什么用指数增长的方式增长拥塞窗口?
比如:在旧社会,有一些地主、他们有一些土地,有个地主张三,有个农民李四,张三来问李四,你欠我房租什么时候还,李四没钱还,张三就说你一天给我一粒米,明天二粒米,后天给4粒米,李四只会算短期的账,给到20多天时,就发现给不起了,但是地主后面也发送这样太害人了,就说换到1000粒时,就不用还2000了,就变到1001、1002了,为了更压榨农民,就从指数增长变为线性增长
李四为什么会给呢,因为前面给的少
但是一旦过了一个临界点增长速度就快
所以慢启动就是指初时慢,但是增长速度非常快,是为了快速让网络恢复,前期慢,是为了探测,但是也不能一直指数增长吧,到达一个阈值就变为线性增长
慢启动的算法指数增长在增长的同时,在增长到一个阈值时,就线性增长,这也是在不断探测新的拥塞窗口的大小,
在未来的时候又发生网络拥塞,就探测到了拥塞窗口的底线为24,然后把下一个拥塞窗口变为1,慢启动,把上一次拥塞窗口大小砍到一半,就是下一个拥塞窗口的阈值,值为12
整个网络发送就是不断慢启动,然后发生网络拥塞,然后又慢启动
所以拥塞窗口会一直被os维护,所以拥塞窗口一直在更新,就是一直在探测,指数增长和线性增长就是在探测
不断调整拥塞窗口的大小,就是间接控制滑动窗口
在我们计算机中,一定时时刻刻,都会存在一个拥塞窗口的大小
延迟应答&&捎带应答
应答,就是通告我的接受能力的,win
我们只要给对方通告更大的win大小,就能在较大概率上,提高传送效率
对方可能就会更新出更大的滑动窗口大小,就可以一次并发给我们发送更多的数据
你怎么给我更新出一个更大的接收窗口,只要让上层尽快交付,才有可能更新出更大的接收能力,但是我又不知道什么时候交付
所以给上层更多的时间,来进行读取
给更多时间好处就是:
1、让他在这段时间,尽快读取
2、如果一直在读,读取更多
所以有延迟应答,所以本质是为了提高效率
其实三次握手就相当4次握手,因为SNY连同ACK一起发过来了,这就是捎带应答
TCP的可靠性:校验和、序列号、确认应答、超时重发、连接管理、流量控制、拥塞控制
提高性能:滑动窗口、快速重传、延迟应答、捎带应答
面向字节流
我们把1000个数据交给操作系统,os可能发生了几次刷新,我们不关心,而我们就只看见认为一次刷新,所以这就是面向字节流
对于底层,我们看到的二进制都是二进制流,在上层读到二进制流,就知道是什么意思,所以就有编码,操作系统并不关心协议,协议本质就是阐述一个事实,解释os里的二进制流
所以协议就是编码
当在读数据的时候,并不是从硬件直接读到用户层,在触发读的时候,就是在系统调用,操作系统先把数据搬到文件缓冲区里,这时候read、write才起作用,般了几次,还是拷多少kb,我们不关心,你可能一次拷完了,但是OS也许拷了很多次
创建一个TCP的socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;
- 调用write时, 数据会先写入发送缓冲区中;
- 如果发送的字节数太长, 会被拆分成多个TCP的数据包发出;
- 如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去;
- 接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;
- 然后应用程序可以调用read从接收缓冲区拿数据;
- 另一方面, TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可以写数据. 这个概念叫做 全双工
由于缓冲区的存在, TCP程序的读和写不需要每次都匹配, 例如:
- 写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节;
- 读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次read一个字节, 重复100次;
粘包问题
TCP会存在粘包问题,是因为他是面向字节流,没有边界
TCP是被使用的,TCP不认识二进制流,但是上层认识,TCP并不知道这些二进制流的意义
当TCP收到http请求,发给对方,也许收到了1个半2个半,但是我们并不知道是不是一个完整的报文,你必须要读完整的报文,读不完整,交给上层,上层解析不出来
这种多读,少读,就是粘包问题,所以序列化和反序列化之前先要解决粘包问题
为什么要分清报头和有效载荷,就是怕粘包问题
比如:蒸包子,就想拿一个,但是拿了多个或就一个皮,所以蒸的时候会一个一个分开,分开,就是解决粘包问题,明确包子与包子的边界
如何解决,比如之前整个报文就有\r\n,就以\r\n来区别报文
UDP不存在粘包问题,因为UDP中携带定长报头加报文的长度,有明显的边界,一定是一个完整的报文,不会出现半个的,所以UDP直接序列化和反序列化
TCP协议只有4位首部长度,没有有效载荷的长度,因为他不需要,能根据4位首部长度去掉报头,剩下的就是有效载荷,还有校验和,也是按序到达的,所以不需要有效长度
内存泄漏,肯定是malloc和new申请空间了,不还,内存申请多了,然后进程挂掉了,曾经的那些堆空间还在不在,这些内存在不在?
不在了,因为进程的pcd、页表、空间都是os管理的,进程挂掉了,os会回收的
同样的网络也是如此,建立一个链接也是os帮你建立的,进程退出了,os会私自给你做四次挥手
关机之前,进程都要先杀掉的,关机前,底层会自动进行4次挥手,这就是为什么关机有时候快,有时候慢
TCP的链接和管理的作用,是由上层来做,是应用层来维护的,因为只有应用层才知道要连接多久
以前QQ是UDP来做的,UDP不用建立连接,不用维护成本,你给我发消息,我就给你转过去,是即时通信的
面试题:用UDP来实现可靠性,怎么做?
引入序列化:保证数据顺序
确认应答:确保对端收到了数据…
二、三次握手和四次挥手
服务端状态转化:
- [CLOSED -> LISTEN] 服务器端调用listen后进入LISTEN状态, 等待客户端连接;
- [LISTEN -> SYN_RCVD] 一旦监听到连接请求(同步报文段), 就将该连接放入内核等待队列中, 并向客户端发送SYN确认报文.
- [SYN_RCVD -> ESTABLISHED] 服务端一旦收到客户端的确认报文, 就进入ESTABLISHED状态, 可以进行读写数据了.
- [ESTABLISHED -> CLOSE_WAIT] 当客户端主动关闭连接(调用close), 服务器会收到结束报文段, 服务器返回确认报文段并进入CLOSE_WAIT;
- [CLOSE_WAIT -> LAST_ACK] 进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据); 当服务器真正调用close关闭连接时, 会向客户端发送FIN, 此时服务器进入LAST_ACK状态, 等待最后一个ACK到来(这个ACK是客户端确认收到了FIN)
- [LAST_ACK -> CLOSED] 服务器收到了对FIN的ACK, 彻底关闭连接.
客户端状态转化:
- [CLOSED -> SYN_SENT] 客户端调用connect, 发送同步报文段;
- [SYN_SENT -> ESTABLISHED] connect调用成功, 则进入ESTABLISHED状态, 开始读写数据;
- [ESTABLISHED -> FIN_WAIT_1] 客户端主动调用close时, 向服务器发送结束报文段, 同时进入FIN_WAIT_1;
- [FIN_WAIT_1 -> FIN_WAIT_2] 客户端收到服务器对结束报文段的确认, 则进入FIN_WAIT_2, 开始等待服务器的结束报文段;
- [FIN_WAIT_2 -> TIME_WAIT] 客户端收到服务器发来的结束报文段, 进入TIME_WAIT, 并发出LAST_ACK;
- [TIME_WAIT -> CLOSED] 客户端要等待一个2MSL(Max Segment Life, 报文最大生存时间)的时间, 才会进入CLOSED状态
较粗的虚线表示服务端的状态变化情况;
较粗的实线表示客户端的状态变化情况;
CLOSED是一个假想的起始点, 不是真实状态;
三次握手
connect本质就是发起三次握手
1、什么是链接
一定在OS内同时存在多个建立好的链接,那么OS要不要把这些已经建立好的链接管理起来呢?要管理,先描述,再组织,OS内为了管理连接维护的数据结构 struct tcp_link{…}
创建并维护连接是有成本的,要消耗内存空间和CPU资源
3次握手的过程,由双方的OS系统中的TCP层自主完成
connect,触发连接,等待完成
accept,等待建立完成,获取连接
报文在路上跑,收到的时间一定有差别
对于双方来说,从来就不担心第一个报文和第二个报文丢失,连接就没成功,就没什么损失
第三次丢失就有影响,只要把最后一个ACK发出去,就说明连接成功了,但是对于客户端来说,我不知道第三次,对方收到没,所以三次握手就是在赌
客户端认为建立好了,服务端认为没建立好,客户端就可能对服务器发送消息,但是服务器认为没连接,就给客户端发送RST响应重置,
所以客户端把连接关掉,就重新进行连接,所以三次握手的本质就是在赌,赌对方收到第三个ACK,所以建立连接成功与否是有概率的
为什么是3次?不是2、4、5、6次呢
2次握手,是绝对不行的,凡是客户端发来的SYN,服务器就认为连接成功了,我拿个单片机,发送海量的SYN,一瞬间就把服务器的链接请求满了,资源就没了,海量发送SYN就叫SYN洪水, 所以2次握手,非常容易收到攻击
如果我们是主动发起连接的一方,最后一次发送ACK是没有应答的,最后一个报文是谁发的,就是谁先连接,如果是偶数次握手,就是服务器主动发起最后一个握手,如果是奇数次握手,就是客户端主动发起最后一个ACK,最后一个报文在网络中是要花时间的,所以是偶数,一定是服务器先建立好链接,所以把不确定是带给服务器,最后一个ACK客户端有没有收到,服务器不确认。
为什么是奇数不是偶数?
因为连接握手一定会发送异常,一定用概率,服务器遇到多个客户端,发送概率是很大的,但建立连接异常是不担心第一和第二次,如果是偶数次,最后一次是ACK承担连接的成本就嫁接到客户端,那服务器端面对的是多个客户端,服务器维护这个异常就是必然的,对服务器不好,所以就直接搞成奇数次握手,对应客户端来讲,就是客户端主动发起连接的,主动发送的人,在奇数当中,都是最后一个发送ACK的人,当服务器收到最后一个ACK时,就会把最后一个ACK发送异常的成本嫁接给客户端,服务器没收到ACK,客户端认为已经连接了,只需要连接重置就行了,服务端的成本维护依旧低
为什么是3次?
1)没有明明显的设计漏洞,一旦建立连接出现异常,成本嫁接到client,server端成本较低
2)验证双方通信信道的通畅情况—
三次握手是验证全双工通信信道通畅的最小成本
四次挥手
四次挥手是断开连接的最小成本
CLOSE_WAIT:一方想关连接,一方还不想关,被动关闭连接的一方,就会维持CLOSE_WAIT
在服务器或客户端通信的时候,一定不要忘记关闭fd,不关闭fd,CLOSE_WAIT会维持一段时间,这个链接状态会一直存在,可用资源会一直变少
主动断开的一方,要进入TIME_WAIT状态
TIME_WAIT:临时性状态,过一会就没了,这时候连接还在,也可以理解成不在了,是一个临界的状态,服务器主动断开的一方,在进入TIME_WAIT时候,双方进程早就退出了,但是我们的底层还是链接的,也就是port号还是在使用的,也就是为什么我们前面主动退出server,在登录的时候,要换port的原因
1、为什么要进行TIME_WAIT
保证最后一个ACK被对方收到,连接建立,异常断开也是可以的,最后一个ACK丢了,就一直等,
2、为什么是TIME_WAIT的时间是2MSL?
MSL是TCP报文的最大生存时间, 因此TIME_WAIT持续存在2MSL的话,就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启, 可能会收到来自上一个进程的迟到的数据, 但是这种数据很可能是错误的),同时,也是在理论上保证最后一个报文可靠到达(假设最后一个ACK丢失, 那么服务器会再重发一个FIN. 这时虽然客户端的进程不在了, 但是TCP连接还在, 仍然可以重发LAST_ACK);
3、sever主动关闭连接的场景?
比如:双11期间,客户端越来越多,后面又来了一个链接,这是server抗不住了,立马挂掉了,以前的连接的客户端,服务器都是主动断开的一方,服务器会存在大量的TIME_WAIT,这是服务器会尽快重启,但是还处于TIME_WAIT状态,服务器就一直等,这段时间就导致服务器无法重启,但是对于大公司来说,服务器挂掉一会也是重大失误,所以要立马重启,所以有个接口setsockopt
为什么是4次挥手?因为要双方确认
listen的第二个参数
三次握手,会在底层自动完成
netstat -natp:查看所以tcp的client和server的链接情况
telnet:测试本地连接情况
没有accept,listen的第二个参数设为1,已经连接的就只有2个,后面就出现SYN_RECV,这些SYN_RECV就是在般连接的队列里,时间非常短
TCP协议,需要在底层维护全连接队列,最大长度是:listen的第二个参数+1
比如:海底捞排队,门口排队就是为了时时刻刻保证自己餐厅内部资源的使用率是100%,海底捞不能没有外部的队列,但是如果海底捞把排队的队列越来越长,人也会离开,资源还浪费了,还比如那店门扩大一点
所以这个队列不能太长,原因:
1、因为会过多消耗OS本身的资源,本来是可以给sever使用的
2、队列太长,client不愿意等