文章目录
- TCP和UDP的宏观认识
- UDP协议
- TCP协议 (绝对的核心重点)
- tcp协议报头各个字段的详解
- tcp协议的机制
- 超时重传机制
- 连接管理机制
- 3次握手
- 4次挥手
- 滑动窗口
- 流量控制
- 拥塞控制
- 延迟应答
- 捎带应答
- 粘包问题
TCP和UDP的宏观认识
首先, TCP/UDP是位于传输层的。
另外的话, TPC和UDP协议都是存在缓冲区的,有各自的缓冲策略。
UDP的缓冲策略 : 一次性收到多少, 就一次性发完 (不允许拼接和拆分)
tcp的缓冲策略 : 随着接受方的接受能力而发送数据的快慢。
缓冲区的意义是什么呢? ------ > 减少IO次数
下图最大的优点是什么呢?
1,提高应用层的效率, 应用层完全不用关心你的底层是如何传输的, 只要把数据拷贝到传输层相应的缓冲区就行了。
2, 将应用层和传输层进行解耦。
UDP协议
UDP报文的特性:
全双工: 支持双向通信
无连接: 知道对端的IP和端口号就直接进行传输, 不需要建立连接;
不可靠: 没有确认机制, 没有重传机制; 如果因为网络故障该段无法发到对方, UDP协议层也不会给应用层
返回任何错误信息;
面向数据报: 不能够灵活的控制读写数据的次数和数量 (就是应用层交给传输层多少数据, udp直接传输完,不能拆分,不能和并)
udp协议报文:
源端口 : 从哪里来
目的端口 : 解决向上交付的问题
udp长度 : 报头 + 有效载荷(数据)的长度, 可以根据其减去固定的8字节获得数据 (做到封装和解包)
检验和:由发送端计算,然后由接收端验证。检验该报文是否有改动,有则直接丢弃。
基于UDP协议的应用层协议:
NFS: 网络文件系统
TFTP: 简单文件传输协议
DHCP: 动态主机配置协议
BOOTP: 启动协议(用于无盘设备启动)
DNS: 域名解析协议
TCP协议 (绝对的核心重点)
TCP协议的特性:
1, 面向链接
2, 可靠性
3, 面向字节流。
4, 全双工
tcp报文:
tcp协议报头各个字段的详解
16位源端口 : 从哪个进程来 16位的目的端口 : 去哪个进程
32位序号:
当发送方发送多个报文的时候, 一定是有顺序的。
tcp协议是有可靠性的,不仅要保证被对方收到, 并且也要保证接受后的顺序准确。
32位确认序号 :
对报文进行精准确认,
一个报文里面,为什么既要有序号,又要有确认序号呢? 为什么又是2个独立的字段呢?
TCP协议是一个全双工协议, 1个报文可以携带要发送的数据(带上自己的序号), 又可以确认之前到的报文(通过确认序号)。
4位的首部长度:取值范围是 [0000, 1111],但是它的单位是4字节, 1个位表示是4个字节。一共是15 * 4 = 60个字节。
其中选项占40个字节(但是选项不用关心)。 通过首部长度可以做到解包和分用
6个标志位:
URG: 紧急指针是否有效
ACK: 确认报文
PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走
RST: 对方要求重新建立连接;
SYN: 请求建立连接;
FIN: 断开链接
16位检验和:
由发送端计算,然后由接收端验证。检验该报文是否有改动,有则直接丢弃。
16位窗口大小:
控制滑动窗口, 本质是接受方告诉发送方我的接受能力的大小
16位紧急指针
标识哪部分数据是紧急数据
tcp协议的机制
tcp协议的机制
1, 确认应答机制
确认报文 : ACK : 1
接收端都要对数据进行确认,这就是确认应答机制, 并且告诉发送方下次从哪个序号发
超时重传机制
主机A给主机B发送数据,可能会丢失, 导致收不到应答,从而重发。
也有可能主机B收到数据, 发送的应答丢包, 主机A认为B仍旧没有受到数据, 也会再次重发。
但是呢? 又会产生一个问题, 主机B可能会受到很多重复的数据。 但是,不用担心, TCP协议中的序号可以帮助去重。
如何控制时间间隔呢?这也是一个问题。
最理想的情况下, 找到一个最小的时间, 保证 “确认应答一定能在这个时间内返回”.
但是这个时间的长短, 随着网络环境的不同, 是有差异的.
如果超时时间设的太长, 会影响整体的重传效率;
如果超时时间设的太短, 有可能会频繁发送重复的包;
TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间.
Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时
时间都是500ms的整数倍.
如果重发一次之后, 仍然得不到应答, 等待 2 倍的 500ms 后再进行重传.
如果仍然得不到应答, 等待 4 倍的 500ms 进行重传. 依次类推, 以指数形式递增.
累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接
连接管理机制
3次握手
为什么是3次握手呢?
1,验证全双工,确认双方是否都有发和收的能力。
2,确认双方主机的健康,网络是否正常。
3,如果是1,2次握手就建立链接, 意味着client只要发出链接请求, server就要建立链接,client不用管确认应答。
对于,那些非法用户,只有一次发起请求,就可以让服务端浪费大量的链接资源。也就是说,不能预防syn洪水攻击。
4, 如果是4,5,6次握手建立链接,就显得有些多余了。因为链接的本质是为了通信, 而不是为了链接而建立链接。
4次挥手
断开链接的本质是:双方都达成断开链接的共识。而4次挥手是协商断开链接的最小次数。
其中 time_wait : 主动断开链接的一方,在发送ACK报文确认后进入的状态,是2MSL(2 * 报文最大生命周期)有什么好处呢?
1, 可以尽最大努力保证滞留在网络中的数据消散。
2, 可以保证最后一个确认报文准确到达。 如何保证呢? 如果服务器一端未收到确认,会进行重发FIN断开链接报文,
然后客户端补发ACK确认报文。 (虽然应用层中的进程关闭了, 但是链接还没有完全关闭, tcp仍旧可以正常发送)。
close_wait : 当收到客户端的断开链接请求后,服务端进入close_wait 。 没有调用close(sock),就会进入close_wait状态
滑动窗口
确认应答: 对每一个发送的数据段, 都要给一个ACK确认应答. 收到ACK后再发送下一个数据段。
这样做最大的缺点, 就是性能太差。
这种一发一收的效率太慢, 我们考虑将数据段一批一批的发,将等待应答的时间重叠。
滑动窗口大小指的是无需等待确认应答而可以继续发送数据的最大值. 上图的窗口大小就是400个字节(四个段).
收到第一个ACK后, 滑动窗口向后移动, 继续发送第五个段的数据; 依次类推;
操作系统内核为了维护这个滑动窗口, 需要开辟 发送缓冲区 来记录当前还有哪些数据没有应答; 只有确认应答过的数据, 才能从缓冲区删掉;,窗口越大, 则网络的吞吐率就越高;
滑动窗口可以应对多种特殊的情况
情况1 : 比如客户端发送了 1- 100 , 101- 200 , 201 - 300, 301–400,,,,的数据, 但是服务端的101, 201, 301的应答报文丢失了。
但是收到了401的确认报文,则客户端可以认为1–400的数据已经被对方收到了。
确认序号: 对历史数据的确认。
情况2: 比如客户端发送了 1- 100 , 101- 200 , 201 - 300, 301–400的数据, 但是呢 101-200的数据端丢失了, 当服务端每收到
201-300, 301—400 ,,,,, 就会发送确认报文101, 就表明了服务端想要的是101-200, 但是呢它丢失了。
当客户端收到3次重复的应答之后, 就会进行补发, 我们又称为快重传
流量控制
TCP支持根据接收端的处理能力, 来决定发送端的发送速度。就叫做流量控制, 其中tcp报头中的窗口大小就是对端的接受能力大小。
不要和滑动窗口给搞混乱了, 滑动窗口呢是发送缓冲区中的。
如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端.
拥塞控制
它衡量的是网络状态, 要知道,在网络中转发数据的时候, 可不是只有两台机器, 而是成千上万台。
TCP引入 慢启动 机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据;
引入一个拥塞窗口的概念 : 它仅仅是一个概念, 并不实际存在于tcp的哪里。
发送开始的时候, 定义拥塞窗口大小为1;
每次收到一个ACK应答(来回传输了1次), 拥塞窗口的大小层指数形式增长 2 ^0 , 2 ^ 1, 2 ^ 2, 2 ^ n.。
但是呢有一个阈值,当到达这个阈值的时候,呈现出线性增长,一个ACK应答, 拥塞窗口 + 1
每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口。
当发生超时重传的时候, 拥塞窗口置回1.
归根结底是TCP协议想尽可能快的把数据传输给对方, 然后又考虑网络和对方接受能力 。
看一道题:
延迟应答
什么是延迟应答呢?
当接受端接受了一定的数据后, 接受缓冲区肯定是会变小的, 然后给发送端确认报文肯定是反馈的是接受能力不足的。
那我们这样, 我们等一定的时间, 让应用层读走后, 然后我们再给发送应答, 也就是说我们反馈的接受能力肯定是好的。
窗口越大,网络吞吐量越高,传输效率越高。
延迟应答考虑2种情况:
数量限制: 每隔N个包就应答一次; (如果接受缓冲区的包很少,也就是说不用延迟应答,接受能力几乎不变)
时间限制: 超过最大延迟时间就应答一次.。 (规定的时间)
上面的N一般取2, 最大延迟时间取200ms
捎带应答
这个很简单, 你给我发送数据以后, 我再给你应答的时候, 顺便发送我给你的数据。
就好像:
A : 你吃饭了吗?
B : 我吃饭了呀! 我能和你去看电影吗?
创建一个TCP的socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;
调用write时, 数据会先写入发送缓冲区中;
如果发送的字节数太长, 会被拆分成多个TCP的数据包发出;
如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去;
接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;
然后应用程序可以调用read从接收缓冲区拿数据;
另一方面, TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可
以写数据. 这个概念叫做 全双工
粘包问题
首先要明确, 粘包问题中的 “包” , 是指的应用层的数据包.
在TCP的协议头中, 没有如同UDP一样的 “报文长度” 这样的字段, 但是有一个序号这样的字段.
站在传输层的角度, TCP是一个一个报文过来的. 按照序号排好序放在缓冲区中.
站在应用层的角度, 看到的只是一串连续的字节数据.
那么应用程序看到了这么一连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是一个完整的应用层
数据包.
那么如何避免粘包问题呢? 明确两个包之间的边界.
对于定长的包, 保证每次都按固定大小读取即可; 例如上面的Request结构, 是固定大小的, 那么就从缓冲
区从头开始按sizeof(Request)依次读取即可;
对于变长的包, 可以在包头的位置, 约定一个包总长度的字段, 从而就知道了包的结束位置;
对于变长的包, 还可以在包和包之间使用明确的分隔符(应用层协议, 是程序猿自己来定的, 只要保证分隔
符不和正文冲突即可);
对于UDP报文, 是不存在粘包问题的。
对于UDP, 如果还没有上层交付数据, UDP的报文长度仍然在. 同时, UDP是一个一个把数据交付给应用
层. 就有很明确的数据边界.
站在应用层的站在应用层的角度, 使用UDP的时候, 要么收到完整的UDP报文, 要么不收. 不会出现"半
个"的情况