目录
一、TCP报头结构
二、确认应答机制
三、超时重传机制
四、连接管理机制
五、滑动窗口
六、拥塞控制
七、应答策略
一、TCP报头结构
TCP全称为传输控制协议(Transmission Control Protocol),数据在传输过程需要严格的控制
TCP协议段落格式
4位TCP报头长度:表示该TCP头部有多少个32位bit,TCP报头的最大长度是 15 * 4B = 60B;TCP报头的标准长度是20字节,即在通信时拿到TCP数据后会先读取20个字节,将其转换成一个结构化数据之后,从标准报头中提取4位首部长度,得到完整报头长度(20~60字节)
完整报头 = 标准报头 + 选项
6个标志位:
- FIN:通知对方,本端要关闭了,携带FIN标识的是结束报文段
- SYN:请求建立连接,携带SYN标识的是同步报文段
- RST:对方要求重新建立连接,携带RST标识的是复位报文段
- PSH:催促接收端应用程序立刻从TCP缓冲区把数据读走
- ACK:确认序号是否有效
- URG:紧急指针是否有效
16位检验和:发送端填充,CRC校验。接收方校验不通过,则数据丢弃。此处的检验和不光包含TCP首部,还包含TCP数据
16位紧急指针:紧急数据(1字节)的偏移量
32位序号&&32位确认信号:发送数据的编号&&应答数据的编号,防止数据丢包和乱序
16位窗口大小:填入自己的接收缓冲区的剩余空间的大小,让对方控制数据发送速度(流量控制)
由于TCP是面向字节流传输,故TCP报头里面不包含有效载荷的长度。
TCP数据解包过程(传输层 ---> 应用层,报头和有效载荷分离)
TCP添加报头和UDP类似,将数据拷贝之后,在数据的前端添加结构化数据。
二、确认应答机制
网络传输和本地传输的本质区别就是数据的传输距离变长,这就引发了数据传输的可靠性问题。
当数据的传输距离变长,就容易出现丢包、乱序、校验错误、重复等情况,这些就是不可靠问题。
在网络通信中,如何确定通信的可靠性?收到应答,才能100%确定对方收到信息!
双方通信中,一定存在最新的消息,没有应答——最新的消息无法保证可靠性!
让最新的信息作为确认信息,保证历史信息的可靠性。
无论是是client向server发数据,还是server向client发数据,每一条数据都需要应答(单一数据应答或者批量数据应答)
数据应答的顺序和数据发送的顺序一样吗?未必一样,故每条数据都需要编号,来使每条应答都能对应到发送的数据(防止乱序和丢包)
确认应答&&确认序号:接收方已经收到了ACK序号之前的所有(连续)的报文
序号&&确认序号,为什么要有两组信号?全双工,从TCP协议角度,client端和server端地位对等,通信本质都是数据的发送和应答(不再是请求和响应)
通信双方都有自己的发送缓冲区和接收缓冲区,以实现全双工通信
接收缓冲区的本质是一个队列queue,保证数据的按序到达
TCP报文也是有类型的!
TCP的报文通过6个标志位区分类型,服务器会收到各种各样的TCP报文,根据报文的不同类型做不同处理。
- SYN:同步序标志,进行三次握手建立连接
- FIN:结束序号标志,进行四次挥手断开连接
- ACK:确认信号标志,在3次握手成功之后,所有通信数据报的ACK都置1,承担对历史数据报可靠性的确认
- PSH:推送序号标志,催促接收方尽快读取数据(尽可能让read/recv执行)
- URG:紧急序号标识,需要被特殊处理的数据(尽快读取,插队处理),由紧急指针表示偏移量,紧急指针指向的数据只有1字节(进行TCP通信之外的管理工作)
- RST:复位标志位,reset,处理双方链接认知不一致问题,使链接重新建立
三、超时重传机制
- 数据丢包:主机A给主机B发送数据之后,可能因为网络拥堵等原因,数据无法到达主机B。如果主机A在一个特定的时间间隔内没有收到主机B的确认应答,就会重发数据。
- 应答丢包:主机A给主机B发送数据成功,但是主机B收到数据之后发送给主机A的确认应答丢包了,导致了主机A在特定的时间间隔内没有收到主机B的确认应答,主机A仍会重发数据。
在发送方发送数据之后,在特定的时间间隔内没有收到确认应答,于是重发数据,这就是超时重传机制。其实发送过程中的数据究竟有没有丢包,发送方并不知道,所以策略就是超时没有收到确认应答就认为是丢包了。
因此接收方很可能会收到很多重复数据,那么怎么处理呢?因此TCP需要能够识别处重复的数据报,并且把重复数据丢弃,这就需要通过数据序号去重。
发送方发出去的数据,不能立刻移除,而是必须维持一段时间(收到确认应答之后再移除),数据维持在哪里?发送缓冲区。(计算里的数据移除,通常是覆盖,通过标志位限制缓冲区的有效性)
在超时重传机制中,特定的时间间隔与网络通信的效率相关联,我们如何设置确认信号的超时等待时间呢?最理想的情况下,找到一个最小的时间间隔,保证正常通信的确认应答信号一定能够在这个时间内返回。但是这个时间的长短,随着网络环境的不同,存在差异。
- 如果超时时间设置太长,会影响整体的数据重传效率
- 如果超时时间设置太短,有可能会频繁的发送重复的数据包
TCP为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间。
- Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时时间都是500ms的整数倍
- 如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传
- 如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增
- 累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接
四、连接管理机制
服务端状态转换:
- [CLOSE -> LISTEN]:服务端调用 listen 后进入LISTEN监听状态,等待客户端连接
- [LISTEN -> SYN_RCVD]:服务端一旦监听到连接请求(同步报文段),就将该连接仿佛内核等待队列中,并向客户端发送SYN+ACK
- [SYN_RCVD -> ESTABLISHED]:服务端一旦收到客户端的确认报文,就进入ESTABLISHED状态,接下来就可以进行通信了
- [ESTABLISHED -> CLOSE_WAIT]:当客户端主动关闭连接(调用close),服务端会收到结束报文段,服务器返回确认报文并进入CLOSE_WAIT状态
- [CLOSE_WAIT -> LAST_ACK]:进入CLOSE_WAIT状态后说明服务器准备关闭连接(需要处理尚未处理完的数据),当服务器真正调用 close 关闭连接时,会向客户端发送FIN,此时服务器进入LAST_ACK状态,等待最后一个ACK到来
- [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状态
建立连接:三次握手
DDOS攻击:也称肉鸡攻击,黑客将木马病毒大量散播,潜入肉鸡电脑,设置时间在某一时刻同时对某一服务器发起请求,造成服务器资源耗尽而崩溃。
断开连接:四次握手
四次挥手动作完成,主动断开连接的一方为什么会维持一段时间的TIME_WAIT状态?
- 保证最后一个ACK尽可能被对方收到
- 双方在断开连接的时候,有可能网络中还有滞留的报文,保证滞留报文消散
TIME_WAIT状态一般维持多长时间的TIME_WAIT状态呢?2*MSL,MSL是单向传输数据时消耗的最大时间。
服务器有时候可以立即重启,有时候无法立即重启(bind error),为什么?因为有时候server是主动断开连接的一方,结束通信后进入TIME_WAIT状态。
在server的TCP连接没有完全断开之前不允许重新监听,某些情况下是不合理的:
- 服务器需要处理非常大量的客户端的连接(每个连接的生存时间可能很短,每秒都有很大数量的客户端来请求)
- 这个时候如果有很多客户端不活跃,就需要被服务器主动关闭连接清理掉,就会产生大量的TIME_WAIT状态连接
- 由于对服务器的请求量很大,就可能导致TIME_WAIT状态的连接数很多,每个连接都会占用一个通信五元组(源IP,目的IP,源端口,目的端口,协议),其中服务器的IP和端口是固定的,如果新来的客户端连接的IP和端口号和TIME_WAIT占用的连接重复了,就会问题。
那么我们怎么解决TIME_WAIT状态引起的bind失败问题呢?
在socket套接字创建之后,使用socketopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但是IP地址不同的多个socket描述符。
int opt = 1;
setsocketopt(listenfd, SOL_SOCKET, SO_REUSERADDR, &opt, sizeof(opt));
对于服务器上出现大量的TIME_WAIT状态连接,原因就是服务器没有正确close(socket),导致四次挥手没有正常完成,这是一个BUG,需要给每次连接加上正确的close()。
五、滑动窗口
流量控制:接收端处理数据的速度是有限的,如果发送端发送太快导致接收端的缓冲区被写满,这个时候如果发送端继续发送数据,就会造成丢包,继而引起丢包重传等等一些列连锁问题。因此TCP支持根据接收端的处理能力,来决定发送端的发送速度。
- 接收端将自己剩余的接收缓冲区大小放入TCP首部的“窗口大小”字段,通过ACK通知发送端
- 窗口大小字段越大,说明网络的吞吐量越高
- 接收端一旦发现自己的缓冲快满了,就会将窗口大小设置为一个更小的值发送给发送端,发送端读取到这个窗口大小信息之后,就会减慢发送速度
- 如果接收端的接收缓冲区写满了,就会将窗口大小设置为0,这时发送端不再发送数据,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端
那么第一次数据的发送,发送端如何知道接收端的窗口大小呢?三次握手。
在TCP首部中,有16位窗口大小字段,那么16位数字表示最大范围就是65535字节吗?实际上,TCP首部40字节选项中还包含了一个窗口大小扩大因子M,实际窗口大小是窗口字段的值左移M位。
TCP协议有确认应答机制,对每一个发送的数据段,都要给一个ACK确认应答,收到ACK应答之后再发送下一个数据段,这样就出现了一个缺陷,就是性能较差,尤其是数据往返的时间较长的情况下。
既然串行一发一收的方式性能较低,那么我们就采用并行发送多条数据,再等待多条数据应答的策略,这样就大大的提高了性能(将多个数据段的等待应答时间重叠在一起)。
- 窗口大小就是指无需等待确认应答而可以继续发送数据的最大值,上图的窗口大小就是3000个字节(假设每个段1000字节)
- 发送前三个段的时候,无需等待确认应答,直接连续发送
- 收到第一个ACK之后,滑动窗口向右移动,继续发送第四个段,依次类推
- OS内核为了维护这个滑动窗口,需要开辟发送缓冲区来记录当前还有哪些数据没有应答,只有确认应答过的数据,才能从缓冲区删除
- 窗口越大,代表网络的吞吐量越高
在TCP通信过程中,数据分为三种状态:①已经发送并且收到确认应答;②正在发送但是尚未收到确认应答;③没有发送。
滑动窗口里的数据正是第②中状态的数据,已经发送但是尚未收到应答。
窗口的初始大小怎么设置?未来怎么变化? 滑动从窗口的大小与对方的接收能力有关,未来无论怎么滑动,都要保证对方能够正常接收。
窗口一定向右滑动吗?会向左滑动吗?由于左边是已经发送且收到确认应答的数据(需要删除),所以窗口一定不会向左滑动,但是不一定向右滑动,可能长时间保持不动。
窗口大小会一直不变吗?窗口大小是浮动变化的,可能会不变,但不会一直不变,变化的依据是对方的接收能力,当对方的接收缓冲区写满了,窗口大小变为0。
滑动窗口的丢包处理:
- 数据没丢,应答丢包
- 数据丢包
序号&&确认序号也用于支持滑动窗口的规则制定!
我们发送的数据在尚未收到应答之前,需要暂时保证起来以支持超时重传,数据保存在哪?滑动窗口之中!
滑动窗口一直向右滑动,空间不够了怎么办?实际上,滑动窗口数组空间是一个循环队列结构。
六、拥塞控制
TCP的可靠性不仅考虑双方主机的问题,还要考虑网络的问题。
如果数据丢包是双方主机的问题,那么会采用超时重传机制;如果数据丢包是网络问题,则不会进行超时重传。
虽然TCP协议通过滑动窗口能够高效可靠的发送大量数据,但是如果在刚开始阶段就发送大量数据仍然可能引发问题。因为网络上有很多计算机,可能当前网络已经进入拥塞状态了,在不清楚当前的网络状态时就贸然发送大量数据,是会加重网络拥塞的。
因此TCP协议引入慢启动机制,在不清楚当前网络状态的情况下,先发送少量数据试探当前网络的拥堵情况,再决定按多快的速度传输数据。
此处引用一个概念是拥塞窗口:
- 开始发送的时候,定义拥塞窗口大小为1,用于试探网络的拥堵状况
- 每次收到一个ACK应答,拥塞窗口+1
- 每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口做比较,取较小的值作为实际发送的窗口
如此可见,拥塞窗口的增长速度是指数级的,慢启动只是初始速度慢,但是增长速度非常快。
为了不让增长速度过快,因此不能使拥塞窗口每次都是单纯的翻倍。于是设置了一个慢启动的阈值,当拥塞窗口超过阈值的时候,不再按照指数方式增长,而是按照线性方式增长。
- 当TCP启动的时候,慢启动阈值等于窗口的最大值
- 当每次发送网络拥塞时,慢启动的阈值降为原来阈值一半,同时拥塞窗口置回1
少量的丢包,触发超时重传机制;大量的丢包,触发拥塞控制机制。
当TCP通信开始后,网络吞吐量会逐渐上升,随着网络发送拥堵,吞吐量立即下降。
拥塞控制,归根结底是TCP协议想尽可能快的把数据传输给对方,但是又要避免给网络造成太大压力的折中方案。
七、应答策略
延迟应答
如果接收主机立刻返回ACK应答,这时候返回的窗口可能比较小
- 假设接收缓冲区为1M,一次收到500K的数据,如果立刻应答,返回的窗口就是500K
- 但是实际上可能处理端对数据的处理速度很快,10ms之内就把数据从缓冲区消费掉了
- 在这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些,也能处理过来
- 如果接收端稍等一会儿再应答,比如等待200ms再应答,若在此期间上层处理端把数据带走了,这个时候返回的窗口大小就是1M
窗口越大,网络的吞吐量就越大,传输效率就越高,我们的目的是在保证网络不拥堵的情况下尽快提高传输效率。
那么所有的数据包都可以延迟应答吗?并不是
- 数量限制:每隔N个包就应答一次
- 时间限制:超过最大延迟时间就应答一次
- 具体的数量和超时时间,依操作系统的不同存在差异,一般N=2,超时时间=200ms
由于ACK确认序号的精妙设计,故延迟应答中被上层消费掉的数据不需要应答,可以通过应答下一个数据包的确认序号保证正常通信。
捎带应答
在延迟应答的基础上,我们发现,很多情况下,客户端服务器也是“一发一收”的,客户端和服务器之间互相发送和接收数据。
如此ACK就可以搭发送数据的顺风车,在发送给对方数据的时候完成确认应答的功能,因为ACK只是一个标志位,ACK确认序号存在每一个数据的报头里。