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报头当中的6位标志位:
TCP报头在内核当中本质就是一个位段类型 给数据封装TCP报头时 实际上就是用该位段类型定义一个变量 然后填充TCP报头当中的各个属性字段 最后将这个TCP报头拷贝到数据的首部 至此便完成了TCP报头的封装
TCP如何将报头与有效载荷进行分离?
当TCP从底层获取到一个报文后 虽然TCP不知道报头的具体长度 但报文的前20个字节是TCP的基本报头 并且这20字节当中涵盖了4位的首部长度
如果TCP报头当中不携带选项字段 那么TCP报头的长度就是20字节 此时报头当中的4位首部长度的值就为 5 (20 / 4 = 5)
TCP如何决定将有效载荷交付给上层的哪一个协议?
应用层的每一个网络进程都必须绑定一个端口号
而我们的TCP报头中含有这么一个字段 十六位目的端口号
因为我们可以从该报头中提取十六位目的端口号 找到对应的应用级进程 进而将有效载荷交给对应的应用层进程进行处理
在内核中 端口号与进程ID之间的映射关系是用哈希的结构 传输层可以通过端口号快速找到其对应的进程ID
怎么才能确定对方收到我的消息
在进行网络通信时 一方发出数据之后 它不能够保证该数据被对端收到 因为在路上可能会遇到各种各样的问题 所以说我们必须想一种办法来确认自己的消息被对端收到了 而在我们现在的网络通信中我们采用的办法就是序号和确认序号 通俗点来说 当自己向对端主机发送信息的时候 对端会给你应答 这个应答就能说明你的消息被对方接收到了
在下面中 实线表示的是消息确认能够被对方收到 虚线表示的是消息不确认能够被对方收到
但是TCP协议需要保证的是通信双方的可靠性 虽然此时主机A能够保证自己上一次发送的数据被主机B可靠的收到了 但主机B也需要保证自己发送给主机A的响应数据被主机A可靠的收到了
因此主机A在收到了主机B的响应消息后 还需要对该响应数据进行响应 但此时又需要保证主机A发送的响应数据的可靠性
因为我们只有在收到对方的响应消息之后我们才能保证自己上一次发送的数据被对端可靠的收到 但是就像上面的图例一样 总会有一条最新的消息不能确认被收到
所以严格意义上来说 互联网中通信的时候不存在百分百的可靠性 因为双方通信的时候总有一条最新的消息得不到应答
但实际没有必要保证所有消息的可靠性
我们只要保证双方通信时发送的每一个核心数据都有对应的响应就可以了 而对于一些无关紧要的数据(比如响应数据) 我们没有必要保证它的可靠性
这种策略在我们的TCP中叫做确认应答机制
需要注意的是 确认应答机制并不是保证双方通信的可靠性 而是只要一方收到了另一方的应答消息 就说明它上一次发送的数据被另一方可靠的收到了
32位序号
如果双方在进行数据通信时 只有收到了上一次发送数据的响应才能发下一个数据 那么此时双方的通信过程就是串行的 效率肯定会很差
因此双方在进行网络通信时 允许一方向另一方连续发送多个报文数据 只要保证发送的每个报文都有对应的响应消息就行了 此时也就能保证这些报文被对方收到了
但是在连续发送多个报文的时候又会产生新的问题 报文到达的先后顺序
由于在发送报文的时候路径选择的不同 所以报文到达的时间不一定相同 也就是说先发的报文不一定会先到
TCP将发送出去的每个字节数据都进行了编号 这个编号叫做序列号
此时接收端收到了这三个TCP报文后 就可以根据TCP报头当中的32位序列号对这三个报文进行顺序重排(该动作在传输层进行) 重排后将其放到TCP的接收缓冲区当中 此时接收端这里报文的顺序就和发送端发送报文的顺序是一样的了
32位确认序号
TCP中的三十二位确认序号是告诉对方 我当前已经收到了哪些数据 你的数据下一次应该从哪里开始发
为什么要用两套序号机制?
主机A和主机B之间通信
A主机只发送数据 B主机只接收数据
那么此时A主机发送序号请求报文的时候 B主机只需要在相同字段填写相同的响应报文就可以了
但是我们的TCP是一种全双工通信协议 所以说在AB主机通信的过程中 B主机也有可能向A主机发送数据
那么此时我们就需要使用两套序号机制来区分序号和响应序号了
TCP的接收缓冲区和发送缓冲区
TCP协议本身是具有接收缓冲区和发送缓冲区的
在应用层调用write函数的时候 实际上就是向TCP的发送缓冲区中写入数据 TCP协议会等待合适的时机发送
与此同时当TCP接收从网络中发送过来数据的时候并不是直接发送到应用层 而是拷贝到接收缓冲区中 等待应用层使用read函数来读取
当我们平时调用write函数的时候 它会将数据写入到TCP的数据缓冲区 当写入成功(或失败)之后它就会返回了 之后数据什么时候发送 怎么可靠的发送就是传输层的事情了 应用层并不关心
TCP的发送缓冲区和接收缓冲区存在的意义
发送缓冲区和接收缓冲区的作用:
经典的生产者消费者模型:
窗口大小
当发送端要将数据发送给对端的时候本质上是将自己发送缓冲区的数据发送到对端的接收缓冲区中
但是我们的缓冲区是有大小的 如果我们的发送缓冲区发送数据的速度大于接收缓冲区接收的数据那么我们的数据就会溢出就会造成丢包废弃数据的情况
TCP协议给出的解决方案就是窗口大小
TCP使用这十六位的窗口大小来表示自身接受缓冲区的大小 此时我们的发送端就可以通过该十六位窗口大小来判断自己应该发送多少数据 控制自己发送数据的速率
SYN
ACK
FIN
URG
什么是十六位紧急指针?
紧急数据的大小有多少?
PSH
接收和发送缓冲区都有一个水位线的概念
我们假设水位线是一百字节 那么 只有当数据达到一百字节的时候才会让read函数读取数据 否则会一直阻塞住
这么设计的原因是 如果缓冲区中有一点数据就读取的话会造成内核态用户态之间频繁的切换 这样子就会造成计算机效率的浪费
但是如果说我们发送报文的时候携带了这个选项 实际上就是在告诉操作系统 我们希望这个数据尽快被应用层读取
RST
确认应答机制是TCP协议保证可靠性的机制之一
确认应答机制是由TCP报头中的32位序号和32位确认序号来保证的
值得我们注意的是确认应答机制并不会保证所有消息的可靠性 最后总会有一条最新的消息是无法被ACK的
在双方使用TCP进行通信的时候 如果发送方发出的数据在一段时间内得不到应答 此时发送方就会重新发送 这就叫做确认应答机制
除了报头之外TCP协议还会通过一些代码来保证其可靠性
超时重传机制就是这些代码中的一个 在发送出一个数据之后TCP就会设置一个“闹钟” 如果在闹钟响之前收到应答这个闹钟就会关闭 如果在闹钟响了之后还没有收到应答就会触发超时重传机制 重发数据
丢包的两种情况
我们通过TCP协议可以确定一个报文是否到了对端 但是我们无法确定一个报文没有到达对端
丢包的情况之一就是这个报文根本就没有到达对端 此时发送端在一定时间内没有收到响应报文就会触发超时重传
丢包的另一种情况就是 报文其实发送到对端了 但是对方的响应报文在传输的时候丢了 此时发送端没有收到对面的响应报文也会进行超时重传
超时重传的等待时间
我们超时重传的时间不能设置的太长或者太短
Linux中会以500ms为一个单位进行控制 每次判定超时重发的时间都是500ms的整数倍
如果下次的数据依然没有得到应答那么此时的响应时间就是500ms * 2 同理如果下下次发送的数据还是没有得到应答此时的响应时间就是500ms * 4 以此类推 以指数的形式传递 但是如果时间累计到了一定的程度 那么TCP就会认为对面的主机出现了问题从而强制关闭连接
TCP是面向连接的
TCP的可靠性机制并不是主机之间 而是连接之间的
TCP协议是基于连接的 保证可靠性传输的前提是建立一个连接
操作系统管理连接
连接是TCP协议的基础 有了连接才能保证可靠性 但是一台机器上可能会有大量的连接 所以说操作系统必须要对这些连接进行管理
在Linux中一定会有一个这样子描述连接的结构体 该结构体中有需要管理连接用的各项属性 每次创建一个连接在系统看来就是定义了一个结构体
系统中创建了结构体之后会将它们用双链表的形式连接起来 此时操作系统对于连接的管理在实际上就变为了对于双链表的增删改查
创建出一个连接结构体 并且填充它的各个字段 之后连接到双链表中
删除该连接对应的结构体