1.认识TCP
在网络通信模型中,传输层有两个经典的协议,分别是UDP协议和TCP协议。其中TCP协议全称为传输控制协议(Transmission Control Protocol),从名称就可以看出,TCP协议需要对数据的传输进行严格的控制。
UDP协议具有无连接、不可靠、面向数据报的特点,而TCP协议恰恰相反,具有 有连接、可靠、面向字节流的特点。而其中,可靠性是TCP最著名的特点;也正因为TCP协议需要保证通信的可靠性,所以TCP协议才会有一系列保证可靠性的机制和策略,这也是我们需要重点学习的内容。
2.TCP协议段格式
所谓协议,其实就是通信双方都认识的结构化的数据。TCP协议是传输层的协议,传输层的协议是在操作系统内部实现的,所以操作系统内部一定有TCP协议相关的代码。
Linux内核中TCP协议部分代码:
把代码形象化便得到了下面这张图:
各个字段的粗略认识:
1、16位源端口和16位目的端口:表明数据从哪个进程来,要发送给哪个进程。
2、32位序号和32位确认序号:序号可以用来对接收到的报文进行按序到达和去重,确认序号表明该序号之前的报文都收到了。(后面会详谈)
3、4位首部长度:表明TCP报头的长度。TCP报头由固定长度的20字节和不固定的选项构成,四位首部长度表明了这两部分共同的长度。其中,首部长度是有基本单位的,基本单位是4字节。4个比特位的最大取值是15,所以四位首部长度的最大范围是60字节。
4、6位标记位:
**URG: **表明紧急指针是否有效
**ACK: **表明确认号是否有效
PSH: 提示接收端应用程序立刻从 TCP 缓冲区把数据读走
RST: 对方要求重新建立连接; 我们把携带 RST 标识的称为复位报文段
SYN: 请求建立连接; 我们把携带 SYN 标识的称为同步报文段
FIN: 通知对方, 本端要关闭了, 我们称携带 FIN 标识的为结束报文段
**5、16 位窗口大小: **表明自己的接收能力,通信双方可以动态的调整发送报文的大小。
**6、16 位校验和: **发送端填充, CRC 校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含 TCP 首部, 也包含 TCP 数据部分.
7、16 位紧急指针: 标识哪部分数据是紧急数据,需要优先处理。
**8、40 字节头部选项: **暂时忽略;
3.可靠性保证的机制
确认应答机制
铺垫:什么是序列号?
我们可以这样理解。操作系统会为TCP分配两个进行通信的缓冲区,我们把缓冲区当成char类型的大数组,那么缓冲区中的数据不就天然的具有编号了吗?这个编号我们把它叫做序列号。序列号是对每个字节的编号,这也体现出了TCP面向字节流的特点。
TCP需要保证可靠性,首先需要保证发送方发送的数据,接收方要能收到。那发送方如何得知接收方是否收到了自己发送的数据呢?这个时候,TCP协议便引入了确认应答机制。
确认应答机制就是接收方对接收到的报文进行应答,这样一来,发送方就知道对方有没有收到我发送的数据了。并且这个应答是需要在指定的时间内收到的,如果主动发送数据的一方没有在规定时间内收到应答,那么它就会认为对方没有收到我发送的数据。
但是问题又来了,发送方发送一次消息,接收方应答一下,这样似乎没有什么问题,但是,这种通信模式的效率是非常低的,所以,通信双方进行通信的时候,数据的发送往往是并行的。
那么问题又来了,接收方收到多个报文的时候,如何保证能够正确的进行应答呢? 其实啊,应答的时候,是通过序列号来完成的。序列号是能够确认顺序的,接收方会对收到的报文根据序列号进行排序,一旦排好序之后,接收方就会先判断最小序列号之前的报文是否全部收到,如果该序列号之前的报文全部收到,才会进行应答,并且应答的时候是需要填充32位确认序号的,确认序号的值是收到的报文的序列号+1,表明该序列号之前的数据我全部收到了,下次你应该从哪里发。这其实这就是按序到达策略。
如果收到了对方的确认序号,下一次发送数据的序号就是 收到的确认序号+要发送报文的长度。
我们已经知道了确认应答机制是通过序列号来完成的,那你有没有这样的疑问,为什么报头中有序列号了,还需要有一个确认序号呢?直接用序列号的字段来表明确认序号不就可以了吗?
这是因为,在实际通信的过程中,接收方往往也需要向发送方响应消息。 也就是说数据的接收方既要响应,又要发送消息;那么这个过程可不可以一步到位呢?这是可以的,序列号表明自己发送的报文,确认序号可以作为接收消息的响应。那么,这样一来,中间的两次发送可以合并为一次发送,这样,不就又提高了通信的效率了吗?这其实就是捎带应答。
超时重传机制
前面我们已经知道了TCP可靠性保证的一个机制 —— 确认应答机制,接收方需要向发送方进行响应,表明自己收到了对方的消息。但是,如果发送方一直没有收到对方的响应,会怎么办呢?这就需要引入超时重传机制了。
发送方发送数据之后,会等待一段时间,在该时间内,如果收到了对方的响应,就表明对方收到了我发送的数据;如果在该时间段内,没有收到对方的应答,发送方就会认为对方没有收到自己发送的数据,这个时候就需要再发送一遍,这就是超时重传机制。
超时重传的时间怎么定呢?
数据是需要通过网络进行发送的,如果网络状态比较好的话,数据发送的速度就会比较快,如果网络比较差的话,数据发送的速度就会比较慢。也就是说,网络的状态是动态变化的,那么超时时间的设置也必须是动态变化的,如果网络状态比较好的话,超时时间就可以设置的短一点,如果网络状态比较差的话,超时时间就设置的长一点。
• Linux 中, 超时时间以** 500ms 为一个单位**进行控制, 每次判定超时重发的超时时间都是500ms 的整数倍.
• 如果重发一次之后, 仍然得不到应答, 等待 2 * 500ms 后再进行重传.
• 如果仍然得不到应答, 等待 4 * 500ms 进行重传. 依次类推, 以指数形式递增.
• 累计到一定的重传次数, TCP 认为网络或者对端主机出现异常, 强制关闭连接.
超时重传机制存在的问题
由于超时重传机制的存在,在规定时间内没有收到应答就会进行重传。那有没有可能,发送方发送的数据在网络中阻塞了一段时间,但是在一段时间后被对方收到,但是这个时候已经重传了?还有没有可能,对方已经收到了报文,但是响应丢了呢?不管是那种情况,都会导致,发送方重复发送对方已经收到的报文,那么接收方就会收到重复的报文,这个时候怎么办呢?
不要忘了TCP协议报头中有序列号,序列号不仅仅可以用来做为应答,还可以用来去重。当接收方接收到消息的时候,它会根据序列号判断这个报文我曾经是否收到过,如果收到过的话,就直接将该报文丢弃了。所以我们不用担心重复报文的问题。
接收方如何判断这个报文我曾经是否收到过呢?
这个问题更具体的解决策略就是,接收方根据自己最新一次的确认序号就能知道多少号报文之前的报文我都收到了,如果对方发过来的报文的序号小于最新一次的确认序号,那么该报文就能丢弃了,也就实现了去重。
连接管理机制
TCP协议是面向连接的协议,通信之前,通信双方必须进行三次握手建立连接,通信之后,通信双方必须进行四次挥手断开连接。
三次握手
使用TCP协议进行通信的时候,通信双方必须建立连接才能进行正常的通信,当通信结束时,通信双方也必须断开连接以确保不会造成服务器端的资源浪费。所以在基于TCP通信的过程中,会有各种各样的报文,有的报文是用来请求建立连接的,有的报文是用来进行正常通信的,有的报文是用来请求断开连接的。为了区分这些不同的报文,于是,TCP协议报头中引入了标记位。
TCP协议中与连接管理有关的标记位:
SYN:SYN标记位也称为同步标记位。如果客户端发送的报文中的SYN标记位被置为1,服务器端就知道对方想与我建立连接了。
FIN:FIN标记位也称为结束标记位。如果客户端发送的报文中的FIN标记位被置为1,服务器端就知道对方想与我断开连接了。
ACK:ACK标记位我们可以称其为应答标记位。用于表明该应答中的确认序号是否有效,也就是表明该报文是否是用于应答的报文。
RST:重置标记位。要求对方重新建立连接。
三次握手过程中套接字的状态变化:
1.双方未建立连接的时候,双方的套接字都处于CLOSED状态。
2.服务器端需要先调用listen接口将自己的套接字状态设为LISTEN状态,等待客户端连接。
3.此时,客户端需要主动调用connect接口向服务器发起连接,此时客户端套接字状态变为SYN_SENT状态。
4.当服务器端监听到连接请求(SYN报文), 就将该连接放入内核等待队列中, 如果它也想与对方建立连接,就会在发送的报文中将SYN和ACK标记位置1,当该报文发送出去的时候,客户端的套接字状态进入SYN_RCVD状态。
5.当客户端收到服务器端的应答的时候,他就认为连接建立好了,客户端的套接字状态进入ESTABLISHED状态,并向服务器端发送一个ACK报文,表明我也愿意与你通信。
6.当服务器端收到这个ACK报文的时候,服务器端也认为连接建立好了,服务器端的套接字状态就进入ESTABLISHED状态。
此时,通信双方都认为连接建立好了;在这个过程中,客户端通过connect函数发起连接,服务器端的accept函数并不参与三次握手。
一个问题:
在这个过程中,我们发现,客户端认为连接建立好的时间是早于服务器端的。如果客户端发送信息的需求非常迫切,一旦认为连接建立好了就要发送消息,但是此时服务器端的连接还没有建立好呢?
这个时候,当服务器端收到客户端的正常的通信报文的时候,就会向客户端响应一个RST标记位被置为1的报文,要求对方重新建立连接。也就是重新进行三次握手。
那你有没有思考过一个问题,为什么建立连接之前要进行三次握手呢?
1.双方要进行通信,首先要确保通信的信道是健康的。三次握手的工程中,客户端发送的数据,被服务器端接收到之后,服务器端要对客户端进行响应,如果客户端也收到了服务器端的应答,说明客户端是能够进行收发的;同理,客户端也要对服务器端进行响应,如果服务器端收到了客户端的应答,说明服务器端也是能够进行收发的。
2.通信双方都能进行数据的收发还不够,还需要检查对方是否愿意和自己通信。三次握手的过程中,都有一次给对方的响应,说明对方是愿意和自己进行通信的。
此时,通信双方都能够进行数据的收发,并且,对方也愿意与自己进行通信,那么此时就可以建立连接进行通信了。
三次握手的本质:
在三次握手的过程中,服务器是提供服务的一方,当有客户来请求建立连接的时候,服务器肯定是愿意的,并且也要询问对方是否愿意和自己建立连接,再者,两次报文中并不涉及数据,只是涉及报头中标记位的变化,所以,两次报文可以进行捎带应答,合并成一个报文,这才有了三次握手。也就是说,三次握手的本质也是四次握手,只不过中间的两次被捎带应答,合二为一了。
谈完三次握手建立连接,我们现在谈谈四次挥手断开连接。
四次挥手
四次挥手过程中套接字状态的变化:
我们假如客户端主动请求断开连接。
1.客户端主动调用 close 时, 向服务器发送结束报文段, 同时进入 FIN_WAIT_1;
2.当客户端主动关闭连接(调用 close), 服务器会收到结束报文段, 服务器返回确认报文段并进入 CLOSE_WAIT;
- 客户端收到服务器对结束报文段的确认, 则进入 FIN_WAIT_2, 开始等待服务器的结束报文段;
4.服务器进入 CLOSE_WAIT 后说明服务器准备关闭连接(需要处理完之前的数据); 当服务器真正调用 close 关闭连接时, 会向客户端发送FIN, 此时服务器进入 LAST_ACK 状态, 等待最后一个 ACK 到来(这个 ACK 是客户端确认收到了 FIN)
5.客户端收到服务器发来的结束报文段, 进入TIME_WAIT, 并发出 ACK;客户端要等待一个 2MSL(Max Segment Life, 报文最大生存时间)的时间, 才会进入CLOSED 状态.
6.服务器收到了对 FIN 的 ACK, 彻底关闭连接
理解CLOSE_WAIT状态和TIME_WAIT状态:
当客户端主动请求断开连接的时候,说明客户端要发送的数据发送完了,但是服务器端要发送的数据不一定发送完了,所以服务器端不能立即调用close函数断开连接,而是进入CLOSE_WAIT状态,直到将要发送的数据发送完之后,才向客户端发送请求断开连接的报文,也就是说进入CLOSE_WAIT状态的一方不会立即关闭文件描述符。所以,如果我们发现我们的服务器上有大量的CLOSE_WAIT状态,很有可能是服务器端没有关闭文件描述符。
如果客户端是主动断开连接的一方,当客户端收到来自服务器端的断开连接的请求报文的时候,就会进入TIME_WAIT状态,处于TIME_WAIT状态的一方不会立即断开连接,而是需要进行一段时间的等待。这是因为网络中可能还有历史报文,如果连接关闭之后,立马又来了相同的连接,那么历史报文就会对新的连接发送的报文造成影响,等待一段时间可以让历史报文消散。当然,还有一个原因。如果服务器端没有收到客户端发送的最后一个ACK报文,服务器端可以要求客户端进行超时重传,此时连接还在,是可以进行超时重传的,也就保证最后一个报文可靠到达。
进入TIME_WAIT状态的一方需要等待的时长是两个MSL(maximum segment lifetime)时间,MSL时间并不是指数据从发送到接收所花费的时间,而是数据在网络中的最大存活时间。
等待两个MSL时间,是因为客户端发送最后一个ACK需要消耗一个MSL时间,如果服务器端要求客户端进行重传,客户端接收消息也需要消耗一个ACK时间。
和三次握手一样,我们来思考一下,为什么断开连接要进行四次挥手呢?
和三次握手一样,断开连接也需要表明通信双方的意愿,这个过程需要双方进行至少一次的互问互答来完成,当双方都发起断开连接的请求之后,并且也都收到了对方肯定的回答,那么这个时候就可以断开连接了。
四次挥手的过程和三次握手的过程挺像的,那中间的两个报文能否合并成一个报文呢?
通信双方断开连接的时候,必须保证待发送的数据都已经发送完了。假如客户端发起断开连接的请求,客户端是知道自己没有数据再要发送了,也就不会再向服务器发送消息了(这里的消息主要是数据,不包括协议报头),所以才会要求断开连接。但是服务器端不一定将待发送的数据都发送完了,服务器必须保证待发送的数据发送完之后才能断开连接。也就是说客户端发起断开连接请求的时候,如果服务器没有需要发送的数据了,那么此时是可以将中间的两个报文合二为一的,但是这种情况的概率非常小。所以断开连接的时候通常是四次挥手。
版权归原作者 手捧向日葵的花语 所有, 如有侵权,请联系我们删除。