一、认识 TCP 的报文

TCP 如何将报头和有效载荷分离?
答:TCP 不包含选项的话,固定大小是 20 个字节,剩下的就是有效载荷,如果包含选项,那么就要看 4 位首部长度,4个比特位的转换成 10 机制取值为:【0,15】,TCP 约定这个取值的单位是4字节,所以所以 TCP 报头的大小取值为:【20,60】字节;那么有效载荷的大小 = 报文大小 – TCP 报头大小,此时就能实现报头和有效载荷分离;因为 TCP 是面向字节流的,不需要关心报文是否完整,只关心报头完整就行,所以 TCP 不包含整个报文的长度;
TCP 可靠性问题?
答:因为 TCP 发送请求,如果对方没有回答,那么就无法保证这个请求是否被对方收到,所以只要对方应答请求,就能保证历史报文都能被对方接收到了;
结论:确认应答,保证TCP的可靠性;
细节:
1.请求 VS 应答:消息 != 应答,消息包含了用户信息的内容,应答就是一个单纯的应答;
2.TCP 不是要保证应答的可靠性,保证的消息的可靠性;
3.应答方不确定应答对方是否收到,但是发送方,有没有收到对方的应答是能确定的;
4.收到应答本质是为了保证历史消息的可靠性;
结论:不存在 100% 可靠的协议,TCP 保证的被应答的报文100%可靠;
1)序号和确认序号
TCP 发送一个请求,收到一个应答,这是串行的,导致效率低下,当然这也是 TCP 的常规方式之一,但是 TCP 一般的通信模式是,一次性发送尽可能多的请求,例如:发送 3 个请求,那么就要收到 3 个应答,这就在时间上进行了重叠,进而提升效率;注意这里的应答用户不参与而是 OS 主动完成的(OS 的应答是只有报头没有数据的);
TCP 另外一个可靠性:TCP 假设一次性发送 3 个请求,那么这 3 个请求一定是按顺序到达目的主机的吗?不一定,所以报文就不可靠,所以 TCP 给每个请求定义了序号,按发送顺序进行排序,接收方收到所有的请求之后进行排序,所以这就保证了请求按序到答;但是,这就带来了一个问题,如果这个 3 个请求的序号为:100、200、300,接收方直收到了 100 和 300 ,200 丢包了,所以 TCP 应答时,会弄个确认序号,确认序号的值和请求的序号的值不一样,是这样的:200 序号的请求丢了,那么应答是100序号的确认序号为 101 ,300 序号的确认序号为 301 ,确认序号含义(ack_seq):该数字之前的报文已经全部收到,下次发送请从 ack_seq 开始发送,例如:101确认序号:已经收到序号为100的序号,但是下次发送请从 101 序号开始发;此时 100 和 300 的确认序号 OS 会应答确认序号值最小的那个也就是 101 ,表明后面 200 和 300 的序号的请求报文没有收到;
真实情况(面试题):首先:客户端向服务器发起请求,这个请求包含报头 + 数据,此时服务器收到这个条报文之后,OS 就有应答,这个应答不是用户应答,这个应答只有报头(报头里面有序号和确认序号),没有数据,OS 做出应答本质是给客户端的 OS 说:我已经收到这个应答了;都是此时服务器端的用户发起了一个报文(含报头 + 数据),那么 OS 再发送这个报文,这样的效率是非常慢的;所以 OS 的应答 和 用户发送的报文合并成为一个报文发送给客户端,这样就大大提升了通信效率;这样的方式称为:捎带应答;所以:结论是:一个报文即是对上一个报文的确认(确认序号),也是一个数据报文(序号);所以 TCP 的报头里面同时存在序号和确认序号;
注意:ack_seq 不是对一个报文做确认,是对历史报文做确认;
2)16 位窗口大小

因为 TCP 通信时,例如:主机 A 向 主机 B 发起消息(报文),此时如果 B 主机的接收缓冲区满了,就会导致这个报文直接丢包,这是变相的浪费资源(电力、路由算力),所以为了防止(不能杜绝,只能控制)这种情况,主机 A 必须知道主机 B 的接收缓冲区大小;两个问题:怎么衡量对方的接收缓冲区的大小——通过接收缓冲区的剩余空间大小衡量,这个空间大小填写到报头的 16 位窗口中;怎么知道对方 16 位窗口——对方进行捎带应答时,此时这个报头是有对方的 16 位窗口大小的(接收缓冲区的剩余空间大小);
根据 16 位窗口大小,调整发送数据速率的机制称为:流量控制.
细节1:流量控制是双向的(如果是单选发送的,就只做单选流量控制);
细节2:16 位窗口大小填写的是自己的接收缓冲区的剩余空间大小;
细节3:如果对方的接收缓冲区非常大,我们可以多发数据来提升效率,所以流量控制即做可靠性也兼顾效率问题;
3)标志位
什么是标志位?
答:就是OS中 TCP 结构体中的结构体位段中的比特位,其实就是:0(无效)or 1(有效),
为什么要有标志位?
答:如果有多个客户端访问服务端,此时服务端就有大量的报文,例如:正在连接的、正在断开连接的、正常的报文、应答报文等等,为了更好的区分这些报文,要通过标志位来区分报文的类型;类型决定了这个报文是用来干嘛的;
标志位有哪些:
URG:紧急指针是否有效,该报文是一个确认报文!大部分情况下,TCP 的报文都是捎带报文,所以大部分的 ACK 都置为 1;
ACK:确认号是否有效
PSH:提示接收端应用程序立刻从TCP缓冲区把数据读走
RST:对方要求重新建立连接;我们把携带RST标识的称为复位报文段
SYN:请求建立连接;我们把携带SYN标识的称为同步报文段
FIN:通知对方,本端要关闭了,我们称携带FIN标识的为结束报文段
Tcp 是面向建立连接的,通信之前先进行三次握手进行建立连接:
什么是三次握手?

