Linux|传输层TCP与UDP


UDP与TCP

    • 再谈端口号
    • pidof命令
    • UDP
      • ①UDP协议端格式
      • ②UDP的特点
      • ③UDP的缓冲区
      • ④UDP使用注意事项
    • TCP
      • TCP协议段格式
      • 可靠性
        • ①校验和
        • ②确认应答(ACK)机制
        • ③序列号
        • ④超时重传机制
        • ⑤流量控制
        • ⑥连接管理
          • TIME_WAIT状态
        • ⑦拥塞控制
      • 提高性能
        • ①滑动窗口与快速重传
        • ②延迟应答
        • ③捎带应答
      • 面向字节流
      • 粘包问题
      • TCP异常情况
    • listen 的第二个参数

传输层:负责数据能够从发送端传输接收端,进程到进程的通信
再谈端口号 端口号标识了应用层中的具体某个进程,有了端口号才能识别传输层向上交付的特定进程
在TCP/IP协议中, 用 “源IP”, “源端口号”, “目的IP”, “目的端口号”, “协议号” 这样一个五元组来标识一个通信
Linux|传输层TCP与UDP
文章图片

端口号范围划分:
0 - 1023: 知名端口号, HTTP, FTP, SSH等这些广为使用的应用层协议, 他们的端口号都是固定的
1024 - 65535: 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的
我们自己写一个程序使用端口号时, 要避开这些知名端口号
pidof命令 语法:pidof [进程名]
功能:根据进程名查找进程pid
UDP ①UDP协议端格式
Linux|传输层TCP与UDP
文章图片

16位UDP长度:表示整个数据报(UDP首部+UDP数据)的长度
16位校验和:校验收到的报文数据与发送的是否一致,如果校验和出错, 就会直接丢弃

UDP如何保证报头和有效载荷分离
UDP是定长报头,读取前8字节就是报头,8字节后面的就是有效载荷
UDP如何决定自己的有效载荷交付给上层的哪个协议
通过16位的目的端口号,目标进程是和端口号绑定的,通过它就会找到上层的协议
传输层是如何通过端口找对对应的服务进程的?
通过直接映射的哈希表,开辟65536大小的数组,数组存放绑定该端口的pid
对于报头的理解:
Linux|传输层TCP与UDP
文章图片

②UDP的特点
1.只要收到应用层发来的数据,不会管对端是否可以通信,没有发送缓冲区,立即按照原样发到网络
2.UDP传输的过程类似于寄信,UDP只管发出去,至于对方是否收到不会关心
3.无连接: 知道对端的IP和端口号就直接进行传输, 不需要建立连接
4.不可靠: 没有确认机制, 没有重传机制; 如果因为网络故障该段无法发到对方, UDP协议层也不会给应用层返回任何错误信息
5.面向数据报: 不能够灵活的控制读写数据的次数和数量,应用层交给UDP多长的报文, UDP原样发送, 既不会拆分, 也不会合并
对于面向数据报的理解:
如果用UDP传输100个字节的数据:发送端调用一次sendto, 发送100个字节, 那么接收端也必须调用对应的一次recvfrom,一次读完, 接收100个字节; 而不能循环调用10次recvfrom, 每次接收10个字节
③UDP的缓冲区
UDP没有真正意义上的发送缓冲区. 调用sendto会直接交给内核, 直接由内核将数据传给网络层协议进行后续的传输动作
UDP具有接收缓冲区,可以临时保存到达的报文, 但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致,如果缓冲区满了, 再到达的UDP数据就会被丢弃
④UDP使用注意事项
UDP协议首部中有一个16位的最大长度. 也就是说一个UDP能传输的数据最大长度是64K(包含UDP首部)
如果我们需要传输的数据超过64K, 就需要在应用层手动的分包, 多次发送, 并在接收端手动拼装
TCP
TCP是保证可靠的协议,为了保证可靠TCP做了更多的处理,效率会降低
而UDP不保证可靠,所以更加的简单,速度也更快
TCP协议段格式
Linux|传输层TCP与UDP
文章图片

序号:发送的数据的起始位置,由计算机随机生成
确认序号:表示下次应该发送报文的起始序号,告知对方确认序号之前的所有字节已经全部收到
TCP首部长度: 表示TCP首部的长度,单位为四字节所以TCP头部最大长度是15 * 4 = 60,包括选项,而最小是20字节的标准头部
保留:还未使用的字段
六位标志位:是用来区分不同的报文种类,以此来执行对应的处理逻辑
SYN:表示建立连接
ACK: 确认号是否有效
FIN:断开连接
PSH:表示告诉上层应用尽快读取缓冲区的数据,缓冲区由水位线的概念,只有到达水位线才会通知上层读取,而PSH是让其没到的时候,也让它读取
RST: 双方连接出问题,对方要求重新建立连接
URG: 紧急指针是否有效,若有效则会查看紧急指针优先读取该部分数据
16位紧急指针: 紧急数据在报文中的偏移量,标识哪部分数据是紧急数据,读多少个数据由选项字段的说明决定
对于RST:
Linux|传输层TCP与UDP
文章图片

