TCP协议段格式
源/目的端口号:表示数据是从哪个进程来,到哪个进程去。
序号:发送数据的序号。
确认序号:应答报文的序号,用来回复发送方的。
4 位首部长度:一个 TCP 报头,长度是可变的,不像 UDP 固定是 8 个字节。描述了 TCP 报头具体多长。因为选项之前的部分都是确定的 20 字节,所以首部长度 -20 字节就是选项部分的长度。如果首部长度值是 5,表示整个 TCP 报头是20字节(相当于没有选项)。因为 4bit => 0 - 15,所以 TCP 报头最长为 15 * 4 = 60 字节(选项相当于是 40 字节)。
保留:占个位置,现在没用但是以后可能会用。如果 TCP 后续引入了一些新功能,就可以使用这些保留位字段。就像现在用 128G 内存的手机够用,但是建议 256G 或者更大,为了后续能有空间。
URG:紧急指针是否有效
ACK:确认号是否有效,如果标志位为 1,表示是应答报文,如果是 0,就表示不是
PSH:提示接收端应用程序立刻从TCP缓冲区把数据读走
RST:对方要求重新建立连接;我们把携带RST标识的称为复位报文
SYN:请求建立连接;我们把携带SYN标识的称为同步报文段
FIN:通知对方,本端要关闭了,我们称携带FIN标识的为结束报文段
检验和:发送端填充,CRC校验。接收端校验不通过,则认为数据有问题。此处的检验和不光包含TCP首部,也包含TCP数据部分。
TCP 内部核心工作机制
确认应答(安全机制)
这是实现可靠传输最核心的机制。TCP 进行可靠性传输,最主要的就是靠这个确认应答机制。A 给 B 发了消息,B 收到之后就会返回一个 应答报文(ACK),此时 A 收到应答之后就知道数据已经顺利到达 B 了。
但是,网络可能会出现“先发后至”的情况,即我给对方发了 吃饭 和 做我女朋友 这两条信息,本来对方是 可以 和 不行 的,但由于“先发后至”,我就会收到 不行 和 可以,这时候就会对我造成信息的误解。
为了解决上述问题,就可以给传输的数据和应答报文进行编号即可:
每一个 ACK 都带有对应的确认序列号,意思是告诉发送者,我已经收到了哪些数据;下一次你从哪里开始发。
应答报文报头中确认序号填写的是 1001,就是刚才 1000 字节的数据基础上 +1。表示的含义是:小于 1001 的数据已经确认收到了、接下来应该从 1001 这个序号开始继续发送数据了。如果 1001 这个 ACK 丢了,但是 2001 这个没丢,就会表示 2001 之前的数据都收到了。
小结:TCP 具有可靠传输能力,最主要就是通过确认应答机制来保证的,通过应答报文,就可以让发送方知道数据是否传输成功;进一步地引入了序号和确认序号,针对多组数据进行详细区分。
超时重传
在讨论确认应答的时候,都是建立在顺利传输的基础上,但如果传输过程丢包了呢?发的数据丢了、返回的 ACK 丢了或者还在路上,此时发送方没有收到 ACK,就会统一认为已经丢包了。此时 TCP 就引入了重传机制。
主机 A 发送数据给 B 之后,可能因为网络拥堵等原因,数据无法到达主机 B。TCP 引入了一个 时间阈值,如果超过这个阈值也没有收到 ACK,此时就会认为已经是丢包了,就会重新传输。
但如果是 应答报文丢了 或者 应答报文还在路上,此时 TCP 又重发了,那么就有可能出现接收到两个相同的数据。 TCP 针对这种重复的数据传输,可以进行特殊的处理进行去重:TCP 存在一个“接收缓冲区”这样的存储空间(每个 TCP 的 Scoket 对象,都有一个发送缓冲区和一个接收缓冲区),即接收方的操作系统内核里的一段内存。B 收到 A 的数据,其实就是 B 的网卡读到了数据,然后把这个数据放到 B 对应的接收缓冲区中。可以想象成是一个优先级阻塞队列,根据数据的序号很容易识别是否有数据重复,如果重复就把后来的那份数据丢弃。TCP 使用这个接收缓冲区对收到的数据按序号进行重新排序,保证应用程序 read 到的数据和发送的顺序一致。
如果重传的数据又丢了的话,就会再一次重传,每次重传所需的时间间隔都会增加,当重传次数达到一定时,TCP认为网络或者对端主机出现异常,就会直接断开连接(避免浪费系统资源)。由于去重和重新排序机制的存在,发送方只要发现 ACK 没有按时到达,就会重新发送数据。
小结:可靠传输是 TCP 最核心的部分。可靠传输是通过 确认应答 + 超时重传 来进行体现的。其中确认应答描述的是传输顺利的情况;超时重传描述的是传输出现问题的情况。这两者相互配合,共同支撑 TCP 整体的可靠性传输。
连接管理
连接:TCP 建立连接,就是在 A 记录 B 的 IP和端口,在 B 记录 A 的 IP和端口,当这两部分信息都被维护好了之后,连接就有了。此时,也把保存这部分信息的空间(数据结构)也称为连接。断开连接就是 A 和 B 把自己储存的连接信息(数据结构)删了。
管理:就是描述了连接如何创建(三次握手),如何断开(四次挥手)。
三次握手
通信双方各自要记录对方的信息,彼此之间要相互认同。
SYN:同步报文段,客户端主动给服务器端发起的建立连接请求。在 TCP 报文段第五位。(synchronize,同步)
三次握手重要的两个状态:
1. LISTEN:服务器的状态。表示当前服务器已经准备就绪,随时可以有客户端来建立连接。
2. ESTABLISHED:客户端和服务器端都有。表示建立完成,接下来可以正常通信了。
此时知道,彼此之间都是对方唯一的羊了之后就相当于确认恋爱关系了,即连接建立完毕。但现在可看到是进行了四次交互,是因为中间两次是可合并成一次的(节约资源)。
所谓的三次握手,本质上是进行了“四次”交互。通信双方各自要向对方发起一个“建立连接”的请求,同时双方都得回应对方一个 ACK。
中间两次交互能合并的原因:封装分用两次,一定比封装分用一次成本要高。这几次交互过程都是纯内核完成的,即服务器系统内核收到 SYN 后,会立即发送 ACK 也会立即发送 SYN。就像在同一家网店下单购买两件商品,发两个快递肯定比直接发一个快递成本要高。
如果是两次握手的话,不能完成建立连接得到过程。如果少了最后一次握手,那么对于 美羊羊 来说,不知道 喜羊羊 是不是她的唯一。
三次握手还有一个重要的作用:可以验证通信双方各自的 发送能力和接收能力 是否正常。 因此,三次握手也一定程度上保证了 TCP 传输的可靠性(只是辅助作用)。
三次握手的意义:
1. 让通信双方各自建立对对方的“认同”(保存对方的信息)
2. 验证通信双方各自的发送能力和接受能力是否 OK
3. 在握手的过程中,双方协商一些重要参数(TCP 通信过程中,有些数据通信双方需要相互同步,此时就需要有这样的交互过程)
四次挥手
和三次握手类似。通信双方各自向对方发起一个断开连接的请求,再各自给对方一个回应。
四次挥手重要的两个状态:
1. CLOSE_WAIT:等待关闭,等待调用 close 方法关闭 Soclet。出现在被动发起断开连接的一方。建立连接一定是客户端主动发起请求的。断开连接可能是客户端注定发起,也可能是服务器注定发起。
2. TIME_WAIT:出现在主动发起断开连接的一方。如上图,在左边客户端的视角,当发完 ACK 的时候就会认为四次挥手已经完成了,就会断开连接。但忽视了一个问题:如果 ACK 丢包了,那么在右边服务器的视角就不知道是 ACK 丢了还是自己的 FIN 没发过去。此时右边服务器就会触发超时重传机制,再发一个 FIN。所以 TIME_WAIT 存在的意义就是解决这一情况,会保持 2MSL 这么长时间,MSL 表示在互联网上,两个节点之间数据传输消耗的最大时间,超过这个时间如果没收到重传的 FIN,就默认 ACK 已经到达,此时就断开连接(如果恰好 ACK 丢了,又恰好重传的 FIN 也丢了,那就没办法了)。只有 TIME_WAIT 是等 2MSL,其他的过程都是根据超时重传的阈值。
此时连接就断开了,就从情侣转变为路人。
这里是四次的原因是:FIN 是由应用程序触发的;ACK 是由内核控制的,在收到 FIN 之后会立即返回 ACK,因此应用程序再发送 FIN 的时候就会存在一个时间差,就不能合并成一个了。除非在代码实现上,ACK 和 FIN 之间的时间间隔比较短,此时就有可能合并成一个。(就像我今天买了个快递,过了一周又买了个)
小结:TCP 作为一个有连接的协议,就需要建立连接和断开连接。其中建立连接的过程是三次握手,断开连接的过程是四次挥手。
滑动窗口
对于每一个发送的数据段,都要给一个ACK确认应答,然后收到ACK后再发送下一个数据段。这样做有一个比较大的缺点,就是性能较差。尤其是数据往返的时间较长的时候。
因为可靠性和传输效率本身是矛盾的,所以在保证可靠性的基础上来尽可能提高传输效率,尽量降低效率的折损(其实是将多个段的等待时间重叠在一起了)。
滑动窗口的本质就是批量发送一组数据。我们把无需等待确认应答而可以继续发送数据的最大值叫做窗口大小,上图的窗口大小是 4000。只要有一个 ACK 到了,就可以继续发送下一条数据了,不必等四个 ACK 都到。(就像去饭馆吃饭,有座位就能去,不用等所有人都吃完)
此时如果出现了丢包,如何进行重传?这里分两种情况讨论。
一、ACK 丢了
这种情况下,部分ACK丢了并不要紧,因为可以通过后续的ACK进行确认,因为有确认序号的设定。所以就引申出两个概念:ACK 并不是所有都发的,有时候会少发一部分(并不影响可靠性,同时节省系统资源);如果所有的 ACK 都恰好丢了,那么就会认为网络已经出现严重故障了。
二、数据丢了
由于 1001-2000 字节的数据丢包了,接下来的 2001-3000 到达主机 B 之后,B 返回给 A 的ACK 确认序号仍然是 1001,意思是在索要从 1001 开头的数据。当 1001-2000 这个数据重传被收到之后,因为后面的 2001-7000 原本都以正常到达,被放到了接收端操作系统内核的接收缓冲区中,所以 ACK 返回序号就是 7001。对于这个丢包重传的方式,叫做“快速重传”,这个可以视为是 超时重传机制 在滑动窗口下的变形。
因此可以得出结论:如果当前数据传输密集,按照滑动窗口方式传输,此时按照快速重传机制;如果当前数据传输稀疏,不再按照滑动窗口方式传输,此时还是按照超时重传机制传输。
流量控制
这是一种干预窗口大小的机制。滑动窗口,窗口越大,传输效率就越高,但是窗口不能无限大:完全不等 ACK,就没有可靠性而言;窗口太大会消耗大量系统资源;发送速度太快接收方处理不过来等于白发。因此接收方的处理能力是一个重要的约束依据,发送方的速度不能超出接收方的能力。
我们使用接收方接收缓冲区的剩余大小来衡量接收方的处理能力(剩的越多说明处理能力越快)。所以每次 A 给 B 发送数据,B 都要算一下剩余空间,然后把这个值通过 ACK 报文返回给 A,A 就会根据这个值来决定接下来发送的窗口大小是多少。
虽然 TCP 报文结构的窗口大小是 16 位,但并不意味最大是 64K,因为在选项部分,引入了窗口扩展因子:比如窗口大小已经是 64K,扩展因子里面写了个 2,意思就是让 64K << 2,即左移两位就变成了 256K。
由于接收方缓冲区剩余空间是一直动态变化的,所以每次返回 ACK 所带的窗口大小都在变化,因此发送方也会进行动态调整。当窗口大小为 0 的时候,发送方就会暂停发送,在这期间会定期给 B 发送 窗口探测报文,只是起到出发 ACK 从而查询窗口大小的功能,不携带具体数据。
拥塞控制
流量控制和拥塞控制共同决定发送方的窗口大小是多少。只不过流量控制考虑的是接收方的处理能力;而拥塞控制描述的是传输过程中,中间节点(路由器 / 交换机)的处理能力。但是由于中间节点不好衡量,因为存在太多变数(路径、数据多少),因此就通过“实践”的方式来测试出一个合适的值。(就像 A 不停挑逗 B,步步逼近,看看 B 什么时候发火)
当增长速率达到阈值(ssthresh),就从指数增长变成了线性增长。增长的前提是不丢包,如果传输的过程中丢包了,说明此时发送的速率已经接近网络的极限了,就会把窗口大小重新开始调整。因此可以知道:拥塞窗口不是固定的数值,而是一直动态变化的,随着时间的推移逐渐达到一个动态平衡的过程。这样既能把问题解决也能随着网络的变化而变化。
拥塞窗口和流量控制的窗口,共同决定了发送方实际的发送窗口。
延时应答
也是与滑动窗口有关的。因为滑动窗口的窗口大一点,传输速度就快一点,因此要在接收方能够处理得了的前提下,尽可能把窗口变大一些。延时:就是收到数据之后,不会立即返回 ACK,而是稍微等一会,在等的时间里,接收方的应用程序会把缓冲区 的数据消费一波,此时返回的剩余空间就变大了。(就是上面说到的不用每一条都发)
捎带应答
是在延迟应答的基础上,引入的捎带应答。很多情况下,客户端服务器在应用层都是 "一发一收" 的,即业务上的请求和响应。
面向字节流
创建一个TCP的socket,同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区:
调用write时,数据会先写入发送缓冲区中;
如果发送的字节数太长,会被拆分成多个TCP的数据包发出;
如果发送的字节数太短,就会先在缓冲区里等待,等到缓冲区长度差不多了,或者其他合适 的时机发送出去;
接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区; 然后应用程序可以调用read从接收缓冲区拿数据;
另一方面,TCP的一个连接,既有发送缓冲区,也有接收缓冲区,那么对于这一个连接,既可以读数据,也可以写数据。这个概念叫做 全双工。
由于缓冲区的存在,TCP程序的读和写不需要一一匹配,例如:
写100个字节数据时,可以调用一次write写100个字节,也可以调用100次write,每次写一 个字节;
读100个字节数据时,也完全不需要考虑写的时候是怎么写的,既可以一次read 100个字节,也可以一次read一个字节,重复100次。
粘包问题
接收缓冲区是把收到的多个数据都得放到一起,就像是都粘在一起了,此时就会出现一个问题:应用程序在进行 read 的时候,怎么知道都到哪里就算是一个完整的应用层数据包呢?
为了解决粘包问题,解决方案就是约定好应用层协议,明确应用层数据包之间的边界即可。比如:约定好分隔符;约定好每个包的长度。
TCP 异常情况
在传输过程中出现了不可抗力。
一、程序崩溃或主机关机
主机关机是要先杀进程然后才正式关机。进程没了,对应的 PCB 就没了,对应的文件描述符表就释放了,相当于 Socket.close(),此时内核会继续完成四次挥手,即使可能没挥完也任然是一个正常断开的流程。
二、主机掉电或网线断开
接收方掉电:发送方仍然在继续发数据,但是一直等不到 ACK,就会触发超时重传,但重传几次后依旧没收到,就会尝试重置 TCP 连接,显然也会失败,因此就会单方面放弃连接。
发送方断电:接收方发现过了很久都没数据来,不知道是还没发还是发送方已经挂了,然后接收方就会周期性给发送方发送一个信息,确认对方是否还正常工作。这个也被称作是“保活机制”,发送的消息也被称为“心跳包”,这是用来确认通信双方是否处于正常的工作状态中。
TCP 和 UDP 的对比
TCP 优势:可靠传输。绝大部分场景中,都需要可靠传输的时候。
UDP 优势:效率更高。有些场景对于性能要求更苛刻的时候(例如同一个机房内服务器之间的网络通信)。同时,UDP 天然支持广播。