客户端向服务端发送请求连接的报文,这个报文的标志位有:SYN(就是把 SYN 置为 1,其余标志位置为 0),此时服务端收到这个报文也给客户端发给了报文(就是应答),这个报文的标志为有:SYN 、ACK,意思是说:我同意并且我也要和你进行连接,此时客户端收到这个报文之后,客户端的应答报文的标志有:ACK,意思就是说:好的;
为什么要建立三次握手?
答:以最短的时间内,服务端和客户端进行通信认识;
四次挥手:

客户端要和服务器进行断开连接了,此时客户端发报文(标志位有:FIN),此时服务端收到这个报文,服务端的应答报文(标志位有:ACK)发给客户端,此时服务端也要断开连接,向客户端发起报文(标志位有:FIN),那么客户端的应答报文(标志位有:ACK)发给服务端;这个过程就是 4 次挥手;
因为服务端应答客户端的 FIN 时也要和客户端进行 FIN ,所以服务端的应答 ACK 和发起 FIN 的报文可以合并称为一个报文(捎带应答),所以说 3 次挥手也行;
注意:三次握手的本质也是四次握手,只不过第二次和第三次往往被捎带应答了;
如果服务端的接收缓冲区满了,此时客户端就只能等一会再发送报文,问题是客户端怎么知道服务端的接收缓冲区什么时候有剩余空间,所以客户端就给服务端发送询问报头,此时服务端就发送报文告诉客户端还没有剩余空间,此时客户端一在发送询问报头,发的不耐烦了,就发送一个携带标志为 PSH 的报文给服务端,让服务端快速让应用层把你的缓冲区里面的数据拿走;
细节:PSH 标志即使对方有空间也能被设置该标志,例如:xshell 的命令
细节:怎么让应用层快速拿走数据,当然。应用层有我们来实现,我就不拿,你也拿我没办法,所以此时 PSH 的作用就是唤醒应用层的读端进程;
重弹三次握手:

因为服务端可以被多个客户端连接,所以会同时存在多个连接,OS 为了管理这些连接,就要创建结构体描述这些连接,所以维护和创建连接是有成本的;
在 三次 握手的过程,不是百分比建立连接成功的,在三次握手中,最害怕是最后一次握手的报头丢失,因为站在客户端的视角是与服务端建立连接完成,站在服务端的视角没有于客户端建立连接,注意:只有发出和收到就是一次握手,不必等到对方应答,因为你也等不到对方应答(最后一次握手客户端是收不到对方的应答报头的),如果最后一次握手的报头丢失,那么客户端给服务端发送报文,此时服务端就会返回一个应答报头(带有标志位:PST),这个标志位就是让对方重新进行连接(三次握手),结论:PST:用来处理连接异常的时候让对方进行连接重置;
除了最后一次握手的报头丢失情况,还有拔网线、突然关机等情况,也是可以设置 PST 让对方异常的主机重新建立连接;
因为报文是按序到达对方的主机的,但是有些报文是要插队的,例如:我往手机里面装个程序,装到一半不想装了,发了取消安装的报文过去,此时取消安装的报文到达对端时竟然要排队,所以这是不行,我们可以往这个报文设置 URG 标志位:紧急指针标志位,用 16 位紧急指针来设置紧急的操作是什么;因为除了这种方案还有另外一种:建立两条连接,一条用来传输数据,一条用户输入控制命令,对端一但读到控制命令,就可以指向指令,所以 URG 是非常冷门的,因为有替代方案;而且插队是会对对端接收报文的产生不良影响;16 位紧急指针指向当前报文有效载荷的一个偏移量,紧急数据只能有一个字节;
二、超时重传机制


