UDP
udp协议端格式
- 16位UDP长度,表示整个数据报(UDP首部+数据)的长度
- 如果校验和出错,就会直接丢弃
1.如何做到封装和解包 udp协议的报头是定长8字节的,因此在报头8字节以后的部分就是数据 2.如何做到向上交付(分用问题) 报头中包含目的端口号,可以找到对应的服务,这也是我们的服务需要绑定端口号的原因
协议细节
特点
- 无连接:知道对端的IP和端口号就直接进行传输
- 不可靠:没有确认机制,没有重传机制。如果因为网络故障该段无法发到对方,UDP协议层也不会给应用层返回任何错误信息
- 面向数据报:不能够灵活的控制读写数据的次数和数量
面向数据报
应用层交给UDP的报文,UDP原样发送,既不会拆分,也不会合并 比方说UDP传输100字节的数据,发送端调用一次sendto,那么接收端也必须调用对应的一次recvfrom接收100字节,而不能循环调用10次recvfrom,每次接收10字节。
UDP缓冲区
udp没有真正意义上的发送缓冲区,调用sendto会直接交给内核,由内核将数据传给网络层协议进行后续的传输动作 UDP具有接收缓冲区,但是这个缓冲区不能保证收到的UDP报文的顺序和发送的一致;如果缓冲区满了,再到达的UDP数据就会被丢弃 缓冲区的存在一方面让传输层能够为数据的收发提供一系列的策略,另一方面让应用层和网络通信细节进行解耦 udp全双工:读写功能可以被同时调用
注意事项
UDP协议首部有一个16位的最大长度,也就是说一个UDP能传输的数据最大长度是64K(包含UDP首部) 然而64K在当今的互联网环境下,是一个很小的数字 如果我们需要传输的数据超过64K,就需要在应用层手动地分包,多次发送,并在接收端手动拼装
基于UDP的应用层协议
- NFS:网络文件系统
- TFTP:简单文件传输协议
- DHCP:动态主机配置协议
- BOOTP:启动协议(用于无盘设备启动)
- DNS:域名解析协议
TCP
特点
1.可靠性 2.面向连接 如何做到分装和解包 TCP的标准长度是20字节,也就是不包含选项的首部部分,在下图我们还可以看到在TCP的首部包含一个4位首部长度,那么它可以表示的数据就是[0, 15],但是这显然连基本的首部都无法容纳,所以我们还需要明白一点:4位首部长度的基本单位是4字节,也就是说它可以表示的数据范围应该是[0, 60]。
序号
TCP的一大特性是能够保证可靠性,而TCP具体保证可靠性的机制是:基于序号的确认应答机制。可以这么理解,当A给B写信时,A寄出了信,如果B在A收到信后回复:我已经收到信了。那么A就可以确定他发的信被B收到,但他不能确定B是不是一定没收到,因为也许B的回信在路上丢了,也有可能A的信确实没送到B的手上。 另一方面,对于保证可靠性来说,保证信息的顺序也是非常重要的,好比A同时给B发出5个命令:
- 1.写篇文章
- 2.跑两圈
- 3.上面的都别做了
- 4.完成作业
- 5.打扫卫生
这里如果把第三条命令的顺序进行任何别的调整都是不合适的,因此TCP需要保证确认应答的顺序性
在上图我们看到TCP首部分别有32位序号和32位确认信号,就可以分别对应A和B,比方客户端需要发送5个请求,分别填写序号1、2、3、4、5,那么服务端在收到了1号请求,就会在它返回的数据报的32位确认序号中填写2(n + 1),代表2以前的数据报我都收到了,下一条从序号2开始发送即可。
那么为什么需要两个序号呢?TCP和UDP一样,都是全双工通信协议,因此在报文中既可以携带要发送的数据,也可以携带对历史报文的确认
窗口大小与流量控制
在讨论窗口大小之前,我们需要再谈谈TCP的缓冲区,TCP比UDP多了发送缓冲区,也就是说,TCP既有发送缓冲区,也有接收缓冲区,因此在调用write和sendto时,并不是把数据发送到网络上,或是直接从网络读取数据,而是把数据拷贝到TCP的发送缓冲区,或从TCP的接收缓冲区拷贝数据。那么为什么应用层不能直接向网络层收发数据呢?好比在操作系统中我们输入输出字节流一样,直接与外设打交道其实效率是很低的,所以人们在内存中设置了缓冲区,让CPU与外设在这个地方存取数据;TCP设置缓冲区的理由与系统类似,另一方面,只有TCP知道当前自己和对方网络状态的明细,而正因为TCP拥有这些功能,因此可以说TCP协议是一种集传输、控制一体的协议。也正因为传输层的存在,可以做到应用层和TCP进行解耦。 既然存在发送和接收缓冲区,那必然会存在发送和接收缓冲区空间不足或过剩的情况,那么当接收端的接收缓冲区已经满的时候,从发送端发出的报文需要怎样处理呢?直接丢弃会产生影响吗?事实上因为TCP协议本身有重传机制,所以报文被丢弃仍然能够通过重传保证接收端最终能够接收到报文,但是,大量的重传与丢弃其实是一种巨大的浪费,那么TCP首部中的窗口大小就可以很好地解决问题,窗口大小中存放的内容代表接收缓冲区剩余空间的大小,也就是本端的接收能力,对端通过这个值就可以做到流量控制
标志位
server可能会在同时面对大量的报文,标志位可以区分不同种类的TCP报文
ACK(确认位)
在收到历史报文后,本端向对端发送报文时会在填写确认序号之外将ACK标志位设为1,几乎在所有的TCP通信中ACK都会被设置
SYN(建立连接)
TCP协议是面向连接的,因此在通信的时候要先connect,建立连接需要进行三次握手,也就是交换三次报文
- 客户端请求建立连接
- 服务端确认收到连接请求并向客户端发起建立连接的请求
- 客户端确认收到建立连接请求
三次握手进行到这就结束了,那么请问在第三次客户端向服务端发送确认标志完成时,是否就表明服务端向客户端的连接也同样建立了?那如果第三个报文在发送时丢失怎么办?实际上在第三次握手后就可以默认双方连接已经建立成功了,因为成功概率是非常大的,所以暂且先以建立成功看待。
什么是建立连接
三次握手成功后,会在双方操作系统内为维护连接创建对应的数据结构,双方维护连接是有时间和空间成本的
RST(重置异常连接)
只要是双方连接出现异常,都可以进行reset,来进行连接重置。比方说如果第三次握手服务端没有收到确认报文时,客户端认为双方连接已经建立,但是服务端不这么认为,因此在之后客户端向服务端发起服务请求时,服务端会认为连接还未完成,接着会将报文中的RST填为1,让客户端重置连接。
PSH(push)
告知对方,尽快将接收缓冲区中的数据进行向上交付
URG
因为TCP有按序到达的特点,所以每一个报文什么时候被上层读到基本上是确定的。如果想让一个数据尽快地被上层督导,可以设置URG,这表明该报文中携带了紧急数据,需要被优先处理
紧急指针
那么这些紧急数据在哪呢?紧急指针可以表示它们的位置,比方说在数据中有"abcdefg",我们想让’c’来作为紧急数据,那就将紧急指针指向’c’,TCP的紧急指针只能传输1个字节。 ############ flag 为了发送和接收紧急指针,
FIN(关闭连接)
断开连接需要四次挥手来达成关闭连接的一致认识,
- 客户端发送的报文中添加FIN标志位告诉服务端自己将关闭连接
- 服务端发送确认标志位
- 服务端发送的报文中添加FIN标志位告诉客户端自己将关闭连接
- 客户端向服务端发送确认标志位
序号与缓冲区
TCP有发送缓冲区,那么我们就可以把这个缓冲区看成是数组结构,我们将需要传送的数据都拷贝到TCP的发送缓冲区,假设当数据放入缓冲区后最后的下标是1000,那么TCP的报文首部就会把序号设为1000
超时重传
TCP是有超时重传机制的,当发送方迟迟没有收到ACK。并不代表接收端一定没有接收到报文数据,也可能是ACK还没送到或者ACK丢失,这些情况在发送端看来都是没有发送成功,会重新传送,如果是ACK丢失的情况,接收端可以利用序列号去重。那么多久没收到ACK算是超时呢?网络状态是不断变化的,因此超时重传的时间也一定是浮动的。
三次握手是双方的OS中TCP自动完成的,用户完全不参与
连接管理机制
三次握手
其实建立连接的三次握手主要的任务是验证全双工,那么对于客户端来说,想要验证自己的收发功能,必然首先要先向服务端发送一个报文(SYN),并收到确认(ACK)来保证自己的发送功能没有问题,而收到确认报文的同时也能保证自己的接收功能没有问题;对于服务端来说,他也要向客户端发送报文(ACK + SYN),并通过接收确认报文来确认自己的发送功能没有问题,这个确认报文同时也能保证自己的接收功能没有问题,因此三次握手是可以保证双方都有收发功能的最小次数 服务端状态转化
- [CLOSED->LISTEN]服务器调用listen后进入LISTEN状态,等待客户端连接
- [LISTEN->SYN_RCVD]一旦监听到连接请求(同步报文段),就将该连接放入内核等待队列中,并向客户端发送SYN和确认报文
- [SYN_RCVD->ESTABLISHED]服务端一旦收到客户端的确认报文,就进入ESTABLISHED状态,可以进行读写数据了
- [ESTABLISHED->CLOSE_WAIT]当客户端主动关闭连接(调用close),服务器会收到结束报文段,服务器返回确认报文段并进入CLOSE_WAIT
- [CLOSE_WAIT->LAST_ACK]进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据);当服务器真正调用close关闭连接时,会向客户端发送FIN,此时服务器进入LAST_ACK状态,等待最后一个ACK到来(这个ACK是客户端确认收到了FIN)
- [LAST_ACK->CLOSED]服务器确认收到了对FIN的ACK,彻底关闭连接
客户端状态转化
- [CLOSED->SYN_SENT]客户端调用connect,发送同步报文段
- [SYN_SENT->ESTABLISHED]connect调用成功,则进入ESTABLISHED状态,开始读写数据
- [ESTABLISHED->FIN_WAIT_1]客户端主动调用close时,向服务器发送结束报文段,同时进入FIN_WAIT_1
- [FIN_WAIT_1->FIN_WAIT_2]客户端收到服务器对结束报文段的确认,就进入FIN_WAIT_2,开始等待服务器的结束报文段
- [FIN_WAIT_2->TIME_WAIT]客户端收到服务端发来的结束报文段,进入TIME_WAIT,并发出LAST_ACK
- [TIME_WAIT->CLOSED]客户端要等待2MSL(Max Segment Life,报文最大生存时间)的时间,才会进入CLOSED状态
四次挥手
断开连接本质上是一个通知对方的机制,四次握手时协商断开连接的最小次数 服务端状态转化:
- CLOSED->LISTEN,服务端调用listen后进入LISTEN状态,等待客户端连接
- LISTEN->SYN_RCVD,一旦监听到连接请求(同步报文段),就将该连接放入内核等待队列中,并向客户端发送SYN确认报文
- SYN_RCVD->ESTABLISHED,服务端一旦收到客户端的确认报文,就进入RSTABLISHED状态,可以进行读写数据了
- ESTABLISHED->CLOSED_WAIT,当客户端主动关闭连接(调用close),服务器会受到结束报文端,服务器返回确认报文段并进入CLOSE_WAIT
- CLOSE_WAIT->LAST_ACK,进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据);当服务器真正调用close关闭连接时,会向客户端发送FIN,此时服务器进入LAST_ACK状态,等待最后一个ACK到来(这个ACK时客户端确认收到了FIN)
- LAST_ACK->CLOSED,服务器收到了对FIN的ACK,彻底关闭连接
客户端状态变化
- CLOSED->SYN_SENT,客户端调用connect,发送同步报文段
- SYN_SENT->ESTABLISHED,connect成功,则进入ESTABLISHED状态,开始读写数据
- ESTABLISHED->FIN_WAIT_1,客户端主动调用close时,向服务器发送结束报文段,同时进入FIN_WAIT_1
- FIN_WAIT_1->FIN_WAIT_2,客户端收到服务器对结束报文的确认,则进入FIN_WAIT_2,开始等待服务器的结束报文段
- FIN_WAIT_2->TIME_WAIT,客户端收到服务器发来的结束报文段,进入TIME_WAIT,并发出LAST_ACK
- TIME_WAIT->CLOSED,客户端要等待一个2MSL(Max Segment Life,报文最大生存时间)的时间,才会进入CLOSE状态
在TIME_WAIT状态下等待2MSL的原因是:
- 尽量保证历史发送的网络数据在网络中消散
- 尽量保证最后一个ACK被对方收到
解决TIME_WAIT状态引起的bind失败的方法
在server的TCP连接没有完全断开之前不允许重新监听,某些情况下是不合理的
- 服务器需要处理非常大量客户端的连接(每个连接的生存时间可能很短,但是每秒都有大量的客户端来的请求)
- 这个时候如果由服务器端主动关闭连接(比如某些客户端不活跃,就需要被服务器端主动清理掉),就会产生大量TIME_WAIT连接
- 由于我们的请求量很大,就可能导致TIME_WAIT的连接数很多,每个连接都会占用一个通信五元组(源ip,源端口,目的ip,目的端口,协议),其中服务器的ip、端口和协议是固定的,如果新来的客户端连接的ip和端口号和TIME_WAIT的连接重复了,就会出现问题
解决办法:使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
滑动窗口
其实我们现在一次发送一次确认的方式传输效率是很低的,特别是在数据往返时间较长的时候,因此我们使用滑动窗口一次性传输多个报文再依次得到确认报文的形式来提高效率: 使用滑动窗口我们会一次性发送多条数据,就可以大大提高性能(将多个段的等待时间重叠在一起),如下图:
- 窗口大小指的是无需等待确认应答而可以继续发送数据的最大值,上图的窗口大小就是4000个字节(四个段)
- 发送前四个段的时候不需要等待任何ACK,直接发送
- 收到第一个ACK后,滑动窗口继续向后移动,继续发送第五个段的数据,以此类推
- 操作系统内核为了维护这个滑动窗口,需要开辟发送缓冲区来记录当前还有哪些数据没有应答;只有确认应答过的数据,才能从缓冲区删掉
- 窗口越大,网络的吞吐率越高
- 在发送这几个数据报时不需要等待任何ACK,直接发送即可
- 窗口大小大小指的是无需等待确认应答而可以继续发送数据的最大值,上面的窗口大小是4000个字节
- 收到第一个ACK后,滑动窗口向后移动,继续发送第五个端的数据,以此类推
- 操作系统内核为了维护这个滑动窗口,需要开辟发送缓冲区来记录当前还有哪些数据没有应答,只有确认应答过的数据,才能从缓冲区删掉
- 窗口越大,则网络的吞吐率越高
丢包处理
-
数据包到达,ACK丢了 这种情况下,只需要依照最后一个送到的ACK进行确认即可,比方说2001,3001丢了,但4001收到了,那么发送端会默认前4000个都收到了 -
数据包没送达
- 当某一段报文丢失后,发送端会一直收到1001这样的ACK
- 如果发送端主机连续三次收到了同样一个“1001”这样的应答,就会将对应的数据1001~2000重新发送
- 这个时候接收端收到了1001之后,再次返回的ACK就是5001了(2001~5000接收端之前已经收到了,被放到了接收端操作系统内核的接收缓冲区中)
- 这种机制被称为“高速重发机制”(也叫“快重传”)
流量控制
接收端处理数据的速度是有限的,如果发送端发的太快,导致接收端的缓冲区被打满,这个时候如果发送端继续发送就会造成丢包,继而引起丢包重传等等一系列连锁反应。 因此TCP支持根据接收端的处理能力,来决定发送端的发送速度,这个机制就叫作流量控制(Flow Control)
- 接收端将自己可以接收的缓冲区大小放入TCP首部中的“窗口大小”字段,通过ACK端通知发送端
- 窗口大小字段越大,说明网络的吞吐量越高
- 接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值发送给发送端
- 如果接收端缓冲区满了就会将窗口置为0,这时发送方不再发送数据,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小发送给发送端
接收端通过TCP首部的16位窗口字段存放窗口大小信息,16位数字最大表示65535,那么TCP窗口最大就是65535字节吗?实际上TCP首部40字节选项中还包含了一个窗口扩大银子M,实际窗口大小是窗口字段的值左移M位
拥塞控制
虽然TCP有了滑动窗口能够高效可靠地发送大量数据,但是如果在刚开始阶段就发送大量的数据,仍然可能引发问题,因为网络上有很多的计算机,可能是当前的网络状态已经比较拥堵,在不清楚当前网络状况的情况下贸然发送大量的数据是很有可能雪上加霜的。为此TCP引入慢启动机制:先发送少量的数据,探探路,再决定传输数据的速度 大家都必须遵守,当大量丢包时,我们的策略就不该是重传,而是拥塞控制,
- 引入一个概念叫拥塞窗口
- 发送开始的时候,定义拥塞窗口大小为1
- 每次收到一个ACK应答,拥塞窗口加1
- 每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小作比较,取较小的值作为实际发送的窗口
像上面这样的拥塞窗口增长速度,是指数级别的。“慢启动”只是指初始时慢,但是增长速度非常快,为了不增长得那么快,不能使拥塞窗口单纯地加倍,要引入慢启动的阈值,当拥塞窗口超过这个阈值的时候,不再按照指数方式增长,而是按照线性方式增长。 当TCP开始启动的时候,慢启动阈值等于窗口最大值,在每次超时重发的时候,慢启动阈值会变成原来的一般,同时拥塞窗口置回1 少量的丢包,只会触发超时重传,大量的丢包,就会认为是网络拥塞,当TCP通信开始后,网络吞吐量会逐渐上升,随着网络发生拥堵,吞吐量会立刻下降。拥塞控制是TCP协议想尽快把数据传输给对方,但是又要避免给网络造成太大压力的折衷方案
延迟应答
如果接收数据的主机立刻返回ACK应答,这时候返回的窗口可能比较小,假设接收端缓冲区为1M,一次收到了500K的数据,如果立刻应答,返回的窗口就是500K,但实际上处理端处理的速度可能很快,马上就把500K数据从缓存区提取了,在这种情况下接收端处理远没有达到自己的极限,即使窗口再放大一些,也能够处理,如果接收端稍微等一会儿再应答,那么这时候返回的窗口大小可能就是1M 窗口越大,网络吞吐量就越大,传输效率就越高,在保证网络不拥塞的情况下应该尽量提高传输效率 所有的包都可以延迟应答吗?
- 数量限制:每个N个包就应答一次
- 时间限制:超过最大延迟时间就应答一次
具体的数量和超时时间,随操作系统不同也有差异,一般N取2,超时时间取200ms
捎带应答
数据和确认可以在一个报文发送,比如三次握手中SYN和ACK可以同时发送
面向数据流
- 调用write时,数据会先写入发送缓冲区中,如果发送的字节数太长,会被拆分成多个TCP的数据包发出,如果发送的字节数太短,就会先在缓冲区里等待,等到缓冲区长度差不多了,或者其他合适的时机发送出去
- 接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区,然后应用程序可以调用read从接收缓冲区拿数据
- 另一方面,TCP的连接既有发送也有接收缓冲区,那么一个连接既可以读数据也可以写数据,这个概念叫全双工
由于缓冲区的存在,TCP程序的读和写不需要一一匹配
粘包问题
- 这里的“包“是指应用层的数据包,在TCP的协议头中,没有如同UDP一样的”报文长度"字段,但有”序号“字段
- 站在传输层的角度:TCP是一个个报文过来的,按照序号排好序放在缓冲区中
- 站在应用层的角度:只是一串连续的字节数据
- 那么应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分,是一个完整的应用层数据包
要避免粘包问题就得明确两个包之间的边界
- 对于定长的包,保证每次都按固定大小读取即可
- 对于边长的包,可以在包头的位置,约定一个包总长度的字段,从而就知道了包的结束位置;还可以在包和包之间使用明确的分隔符
UDP不存在粘包问题
- TDP如果还没有向上层交付数据,UDP的报文长度仍然在,同时,UDP是一个一个把数据交付给应用层,有很明确的数据边界
- UDP要么收到完整的UDP报文,要么不收,不会出现”半个“的情况
TCP异常情况
进程终止:进程终止会释放文件描述符,仍然可以发送FIN,和正常关闭没什么区别 机器重启:和进程终止的情况相同 机器掉电/网线断开:接收端认为连接还在,一旦接收端有写入操作,接收端发现连接已经不在了,就会进行reset;即使没有写入操作TCP也内置了一个保活定时器,会定期询问对方是否还在,如果对方不再,也会把连接释放 另外,应用层的某些协议也有一些这样的检测机制,例如HTTP长连接中,也会定期检测对方的状态,例如QQ,在QQ断线之后,也会定期禅师重新连接
TCP连接管理
Linux内核协议栈为一个tcp连接管理使用两个队列:
- 半链接队列:用来保存处于SYN_SENT和SYN_RECV状态的请求
- 全连接队列(accept队列):用来保存处于established状态,但是应用层没有调用accept取走的请求
全连接队列的长度等于listen第二个参数加1,全连接队列满了的时候,就无法继续让当前的连接进入established状态了
|