目录
[5.1 三次握手](#5.1 三次握手)
[5.3 四次挥手](#5.3 四次挥手)
[5.3 补充](#5.3 补充)
一、介绍
TCP全称为传输控制协议(Transmission Control Protocol)是对数据的传输进行详细的控制。
二、TCP协议段格式

上图是TCP协议的报文,那它是怎么实现解包(即报头与有效载荷分离)和分用(向上层交付)呢?
解包问题:tcp协议报头的固定大小是20个字节(不包括选项),在报头里有4位首部长度它是实际报头大小。那这么说它的范围只有[0字节, 15字节],但一个报头最少都20个字节,很明显这不合理。其实这里的单位是4个字节即范围[0, 60],又因为报头是20个字节所以实际可用范围是[20, 60],所以如果没有选项那么默认的值为0101。所以选项的大小就是实际大小减去20即可。
分用问题:在报头中有目标端口号,在解包完成后,会向上交付给与该端口号绑定的进程。
6个标志位
用于标明不同类型的报文,为1则有效0则无效
- ACK:确认接收,通常与确认序号搭配使用,指定后续发送起点
- SYN:发起连接请求
- FIN:请求关闭连接
- RST:立即终止连接
- PSH:提醒对端加快对TCP读缓冲区内容读取
- URG:标识紧急指针是否有效
对于剩下的字段后面再介绍,保留(6位)是为未来协议扩展预留的,当前使用TCP时这几位必须置为0。
三、确认应答机制
TCP是可靠性协议,那发出报文后TCP是怎么知道该报文被对方接收的呢?
当发出报文后,对端接受到会返回一个确认应答(确认应答是OS自动发送),告诉对端你的报文我收到了。TCP是不会对确认应答做应答(这里不考虑确认应答丢失的情况),否则会无限套娃下去。也就是说除了最新的报文,历史上的报文都具有可靠性。
在实际的传输中,TCP会将每个字节的数据进行编号,即序列号。在发送时报头的序号字段会记录这次发送的数据的起始序号,对端接受后在确认应答的报文中的确认序号会记录告诉下次从哪个位置开始发送也就是末尾序号+1(比如客户端发送的序号是1000,服务器成功接受后在确认序号中写入1001;如果服务器只接受了800个字节确认序号为801)。这样即使数据到达的顺序不一样或者多发了,通过序号就可以解决上述问题。
如果需要对报文做出确认和返回数据时,如果返回两个报文就有点没必要,这时应答和数据会在一个报文上,这种报文是携带报文,它是需要对端返回确认应答的。
四、超时重传机制
发送方没收到确认应答真的是丢包了吗?实则不然,有一种情况是对端收到报文,但确认应答丢失了,导致发送方没收到确认应答。不管是哪种情况,发送方都会认为发送的报文丢失了,会触发超时重传。
其实就算是对端收到但确认应答丢了也没问题,对端就算再次收到相同的报文,通过序号就可以把重复的报文给丢弃。
这里提一下超时的时间一般以500ms为单位每次超时就会以500ms的指数倍增长。如果累积一定的超时次数,TCP会认为网络或对方主机异常,会强制关闭连接。
五、连接管理机制
5.1 三次握手
三次握手由OS自动完成,下图是三次握手的过程:

预备:服务器调用socket()创建套接字此时该fd对应的TCP状态为CLOSED,然后调用bind()绑定IP和端口号这时依然是CLOSED,然后调用listen()这时状态变为LISTEN状态,开始监听请求。客户端调用socket()创建套接字并TCP状态为CLOSED。
当客户端调用connect()时,发起三次握手这时会发送SYN报文(告诉对方我想和你建立连接)并且会把TCP的状态改为SYN_SENT。当服务器接受到后,服务器会返回ACK和SYN报文(意思是你的信息我收到了,我也要和你建立连接)这是携带应答,并且此时TCP的状态变为SYN_RCVD。当客户端接受到后会发送确认应答并且TCP状态改为ESTABLISHED。服务器收到后会把TCP状态改为ESTABLISHED。之后调用accept()从就绪队列拿连接。
补充:服务器的监听套接字仅负责接收连接请求,三次握手成功后会生成新的fd对应ESTABLISHED状态的连接套接字,原监听套接字仍保持LISTEN状态以接收新请求。
在建立过程中如果出现报文丢失情况。会在超时后再次重新发送报文,如果重复多次未果,会返回最初的状态。
在建立连接时客户端会告诉服务器我的初始序列号(可以看成是随机的,之后所有发送的数据都是从这个基准开始的递增的)。服务器也会告诉客户端我的初始序列号,并确认了你的初始序列号。客户端也会确认你的初始序列号。这么做可以通过序号比对来避免重复报文或旧报文来干扰新连接建立。
5.3 四次挥手
下图是四次挥手的过程:

当客户端没有数据要发送时会调用close(),这时自动发送FIN报文(告诉对方我要关闭连接了)并把TCP状态设为FIN_WAIT_1。当服务器收到后会返回一个确认应答并且此时状态设为CLOSE_AWIT。当客户端接收到后会把自身状态设为FIN_WAIT_2。注意此时客户端会进入"半关闭"状态,它可以接受到服务器发来的消息,但是不能向服务器发应用数据了,对于ACK或FIN还是可以发送的,因为这是OS自动完成的。
当服务器也没有数据要发送时也会调用close()并向客户端发送FIN报文并把状态设为LAST_ACK(这时服务器已经不能向客户端发送应用数据了)。客户端收到后返回一个确认应答并把状态设为TIME_WAIT,过一段时间后变为CLOSED。服务器收到后状态立即变为CLOSED。
在上述过程中如果出现报文丢失情况。会在超时后再次重新发送报文,如果重复多次未果,状态会直接变为CLOSED。
当状态变为TIME_WAIT为什么要等一会儿在变为CLOSED而不是立即变为CLOSED呢?因为:一是防止发出去ACK报文丢失,当服务器长时间没收到应答时,会触发超时重传即再次发送FIN报文,这时客户端依然可以发送ACK报文。二是清除网络中可能存在的残留报文,避免对下一次建立连接造成影响。时间一般为2MSL。
如果通信的某一方被强制中止,还会遵循四次握手吗?一般来说被强制终止的进程不会发送任何报文,当另一方继续发送数据时,操作系统会检测到连接异常然后发送RST报文,这会导致连接快速终止跳过四次握手。但如果在终止之前调用close()那么会触发四次握手。
5.3 补充
为什么一定要握三次手?
一、这是以最小成本确定双方通信意愿。 客户端发送我想和你通信,服务器回收到我也想和你通信,之后客户端回收到。这样以最少的成本确定了双方通信的意愿。
二、以最短的方式验证全双工。客户端先发,服务器收到并发送,之后服务器收。
还有就是握手不一定是三次,本质上是四次,因为服务器会无条件接受客户端连接所以把SYN和ACK放到一起发送了。同理四次挥手也可以变为三次挥手
六、滑动窗口
上面的确认应答策略,对于每一个发送的数据段,都需要接受到一个ACK报文后,才可以发送数据段。显然这样做的话太慢了。所以一般是一次发送多个数据报。

这里以发送缓冲区举例。上图被框起来的部分,可以看成一个窗口,显然上图窗口大小是4000字节,在窗口里又划分了4段每段1000字节。这里可以简单地把发送缓冲区理解为一个字符数组,其中滑动窗口就在缓冲区里。滑动窗口的范围可以用start和end两个指针确定,其中end-start就可以计算出滑动窗口大小。滑动窗口大小由对方接受缓冲剩余大小决定(暂时认为),那它怎么知道对方接受缓冲剩余大小呢?在对方发送的ACK报文中的窗口大小就是剩余大小。另外的在三次握手时双方也会交换各自接受缓冲区的大小。
七、流量控制
流量控制即通过窗口大小来判断对方接收能力并选择下次发送数据量。
在窗口里的数据是无需等待可以直接发送的。当接受到第一个段的ACK后滑动窗口向后移动,假如发送方发送了1~1000,1001~2000,2001~3000这三段,当返回的报文的确认序号是1001,窗口大小是2000,那么start变为1001,end变为1001+2000。不过1001~2000,2001~3000这两段数据不会再次发送。如果窗口大小是3000那么会发送3001~4000。如果对方的缓冲区满了那么返回的窗口大小会直接返回0,窗口大小为0时,发送方会停止发送数据,但会定期发送窗口探查报文(1字节数据),用于触发接收方重新通告窗口。
现在让我们解决一个关键的问题,丢包了怎么办?丢包有三种情况:左侧丢包、中间丢包、右侧丢包。现在先考虑左侧丢包问题,丢包有二种情况:数据收到了,但确认应答丢了、数据真丢了。
一、确认应答丢。其实不受什么影响,只要后续的报文没有丢失。什么意思呢?假如发送方发送了1~1000,1001~2000,2001~3000这三段。然后只收到了1001~2000的应答即确认序号是2001,那么TCP就会认为前面的1~1000已经被对方收到,只不过在传输的过程中丢失了而已。这时窗口的start会变为2001接着继续向后发送。如果很不幸数据都被对方接受到,但是发送的ACK报文都丢失了,那么发送方会认为自己发送的报文都丢失了,它会重新发送1~1000,1001~2000,2001~3000这三段,因为对方是成功接受到的,这时通过序号可以把重复发送的报文会给丢失,并且发送一个ACK报文,它的确认序号是3001,这时当发送方接收到后会直接把start变为3001。
二、数据丢。发送方发送了1~1000,1001~2000,2001~3000这三段。最左侧的数据丢失,其余的数据没丢。接收方收到1001~2000,2001~3000,但是接收方期望收到的是从1开始,由于TCP要求按序交付,所以发送的确认应答的确认序号是1(告诉对方请从序号为1的数据开始发送)接受到的数据会被缓存起来。这时start不动,超时后重新发送第一段数据。之后当接收方收到了第一段数据后会加上缓存的1001~3000数据然后确认应答的确认序号是3001。
如果是中间丢失和右侧丢失其实也就是左侧丢失,因为返回的确认序号是连续的序号!
如果每次都用超时重传机制那么就有点慢了,所以引入了快重传即当接受到3个同样的确认序号的ACK报文则触发。比如:1~1000,1001~2000,2001~3000,3001~4000。第一段丢失后面的都没丢失,那么会返回三个确认序号都是1的ACK报文,这时会触发快重传发送1~1000。超时重传是一种兜底机制而快重传是一种优化策略。
八、拥塞控制
上面的滑动窗口已经可以实现高效的发送数据,但是如果当前网络状态不好(网络中并不是只有你一台主机发送报文)并且一下发送太多数据就会出现问题。所以TCP引入了慢启动机制,开始的时候先少量发送些数据,摸清当前网络拥堵情况,然后决定后续发送数据量。
所以就有了拥塞窗口它是限制任何时刻发送到网络中的数据量,在开始的时候拥塞窗口是1注意单位是MSS(报文可以发送的数据量由MSS决定,例如1460个字节),后续以2的指数倍递增。所以实际发送的数据量是对端接收缓冲区的剩余大小与拥塞窗口之间的最小值!慢启动阶段是会以指数倍增长(尽快恢复网络通信),到一定程度后进入拥塞避免阶段该阶段改为线性增长(探测新的拥塞窗口)。具体如图:

上图中的ssthresh它是慢启动阈值,用于划分慢启动和拥塞避免,默认情况下是一个很大的值。
当出现超时重传时cwnd变为1,并且ssthresh变为上一次拥塞窗口的一半。如果是快速重传简单来说cwnd会直接减半而ssthresh会等于cwnd,cwnd会进入拥塞避免阶段然后增加。
MSS即最大报文段长度,在三次握手的时候通过选项字段确认双方MSS的大小并选择较小的一方,确定后一般不轻易改变。在以太网中规定数据链路层接收上层传下来的报文最大为1500个字节又称MTU,传输层的报头是20个字节,加上TCP的报头最小是20个字节即MSS最大是1460个字节)。
九、延迟应答
当对端接受到数据后,可以不用立即发送确认应答,而是可以等一下,因为在该过程中接受缓冲区里的内容可能会被读取,这样就腾出了更大的接收缓冲区。
当然并不是所有包都要延迟一般是每隔2个包就应答一次或超过最大延迟时间就应答一次。
十、面向字节流理解
TCP设计为面向字节流,是为了应付复杂的网络环境。TCP会给每个字节分配序列号,通过序列号机制可以解决丢包、重复报文、乱序问题。并且面向字节流极大的提高了灵活性,发送方可以任意拆分数据发送,接收方可以以任意大小从接收缓冲区拿数据。
不过这样会导致粘包问题------因为TCP不维护消息边界,解决方法是通过应用层协议自定义来边界规则,像http就用\r\n\r\n来区分,有的是采用长度+内容的方式。
十一、小结
在上述中我们讨论了很多,但这么多就是为了保障TCP协议的可靠性!具体如下:
- 校验和
- 序列号(按序到达)
- 确认应答
- 超时重发
- 连接管理
- 流量控制
- 拥塞控制
同时为了提高效率也采取了如下措施:
- 滑动窗口
- 快速重传
- 延迟应答
- 捎带应答