【Linux后端服务器开发】TCP协议

目录

一、TCP报头结构

二、确认应答机制

三、超时重传机制

四、连接管理机制

五、滑动窗口

六、拥塞控制

七、应答策略


一、TCP报头结构

TCP全称为传输控制协议(Transmission Control Protocol),数据在传输过程需要严格的控制

TCP协议段落格式

【Linux后端服务器开发】TCP协议_第1张图片

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数据解包过程(传输层 ---> 应用层,报头和有效载荷分离)

【Linux后端服务器开发】TCP协议_第2张图片

TCP添加报头和UDP类似,将数据拷贝之后,在数据的前端添加结构化数据。

二、确认应答机制

网络传输和本地传输的本质区别就是数据的传输距离变长,这就引发了数据传输的可靠性问题。

当数据的传输距离变长,就容易出现丢包、乱序、校验错误、重复等情况,这些就是不可靠问题。

在网络通信中,如何确定通信的可靠性?收到应答,才能100%确定对方收到信息!

双方通信中,一定存在最新的消息,没有应答——最新的消息无法保证可靠性!

让最新的信息作为确认信息,保证历史信息的可靠性。

无论是是client向server发数据,还是server向client发数据,每一条数据都需要应答单一数据应答或者批量数据应答

【Linux后端服务器开发】TCP协议_第3张图片

数据应答的顺序和数据发送的顺序一样吗?未必一样,故每条数据都需要编号,来使每条应答都能对应到发送的数据(防止乱序和丢包) 

确认应答&&确认序号:接收方已经收到了ACK序号之前的所有(连续)的报文

序号&&确认序号,为什么要有两组信号?全双工,从TCP协议角度,client端和server端地位对等,通信本质都是数据的发送和应答(不再是请求和响应)

通信双方都有自己的发送缓冲区接收缓冲区,以实现全双工通信

接收缓冲区的本质是一个队列queue,保证数据的按序到达

TCP报文也是有类型的!

TCP的报文通过6个标志位区分类型,服务器会收到各种各样的TCP报文,根据报文的不同类型做不同处理。

  • SYN:同步序标志,进行三次握手建立连接
  • FIN:结束序号标志,进行四次挥手断开连接
  • ACK:确认信号标志,在3次握手成功之后,所有通信数据报的ACK都置1,承担对历史数据报可靠性的确认
  • PSH:推送序号标志,催促接收方尽快读取数据(尽可能让read/recv执行)
  • URG:紧急序号标识,需要被特殊处理的数据(尽快读取,插队处理),由紧急指针表示偏移量,紧急指针指向的数据只有1字节(进行TCP通信之外的管理工作)
  • RST:复位标志位,reset,处理双方链接认知不一致问题,使链接重新建立

【Linux后端服务器开发】TCP协议_第4张图片

三、超时重传机制

【Linux后端服务器开发】TCP协议_第5张图片

  • 数据丢包:主机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认为网络或者对端主机出现异常, 强制关闭连接
     

四、连接管理机制

【Linux后端服务器开发】TCP协议_第6张图片

