TCP协议
- Tcp协议
- 可靠性
- 冯诺依曼体系结构
- TCP的协议格式
- 序号与确认序号
- 窗口大小
- 六个标志位
- 确认应答机制 (ACK)
- 超时重传机制
- 连接管理机制
Tcp协议
TCP全称为 “传输控制协议”(Transmission Control Protocol)
TCP协议被广泛应用 其根本原因就是提供了详尽的可靠性保证 基于TCP的上层应用非常多 比如HTTP、HTTPS、FTP、SSH、MySQL等。
可靠性
为什么网络中会存在不可靠
冯诺依曼体系结构
这里的 输入设备 内存 输出设备 cpu这些硬件都是相互独立的
如果它们之间要进行数据交互 就必须要想办法进行通信 这几个设备是用“线”连接起来的
其中连接内存和外设之间的“线”叫做 IO总线 而连接内存和CPU之间的“线”叫做 系统总线
由于这几个硬件设备都是在一台机器上的 因此这里传输数据的“线”是很短的 传输数据时出现错误的概率也非常低
但如果要进行通信的各个设备相隔千里 那么连接各个设备的“线”就会变得非常长 传输数据时出现错误的概率也会大大增高 此时要保证传输到对端的数据无误 就必须引入可靠性
总之网络不可靠的根本原因就是 长距离传输数据使用的‘线’太长了 所以说数据在长距离传输的时候可能会遇到一些错误 而TCP就是在这种背景下诞生的 TCP就是一种可靠的协议
- 单独的一台计算机可以看作成一个小型的网络 计算机上的各种硬件设备之间实际也是在进行数据通信 并且它们在通信时也必须遵守各自的通信协议 只不过它们之间的通信协议更多是描述一些数据的含义
为什么会存在UDP协议?
TCP协议是一种可靠的协议 而UDP是一种不可靠的协议
TCP是一种可靠的协议 这也就意味着TCP需要做更多的工作来保证数据传输的可靠 并且引起不可靠的因素越多 我们要保证可靠的成本就越高 其中常见的不可靠情况有丢包 乱序等 而我们的TCP由于要保证可靠所以说要想办法解决这些问题
UDP协议是不可靠的协议 这也就意味着UDP不需要考虑数据传输时需要处理的问题 因此UDP无论是使用还是维护都足够简单
但是虽然说TCP的使用比UDP更加复杂 但是TCP的效率缺不比UDP低
如果我们严格要求了数据在传输过程中可靠性 那么我们就必须采用TCP协议 如果说允许数据有一点点丢包的话我们就可以使用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:表示要求对方重新建立连接
- SYN:表示请求与对方建立连接
- FIN:通知对方 本端连接要关闭了
TCP报头在内核当中本质就是一个位段类型 给数据封装TCP报头时 实际上就是用该位段类型定义一个变量 然后填充TCP报头当中的各个属性字段 最后将这个TCP报头拷贝到数据的首部 至此便完成了TCP报头的封装
TCP如何将报头与有效载荷进行分离?
当TCP从底层获取到一个报文后 虽然TCP不知道报头的具体长度 但报文的前20个字节是TCP的基本报头 并且这20字节当中涵盖了4位的首部长度
- 当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中的三十二位确认序号是告诉对方 我当前已经收到了哪些数据 你的数据下一次应该从哪里开始发
为什么要用两套序号机制?
主机A和主机B之间通信
A主机只发送数据 B主机只接收数据
那么此时A主机发送序号请求报文的时候 B主机只需要在相同字段填写相同的响应报文就可以了
但是我们的TCP是一种全双工通信协议 所以说在AB主机通信的过程中 B主机也有可能向A主机发送数据
那么此时我们就需要使用两套序号机制来区分序号和响应序号了
窗口大小
TCP的接收缓冲区和发送缓冲区
TCP协议本身是具有接收缓冲区和发送缓冲区的
- 我们调用write和read函数的时候并不是直接从磁盘而是从内存缓冲区中读取数据 根本原因是因为磁盘的IO效率太低了
在应用层调用write函数的时候 实际上就是向TCP的发送缓冲区中写入数据 TCP协议会等待合适的时机发送
与此同时当TCP接收从网络中发送过来数据的时候并不是直接发送到应用层 而是拷贝到接收缓冲区中 等待应用层使用read函数来读取
当我们平时调用write函数的时候 它会将数据写入到TCP的数据缓冲区 当写入成功(或失败)之后它就会返回了 之后数据什么时候发送 怎么可靠的发送就是传输层的事情了 应用层并不关心
TCP的发送缓冲区和接收缓冲区存在的意义
发送缓冲区和接收缓冲区的作用:
- TCP是一个要保证可靠性的协议 如果发送端没有收到确认的应答 发送的数据就会保存一段时间之后继续发送 直到收到应答这部分数据才被覆盖
- 和UDP协议一样TCP协议也有接收缓冲区 这是因为应用层梳处理数据的速度是有限的 为了保证没来得及处理的数据不被直接丢弃所以说我们要设立接收缓冲区 数据在网络中传输是十分耗费时间和空间的 如果仅仅是因为没有即使处理就丢弃资源会造成对于网络资源极大的浪费
经典的生产者消费者模型:
- 对于发送缓冲区来说 上层不断的调用系统函数发送数据 扮演者一个生产者的角色 下层不断的调用系统函数接收数据 扮演着一个消费者的角色 发送缓冲区就是一个交易场所
- 对于接收缓冲区来说 下层不断的向接收缓冲区中写入数据 扮演一个生产者的角色 上层不断的调用系统函数接收数据 扮演者一个消费者的角色 接收缓冲区就是一个交易场所
- 因此 引入发送缓冲区和接收缓冲区就相当于引入了两个生产者消费者模型 该模型将上层应用和底层通信进行了解耦 同时它的引入同时也支持了并发和忙闲不均
窗口大小
当发送端要将数据发送给对端的时候本质上是将自己发送缓冲区的数据发送到对端的接收缓冲区中
但是我们的缓冲区是有大小的 如果我们的发送缓冲区发送数据的速度大于接收缓冲区接收的数据那么我们的数据就会溢出就会造成丢包废弃数据的情况
TCP协议给出的解决方案就是窗口大小
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函数读取数据 否则会一直阻塞住
这么设计的原因是 如果缓冲区中有一点数据就读取的话会造成内核态用户态之间频繁的切换 这样子就会造成计算机效率的浪费
但是如果说我们发送报文的时候携带了这个选项 实际上就是在告诉操作系统 我们希望这个数据尽快被应用层读取
RST
- 报文中的RST被设置为1 表示希望重新建立连接
- 在通信双方在连接未建立好的情况下 一方向另一方发数据 此时另一方发送的响应报文当中的RST标志位就会被置1 表示要求对方重新建立连接
- 如果连接的过程中出现了任何的异常也会要求重新建立连接
确认应答机制 (ACK)
确认应答机制是TCP协议保证可靠性的机制之一
确认应答机制是由TCP报头中的32位序号和32位确认序号来保证的
值得我们注意的是确认应答机制并不会保证所有消息的可靠性 最后总会有一条最新的消息是无法被ACK的
超时重传机制
在双方使用TCP进行通信的时候 如果发送方发出的数据在一段时间内得不到应答 此时发送方就会重新发送 这就叫做确认应答机制
除了报头之外TCP协议还会通过一些代码来保证其可靠性
超时重传机制就是这些代码中的一个 在发送出一个数据之后TCP就会设置一个“闹钟” 如果在闹钟响之前收到应答这个闹钟就会关闭 如果在闹钟响了之后还没有收到应答就会触发超时重传机制 重发数据
丢包的两种情况
我们通过TCP协议可以确定一个报文是否到了对端 但是我们无法确定一个报文没有到达对端
丢包的情况之一就是这个报文根本就没有到达对端 此时发送端在一定时间内没有收到响应报文就会触发超时重传
丢包的另一种情况就是 报文其实发送到对端了 但是对方的响应报文在传输的时候丢了 此时发送端没有收到对面的响应报文也会进行超时重传
超时重传的等待时间
我们超时重传的时间不能设置的太长或者太短
- 如果我们超时重传的时间设置的过长会导致数据丢失之后对面长时间得不到对应的数据 进而影响整体重传的效率
- 如果我们超时重传的时间设置的过短会导致数据没有丢失还是会触发超时重传机制 此时对面就会收到大量的重复报文 甚至太短的话整个网络都可能会崩溃
Linux中会以500ms为一个单位进行控制 每次判定超时重发的时间都是500ms的整数倍
如果下次的数据依然没有得到应答那么此时的响应时间就是500ms * 2 同理如果下下次发送的数据还是没有得到应答此时的响应时间就是500ms * 4 以此类推 以指数的形式传递 但是如果时间累计到了一定的程度 那么TCP就会认为对面的主机出现了问题从而强制关闭连接
连接管理机制
TCP是面向连接的
TCP的可靠性机制并不是主机之间 而是连接之间的
TCP协议是基于连接的 保证可靠性传输的前提是建立一个连接
操作系统管理连接
连接是TCP协议的基础 有了连接才能保证可靠性 但是一台机器上可能会有大量的连接 所以说操作系统必须要对这些连接进行管理
在Linux中一定会有一个这样子描述连接的结构体 该结构体中有需要管理连接用的各项属性 每次创建一个连接在系统看来就是定义了一个结构体
系统中创建了结构体之后会将它们用双链表的形式连接起来 此时操作系统对于连接的管理在实际上就变为了对于双链表的增删改查
- 建立连接
创建出一个连接结构体 并且填充它的各个字段 之后连接到双链表中
- 删除连接
删除该连接对应的结构体