其实凡是中途一方出现异常断开连接,在收到对方的报文时,都会发送RST给对方,表示对方认为连接还在,而自己已经断开连接,请求重新建立连接
报头和有效载荷怎么分离:通过4位首部长度字段
TCP不仅保证了可靠性,而且还实现了提高传输效率的措施
可靠性
TCP的可靠性包括两方面:一方面是通过报头体现的,另一方面是通过tcp的代码逻辑体现出来的
①校验和 校验整个报文,如果出错直接丢弃
②确认应答(ACK)机制
一般而言,如果我们发送的消息收到了回应,那么可以认为该消息可靠地被对方收到,这就是确认应答机制
如果一段时间内没有收到确认应答,那么发送端可以认为数据已经丢失,并进行重发,这样即使产生的丢包,也能保证数据到达对方实现可靠传输
Linux|传输层TCP与UDP
文章图片

③序列号
TCP将发送数据的每个字节都进行了编号. 即为序列号
序号标识是否已经收到该数据
Linux|传输层TCP与UDP
文章图片

每一个ACK都带有对应的确认序列号, 意思是告诉发送端, 在这之前的序号已经收到,下一次请从这里开始发
序号实现按序到达
TCP是面向字节流的,发送的报文到达接收方缓冲区的顺序是不可预知的,那么如何保证接收缓冲区的报文有序呢?
当收到若干报文时,通过32位序号,将报文在传输层按序号进行顺序重排,放到接收缓冲区中,从而保证了发送顺序与接收顺序一致

序号去重:
如果收到相同的序号报文,接收端会直接丢弃报文,以此达到去重的作用
为什么需要两套序号?
TCP是全双工的,双方能同时发送数据,双方都需要确认应答机制,所以需要两套序号,一个是自己发送数据的序号,另一个是自己对收到数据的序号
④超时重传机制 上面提到,判读自己发送的数据是否收到的根据是是否收到响应,如果超过一定时间没有收到,就会重传
而导致没有收到的原因无非两个,发送报文丢失或者响应报文丢失
情况一:发送报文丢失
Linux|传输层TCP与UDP
文章图片

情况二:响应报文丢失Linux|传输层TCP与UDP
文章图片

收到重复的报文,此时则会根据序号判定重复,直接丢弃发送的报文
那么, 如果超时的时间如何确定?
超时以500ms为一个单位进行控制, 每次判定超时重发的超时时间都是500ms的整数倍

如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传,如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增
累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 停止重传,强制关闭连接,并且通知应用通信异常强行终止
⑤流量控制 数据的发送过程:
Linux|传输层TCP与UDP
文章图片

流量控制解决的是发送方数据过快导致接收方来不及接收而造成丢包的问题,也就是协调发送数据与接收能力
Linux|传输层TCP与UDP
文章图片

发送端接受到这个窗口之后, 就会通过滑动窗口减慢自己的发送速度
Linux|传输层TCP与UDP
文章图片

那么第一次发送数据时,是如何知道对端的窗口大小呢?
在三次握手期间双方就协商了窗口大小
所以报文中的窗口大小指的是自己的接收缓冲区中剩余空间的大小
通过收到的对方的窗口大小,来调整自己的发送数据的速度,以此达到流量控制的目的
⑥连接管理 在正常情况下, TCP要经过三次握手建立连接, 四次挥手断开连接
三次握手:
Linux|传输层TCP与UDP
文章图片

四次挥手:
Linux|传输层TCP与UDP
文章图片

TIME_WAIT状态
TCP协议规定,主动关闭连接的一方要处于TIME_ WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能到CLOSED状态
所以在TIME_WAIT状态时,客户端还没有释放干净连接,端口号还在被占用,导致无法再绑定该端口号
那么怎么解决这个绑定失败问题呢?
在创建监听套接字后,使用setsockopt()设置socket描述符, 选项SO_REUSEADDR为1, 表示允许创建端口号相同但IP地址不同的多个socket描述符
Linux|传输层TCP与UDP
文章图片

⑦拥塞控制
至于之前的流量控制那些都是只考虑了双端主机的情况,并没有考虑网络状况
网络拥塞虽然不能很好地解决它,但是可以不加重拥塞状况,等待状况的恢复,相当于堵车时,减少车辆进入拥塞区域,减轻拥塞
网络拥塞影响的是整个该网络中的主机,所有的主机都有责任,所有不只是单个主机实行拥塞控制
少量的丢包, 我们仅仅认为是触发超时重传; 大量的丢包, 我们就认为网络拥塞
为了在发送端限制发送的数据量,引入拥塞窗口的概念:
拥塞窗口就是一个数值,代表一次发送的数据大于拥塞窗口,可能引发网络拥塞问题
每个主机都有拥塞窗口大小,并且拥塞窗口大小是动态变化的,不同的主机拥塞窗口可能不一样
滑动窗口VSTCP报文窗口VS拥塞窗口
滑动窗口 代表发送能力的大小,等于min(TCP窗口大小,自己拥塞窗口大小)
TCP窗口大小 代表TCP发送方的接收能力的大小(接收缓冲区的剩余空间)
拥塞窗口 代表可能发生网络拥塞的数值
下面讨论单个主机的如何解决:
TCP引入慢启动机制:
先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再慢慢恢复通信
具体:发送开始的时候, 定义拥塞窗口大小为1,此时拥塞窗口增长速度, 是指数级别的
为什么要恢复到1后再进行指数增长呢?
1.要确认网络是否在只有少量数据的情况下依旧会发生丢包
2.在指数前期增长较慢,可以更精确地试探,此后以指数形式快速恢复通信状态
但是在恢复时,指数型增长太快,如果大于了对方接收窗口大小就失去了拥塞窗口的意义,所以设置慢启动的阈值,超过该阈值就改用线性增长
Linux|传输层TCP与UDP
文章图片