服务端状态转换:

  • [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状态

建立连接:三次握手

【Linux后端服务器开发】TCP协议_第7张图片

DDOS攻击:也称肉鸡攻击,黑客将木马病毒大量散播,潜入肉鸡电脑,设置时间在某一时刻同时对某一服务器发起请求,造成服务器资源耗尽而崩溃。

断开连接:四次握手

【Linux后端服务器开发】TCP协议_第8张图片

四次挥手动作完成,主动断开连接的一方为什么会维持一段时间的TIME_WAIT状态?

  1. 保证最后一个ACK尽可能被对方收到
  2. 双方在断开连接的时候,有可能网络中还有滞留的报文,保证滞留报文消散

TIME_WAIT状态一般维持多长时间的TIME_WAIT状态呢?2*MSL,MSL是单向传输数据时消耗的最大时间。

服务器有时候可以立即重启,有时候无法立即重启(bind error),为什么?因为有时候server是主动断开连接的一方,结束通信后进入TIME_WAIT状态。

在server的TCP连接没有完全断开之前不允许重新监听,某些情况下是不合理的:

  1. 服务器需要处理非常大量的客户端的连接(每个连接的生存时间可能很短,每秒都有很大数量的客户端来请求)
  2. 这个时候如果有很多客户端不活跃,就需要被服务器主动关闭连接清理掉,就会产生大量的TIME_WAIT状态连接
  3. 由于对服务器的请求量很大,就可能导致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应答之后再发送下一个数据段,这样就出现了一个缺陷,就是性能较差,尤其是数据往返的时间较长的情况下。

既然串行一发一收的方式性能较低,那么我们就采用并行发送多条数据,再等待多条数据应答的策略,这样就大大的提高了性能(将多个数据段的等待应答时间重叠在一起)。

【Linux后端服务器开发】TCP协议_第9张图片

  • 窗口大小就是指无需等待确认应答而可以继续发送数据的最大值,上图的窗口大小就是3000个字节(假设每个段1000字节)
  • 发送前三个段的时候,无需等待确认应答,直接连续发送
  • 收到第一个ACK之后,滑动窗口向右移动,继续发送第四个段,依次类推
  • OS内核为了维护这个滑动窗口,需要开辟发送缓冲区来记录当前还有哪些数据没有应答,只有确认应答过的数据,才能从缓冲区删除
  • 窗口越大,代表网络的吞吐量越高

在TCP通信过程中,数据分为三种状态:①已经发送并且收到确认应答;②正在发送但是尚未收到确认应答;③没有发送。

滑动窗口里的数据正是第②中状态的数据,已经发送但是尚未收到应答。

【Linux后端服务器开发】TCP协议_第10张图片

窗口的初始大小怎么设置?未来怎么变化? 滑动从窗口的大小与对方的接收能力有关,未来无论怎么滑动,都要保证对方能够正常接收。

窗口一定向右滑动吗?会向左滑动吗?由于左边是已经发送且收到确认应答的数据(需要删除),所以窗口一定不会向左滑动,但是不一定向右滑动,可能长时间保持不动。

窗口大小会一直不变吗?窗口大小是浮动变化的,可能会不变,但不会一直不变,变化的依据是对方的接收能力,当对方的接收缓冲区写满了,窗口大小变为0。

滑动窗口的丢包处理

  1. 数据没丢,应答丢包
  2. 数据丢包

【Linux后端服务器开发】TCP协议_第11张图片

【Linux后端服务器开发】TCP协议_第12张图片

序号&&确认序号也用于支持滑动窗口的规则制定!

我们发送的数据在尚未收到应答之前,需要暂时保证起来以支持超时重传,数据保存在哪?滑动窗口之中!

滑动窗口一直向右滑动,空间不够了怎么办?实际上,滑动窗口数组空间是一个循环队列结构

六、拥塞控制

TCP的可靠性不仅考虑双方主机的问题,还要考虑网络的问题。

如果数据丢包是双方主机的问题,那么会采用超时重传机制;如果数据丢包是网络问题,则不会进行超时重传。

虽然TCP协议通过滑动窗口能够高效可靠的发送大量数据,但是如果在刚开始阶段就发送大量数据仍然可能引发问题。因为网络上有很多计算机,可能当前网络已经进入拥塞状态了,在不清楚当前的网络状态时就贸然发送大量数据,是会加重网络拥塞的。

因此TCP协议引入慢启动机制,在不清楚当前网络状态的情况下,先发送少量数据试探当前网络的拥堵情况,再决定按多快的速度传输数据。

【Linux后端服务器开发】TCP协议_第13张图片

此处引用一个概念是拥塞窗口

  • 开始发送的时候,定义拥塞窗口大小为1,用于试探网络的拥堵状况
  • 每次收到一个ACK应答,拥塞窗口+1
  • 每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口做比较,取较小的值作为实际发送的窗口

如此可见,拥塞窗口的增长速度是指数级的,慢启动只是初始速度慢,但是增长速度非常快。

为了不让增长速度过快,因此不能使拥塞窗口每次都是单纯的翻倍。于是设置了一个慢启动的阈值,当拥塞窗口超过阈值的时候,不再按照指数方式增长,而是按照线性方式增长。

  • 当TCP启动的时候,慢启动阈值等于窗口的最大值
  • 当每次发送网络拥塞时,慢启动的阈值降为原来阈值一半,同时拥塞窗口置回1

少量的丢包,触发超时重传机制;大量的丢包,触发拥塞控制机制。

当TCP通信开始后,网络吞吐量会逐渐上升,随着网络发送拥堵,吞吐量立即下降。

拥塞控制,归根结底是TCP协议想尽可能快的把数据传输给对方,但是又要避免给网络造成太大压力的折中方案。

七、应答策略

延迟应答

如果接收主机立刻返回ACK应答,这时候返回的窗口可能比较小

  • 假设接收缓冲区为1M,一次收到500K的数据,如果立刻应答,返回的窗口就是500K
  • 但是实际上可能处理端对数据的处理速度很快,10ms之内就把数据从缓冲区消费掉了
  • 在这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些,也能处理过来
  • 如果接收端稍等一会儿再应答,比如等待200ms再应答,若在此期间上层处理端把数据带走了,这个时候返回的窗口大小就是1M

窗口越大,网络的吞吐量就越大,传输效率就越高,我们的目的是在保证网络不拥堵的情况下尽快提高传输效率。

那么所有的数据包都可以延迟应答吗?并不是

  • 数量限制:每隔N个包就应答一次
  • 时间限制:超过最大延迟时间就应答一次
  • 具体的数量和超时时间,依操作系统的不同存在差异,一般N=2,超时时间=200ms

由于ACK确认序号的精妙设计,故延迟应答中被上层消费掉的数据不需要应答,可以通过应答下一个数据包的确认序号保证正常通信。

捎带应答

在延迟应答的基础上,我们发现,很多情况下,客户端服务器也是“一发一收”的,客户端和服务器之间互相发送和接收数据。

如此ACK就可以搭发送数据的顺风车,在发送给对方数据的时候完成确认应答的功能,因为ACK只是一个标志位,ACK确认序号存在每一个数据的报头里。

【Linux后端服务器开发】TCP协议_第14张图片

你可能感兴趣的:(Linux后端服务器开发,服务器,tcp/ip,linux)