作者:@小萌新
专栏:@网络
作者简介:大二学生 希望能和大家一起进步
本篇博客简介:较为详细的介绍TCP协议
TCP协议
- TCP协议
-
- 可靠性
- TCP的协议格式
- 序号与确认序号
- 窗口大小
- 六个标志位
- 确认应答机制 (ACK)
- 超时重传机制
- 连接管理机制
-
- 三次握手
- 四次挥手
- 流量控制
- 滑动窗口
- 拥塞控制
- 延时应答
- 捎带应答
- 面向字节流
- 粘包问题
- TCP的异常情况
- TCP小结
- 基于TCP的应用层协议
TCP协议
TCP全称为“传输控制协议(Transmission Control Protocol)” TCP协议是当今互联网当中使用最为广泛的传输层协议
TCP协议被广泛应用 其根本原因就是提供了详尽的可靠性保证 基于TCP的上层应用非常多
比如HTTP、HTTPS、FTP、SSH等 甚至MySQL底层使用的也是TCP
可靠性
为什么网络中会存在不可靠?
我们现在的大部分计算机都是基于冯诺依曼体系的
虽然说这里的输入设备 内存 输出设备 cpu都是在一台机器上的 但是这些硬件都是相互独立的
如果它们之间要进行数据交互,就必须要想办法进行通信 因此这几个设备实际是用“线”连接起来的
其中连接内存和外设之间的“线”叫做IO总线而连接内存和CPU之间的“线”叫做系统总线
由于这几个硬件设备都是在一台机器上的 因此这里传输数据的“线”是很短的 传输数据时出现错误的概率也非常低
但如果要进行通信的各个设备相隔千里 那么连接各个设备的“线”就会变得非常长 传输数据时出现错误的概率也会大大增高 此时要保证传输到对端的数据无误 就必须引入可靠性
总之网络不可靠的根本原因就是 长距离传输数据使用的‘线’太长了 所以说数据在长距离传输的时候可能会遇到一些错误 而TCP就是在这种背景下诞生的 TCP就是一种可靠的协议
思维扩展:
- 实际单独的一台计算机可以看作成一个小型的网络 计算机上的各种硬件设备之间实际也是在进行数据通信 并且它们在通信时也必须遵守各自的通信协议 只不过它们之间的通信协议更多是描述一些数据的含义
为什么会存在UDP协议?
TCP协议是一种可靠的协议 而UDP是一种不可靠的协议 那么我们都去用TCP就好了啊 为什么还会存在UDP协议呢
TCP是一种可靠的协议 这也就意味着TCP需要做更多的工作来保证数据传输的可靠 并且引起不可靠的因素越多 我们要保证可靠的成本就越高 其中常见的不可靠情况有丢包 乱序等 而我们的TCP由于要保证可靠所以说要想办法解决这些问题
UDP协议是不可靠的协议 这也就意味着UDP不需要考虑数据传输时需要处理的问题 因此UDP无论是使用还是维护都足够简单
但是虽然说TCP的使用比UDP更加复杂 但是TCP的效率缺不比UDP低
如果我们严格要求了数据在传输过程中可靠性 那么我们就必须采用TCP协议 如果说允许数据有一点点丢包的话我们就可以使用UDP协议 因为UDP协议足够的简单
TCP的协议格式
TCP协议格式如下:
TCP报头当中各个字段的含义如下:
- 源/目的端口号:表示数据是从哪个进程来 到发送到对端主机上的哪个进程
- 32位序号/32位确认序号:分别代表TCP报文当中每个字节数据的编号以及对对方的确认 是TCP保证可靠性的重要字段
- 4位TCP报头长度:表示该TCP报头的长度 以4字节为单位
- 6位保留字段:TCP报头中暂时未使用的6个比特位
- 16位窗口大小:保证TCP可靠性机制和效率提升机制的重要字段
- 16位检验和:由发送端填充 采用CRC校验 接收端校验不通过 则认为接收到的数据有问题 (检验和包含TCP首部+TCP数据部分)
- 16位紧急指针:标识紧急数据在报文中的偏移量 需要配合标志字段当中的URG字段统一使用
- 选项字段:TCP报头当中允许携带额外的选项字段 最多40字节
TCP报头当中的6位标志位:
- URG:紧急指针是否有效
- ACK:确认序号是否有效
- PSH:提示接收端应用程序立刻将TCP接收缓冲区当中的数据读走
- RST:表示要求对方重新建立连接 我们把携带RST标识的报文称为复位报文段
- SYN:表示请求与对方建立连接 我们把携带SYN标识的报文称为同步报文段
- FIN:通知对方 本端要关闭了 我们把携带FIN标识的报文称为结束报文段
TCP报头在内核当中本质就是一个位段类型 给数据封装TCP报头时 实际上就是用该位段类型定义一个变量 然后填充TCP报头当中的各个属性字段 最后将这个TCP报头拷贝到数据的首部 至此便完成了TCP报头的封装
TCP如何将报头与有效载荷进行分离?
当TCP从底层获取到一个报文后 虽然TCP不知道报头的具体长度 但报文的前20个字节是TCP的基本报头 并且这20字节当中涵盖了4位的首部长度
因此TCP是这样分离报头与有效载荷的:
- 当TCP获取到一个报文后 首先读取报文的前20个字节 并从中提取出4位的首部长度 此时便获得了TCP报头的大小size
- 如果size的字节大于20字节 则再次从报文中读取size – 20的数据 这些数据是选项大小
- 读取完报头和选项之后剩下的就是有效载荷了
如果TCP报头当中不携带选项字段 那么TCP报头的长度就是20字节 此时报头当中的4位首部长度的值就为 5 (20 / 4 = 5)
TCP如何决定将有效载荷交付给上层的哪一个协议?
应用层的每一个网络进程都必须绑定一个端口号
- 服务端进程必须显示绑定一个端口号
- 客户端进程由系统动态绑定一个端口号
而我们的TCP报头中含有这么一个字段 十六位目的端口号
因为我们可以从该报头中提取十六位目的端口号 找到对应的应用级进程 进而将有效载荷交给对应的应用层进程进行处理
说明:
内核中用哈希的方式维护了端口号与进程ID之间的映射关系 因此传输层可以通过端口号快速找到其对应的进程ID 进而找到对应的应用层进程
序号与确认序号
怎么才能确定对方收到我的消息了?
在进行网络通信时 一方发出数据之后 它不能够保证该数据被对端收到 因为在路上可能会遇到各种各样的问题 所以说我们必须想一种办法来确认自己的消息被对端收到了 而在我们现在的网络通信中我们采用的办法就是序号和确认序号 通俗点来说 当自己向对端主机发送信息的时候 对端会给你应答 这个应答就能说明你的消息被对方接收到了
**在下面的图例中 实线表示的是消息确认能够被对方收到 虚线表示的是消息不确认能够被对方收到 **
但是我们的TCP协议需要保证的是通信双方的可靠性 虽然此时主机A能够保证自己上一次发送的数据被主机B可靠的收到了 但主机B也需要保证自己发送给主机A的响应数据被主机A可靠的收到了
因此主机A在收到了主机B的响应消息后 还需要对该响应数据进行响应 但此时又需要保证主机A发送的响应数据的可靠性 这样就陷入了一个死循环
因为我们只有在收到对方的响应消息之后我们才能保证自己上一次发送的数据被对端可靠的收到 但是就像上面的图例一样 总会有一条最新的消息不能确认被收到
所以严格意义上来说 互联网中通信的时候不存在百分百的可靠性 因为双方通信的时候总有一条最新的消息得不到应答
但实际没有必要保证所有消息的可靠性
我们只要保证双方通信时发送的每一个核心数据都有对应的响应就可以了 而对于一些无关紧要的数据(比如响应数据) 我们没有必要保证它的可靠性
这种策略在我们的TCP中叫做确认应答机制
需要注意的是 确认应答机制并不是保证双方通信的可靠性 而是只要一方收到了另一方的应答消息 就说明它上一次发送的数据被另一方可靠的收到了
32位序号
如果双方在进行数据通信时 只有收到了上一次发送数据的响应才能发下一个数据 那么此时双方的通信过程就是串行的 效率肯定会很差
因此双方在进行网络通信时 允许一方向另一方连续发送多个报文数据 只要保证发送的每个报文都有对应的响应消息就行了 此时也就能保证这些报文被对方收到了
但是在连续发送多个报文的时候又会产生新的问题 报文到达的先后顺序
由于在发送报文的时候路径选择的不同 所以报文到达的时间不一定相同 也就是说先发的报文不一定会先到
所以说 就会造成一些数据问题 比如说下面三句话句话
- 今晚作业是写一篇博客
- 另外再加一张试卷
- 上面的作业不用做了
这三句话如果换个顺序意思就完全不一样了 所以说我们需要避免这种情况的发生
TCP将发送出去的每个字节数据都进行了编号 这个编号叫做序列号
- 比如现在发送端要发送3000字节的数据 如果发送端每次发送1000字节 那么就需要用三个TCP报文来发送这3000字节的数据
- 此时这三个TCP报文当中的32位序号填的就是发送数据中首个字节的序列号 因此分别填的是1、1001和2001
此时接收端收到了这三个TCP报文后 就可以根据TCP报头当中的32位序列号对这三个报文进行顺序重排(该动作在传输层进行) 重排后将其放到TCP的接收缓冲区当中 此时接收端这里报文的顺序就和发送端发送报文的顺序是一样的了
- 接收端在进行报文重排时 可以根据当前报文的32位序号与其有效载荷的字节数 进而确定下一个报文对应的序号
32位确认序号
TCP中的三十二位确认序号是告诉对方 我当前已经收到了哪些数据 你的数据下一次应该从哪里开始发
以刚才的例子为例 当主机B收到主机A发送过来的32位序号为1的报文时 由于该报文当中包含1000字节的数据 因此主机B已经收到序列号为1-1000的字节数据 于是主机B发给主机A的响应数据的报头当中的32位确认序号的值就会填成1001 回复这个值有两个解释
- 告诉对方你前面1~1000的数据我已经全部收到了
- 下次发送数据从1001开始发
需要注意的是虽然我们这里图上发送的好像是序号1 1001这些 但是实际在发送过程中发送的其实是一个完整的报文
报文丢失怎么办?
我们假设主机A发送了三个数据给主机B 每个报文的有效载荷都是1000 也就是说发送的三个序号是1 1001 2001
假设这三个报文在传输的过程中 序号1001~2001的数据发生了丢包 在主机B将这些报文进行排序的时候 会发现中间缺失了一段 此时主机B会向主机A发送1001的确认序号
此时主机A就会明白自己的数据可能是发生丢包了 之后便会重新发送从1001序号开始的数据 也包括2001~3000
为什么要用两套序号机制?
大家想象这么一个场景 主机A和主机B之间通信
A主机只发送数据 B主机只接收数据
那么此时A主机发送序号请求报文的时候 B主机只需要在相同字段填写相同的响应报文就可以了
但是我们的TCP是一种全双工通信协议 所以说在AB主机通信的过程中 B主机也有可能向A主机发送数据
那么此时我们就需要使用两套序号机制来区分序号和响应序号了
窗口大小
TCP的接收缓冲区和发送缓冲区
TCP协议本身是具有接收缓冲区和发送缓冲区的
其中:
- 接收缓冲区用来暂存收到的数据
- 发送缓冲区用来暂存即将发送的数据
事实上在我们之前讲解基础IO的时候就知道了 我们调用write和read函数的时候并不是直接从磁盘而是从内存缓冲区中读取数据 根本原因是因为磁盘的IO效率太低了
在网络中也一样 我们之前在学习应用层的时候忽略了下层的作用 认为调用write就是直接向网络中发送数据 但是其实并不是这样子
同样的 因为我们今天学习的是传输层协议 我们也忽略网络层和数据链路层的作用 认为传输层的数据就是直接发送到网络中的
在应用层调用write函数的时候 实际上就是向TCP的发送缓冲区中写入数据 TCP协议会等待合适的时机发送
与此同时当TCP接收从网络中发送过来数据的时候并不是直接发送到应用层 而是拷贝到接收缓冲区中 等待应用层使用read函数来读取
当我们平时调用write函数的时候 它会将数据写入到TCP的数据缓冲区 当写入成功(或失败)之后它就会返回了 之后数据什么时候发送 怎么可靠的发送就是传输层的事情了 应用层并不关心
TCP的发送缓冲区和接收缓冲区存在的意义
发送缓冲区和接收缓冲区的作用:
- 我们在前面说过 数据在网络中的传输并不是绝对安全的 可能会有数据丢失等问题而我们的TCP是一个要保证可靠性的协议 所以说TCP是不能容忍这些错误存在的 此时发送缓冲区的作用便显现出来了 如果发送端没有收到确认的应答 发送的数据就会保存一端时间之后继续发送 知道收到应答这部分数据才可以被覆盖
- 和UDP协议一样TCP协议也有接收缓冲区 这是因为应用层梳处理数据的速度是有限的 为了保证没来得及处理的数据不被直接丢弃所以说我们要设立接收缓冲区 数据在网络中传输是十分耗费时间和空间的 如果仅仅是因为没有即使处理就丢弃资源会造成对于网络资源极大的浪费
经典的生产者消费者模型:
- 对于发送缓冲区来说 上层不断的调用系统函数发送数据 扮演者一个生产者的角色 下层不断的调用系统函数接收数据 扮演着一个消费者的角色 发送缓冲区就是一个交易场所
- 对于接收缓冲区来说 下层不断的向接收缓冲区中写入数据 扮演一个生产者的角色 上层不断的调用系统函数接收数据 扮演者一个消费者的角色 接收缓冲区就是一个交易场所
- 因此 引入发送缓冲区和接收缓冲区就相当于引入了两个生产者消费者模型 该模型将上层应用和底层通信进行了解耦 同时它的引入同时也支持了并发和忙闲不均
窗口大小
当发送端要将数据发送给对端的时候本质上是将自己发送缓冲区的数据发送到对端的接收缓冲区中
但是我们的缓冲区是有大小的 如果我们的发送缓冲区发送数据的速度大于接收缓冲区接收的数据那么我们的数据就会溢出就会造成丢包废弃数据的情况
TCP协议给出的解决方案就是窗口大小
TCP使用这十六位的窗口大小来表示自身接受缓冲区的大小 此时我们的发送端就可以通过该十六位窗口大小来判断自己应该发送多少数据 控制自己发送数据的速率
- 窗口大小字段越大 说明接受端接受数据的能力越强 此时发送端发送数据的速率越快
- 窗口大小字段越小 说明接收端接收数据的能力越弱 此时发送端发送数据的速率越慢
- 窗口大小字段为零 说明接收端不能接收数据了 此时发送端应该停止发送数据 避免数据浪费
理解现象:
- 在编写TCP套接字的时候 我们调用read函数读取数据 可能会因为套接字中没有数据而被阻塞住 不是因为我们调用该函数出问题了 而是因为TCP接收缓冲区中没有数据了 因为一直阻塞在缓冲区
- 我们调用write函数发送数据的时候也可能会阻塞住本质上是因为TCP的发送缓冲区已经被写满了
六个标志位
为什么会存在标志位?
回答这个问题之前我们首先来UDP协议 为什么UDP协议不存在标志位呢?
因为UDP是一个不可靠的协议 它甚至没有发送缓冲区 从上层接收到数据之后直接丢到网络中就完事了
但是TCP协议不同 它是要保证可靠性的 需要包括但不限于三次握手四次挥手 按序到达 紧急数据等
也就是说TCP协议的报文多种多样 除了正常的通信的通信报文 建立连接的连接报文 断开连接的报文等等
TCP报文中的标志位是什么样子的?
实际上这六个标志位在报文中的存在形式类似于位图 每个标志位占用一个比特位 1表示真 0表示假
补充:
不光是TCP报头中 在系统的设计中 但凡只需要考虑一个选项存在还是不存在我们一般都会使用位图 因为这样子空间的使用效率是最高的 当然我们还可以配合使用一些哈希算法来提高搜索效率
下面我们会逐一了解这些标志位
SYN
- 报文当中的SYN被设置为1 表明该报文是一个连接建立的请求报文
- 只有在连接建立阶段 SYN才被设置 正常通信时SYN不会被设置
ACK
- 报文当中的ACK被设置为1 表明该报文可以对收到的报文进行确认
- 一般来说 除了第一次发送请求连接的报文不需要设置ACK之外其余所有的报文都需要设置ACK 因为向对方发送的数据的时候也可以确认对方之前发送的一些数据
FIN
- 报文当中的FIN被设置为1 表明该报文是一个连接断开的请求报文
- 只有在断开连接阶段 FIN才被设置 正常通信时FIN不会被设置
URG
- 当URG标志位被设置为1时 需要通过TCP报头当中的16位紧急指针来找到紧急数据 否则一般情况下不需要关注TCP报头当中的16位紧急指针
什么是十六位紧急指针呢?
它其实就是紧急数据在报文中的偏移量 也就是说当紧急指针为0时紧急数据就在最前面 紧急指针越大偏移量越大 紧急数据就越在后面
紧急数据的大小有多少呢?
紧急数据的大小一般只有一个字节 至于为什么设置一个字节这里就不过多讨论了 大家记住结论即可
PSH
- 报文中的PSH被设置为1就是在告知对方该数据要尽快的交付给上层
我们之前的理解是这样子的 当我们使用函数从缓冲区中读取数据的时候 如果缓冲区中有数据那么read函数就会从该缓冲区中读取数据 如果没有数据就会阻塞 直到接收缓冲区中接收了数据为止才返回
但是实际上这种说法是不太准确的 接收和发送缓冲区都有一个水位线的概念
我们假设水位线是一百字节 那么 只有当数据达到一百字节的时候才会让read函数读取数据 否则会一直阻塞住
这么设计的原因是 如果缓冲区中有一点数据就读取的话会造成内核态用户态之间频繁的切换 (因为有可能这些数据发过来隔的时间很短并且数据量很小) 这样子就会造成计算机效率的浪费
但是如果说我们发送报文的时候携带了这个选项 实际上就是在告诉操作系统 我们希望这个数据尽快被应用层读取
RST
- 报文中的RST被设置为1 表示希望重新建立连接
- 在通信双方在连接未建立好的情况下 一方向另一方发数据 此时另一方发送的响应报文当中的RST标志位就会被置1 表示要求对方重新建立连接
- 如果连接的过程中出现了任何的异常也会要求重新建立连接
确认应答机制 (ACK)
确认应答机制是TCP协议保证可靠性的机制之一
确认应答机制是由TCP报头中的32位序号和32位确认序号来保证的
值得我们注意的是确认应答机制并不会保证所有消息的可靠性 具体原因上面讲解这个字段的时候讲解过了 有兴趣的同学可以翻到上面观看 简单来说最后总会有一条最新的消息是无法被ACK的
但是我们上面也说过 我们无需保证所有的消息都可靠 只需要重要的消息对面收到了就可以 像下面这样子
如何理解TCP将每个字节的数据都进行了编号?
TCP的传输是面向字节流的 我们可以将TCP的发送缓冲区和接收缓冲区都想象成一个数组
当我们将数据从应用层拷贝到TCP的发送缓冲区当中时 这些按字节为单位的数据就具有了一个天然的序号 那就是缓冲区这个数组的下标 但是和真正数组不同的是 这个下标是从1开始的
而双方进行通信的过程本质上就是将发送缓冲区的数据拷贝到接收缓冲区中
其中报头的序号其实就是发送的若干字节的数据的首个字节数据对应的下标 而接收端发送的确认序号其实就是该段若干数据的下一个字节对应的下标
当发送端收到确认序号之后就会覆盖该序号之前的内容并且从该序号开始发送数据
超时重传机制
在双方使用TCP进行通信的时候 如果发送方发出的数据在一段时间内得不到应答 此时发送方就会重新发送 这就叫做确认应答机制
那么我们是如何实现这个超时重传机制的呢?
大家学习了TCP的报头之后可能会陷入一个误区 就是TCP的可靠性是由报头保证的 但是实际上并不是这样 除了报头之外TCP协议还会通过一些代码来保证其可靠性
超时重传机制就是这些代码中的一个 在发送出一个数据之后TCP就会设置一个“闹钟” 如果在闹钟响之前收到应答这个闹钟就会关闭 如果在闹钟响了之后还没有收到应答就会触发超时重传机制 重发数据
丢包的两种情况‘
我们通过TCP协议可以确定一个报文是否到了对端 但是我们无法确定一个报文没有到达对端
丢包的情况之一就是这个报文根本就没有到达对端 此时发送端在一定时间内没有收到响应报文就会触发超时重传
丢包的另一种情况就是报文其实发送到对端了 但是对方的响应报文在传输的时候丢了 此时发送端没有收到对面的响应报文也会进行超时重传
也就是说当出现丢包的时候我们无法确定是报文丢失了 还是响应报文丢失了但是TCP协议不关心这些 只要收不到响应报文就会进行超时重传
当主机A在一定的时间内没有收到响应报文的时候 主机A就会触发超时重传机制 将没有响应的数据从缓冲区中再次发送 因为我们说过这个时候发送缓冲区的数据并不会删除 至少收到确认报文的时候才会覆盖
如果说主机B实际上是收到了主机A前面发送的报文 我们此时也不用担心重新发送的问题 因为此时主机B会根据主机A的序号来进行排序 排序的同时会去重
超时重传的等待时间
我们超时重传的时间不能设置的太长或者太短
- 如果我们超时重传的时间设置的过长会导致数据丢失之后对面长时间得不到对应的数据 进而影响整体重传的效率
- 如果我们超时重传的时间设置的过短会导致数据没有丢失还是会触发超时重传机制 此时对面就会收到大量的重复报文 甚至太短的话整个网络都可能会崩溃
所以说我们超时重传的时间一定要是一个合理的值不能太长也不能太短
其中最理想的情况就是找到一个最小的时间 这个时间内响应报文一定能够返回
但是我们都知道网络的通信状态并不是固定的 可能有的时候特别快 有的时候特别慢 所以说超时重传的时间如果要最优的话一定不能是一个固定的值 因此TCP为了保证在任何环境下都能保持高性能的通信 会动态的计算这个最大时间
Linux中会以500ms为一个单位进行控制 每次判定超时重发的时间都是500ms的整数倍
如果下次的数据依然没有得到应答那么此时的响应时间就是500ms2 同理如果下下次发送的数据还是没有得到应答此时的响应时间就是500ms4 以此类推 以指数的形式传递 但是如果时间累计到了一定的程度 那么TCP就会认为对面的主机出现了问题从而强制关闭连接
连接管理机制
TCP是面向连接的
TCP的可靠性机制并不是主机之间 而是连接之间的
一个很简单的推论 如果TCP协议不是基于连接的 那么我们的接收端实际上就只会有一个缓冲区(因为只有一个服务器) 那么多个客户端发送过来的数据就会相互干扰
所以说我们的TCP协议是基于连接的 保证可靠性传输的前提是建立一个连接
操作系统管理连接
连接是TCP协议的基础 有了连接才能保证可靠性 但是一台机器上可能会有大量的连接 所以说操作系统必须要对这些连接进行管理
那么应该如何进行管理呢?
根据系统管理的第一法则 先描述 再组织 而我们的Linux是c语言写出来的 c语言中描述一个对象一般是用结构体来描述 所以说在Linux中一定会有一个这样子描述连接的结构体 该结构体中有需要管理连接用的各项属性 每次创建一个连接在系统看来就是定义了一个结构体
描述完毕之后就是组织了 系统中创建了结构体之后会将它们用双链表的形式连接起来方便管理 此时操作系统对于连接的管理在实际上就变为了对于双链表的增删改查
- 建立连接
我们站在操作系统的角度理解建立连接实际上就是创建出一个连接结构体 并且填充它的各个字段 之后连接到双链表中
- 删除连接
我们站在操作系统的角度理解删除连接实际上就是删除该连接对应的结构体 .
三次握手
TCP协议在通信之前要建立连接 我们很形象的把建立连接的过程称为三次握手
我们以客户端首先向服务器发送请求为例
- 首先客户端向服务器发送的报文中SYN位被设置为1 表示请求与服务器建立连接
- 服务器在接收到该报文后会发送一个SYN+ACK位都被设置的报文 表示服务器接收到了客户端的SYN请求 并且也想要和客户端建立连接
- 客户端在收到服务器的报文之后就会直到客户端同意了自己的请求并且想要和自己建立连接 最后客户端会对于该报文进行响应
因为我们的TCP是全双工通信机制 所以服务器在收到客户端建立连接的请求的时候也需要向客户端发送请求建立连接的申请
为什么是三次握手?
建立连接的本质是确认两件事情
- 网络是否畅通
- 对方是否能和我正常通信
第一点网络是否畅通很好理解 如果网络不畅通的话我们的连接请求根本就发送不过去 就别说通信消息了
第二点确认对方能够和自己可以正常通信我们需要理解下
就像我们打电话的时候 我们的第一句话肯定是 “喂“ 而说出这个喂其实就是在确认对方是否能够听到我们的声音并且能够对我们的声音做出回应
那么什么时候我们才能确认我们是可以和对方正常通信的呢? 是我们发出”喂“这个声音对面听到的时候吗?
显然不是 真正的确认时间应该是对方也回复一个”喂“的时候 我们才能直到我们是和对方是能够正常通信的
换到对方视角也一样 对面首先跟我说了一句”喂“ 这就说明我们能够正常听到对方的声音 可是这对于双方通信来说是不够的 我们还需要确认自己的声音能不能被对方听到 所以我们也说了一句”喂“ 但是大家仔细想象 我们说出这”喂“ 之后就能够确认自己的声音能被对方听到吗? 显然也不是 我们至少要对方回答”能听到“ 或者是其他能够回复我们上一句 ”喂“ 的话 我们才能确认双方可以正常通信
将建立三次连接的过程带入到上面的例子当中
客户端像服务端说出的第一个”喂“就是SYN请求
服务端向客户端说出的第一个”喂“就是SYN+ACK
客户端说出的”能到到“就是ACK
所以说我们选择三次握手最大的原因就是 三次握手是能够使用最少的次数来确认网络畅通 对方能和我正常通信的
但是我们前面也说过 确认应答机制无法保证最后一次通信数据能够送达 所以说最后一次握手的可靠性是无法保证的
那么当最后一次握手失败的时候会发生什么情况呢?
由于客户端已经发出了一个报文 并且这个报文在被服务端收到并应答之后服务端再次发送一个报文给客户端 客户端收到了这个报文 那么此时站在客户端的视角它就会认为 网络是畅通的 对方能和我正常通信 于是客户端就会维护起一个连接 但是此时服务器缺没有收到客户端的回复 于是服务器就认为自己不能和对方正常通信 服务器就不会维护起连接
而对于服务器来说 它维护的连接肯定是要比客户端多得多的 所以说将连接失败的责任算在客户端头上也能够极大的减轻服务器的压力
那么此时进行三次握手的优点就有两个了
- 三次握手是能够使用最少的次数来确认网络畅通 对方能和我正常通信的
- 三次握手将连接失败所需要承担的风险转移到了客户端头上
为什么不能一次握手 或者两次握手就建立连接呢?
首先如果一次两次握手就建立连接肯定就没有上面的优点了
其次如果使用一次握手或者两次握手的方案还有极大的可能会收到攻击
我们假设是使用一次握手的情况 客户端发起一次SYN请求之后服务器立马建立连接并且在内存中创建对应的结构体并且维护它
那么我们客户端只要不停的发送SYN 服务器就要不停的建立连接创建结构体 直至崩溃 而客户端缺没有任何的代价 我们将这种攻击称为SYN洪水攻击
二次握手的情况同理
三次握手时的状态变化
我们从上往下来看 这里其实有一个变量一直在走 那就是时间 客户端和服务器的状态变化是随着时间而变化的
最开始的时候客户端和服务器都处于CLOSED的状态
之后服务器为了能让客户端发送连接申请 让自己处于LISTEN状态
客户端开始发送SYN连接申请 自己的状态变为SYN_SENT状态
服务器收到客户端发送的SYN连接申请之后会给客户端响应一个SYN+ACK的响应报文 并且状态变为SYN_RCVD
客户端收到服务器的回复之后便会认为网络畅通 自己能和客户端正常通信 于是状态变为ESTABLISHED并且像服务器发送响应报文
服务器收到响应报文之后便认为三次握手完成 连接建立成功于是状态也会变为ESTABLISHED
套接字和三次握手的关系
我们服务器之所以会进入LISTEN状态是因为我们调用了LISTEN函数
当服务器进入LISTEN状态之后客户端就可以发起三次握手了 此时客户端对应的是connect函数
不过需要我们注意的是 本身connect函数不参与三次握手 它只是发起三次握手的请求 当connect返回的时候要么是三次握手成功了 要么是三次握手失败了
如果服务器和客户端完成了三次握手 此时在服务器端就会建立一个连接 但这个连接在内核的等待队列当中 服务器端需要通过调用accept函数将这个建立好的连接获取上来
当服务器将连接获取上来之后 双方就可以通过read 或者 write函数进行数据交互了
四次挥手
TCP协议在通信之后要断开连接 我们很形象的将断开连接的过程称为四次挥手
由于双方维护连接都是需要成本的 所以在TCP通信结束之后需要断开连接
- 首先客户端向服务器发送的报文中FIN位被设置为1 表示请求和服务器断开连接
- 服务器收到客户端断开连接的请求之后响应
- 服务器向客户端发送的报文中FIN位被设置为1 表示请求和客户端断开连接
- 客户端收到服务器断开连接的请求之后响应
为什么要进行四次挥手
因为TCP是一种全双工的通信方式 建立连接的时候需要双方都要建立建立 当然断开连接的时候也都要双方断开连接 其中每两次挥手就断开一个方向到另一个方向的通信信道 所以说断开连接需要四次挥手
为什么不能像三次握手那样FIN和ACK一起发呢?
因为挥手了就代表要断开连接
客户端向服务器挥手的时候就说明客户端已经没有数据要向服务器发送了 此时服务器向客户端发送ACK确认应答
但是这个时间并不代表服务器没有数据要发送给客户端了 可能还有一些残留的数据没有发送
也就是说服务器和客户端断开连接的时间并不是一致的 所以说我们不能像三次握手那样同时发送ACK和FIN
四次挥手时的状态变化
四次挥手的状态变化如下:
首先在第一次挥手发出前双方都处于ESTABLISHED状态
客户端想要和服务器断开连接主动向服务器发送断开连接请求 发送FIN位被设置的报文 此时状态变化为FIN_WAIT1
服务器在收到客户端断开连接的请求之后对其进行响应 此时服务器的状态就会变为ClOSE_WAIT
当服务器发送完所有的数据也想要断开连接的时候会发送FIN报文 此时服务器的状态会变为LASE_ACK
客户端收到服务器发来的第三次挥手后 会向服务器发送最后一个响应报文 此时客户端进入TIME_WAIT状态
当服务器收到客户端的一个响应报文的时候 服务器就会彻底的关闭连接变为CLOSED状态
客户端在等待2MSL(Maximum Segment Lifetime 报文最大生存时间)的时间之后也会变为CLOSED状态
自此之后双方才断开连接
套接字和四次挥手之间的关系
客户端发起断开连接的请求时 对应的就是客户端主动调用CLOSED函数
服务器发起断开连接的请求时 对应的就是服务器主动调用CLOSED函数
一个CLOSED对应的就是两次挥手 两个CLOSED对应的就是四次挥手
CLOSE_WAIT
- 双方在进行四次挥手时 如果只有客户端使用了CLOSED函数 服务器没有调用CLOSED函数 则客户端的状态变为FIN_WAIT_1 服务器对于客户端发送过来的请求进行响应之后状态变为CLOSE_WAIT 当客户端收到服务器对于第一个FIN的ACK之后客户端的状态会变为FIN_WAIT_2
只有完成四次挥手之后通信才会真正的结束 此时双方才会释放对应的资源 如果服务器不主动关闭文件描述符的话 那么此时服务器中就会存在大量的CLOSE_WAIT状态 其中每个链接都会占用服务器的资源 最终就会导致服务器的资源越来越少
所以说如果我们编写服务器代码的时候如果发现服务器中出现大量的CLOSE_WAIT状态 我们就要检查下是不是自己的代码逻辑中忘记写文件描述符了
TIME_WAIT
当我们进行四次挥手时:
- 第一次挥手丢包:客户端收不到服务器的应答 进而进行超时重传
- 第二次挥手丢包:客户端收不到服务器的应答 进而进行超时重传
- 第三次挥手丢包:服务器收不到客户端的应答 进而进行超时重传
- 第四次挥手丢包:服务器收不到客户端的应答 进而进行超时重传
通过上面的四次挥手我们可以发现TIME_WAIT状态是必要的
因为如果像上图这样(实际不是这样子) 没有TIME_WAIT状态 如果说客户端的ACK在路上丢包了 服务器触发了超时重传机制 重新发送FIN报文 此时客户端也不会有任何回应了 因为客户端此时已经是CLOSED状态了
此时服务器就会不停的触发超时重传机制 从而不停的发送FIN报文给客户端 虽然说经过若干次发送之后会强制关闭连接 但是在这期间的维持连接的开销还是要服务器承担的
所以说为了避免这种情况我们需要设置TIME_WAIT状态 即使是客户端的ACK报文丢失了 服务器超时重传之后还是有较大概率能收到客户端的响应报文
TIME_WAIT存在的必要性
- 当四次挥手的报文丢失触发服务器的超时重传机制的时候客户端能够对该报文做出响应 从而较大概率的保证服务器能否收到ACK报文关闭连接释放资源
- 客户端发出最后一次挥手时 双方历史通信的数据可能还没有发送到对方 因此客户端四次挥手后进入TIME_WAIT状态 还可以保证双方通信信道上的数据在网络中尽可能的消散
我们要注意的是 就算我设置了TIME_WAIT状态 它们之间的通信也不是绝对可靠的 有可能因为网络环境特别差 ACK报文就是怎么都发送不到
这个时候客户端的TIME_WAIT状态在一段时间的等待之后就会关闭 服务器经过几次超时重传之后也会强制关闭连接释放资源 但是这毕竟是小概率时间
我们前面也说过服务器要建立的连接数肯定要远远大于客户端所需要的连接数 所以说三次挥手也好 TIME_WAIT机制也好 都是要尽量的让客户端承担代价
流量控制
TCP通过接收端的接收数据能力来决定发送端的发送速度 这种机制叫做流量控制
我们学习到现在都知道了 通信其实就是将发送缓冲区的数据写到对端的接收缓冲区去
既然是缓冲区那么它们的大小肯定就是有极限的 如果说发送端一次性发送大量的数据那么接收端的缓冲区很可能就会出现溢出的情况 如果数据溢出之后接收端的缓冲区就没有办法再接收数据了 那么此时耗费了大量的网络资源传输过来的数据就要被迫丢弃 这很明显是严重浪费网络资源的行为
那么有没有什么办法可以解决这个问题呢 答案当然是有 接收端可以将自己的接收能力告知发送端 然后发送端根据接收端的接收能力动态的调整自己发送数据的速度 这就叫做流量控制机制
接收端是什么时候通过什么方式将自己的接收能力告知发送方的呢
接收端将自己可以接收的缓冲区大小放入TCP首部中的“窗口大小”字段 通过ACK通知发送端
窗口大小字段越大 说明网络的吞吐量越高 说明发送方可以发送数据的速度越快
如果说接收端发现自己的接收缓冲区快满了 那么在发送下次报文的时候就会减小滑动窗口的大小 此时发送端的发送速度就会变慢
如果说接收端的接收缓冲区大小为0了发送的十六位窗口大小为0 那么此时发送端就不会再发送数据了 会进入一个等待状态
那么发送端会何时继续发送数据呢?
有两种情况:
- 等待告知 即接收端等待上层将自己的接收缓冲区读取一部分数据的时候 会主动发送一个报文给发送端 该报文会附带自己目前的窗口大小
- 主动询问 发送端每隔一段时间会给接收端发送一个报文 该报文不携带任何有效数据 只是为了询问发送端的窗口大小 知道接收端的接收缓冲区有大小之后就可以继续发送数据了
十六位二进制数字表示的最大值是65535 那TCP窗口的最大值是65535吗
不是的 在TCP报头的选项中有一个窗口扩大因子M 实际窗口大小是窗口字段的值左移M位得到的
第一次向对方发送数据时如何得知对方的窗口大小?
在双方三次握手的阶段其实就通过报文交换了各自的一些数据 其中就包括了告知对方自己的接收能力
所以说在双方正式开始通信的时候就知道了双方的数据接收能力 所以在第一次发送数据的时候不会出现数据溢出问题
滑动窗口
我们可以使用滑动窗口连续发送多个数据
我们前面说过如果双方在进行数据通信时 只有收到了上一次发送数据的响应才能发下一个数据 那么此时双方的通信过程就是串行的 效率肯定会很差
所以说我们采用TCP通信的时候可以一次性像对方发送多条数据 这样子我们就可以将等待多个响应的时间重叠起来 提高通信的效率
此外我们还引用了序号和确认序号机制来保证我们大量发送的信号不会乱序
但是我们还是不能一股脑的将我们的数据全部发送给对端 我们还需要考虑对端的接收能力于是我们又引入了窗口机制
我们的滑动窗口机制其实就是对于对方返回的窗口大小的一种应用
滑动窗口
我们发送方可以一次性将大量的报文发送给对方 此时也就意味着这一部分报文中肯定会又一部分发出去了但是还没有收到应答
我们可以根据是否发出 是否收到应答 将我们的发送缓冲区的数据分为三部分
我们具体的将这个窗口分为三部分
- 已经发送并且收到ACK的数据
- 已经发送并且没有收到ACK的数据
- 未发送的数据
我们一般把中间蓝色的那一段叫做滑动窗口 当然也有人将这三部分整体称为滑动窗口 其中蓝色的部分叫做滑动窗口大小
滑动窗口的描述是 发送方不用等待ACK所能一次性发送数据的最大量
滑动窗口最大的意义是可以提高数据发送的效率
- 滑动窗口的大小等于对方窗口大小和自身拥塞窗口的较小值 自身拥塞窗口是跟网络有关的 因为我们发送数据不光要考虑对方的接收状况还要考虑网络状况
- 假设我们这里的拥塞窗口非常大 (肯定大于对方的窗口) 并且我们的滑动窗口的大小一直是4000个字节 那么此时发送方不用ACK就能够发送的数据大小就是4000字节
- 也就是说我们现在可以直接发送 1001-2000、2001-3000、3001-4000、4001-5000这四千个字节的数据
- 如果说对方给我ACK了一个2001 也就是说1001~2000的数据全部被接收了 那么1001~2001这一段就应该被划分到我们数据段的第一部分
- 如果此时的滑动窗口大小还是4000 那么滑动窗口就应该往右边移动 覆盖我们的5001~6000这一字段 以此类推
- 滑动窗口越大则说明网络的吞吐效率越高
网络的吞吐效率: 网络的吞吐效率是指网络在单位时间内成功地传送数据的数量 常以每秒钟能够传输的数据比特来衡量(bit per second, bps)
当我们发送的数据被ACK的时候这部分数据就可以被归类到滑动窗口的前面那一段去
(这里可能有同学有疑问 万一ACK的数据不是2001 而是4001TCP协议会怎么处理呢 事实上如果说对端返回我们是4001的确认序号就说明4001前面的数据全部被排序并接收了 所以说我们直接将4001之前的数据全部放到第一段就可以)
如果说此时的滑动窗口大小还是之前的4000话 滑动窗口就会右移
我们在前面讲过TCP发送的数据如果超过一段时间没有被应答就会触发超时重传机制 也就是说我们TCP必须要保存我们缓冲区中的数据一段时间
事实上我们要在发送缓冲区中保存的代码就是滑动窗口里面的数据
等我们的数据被ACK从而放置到第一段之后这些数据就是可以被删除的了
滑动窗口一定会整体右移吗?
滑动窗口不一定会整体右移
还是拿上面的发送来举例 假设我们一开始的滑动窗口大小就是4000字节
假设对面接收了1001~2000的数据之后给我们ACK了一个2001 但是此时接收端滑动窗口的大小变为3000了
那么对于我们发送端来说他会将1001~2000的数据放到第一段 但是此时滑动窗口不会进行右移了
如何实现滑动窗口
TCP接收和发送缓冲区都看作一个字符数组 而滑动窗口就可以看作是两个指针限定的一个范围
比如说我们使用start指针指向滑动窗口的左侧 end窗口指向滑动窗口的右侧 此时在start和end指针范围内的就可以叫做滑动窗口
当我们的发送端接收到对方的响应时 如果响应的确认序号是x 窗口大小和拥塞窗口大小中的较小值是win
那么我们就可以将start设置为x end设置为 x + win
当然我们缓冲区的大小也不是无线的 可以考虑通过模上缓冲区大小的长度来控制滑动窗口
丢包问题
当发送端一次性发送多个报文的时候有可能会发生丢包问题 丢包的情况可以分为下面两种
一: 数据到达 ACK丢包
当我们一次发送多个报文的时候其实部分ACK丢包无关紧要 因为我们可以通过后面的一些ACK报文来确定前面的数据有没有被接收
比如说我们同时发送了1~6000个字节的数据 但是只有6001这一个ACK 前面的ACK都丢了
此时发送端仍然会认为前面的6000个字节的数据全部被接收了 因为如果没有被接收端接收它是不会返回6001的ACK的
二 数据包丢了
比如说我们1001 ~ 2000字节的数据包丢了 那么此时主机B在收到后面序号的数据之后给我们的应答会一直都是1001
此时如果发送端连续三次相同序号的报文就会触发快重传机制 将1001~2000字节的数据重发
当主机B收到1001~2000的报文之后它的下一个ACK报文的确认序号就是6001 (因为前面的数据全部都有了)
不过需要注意的是 我们在触发快重传机制的时候并不是直接将收到序号之后的所有输出重发 因为在路上可能只有1001~2000这一个数据包丢了 其他的数据包并没有丢 如果重发所有的数据就就会造成一定的网络资源浪费
它的策略应该是先重发1001~2000这一数据包 之后看对面主机的ACK确认序号再做后面的决定
滑动窗口中的数据一定都没有被对方收到吗?
不一定 如下图所示
我们可以看到在该图中 2001~4001的数据其实已经被ACK了 但是在传输过程中1001 – 2001的数据丢了 所以说对端回复的ACK确认序号一直是1001
此时如果我们补发了1001~2000的数据 对端就会立马补发5001的ACK 1001 – 5000的数据也就会立刻被发送到滑动窗口左侧
快重传 VS 超时重传
- 快重传光看名字也就知道了一定要比超时重传快 快重传是接收三次相同确认序号的ACK立马重传 而超时重传则是设定计时器在计时器时间到之后才会进行重传
- 虽然快重传机制能够快速判定数据包的丢失但是它并不能完全取代超时重传机制 因为报文丢失后三次ACK可能因为网络原因没有发送到发送端主机上从而只能进行超时重传
- 所以说快重传对于TCP来说是一种效率上的提升 而超时重传是所有重传机制的保底策略
拥塞控制
TCP协议中的拥塞控制是一种避免网络拥塞的算法 它可以根据网络的状况动态地调整发送数据的速率
我们前面在介绍滑动窗口大小的时候介绍过 滑动窗口大小不完全取决于对端的窗口大小 而是对端窗口大小和自身拥塞窗口的中的较小值
由于网络的不确定性 我们都知道在网络传输的过程中丢包是不可避免的现象 此时我们就可以通过快重传和超时重传来进行重发 但是如果双方在进行通信时进行大量丢包 那么我们此时就不认为这是一个正常现象而是网络出现问题了
所以说TCP不仅考虑了通信双端主机的问题 同时也考虑了网络的问题
- 流量控制:根据对方的窗口大小动态的调整自己的数据发送速率 避免对端接收缓冲区溢出
- 滑动窗口:在不用ACK的情况下一次性能够发送的最大数据量
- 拥塞控制:考虑双方通信网络问题 如果发送的数据超过了拥塞窗口的大小就会引起网络阻塞
如何解决网络拥塞问题?
当网络出现大面积崩溃的时候通信双方作为网络当中两台小小的主机 看似并不能为此做些什么 但“雪崩的时候没有一片雪花是无辜的” 网络出现问题一定是网络中大部分主机共同作用的结果
- 如果网络中的主机在同一时间发送给网络大量的数据 那么就有可能导致网络中的某些关键节点的路由器下就可能排了很长的报文 最终会导致报文无法在ddl之内抵达对面的主机
当网络出现堵塞的时候双方主机虽然不能提出有效的解决方案但是它们能够做到不加重网络的负担
如果双方在通信的出现了大量丢包 那么我们就可以认为是网络阻塞了 在此时我们不应该立即将这些报文进行重传 而应该少发数据甚至不发数据等待网络状况恢复后在慢慢恢复传输数据的效率
需要注意的是网络拥塞的时候影响的并不只是一台主机 而是网络中的大部分主机 所以此时所有采用TCP协议的主机都会采用拥塞避免算法
因此拥塞控制看似只是谈论的一台主机上的通信策略 实际这个策略是所有主机在网络崩溃后都会遵守的策略
一旦出现网络拥塞 该网络当中的所有主机都会受到影响 此时所有主机都要执行拥塞避免 这样才能有效缓解网络拥塞问题 通过这样的方式就能保证雪崩不会发生 或雪崩发生后可以尽快恢复
拥塞控制
此时我们讨论的是网络拥塞后的情况
虽然说我们可以通过滑动窗口来高效可靠的发送大批量的数据 但是如果我们在刚开始的时候就发送大批量的数据就不可避免的会发生一些问题
因为此时网络中的状况已经足够拥塞了 如果我们还不断的发送大量的数据就会让网络更加的拥塞
所以说我们的TCP引入了慢启动机制 在阻塞后的一定时间内先发少量的数据探路 等待网络状况良好之后再发送大量的数据
我们TCP除了有窗口大小和滑动窗口的概念之外还有拥塞窗口的概念 拥塞窗口是引起网络拥塞的阈值 如果说我们发送的数据超过了拥塞窗口那么就有可能再次引起网络拥塞
我们刚开始发送的时候拥塞窗口的大小为1 之后每次收到一个ACK后拥塞窗口的大小就增加1(不准确)
之后我们发送数据的时候取拥塞窗口大小和对端窗口大小中的较小值 即我们的滑动窗口大小
我们现在假设对方窗口的大小无限大 网络的情况也特别好 那么此时拥塞窗口增长的速度其实是很快的 因为这时候拥塞窗口是指数级增长
1 … 2 … 4… 8… 16… 32 … 64 … 128 … 256 …
仅仅通过十次来回 拥塞窗口的大小就会变成1024 后面增长的速率还会越来越快
所以说拥塞窗口只是 “慢启动” 即初始时的速率比较慢 但是越往后面增长的速率越来越快 如果说我们让拥塞窗口的值一直以指数级增长 那么网络就会很快的阻塞住
所以说我们不能够一直让堵塞窗口以指数级别增长 此时我们就引入了慢启动的阈值 当指数增长的大小超过这个阈值的时候我们的阻塞窗口就不以指数级别增长而是以线性增长了
当TCP刚刚启动的时候慢启动的阈值设计为对方窗口的最大值
当每次超时重发的时慢启动的阈值被设计为当前拥塞窗口大小的一半 同时拥塞窗口的大小被设置为1 如此循环
如图 这其中分别出现了指数增长 线性增长和乘法减小
- 指数增长:在慢启动刚开始的一段时间 拥塞窗口的大小是指数增长的
- 线性增长:当我们慢启动后拥塞窗口的大小达到了阈值 就开始了线性增长
- 乘法减小:当我们开始线性增长之后如果此时的拥塞窗口大小引起了网络拥塞此时慢启动的阈值就会就会减小为网络拥塞时拥塞窗口大小的一半 并且拥塞窗口被设置为1 继续慢启动的过程
主机在网络通信的时候实际上就是不断再重复指数增长线性增长乘法减小的状态
但是我们要注意的是 这个过程并不是同步的 在同一个网络中的两个不同主机 它们有可能一个发生网络拥塞了 另一个还在正常通信
为什么我们不直接设置一个固定的拥塞窗口大小呢? 比如说拥塞窗口大小增长到20之后就不再线性增长保持这个速率
首先直接说结论 我们不能这么做 这样子做会浪费网络传输的效率
因为我们网络的状况其实是不确定的 有时候可能特别快有时候可能特别慢 如果我们把速率限定死一个值 那么我们实际上传输的速率就会受到这个值的制约而会变得特别慢
而反观我们的慢启动机制 它其实是在不停的试验网络中的最大传输速度从而调整拥塞窗口的大小 就算造成网络拥塞了 慢启动也能很快的恢复正常的传输效率 “慢启动可一点也不慢”
延时应答
如果我们收到对方主机发送的报文之后立马进行应答 此时接收端返回的窗口大小就可能会比较小
比如说 假设我们接收缓冲区能够接收的数据有1MB 此时对方发来500kb的数据放入到我们的接收缓冲区中
如果说我们此时立刻给予对方应答 我们的窗口大小就只有500kb
但是实际上我们应用层处理数据的速度可能是很快的 10ms之内就能够处理掉这500kb的数据
也就是说如果我们能够等待10ms之后再发送报文 此时我们的窗口大小就是1mb了
从上面的描述中相信大家也能够看出来 我们的延迟应答机制并不是为了保证可靠性 而是为了提高传输效率的
此外我们并不是每一个包都要延时应答 有可能对面发过来1~6001 6个1000字节的数据包 我们只需要回复三个ACK就可以了
当然我们延时应答也有时间限制 不知道大家还记不记得我们上面所说的超时重传机制 如果说接收端超过了某个时间没有应答的话就会触发发送端的超时重传机制从而导致网络资源的浪费了
所以说我们的知识并不是孤立起来的 而是具有练习的 由各个机制同时组成了TCP协议 保证了TCP的可靠性高效
在不同的操作系统上 我们的限制数量和限制时间是不同的 感兴趣的同学可以取自行了解下
捎带应答
捎带应答是TCP协议中最常用的一种通信机制
比如说主机A此时给主机B发送了一条消息 此时主机B收到之后刚好也要向主机A发送一条消息 那么此时主机B的ACK就不必使用单独的报文发送而是可以搭上主机B要发送消息的这个报文的顺风车 那么此时主机B既发送了消息又应答了主机A的消息 这就叫做捎带应答
捎带应答是一种直接的提升效率的方式 我们此时就不用发送一个空报文去应答对方的数据了
此外由于我们捎带应答发送的报文携带了有效的数据 所以对端主机需要对于我们的报文进行应答 那么当我们收到该响应报文后我们就不光能确定我们的数据被对面接收到了 我们对于上次数据的ACK也被对端收到了
面向字节流
和UDP面向数据包不同的是 我们的TCP是面向字节流的服务
当我们创建一个TCP的套接字的时候 就相当于在传输层中创建了发送缓冲区和接收缓冲区
当在应用层调用wirte函数的时候 它的作用就是将数据全部拷贝到发送缓冲区中 拷贝成功就返回0 失败就返回-1
如果要发送的字节太长我们TCP会将它分成多个包之后发送出去 如果要发送的字节太短TCP会将它留在缓冲区中等待其他数据一起发送(蓄水池机制 上面介绍PSH位的时候有讲解)
当我们接收数据的时候数据直接从网卡驱动发送到内核的接收缓冲区 我们此时就可以通过read函数来读取有效数据 需要注意的是和UDP不同的是 我们使用read函数在缓冲区中读取数据的时候可以读取任意个字节的数据
由于缓冲区的存在 TCP的读和写不需要一一匹配
- 我们可以调用一百次write发送100字节的数据 每次发送一个字节
- 接收的时候我们可以只调用一次read函数读取100字节的数据
对于TCP来说 它并不关心底层的数据是什么样子的 在它看来这些只不过是一个个的字节数据而已 它的任务就是准确无误的将这些数据从一段的发送缓冲区传输到另一段的接收缓冲区 至于上层应用要怎么使用这些数据 那就是应用层的事情了 传输层的TCP协议并不关心 这就叫做面向字节流
粘包问题
什么是粘包
我们首先要明确 粘包的包是什么 它指的是应用层的数据包
在TCP协议的报头中 是没有和UDP协议报头中报文长度这一字段的
站在传输层的视角TCP报文是一个个的排好序放在接收缓冲区中的
站在应用层的视角缓冲区就就是由多个字节组成的字节流
那么应用层看到了这一串字节流之后就会认为这是一个完整的数据包 它不知道应该把这个包从哪里到哪里分开 这就是粘包问题
如何解决粘包问题
- 对于定长的包来说 我们每次读取固定大小长度的数据就可以了
- 对于非定长的包来说 我们可以在应用层的报头上增加一个长度字段 通过这个字段我们就能知道该数据包的结束位置从而防止粘包问题了 比如HTTP报头当中就包含Content-Length属性 表示正文的长度
- 对于非定长的包来说 我们还可以在每个包的结尾设置一个明确的分隔符 这个工作也是由应用层解决的
TCP的异常情况
进程终止
当我们在正常通信的时候 如果进程崩溃了 那么建立好的连接会怎么办呢?
我们在信号这一章节讲过 进程崩溃的本质实际上就是操作系统给进行发送了信号从而杀死了进程
进程被杀死的时候它会关闭底层所有的文件描述符和释放资源 也就是说底层它仍然会调用close函数进行四次挥手和正常关闭进行没有什么两样
关机
如果我们在正常通信的时候关机了 那么建立好的连接会怎么办呢?
其实这种情况和第一种情况类似 因为如果我们关机了 操作系统也会杀死所有的进程释放所有的进程资源包括文件描述符 所以说在我们关机的情况下也会正常的进行四次挥手
突然断电
如果我们在正常通信的情况下突然断电了 那么建立好的连接会怎么办呢?
当我们的客户端突然掉线之后实际上我们的服务器是无法得知客户端掉线了的 所以说在短期内服务器仍然会维护这个连接 但是时间一长 服务器发现自己发送的数据对方都没有应答之后就会强制关闭连接 这和TCP的保活策略有关
- 服务器会定期询问客户端的情况 如果服务器的多次询问都没有收到ACK那么此时服务器会强制关闭连接
- 客户端也会定期向服务器报平安 如果服务器长期没有收到客户端的消息 服务器也会强制关闭连接
其中服务器定期询问客户端的存在状态的做法 叫做基于保活定时器的一种心跳机制 是由TCP实现的
此外 应用层的某些协议 也有一些类似的检测机制 例如基于长连接的HTTP 也会定期检测对方的存在状态
TCP小结
TCP协议既要保证可靠性又要保证效率 所以说设计了很多机制
保证可靠性:
- 检验和
- 序列号
- 确认应答
- 超时重传
- 连接管理
- 流量控制
- 拥塞控制
保证效率:
- 滑动窗口
- 快速重传
- 稍微应答
- 延时应答
此外我们的TCP协议不光是通过报头还通过了一些代码逻辑来保证效率和可靠性
TCP定时器
TCP协议中有各种的定时器来保证效率和可靠性
- 重传定时器:数据发送后会激活重传定时器 如果一定时间内收不到响应就会触发超时重传机制
- 坚持定时器: 为了对方零窗口大小而设置的 向对方发送窗口探测的时间间隔
- 保活定时器: 为了检查空间连接的存在 也就是向对方发送探查报文的时间间隔
- TIME_WAIT定时器 : 双方在进行四次挥手之后 主动断开连接的一方要等待更长的时间
理解传输控制协议
其实在整个TCP协议中 都没有涉及到数据的具体发送 而是提供了一套的理论支持 因为传输层的作用就是保证数据传输的可靠性
在整个网络传输的过程中 其实由TCP做决策 IP+MAC做执行 它们的最终目的就是将数据可靠高效的传输到对端 至于到达对端之后对方要怎么使用那是应用层的事情了
所以说我们应用层决定通信的意义 传输层即以下决定通信的方式
基于TCP的应用层协议
常见的基于TCP的应用层协议如下
- HTTP协议(超文本传输协议)
- SSH协议(安全外壳协议)
- FTP协议 (文件传输协议)
- SMTP协议 (电子邮件传输协议)
- TELNET协议 (远程终端协议)
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net