Linux【网络编程】之深入理解TCP协议

Linux【网络编程】之深入理解TCP协议

TCP协议

TCP全称为 "传输控制协议(Transmission Control Protocol"). 人如其名, 要对数据的传输进行一个详细的控制

TCP协议段格式


源端口,目的端口:OS给的,也就是我前面模拟基于tcp的应用层协议为什么要用16位端口号

16位校验和: 发送端填充, CRC校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含TCP首部, 也包含TCP数据部分.

16位紧急指针: 这个指针指向了紧急数据最后一个字节的下一个字节

4位首部长度---TCP报头长度信息

  1. tcp协议是有标准长度的20字节,但是报头中可能含有选项,选项占一定字节,也就是说报头的长度是变化的,最小是20字节

  2. 当服务端收到报文后,转化成一个结构化的数据,就可以得到标准报头 中的四位首部长度

    TCP约定:TCP报头总长度=四位首部长度*4字节,因此TCP报头长度范围是[20,60]字节。

    假设报头是20字节,那么首部长度就是5,即0101

  3. 基于上面两条,就可以得到选项的字节:四位首部长度*4-20

  4. 报头全部读取完毕,就可以得到有效载荷

TCP协议的解包参考以上,封装就是添加报头信息(同UDP),分用就是根据目的端口号,找到应用层对应的进程,交给它


收到的报文是如何找到曾经bind过的特定port进程的?

应用层维护一张port哈希表,当有端口号到来,就会查表找到对应进程PCB


TCP可靠性(确认应答)&& 提高传输效率

网络传输中,因为距离变长,存在着不可靠问题,常见的不可靠场景有丢包、乱序、校验错误、重复

确认应答(ACK)机制

TCP收发消息最基本的工作模式:发一个新消息给一件应答

双方再进行通信的时候,除了正常的数据段,还有可能带有确认数据段,对于确认数据段,不需要再次应答

32位序号与32为确认序号

  1. TCP层双方通信是对等的:像HTTP层,客户端只能请求,服务端只能应答,地位是不对等的;而TCP层双方都是确认应答
  2. 无论是正常数据段还是确认数据段,都有方式标识数据段本身;只不过是包不包括数据的区别

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

例如:ACK:14表示已经收到了14号报文之前的所有报文,下次从14开始发


TCP将每个字节的数据都进行了编号. 即为序列号

每一个ACK都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下一次你从哪里开始发


为什么要有两组序号?

这是因为TCP是全双工 的,当Client向Server发送的时候,Client的序号与Server的确认序号为一组;Server也可以向Client发送,Server的序号与Client的确认序号又成为一组;如下图

16位窗口大小---自己接收缓冲区剩余空间的大小

前面我们说到,TCP协议有两个缓冲区:发送缓冲区和接收缓冲区,当双方进行通信的时候,需要把数据拷贝到缓冲区内,交给对方的接受缓冲区,对方再交付给上层。如果发送过快或者过慢,就会影响传输效率

在报头中有一个16位窗口大小的字段,供发送方使用,里面填写自己的 接收缓冲区大小,正因为不知道对方的,才要填自己的,来获取对方的;构建的报文都是要发送给对方的,这样就就换了双方的接收缓冲区剩余大小,也就可以进行流量控制


由于窗口大小与缓冲区挂钩,所以一次只能发64KB的数据,如果想要扩容,需要研究"选项"字段,这里不做讨论

16位紧急指针---紧急数据处理

当报文中有紧急数据需要处理的时候(不是所有的有效载荷,是有效载荷的一部分),会将报头中urg标记位置为1,16位紧急指针作为偏移量指向有效载荷中的紧急数据的下一字节,TCP紧急指针指向的数据只能有一个字节,也就是说,根据偏移量找到数据后,只有一个字节的数据是紧急数据

6位标记位---区分TCP报文类型

TCP报文是有类型的,6位标记位就是来区分不同类型的报文
SYN: 同步标记位 ,默认为0,只有在建立连接的时候被置为1,三次挥手再详谈
FIN: 断开连接标记位 ,四次挥手详谈
ACK: 确认标记位 ,报文带有确认,就会将此标记为置1
PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走
URG: 紧急指针是否有效

RST: 复位标记位 ,对方要求重新建立连接; 我们把携带RST标识的称为复位报文段