问题:我发的报文,你有没有收到,我知道吗?
答:我收到应答,对方100%收到了我发的报文;
问题:我发的报文丢失了,我知道吗?
答:丢包只有两种情况:a.请求丢失,b.应答丢,两种情况不可能同时发生;所以我发的报文丢失我不知道,所以 TCP 约定一个时间,在这个时间内对方返回应答,我就能确认应答,但是如果没有在这个时间内收到应答,我们判定超时(不叫判定丢包),重新发送请求;
细节1:报文没丢,应答丢了?
答:重传导致发送的报文不可能重复,因为有序号去重;
细节2:这个约定的时间是多少?
答:超时时间太长导致:请求方发送的效率降低;时间太短导致:误判,导致重传机制被高频的触发;又因为网络的通畅程度是浮动的,所以:
Linux中(BSD Unix和Windows也是如此),超时以500ms为一个单位进行控制,每次判定超时重的超时时间都是500ms的整数倍,如果重发一次之后,仍然得不到应答,等待2*500ms后再进行重传.如果仍然得不到应答,等待4*500ms进行重传,依次类推,以指数形式递增.累计到一定的重传次数,TCP认为网络或者对端主机出现异常,强制关闭连接。
三、连接管理机制
TCP 通信之前,进行三次握手;
connect 发起三次握手,这三次握手由双方的 OS 主动完成;accept 不参与三次握手,只获取已经建立好的连接;
经典面试题:TCP 三次握手,为什么是三次?

答:1.验证全双工——外界同意;客户端发送请求,为了验证服务端确实收到了这条消息,服务端应答了,进而验证了客户端能发送消息,也能收到消息,客户端全双工验证完成;因为服务端应答了,为了验证服务端成功发送了这条消息,所以客户端应答给服务端,此时服务端收到了应答,验证服务端可以收发消息;所以这个三次握手的过程,保证网络是通畅的,进而保证了 TCP 通信的可靠性;
2.三次握手的本质是四次握手——自己同意,因为有捎带应答变成了三次握手;三次握手是最少次数握手建立双方要通信的共识,双方都知道我要和对方进行通信了;
为什么是四次挥手?
答:
1.以最少次数建立断开连接的共识;
2.保证网络通畅,进而释放全双工;
3.客户端发送一次 FIN 本质就是:我要发送的数据发完了,我要关闭一个写端了,所以服务端应答之后客户端关闭写端了(通过系统调用:shutdown 来指明关闭哪端),虽然客户端没有数据了,多少有可能我服务端有数据,我还要给你发,所以服务端的 ACK 和 FIN 是分开的原因,进而造成了四次挥手的具体描述,如果服务端没有数据了,绝对是捎带应答变成三次握手;
如果 Server 忘记关闭自己的 fd ,就会导致 Client 断开之后,Server 链接处于 CLOSE_WAIT 终端,进而导致 fd 泄漏;
主动断开连接的那一方,4 次挥手已经完成;自己不能立即退出,而是等待一段时间,在退出,在这段时间内,让自己处于 TIMEp_WAIT 状态;
这段时间的多长?
答:2 MSL(Max Segment Life): 报文最大生存的时间(在网络中);
为什么要等待这段时间?
答:原因:1、强制更换服务端的端口号,因为:当客户端历史数据在网络中因为各种原因阻塞在网络时,客端端直接断开,再次连接时,如果服务端还是那个端口号,此时历史数据就会发送到服务端那里,造成新旧数据混乱;因为在等待的这段时间,连接是没有完全断开的,所以服务端的这个端口号就被占用,进而如果有服务端要绑定同一个端口号就会失败,进而旧的数据就不会发送到新的端口号的服务端;如果一个服务端和客户端在连接过程发送的数据在网络中积压,报文的序号会解决数据混乱的问题;
2.等待陈旧报文从网络中消散;
总之就是:
1.防止陈旧报文对新连接产生干扰,让陈旧报文消散;
2.没有消息就是好消息:主动连接的最后一次发送报文携带标志位: ACK,他是不知道 ACK 是否被对方收到的,所以在 2 MSL 时间内没有收到对方发送的 FIN 就认为报文被对方接收到了;
服务端不敢随便更改端口号,所以服务端即使处于 TIME_WAIT 状态,也要让服务器重启,函数如下:
int opt = 1;
//地址复用——> socket addr
setsockopt(_sockfd,SOL_SOCKET,SO_REUSEADDR | SO_REUSEPORT,&opt,sizeof(opt));//忽略 TIME_WAIT 状态
服务器重启之后,客户端就要重新发起连接请求,此时客户端的端口号就要改边,进而防止客户端的新旧数据混乱我们服务端;
问题:主机 A 第一次向主机 B 发送数据时,是怎么知道主机 B 的窗口大小的呢?
答:在通信之前双方是经历过三次握手的,前两次握手,一定不携带任何数据(通信未完成),这两次握手就已经交换过双方的窗口大小了;
细节:窗口大小是 2 ^ 16 个字节 = 64 kb ,但是这个大小会根据不会的 OS 缓冲区的大小进行变动,因为报头的40个字节的选项,包含了一个窗口扩大因子:M,如果OS缓冲区的大小大于 64 kb 就这个 2^16 左移 M 位;
网硕互联帮助中心





评论前必须登录!
注册