在每次出现网络拥塞时, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1
提高性能
①滑动窗口与快速重传 如果使用刚才谈论到的确认应答机制,一发一收地串行发送数据段,这样效率非常低
Linux|传输层TCP与UDP
文章图片

那么为什么发送方不一次把数据发完呢?
发送方发送数据时不仅要考虑对方接收能力还要考虑网络的拥塞情况
所以引入了滑动窗口并行发送数据,提高效率
滑动窗口描述的是,发送方不用等待ACK一次所能发送数据的最大量
Linux|传输层TCP与UDP
文章图片

收到确认应答时,窗口会滑动到确认应答中的序列号位置,并且窗口大小是动态变化的
在滑动窗口发送数据时发生丢包会出现两种情况:
1.数据包已经抵达, ACK丢了
Linux|传输层TCP与UDP
文章图片

2.发送的数据包丢了
Linux|传输层TCP与UDP
文章图片

为什么有了快重传后还需要超时重传呢?
快重传必须要收到3次同样的确认应答,如果没有就不会触发。
所以超时重传属于保底策略,保证丢失后一定能被重传
②延迟应答
如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小
实际上上层拿走数据的速度比网络中的应答速度要更快
如果接收端稍微等一会再应答,那么该缓冲区的数据将更多地被上层拿走,此时在返回窗口大小就更大
窗口更大,网络吞吐量就越大, 传输效率就越高,在保证网络不拥塞的情况下提高了传输效率
③捎带应答
在响应发出确认的同时,也捎带出要发出的数据
报文既能确认,又能发送数据
比如四次挥手将中间两次合并为一个报文
面向字节流
字节流:
TCP发送的数据像不间断的流水一样,所以必须防止粘包问题
特点:
在发送数据到缓冲区和读取缓冲区里面的数据时,不必一次性将数据写入或读取完
也就是可以任意写入或者读取只要把数据写入或者读取完就行

粘包问题
粘包:应用层多读或者少读报文,从而导致别的报文无法使用
那么如何避免粘包问题呢?
明确两个包之间的边界
主要有以下方法
1.定长报文,读取定长的大小
2.特殊字符,读到该特殊字符表示读完该报文
3.自描述大小+定长(UDP)或 自描述大小+特殊字符(HTTP)
对于UDP协议来说, 是否也存在 “粘包问题” 呢?
UDP采用自描述+定长的方式.表明报文边界
其次UDP面向数据报, 要么收到完整的UDP报文, 要么不收. 不会出现"半个"的情况

那么使用TCP的应用层协议为什么会存在粘包问题?
TCP面向字节流,写入读取的报文可能不完整;并且TCP报文没有标识报文长度的字段,并没有报文和报文边界,需要应用层自行解决该问题

TCP异常情况
1.进程终止: 进程终止会释放文件描述符, 仍然可以发送FIN. 和正常关闭没有什么区别.
2.机器重启: 会杀掉所有进程,和进程终止的情况相同
3.机器掉电/网线断开: 对端认为连接还在一旦对端有写入操作, 对端发现连接已经不在了, 就会进行reset
即使没有写入操作, TCP自己也内置了一个保活定时器(一般由应用层实现),如果长时间对方没有发送请求,会定期询问对方是否还在. 如果对方不在, 也会把连接释放,或者通过定期向对端报平安的方式,确保对方还存在
listen 的第二个参数 Linux内核协议栈为一个tcp连接管理使用两个队列:
  1. 半链接队列(用来保存处于SYN_SENT和SYN_RECV状态的请求)
  2. 全连接队列(用来保存处于established状态,已经三次握手,可直接被accept取走的连接)
而全连接队列的长度会受到 listen 第二个参数的影响,这个队列的长度是 listen 的第二个参数 + 1
全连接队列满了的时候, 就无法继续让当前连接的状态进入 established 状态了.
为什么要有队列?
当内部有连接断开的时候,能够立刻从连接队列选取一个连接进行处理,从而保证服务器几乎100%负载工作
为什么不能太长呢?
如果太长会增加服务器的维护成本,并且太长会增加等待时间,会让客户端放弃等待
【Linux|传输层TCP与UDP】SYN洪水攻击:
Linux|传输层TCP与UDP
文章图片

    推荐阅读