当客户端和服务器建立连接、断开连接的时候,有可能会失败,即使成功,通信过程一方也可能会出问题;假设服务端出现问题断开重启,但是客户端并不知道,依旧发送报文,服务端就会发送相应报文,携带RST标记位,告诉客户端重新建立连接。同理,客户端如果断开,会给服务端发信息

超时重传机制

第一种:主机A给主机B发送数据,但是没有到达主机B,经过特定的时间间隔没有收到来自B的确认应答,就会再次给B发送数据

第二种:

主机A给主机B发送数据,主机B收到了给A确认应答,但是A没有收到,在A看来与第一种一样,过段时间再次给B发送数据

  1. A是根据时间间隔来判断是否丢包的!!!

  2. 前面说到,重复也是不可靠的一种,那么超时重传机制中B主机会收到重复数据,就会曾经提到过的序列号来去重

  3. 被发出的数据不会被立即移除(为了超时重传),需要收到应答才能移除

连接管理机制

三次握手

在前面我提到过:三次握手只是手段不是目的,也就是说,三次握手不一定会成功

在上图中三条报文前两条不用担心丢失(超时重传机制),需要考虑的是第三条报文丢失的情况

为什么需要三次握手?一次?两次?四次不可以吗?

  • 一次:不可以

    如果只需要一次握手,就会导致服务器收到来自同一个客户端多个链接,服务器需要维护这些链接,成本太高----SYN洪水

  • 两次:不可以

    1.二次握手的话可能出现第二条报文丢失的情况,导致客户端再给服务器发送连接请求,出现同上的问题

    2.还有一种情况是第一条报文因为延迟长时间未到服务端,客户端再发一条报文与服务端建立连接进行通信,当断开连接的时候,之前的第一条报文到了,服务端就会给客户端发送带有ACK字段的报文,但是此时客户端已经断开连接了,这就会让服务端一直等待,造成资源浪费

  • 三次:可行

    1.通信之前,需要验证信道是全双工的,而三次握手就是用最小成本验证该信道是通畅的

    第一条可以验证:客户端是可以发送的,服务端是可以收到的

    第二条可以验证:客户端是可以收到的,(这里服务端虽然发送了,但是无法确认被客户端收到了)

    第三条可以验证:服务端是可以发送的

    2.三次握手可以有效防止单主机对服务器进行攻击(最后一条报文保证了客户端先建立好连接,然后服务器建立好连接);保证收到同等伤害

  • 四次:可以,但没必要

    1.三次握手已经最小成本建立成功了,四次就显得多余了

    2.如果是四次握手,那么就会导致最后一条是服务端发送的,如果一直丢失,就会导致服务端连接一一直挂着,与两次握手一样有隐患

四次挥手与地址复用

主动断开的一方,最终状态是TIME_WAIT状态

被动断开的一方,两次挥手完成后,会进入CLOSE_WAIT状态

由于TCP双方对等,主动、被动C/S都有可能


如果服务器出现了大量的close_wait原因有:

1.服务器存在bug,没有做close文件描述符

2.服务器有压力,可能一直推送消息给client,导致来不及close

当关闭服务器的时候,系统层面会去执行第三次第四次挥手,完成断开连接


四次挥手已经完成,主动断开的一方会维持一段时间的TIME_WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态;我们使用Ctrl+C终止了server, 所以server是主动关闭连接的一方, 在TIME_WAIT期间仍然不能再次监听同样的server端口,也就不能绑定同样的端口

解决办法:添加地址复用

使用setsockopt()设置socket描述符的 选项SO_REUSEADDR为1, 表示允许创建端口号相同但IP地址不同的多个socket描述符

cpp 复制代码
//设置地址复用
int opt=1;
setsockopt(listen_sockfd_,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));

为什么是TIME_WAIT的时间是2MSL?

  1. MSL是TCP报文的最大生存时间, 因此TIME_WAIT持续存在2MSL的话
  2. 就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启, 可能会收到
  3. 来自上一个进程的迟到的数据, 但是这种数据很可能是错误的;同时也是在理论上保证最后一个报文可靠到达(假设最后一个ACK丢失, 那么服务器会再重发一个FIN. 这时虽然客户端的进程不在了, 但是TCP连接还在, 仍然可以重发LAST_ACK);

流量控制

接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被充满, 这个时候如果发送端继续发送,就会造成丢包, 继而引起丢包重传等等一系列连锁反应.因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow Control)

  1. 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 "窗口大小" 字段, 通过ACK端通知发送端
  2. 窗口大小字段越大, 说明网络的吞吐量越高;
  3. 接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;
  4. 发送端接受到这个窗口之后, 就会减慢自己的发送速度;
  5. 如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端.

