UDP/TCP
UDP协议
UDP协议格式
- 16位UDP长度,表示整个数据报(UDP报头 + 有效载荷)的总长度
- 如果校验和出错,就会直接丢弃
因为UDP数据报最大长度为16字节,也就是64KB,所以如果上层要传输超过64KB的数据,就必须手动分包
UDP的特点
- 无连接:知道对端的IP和端口号就可以进行通信,不需要进行连接
- 不可靠:无确认应答机制,没有重传机制,如果因为网络原因无法发送数据,UDP也不给上层返回任何错误数据
- 面向数据报:不能够灵活的控制读写数据的次数和数量
面向数据报
应用层交给UDP多少数据,UDP就会发送多少,不会进行拆分或者合并
例如用UDP发送100字节的数据,那么sendto就必须发送100字节,recvfrom也必须一次性接收100字节,不能循环分多次读取
UDP的缓冲区
- UDP没有真正意义上的发送缓冲区,调用sendto会直接交给内核,由内核交给网络层后继续执行后续动作
- UDP具有接收缓冲区,但UDP不能保证发送和接收的顺序保持一致,如果UDP的缓冲区满了,后来的数据将会被直接丢弃
- UDP的socket既能读,也能写,因此它是全双工的
TCP协议
TCP全称为“传输控制协议(Transmission Control Protocol)”
TCP协议格式
- 源目端口号:从哪个进程来,到哪个进程去
- 32为序号/32为确认序号:后面详细说明
- 4位首部长度:标识TCP报头的长度,基本单位为4字节,所以可以表示的最大长度为15(1111) * 4 = 60字节
- 6位标志位:
URG: 紧急指针是否有效
ACK: 确认号是否有效
PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走
RST: 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段
SYN: 请求建立连接; 我们把携带SYN标识的称为同步报文段
FIN: 通知对方, 本端要关闭了, 我们称携带FIN标识的为结束报文段
- 16位窗口大小:后面详细说明
- 16位校验和:进行CRC校验,若不通过,则认为数据有问题
- 16位紧急指针:标识哪部分数据是紧急数据
TCP的特点
确认应答(ACK)
TCP将每个字节的数据都进行了编号,即为序列号
例如发送2000字节的数据
序号和确认序号可不可以为同一字段呢?
不可以!序号是用来标识需要发送的数据,而确认序号是用来标识响应的数据,必须分开使用,因为服务端也可能向客户端发送数据,这时候服务端也有自己的序号(向客户端发送数据)和确认序号(收到客户端的响应)
确认应答机制能够更细粒度确认丢包原因,也可以允许少量ACK丢失
- 例如,假设发送了7000字节数据(分成七等份),如果发送第一份数据(1-1000)后收到了确认应答(1001),而发送后面的数据时并没有收到确认应答,这时发送端就知道自己发的数据因为某种原因丢失了,便会重新传输数据(例如超时重传机制)
- 同样的,如果发送后6000字节的数据的途中,并没有收到每一份数据的确认应答,而在发送最后一份数据后收到了确认应答(7001),这时发送端能够确认自己之前发的数据并没有丢失,也就是说可以允许少量ACK丢失
如果TCP报文为确认应答报文,则TCP报头中ACK标志位会被设置成为1
超时重传
- 主机A向主机B发送数据时,因为某种原因导致数据丢包,在特定时间内主机A没有收到确认应答,主机A会重新发送数据
- 主机A向主机B发送数据时,因为某种原因导致返回的确认应答丢包,在特定时间内主机A没有收到确认应答,主机A会重新发送数据
主机B可能收到多份相同的数据,这时就需要通过TCP报头中的序号来进行去重
这个超时时间如何设置呢?
- 理想情况下,会找到一个最小时间,也就是说确认应答一定可以在这个最小时间内返回
- 但是这个时间长短会因为网络原因而有所差异
- 如果设置太长,会影响传输效率
- 如果设置太短,有可能会导致多次发送相同的数据,造成资源浪费
TCP为了保证无论在任何情况下都有较高的性能,会动态调整这个超时时间
Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时时间都是500ms的整数倍
如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传
如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增
累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接
连接管理
在正常情况下,TCP要经过“三次握手”建立连接,“四次挥手”断开连接
三次握手
TCP连接管理的本质:OS创建连接对象,并通过特定的数据结构进行管理!
三次握手的过程
- C端调用connect发起连接请求,阻塞式等待S端响应(SYN)
- S端监听成功后,调用accept将连接放入等待队列中等待连接成功,并向C端发送SYN确认报文(SYN + ACK)
- C端connect返回,建立连接(ESTABLISHED状态),发送确认应答(ACK)
- 此时服务端收到ACK,转化为ESTABLISHED状态
为什么是“三次”握手
- 一次当然不行,因为C端不能在没有收到确认应答的情况下就建立连接
- 如果是两次,S端在发出SYN确认报文之后不能收到该报文的ACK,S端就不能知道C端是否连接成功,于是S端只能在冒险的情况下建立连接,这种情况下如果C端并没有成功建立连接,就会增加S端的成本
如果是两次握手,C端向服务端发送大量SYN,S端向C端发送完SYN确认报文后就建立连接,S端会花费大量成本维护连接,引发“SYN洪水”的问题
- 四次、六次等偶数次同理,偶数次的情况下风险成本都将S端承担,这当然是不可行的
- 五次、七次等奇数次只是在三次的基础上重复了过多的ACK动作
因此握手三次是最合理且较为高效的情况,虽然三次握手也会存在问题,但是将成本嫁接到C端,S端成本更低是更合理的选择,没有明显的设计漏洞
四次挥手
四次挥手的过程
- C端调用close关闭连接(FIN)
- S端调用read,获取到C端已经关闭连接,并转化为CLOSE_WAIT状态,发送FIN确认报文(ACK)
- S端调用close关闭连接(FIN)
- C端收到确认应答和FIN报文,向S端发送FIN确认报文(ACK),转化为TIME_WAIT状态,等待2个MSL时间后转化为CLOSED状态
- S端收到确认应答后转化为CLOSED状态
关于CLOSE_WAIT状态
S端处于CLOSE_WAIT状态的原因:没有调用close关闭文件描述符
导致四次挥手没有成功完成,所以避免S端出现CLOSE_WAIT只需要最后close就可以
关于TIME_WAIT状态
在四次挥手的过程中,主动关闭连接的一方会出现TIME_WAIT状态
,必须要等待2个MSL时间后才能回到CLOSED状态
MSL时间是指TCP报文的最大生存时间
等待2个MSL时间是保证在传输的两个方向上尚未被接受或迟到的报文都已经消失(否则如果服务器立即重启,可能会收到上一个进程所发的数据,但这种数据很可能是错误的)
同时也是保证最后一个报文(ACK)可靠到达(假如最后一个ACK丢失了,那么服务器会再重发一个FIN. 这时虽然客户端的进程不在了, 但是TCP连接还在, 仍然可以重发LAST_ACK)
解决因为TIME_WAIT而导致bind失败的方法
使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同,但IP地址不同的socket描述符
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
滑动窗口
在讨论TCP的确认应答机制时,对于每一个发送的数据段,都要收到ACK后才能发送下一个数据段,这种串行发送有一个很大的缺点:性能较差,但实际上TCP在发送时是一次发送多个数据段,将这多个数据段的等待确认的时间重叠在一起
TCP报头中的16位窗口大小就是用来控制不需要等待ACK能发送数据段的大小
- 滑动窗口实际上是TCP发送缓冲区的一部分,通过头尾指针控制滑动窗口的大小以及移动
- 滑动窗口的大小如何更新:双发在发送TCP报文时,设置TCP报头中16位窗口大小字段
滑动窗口为0 = 对方缓冲区已满,不能再接收数据
- 如果数据段丢失怎么办(头、中、尾)
1.如果是头部数据段丢失,那么发送端在接受确认应答时就可以辨别,例如上图中如果是1001-2000数据段丢失,那么收到的ACK只会是2001!
2.如果是中间数据段丢失,说明头部数据段已经收到了确认应答,滑动窗口头尾指针移动后,中间数据段就变成了头部数据段!
3.尾部数据段同理!因此不管是哪一部分数据段丢失,都可以转化为头部数据段丢失的情况
- 如果是ACK丢失怎么办
数据要支持重传,因此必须暂存在滑动窗口中
快速重传
- 当某一段报文丢失后,发送端会一直收到1001这样的ACK
- 这时发送端会重新发送1001-2000的数据段
- 最后发送成功后,发送端会收到7001的ACK,因为2001-7000这部分数据,接收端已经接收成功
流量控制和拥塞控制
流量控制
因为接收端的接收能力是有限的,如果接收端缓冲区已经打满,这个时候如果发送端继续发送,就会造成丢包的问题,因此TCP会根据接收端的接收能力,来控制发送端的发送能力
- 接收端可以根据自己的接收缓冲区的大小,设置TCP报头中窗口大小字段,并通过ACK的形式发送给对端
- 接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值通知发送端
- 发送端收到这个通知后,就会减缓自己的发送速度
- 如果接收端缓冲区满了,就会将窗口设置为0,此时发送端不再发送数据,但会定时探测接收端的窗口大小,让接收端告诉发送端接收窗口大小
拥塞控制
虽然滑动窗口能够高效可靠的发送数据,但是如果在开始阶段或者网络状况较差的情况下,贸然发送大量数据,可能会引起严重的数据丢包
为了解决这个问题,TCP引入慢启动机制,先发送少量数据“探路”,摸清网络拥塞的状况,再决定发送数据的速度
- 这时引入了一个拥塞窗口的概念
- 发送开始时,定义拥塞窗口大小为1
- 每收到一个ACK应答,拥塞窗口大小+1
- 每次发送数据包时,发送端根据拥塞窗口和对端发送的接收窗口大小作比较,取较小值作为实际发送窗口大小(min(拥塞窗口大小,接收窗口大小))
但是这种慢启动机制,只是“开始慢”,但是增长速度非常快
- 这时又引入一个慢启动阈值的概念
- 当拥塞窗口超过这个阈值时,将指数增长转变为线性增长
因为网络状况是变化的,所以这是一个循环往复的过程!
延迟应答和捎带应答
- 延迟应答:延迟发送应答,提高窗口大小,在网络不拥塞的情况下尽量提高传输效率
- 捎带应答:在ACK应答报文中,捎带传输数据,也是一种提高传输效率的手段
面向字节流
TCP与UDP不同,它同时拥有发送缓冲区和接受缓冲区
- 调用write时,数据会先写入发送缓冲区中
- 如果发送的字节数太长,会被拆分成多个TCP的数据包发出; 如果发送的字节数太短,就会先在缓冲区里等待,等到缓冲区长度差不多了,或者其他合适的时机发送出去
- 接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区
- 然后应用程序可以调用read从接收缓冲区拿数据
- 另一方面,TCP的一个连接,既有发送缓冲区,也有接收缓冲区,那么对于这一个连接,既可以读数据,也可以写数据,所以TCP是全双工的
- 所以TCP可以循环多次读写
TCP的特点
TCP与UDP不同,TCP是有连接,可靠,面向字节流的
可靠性
- 确认应答(最主要)
- 连接管理
- 流量控制
- 拥塞控制
- 序列号(按序到达)
- 超时重传
- 校验和
提高性能
- 滑动窗口
- 快速重传
- 延迟应答
- 捎带应答
关于listen()的第二个参数
Linux内核在管理TCP连接时使用了两个队列
- 全连接队列:用来保存处于ESTABLISHED状态,但应用层没有调用accept的请求
- 半连接队列:用来保存SYN_SENT和SYN_RECV状态的请求
当全连接队列满时,就无法让新的连接状态进入ESTABLISHED状态了,而这个连接的长度就是listen第二个参数值+1
这就好比买东西排队一样,有一批人正在被处理请求,有一批人必须等待被处理请求
对比TCP与UDP
TCP虽然是可靠连接的,但并不是所有情况下都不去使用UDP,不能一概而论
- TCP用于可靠传输,应用于文件传输,重要状态更新等场景
- UDP用于高速传输和实时性要求较高的领域
应该根据不同的应用场景,灵活使用UDP或TCP