在TCP三次握手阶段,完成窗口大小的交换,未来通信的时候就可以控制发送数据大小

滑动窗口

前面我提到过:发送的数据未收到应答之前,会将数据暂时保存起来,而保存的位置就是发送缓冲区 根据课本上讲的,可以将缓冲区分为以下几段

对上图进行抽象,可以处理成一个一维数组:而滑动窗口就是就是两个数组下标,滑动窗口的移动就是数据下标的更新


滑动窗口的大小如何设置?

滑动窗口大小和对方接受能力有关
滑动窗口会向左移动吗?一直向右移动吗?

不会向左,也不会一直向右移动,存在不动的情况
窗口会一直不变吗?会变大吗?会变小吗?依据是什么

不会一直不变,可能变大,可能变小;依据是对方接收缓冲区大小

如上图:当收到对方发来的确认序号的时候,滑动窗口start端会向右移动,如果接收方此时接受了但是没有取出,就会导致接收方缓冲区越来越小,也就导致了发送方窗口大小越来越小。

当越来越小缩小为0的时候,接收方把它接受缓冲区数据全拿走,这个时候发送方滑动窗口就会增大

收到应答确认的时候,如果不是最左端发送的报文的确认,而是中间的,结尾的该怎么办?要滑动吗?

以上存在两种丢包情况:

  1. 数据没丢,只是应答丢失了
    根据确认应答机制,确认序号表示当前序号+1之前的数据已经收到了,不需要再管之前的,这个时候滑动窗口start端直接到确认序号的下一位置进行滑动
  2. 数据丢了
    如果数据真的丢了,那么中间报文收到的确认序号一定是前面丢失报文的序号,连续收到三个一样的,这个时候就需要补发了,滑动窗口不需要滑动

滑动窗口一直滑动,剩余空间不够了怎么办

滑动窗口其实被内核组织成环形结构

拥塞控制与拥塞窗口

TCP连接的可靠性不仅考虑了主机之间的问题,也考虑了网络的问题

少量的丢包, 我们仅仅是触发超时重传; 大量的丢包, 我们就认为网络拥塞

如果网络比较拥堵,发送大量数据可能会引起大量丢包,TCP引入慢启动机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据

发送数据不仅要考虑点到点,还需要考虑网络状况:引入拥塞窗口 ,发送开始的时候, 定义拥塞窗口大小为1,指数级别的. "慢启动" 只是指初使时慢, 但是增长速度非常快.当拥塞窗口超过阈值的时候, 不再按照指数方式增长 , 而是按照线性方式增长

发送方---->网络---->接收方;

发送方拥有滑动窗口;网络拥有拥塞窗口;接受方有自己的窗口大小
滑动窗口大小取决于:MIN(拥塞窗口,接收方窗口大小)

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

延迟应答

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

  1. 假设接收端缓冲区为1M. 一次收到了500K的数据; 如果立刻应答, 返回的窗口就是500K;
  2. 但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了;
  3. 在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来;

如果接收端稍微等一会再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M;
窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率;

那么所有的包都可以延迟应答么? 肯定也不是;

  1. 数量限制: 每隔N个包就应答一次;
  2. 时间限制: 超过最大延迟时间就应答一次;

具体的数量和超时时间, 依操作系统不同也有差异; 一般N取2, 超时时间取200ms,需要考虑超时重传机制

捎带应答

三次握手中服务端回复的时候SYN+ACK就是捎带应答,不是一问一答

面向字节流

创建一个TCP的socket, 同时在内核中创建一个发送缓冲区 和一个接收缓冲区;

  1. 调用write时, 数据会先写入发送缓冲区中;
  2. 如果发送的字节数太长, 会被拆分成多个TCP的数据包发出;
  3. 如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去;
  4. 接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;
  5. 然后应用程序可以调用read从接收缓冲区拿数据;
  6. 另一方面, TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可以写数据. 这个概念叫做 全双工

由于缓冲区的存在, TCP程序的读和写不需要一一匹配,:这就是面向字节流

  1. 写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节;
  2. 读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次read一个字节, 重复100次;

粘包问题

首先要明确, 粘包问题中的 "包" , 是指的应用层的数据包.

在TCP的协议头中, 没有如同UDP一样的 "报文长度" 这样的字段, 但是有一个序号这样的字段.

站在传输层的角度, TCP是一个一个报文过来的. 按照序号排好序放在缓冲区中.

站在应用层的角度, 看到的只是一串连续的字节数据.

那么应用程序看到了这么一连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是一个完整的应用层数据包.
如何避免粘包问题呢? 归根结底就是一句话, 明确两个包之间的边界.

1.对于定长的包, 保证每次都按固定大小读取即可; 每次读取就从缓冲区从头开始按sizeof(包长)依次读取即可;

2.对于变长的包, 可以在包头的位置, 约定一个包总长度的字段, 从而就知道了包的结束位置;

3.对于变长的包, 还可以在包和包之间使用明确的分隔符(应用层协议, 是程序猿自己来定的, 只要保证分隔符不和正文冲突即可)

TCP异常情况

  1. 进程终止: 进程终止会释放文件描述符, 仍然可以发送FIN. 和正常关闭没有什么区别.
  2. 机器重启: 和进程终止的情况相同.
  3. 机器掉电/网线断开: 接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行reset. 即使没有写入操作, TCP自己也内置了一个保活定时器, 会定期询问对方是否还在. 如果对方不在, 也会把连接释放.
    另外, 应用层的某些协议, 也有一些这样的检测机制. 例如HTTP长连接中, 也会定期检测对方的状态. 例如QQ, 在QQ断线之后, 也会定期尝试重新连接.

TCP小结

为什么TCP这么复杂? 因为要保证可靠性, 同时又尽可能的提高性能.
可靠性 :校验和、序列号(按序到达)、确认应答、超时重发、连接管理、流量控制、拥塞控制

提高性能:滑动窗口、快速重传、延迟应答、捎带应答(流量控制、拥塞控制)

其他:定时器(超时重传定时器, 保活定时器, TIME_WAIT定时器等)

连接队列与listen 的第二个参数

TCP协议要为上层维护一个链接队列,这个队列不能没有也不能太长,这个连接队列叫做全连接队列,相当于咱们日常吃饭排队等待

cpp 复制代码
int listen(int sockfd, int backlog);

sockfd:表示要监听的套接字描述符。

backlog:指定在等待被接受的连接队列中允许的最大连接数。

TCP底层允许backlog+1个完整链接,后续来的都只能是半连接 状态,即三次握手中的SYN_RCVD状态 ,并添加到半连接队列,如果后续还没完成连接,就会被Server关闭,转变为close_wait状态

Linux内核协议栈为一个tcp连接管理使用两个队列:

  1. 半链接队列:当客户端发起 SYN 到服务端,服务端收到以后会回 ACK 和自己的 SYN。这时服务端这边的 TCP 从 listen 状态变为 SYN_RCVD ,此时会将这个连接信息放入【半连接队列】
  2. 全连接队列 (accpetd队列)用来保存处于established状态,包含了服务端所有完成了三次握手,但是还未被应用调用 accept 取走的连接队列,这也说明了,accept并不参与三次握手
    全连接队列的长度是listen的第二个参数+1
  3. 全连接队列满了的时候, 就无法继续让当前连接的状态进入 established 状态了

区别全连接队列中的连接和已建立好的连接:就像是排队等待的人和已经进入饭店吃饭的人;这个队列相当于一个连接缓冲区,上层服务完毕的时候可以从缓冲区取出新连接

相关推荐
Tassel_YUE42 分钟前
网络自动化04:python实现ACL匹配信息(主机与主机信息)
网络·python·自动化
hjjdebug1 小时前
linux 下 signal() 函数的用法,信号类型在哪里定义的?
linux·signal
其乐无涯1 小时前
服务器技术(一)--Linux基础入门
linux·运维·服务器
Diamond技术流1 小时前
从0开始学习Linux——网络配置
linux·运维·网络·学习·安全·centos
斑布斑布1 小时前
【linux学习2】linux基本命令行操作总结
linux·运维·服务器·学习
Spring_java_gg1 小时前
如何抵御 Linux 服务器黑客威胁和攻击
linux·服务器·网络·安全·web安全
✿ ༺ ོIT技术༻1 小时前
Linux:认识文件系统
linux·运维·服务器
会掉头发2 小时前
Linux进程通信之共享内存
linux·运维·共享内存·进程通信
我言秋日胜春朝★2 小时前
【Linux】冯诺依曼体系、再谈操作系统
linux·运维·服务器