目录
一、UDP协议
1.1 UDP协议格式
以下为UDP的特点:
- 无连接
- 不可靠传输
- 面向数据报
- 有接收缓冲区,无发送缓冲区
- 大小受限:一次最多传输64k.
端口:
我们常说的端口号的范围在0~65535(十进制) ,0~1111111111111111(二进制)的原因是因为在传输层协议中源端口号的长度是两个字节。
一个UDP报文(包括报头)是64kb,但是随着网络的发展,单次传输的数据的越来越多,因此我们一般有两种解决方案:
- 在应用层代码这里,针对数据拆包(接收端在进行手动拼装),拆成多个UDP数据报,分别传输,但是这其中存在一些丢包等问题。
- 因此一般我们会选择使用TCP协议来进行传输,因为TCP协议是面向字节流的,没有对包的长度进行限制。
校验和:
作用是检查数据是否出错了。
网络传输的过程中,难免会受到一些干扰,导致传输的数据出错。(毕竟光信号和电信号,在一些特殊环境会收到影响)
而UDP中采取的是CRC算法(循环冗余校验),接收方和发送方都会计算一遍校验和。这样出错的概率会大大降低。
注:如果校验和出错,就会直接丢弃。
1.2 基于UDP的应用层协议
- NFS:网络文件系统
- TFTP:简单文件传输协议
- DHCP:动态主机配置协议
- BOOTP:启动协议(用于无盘设备启动)
- DNS:域名解析协议
二、TCP协议
TCP全称为 传输控制协议(Transmission Control Protocol),TCP协议被广泛应用,其根本原因就是提供了详尽的可靠性保证,基于TCP的上层应用非常多,比如HTTP、HTTPS、FTP、SSH等,甚至MySQL底层使用的也是TCP。
我们知道,TCP的基本特性为 面向字节流,有连接,全双工,可靠传输。虽然只有前面几个特点能在代码中体现(UDP与TCP网络编程),但是可靠性才是TCP中最最核心的特性。
2.1 TCP协议格式
- 源/目的端口号:表示数据是从哪个进程来,到哪个进程去
- 32位序号/32位确认号:分别代表TCP报文当中每个字节数据的编号以及对对方的确认,是TCP保证可靠性的重要字段
- 数据偏移:表示的是TCP数据起始处与TCP报文起始处之间的距离,其实就是TCP首部报头长度,因为这里的单位是4个字节,所以4位bit位(0000~1111)表示十进制范围就是0~15 ,长度就是0~60字节
- 16位窗口大小:保证TCP可靠性机制和效率提升机制的重要字段
- 16位校验和:发送端填充,CRC校验。接收端校验不通过,则认为数据有问题。此处的检验和不光包含TCP首部,也包含TCP数据部分
6位标志位:
- URG:紧急指针是否有效
- ACK:确认号是否有效
- PSH:提示接收端应用程序立刻从TCP缓冲区把数据读走
- RST:对方要求重新建立连接;我们把携带RST标识的称为复位报文段
- SYN:请求建立连接;我们把携带SYN标识的称为同步报文段
- FIN:通知对方,本端要关闭了,我们称携带FIN标识的为结束报文段
注:TCP的校验和是不可以关闭的,而UDP是可以关闭的。
什么是真正的可靠?
在进行网络通信的过程中,一方发出数据后,它不能保证该数据是否会被对端接受,因为在实际生活中,可能会出现各种各样的问题,因此需要对端回复所谓的“响应信息”后,发送端才能确保对端是接受到自己的信息。这也就是我们所说的可靠。
但是我们会发现一个问题,那就是上述场景中,对端主机如果想知道自己发送的响应信息是否被发送端所接收,那么就需要发送端在发送一条响应信息,这样就会进入一个死循环......
所以:在严格意义上来说,互联网通信的过程中是不存在百分比的可靠传输的,因为总有一条消息得不到所谓的响应。其实实际上并不需要保证每条消息的可靠性,只需要对核心消息做出保证即可,对于非核心信息(比如响应数据),如果其无法发送到对端,发送端可以进行重传操作。(重传后信息再次丢失的概率就极低了)。
2.2 确认应答机制
确认应答机制是TCP协议保证传输可靠性的最核心机制
上述所描述的场景就说TCP当中的确认应答机制,确认应答的关键就是发送方发送数据给接收方后,接收方会自动返回一个响应表示收到数据了。
而其中的应答报文,也称之为ack(acknowledge)报文,顾名思义,ack报文中标志位ACK位为1.普通报文ACK位为0.
2.2.1 序号与确认序号(应用)
在数据传输的过程中,可能会出现后发先至的情况,这是当下的网络结构操作造成的,无法避免,解决方案就是使用序号和确认序号。
序号(32位):针对请求数据进行的编号
确认序号(32位):只针对ACK报文有效
将上述过程进行编号,就不会产生歧义了:
我们知道,TCP是面向字节流的,实际中传输过程中,是针对每一个字节进行编号,比如传输数据的序号是1,发送了1000个字节的数据,那么接收方收到数据后会返回一个1001,表示1001之前的数据以及全部收到了。
发送的数据:
传输过程:
我们知道确认应答机制,会让接受端在收到消息时返回一个应答报文(ACK),可是这个应答报文在传输的过程中可能会出现丢包的情况。
2.3 超时重传机制
发生超时重传的两种情况:发送端数据丢失和应答报文丢失(ACK);
- 主机A发送数据给B之后,可能因为网络拥堵等原因,数据无法到达主机B
- 如果主机A在一个特定时间间隔内没有收到B发来的确认应答,就会进行重发
- 上述的两种情况中,如果是发送方的信息丢了,那么就正常重传
- 但是如果是第二种情况,ACK丢了,(按照常理)就会导致发送方再次传送相同的消息给接受方,TCP协议的设计者当然想到了这一问题,所以接收方会在收到数据的时候进行去重操作(根据序号)。
特定时间间隔:
这里的时间间隔是由系统内部的配置项决定的,描述的是超时重传需要等待的时间,例如第一次出现丢包,发送方就会在达到超时时间阈值后,进行重传,如果仍无响应,那么就会继续重传,当然,第二次的等待重传时间会比第一次长。
注:这里的重传,并非是无止境的,一般第二次重传就会成功到达(两次都发生丢包的概率太低了),如果第二次重传未成功,可能说明当时网络情况很差,后续可能会进行重置TCP连接(断开重连)的操作,甚至是释放连接(彻底放弃)。
2.4 连接管理(三次握手,四次挥手)
TCP的连接不是物理上的,而是逻辑上的,举个栗子:
如果主机A和主机B建立连接,那么在主机A和主机B的系统内核中,会分别记录了一个数据结构,在其中包含了与其建立连接对象的信息(IP地址,端口号等)。
在正常情况下,TCP要经过三次握手建立连接,四次挥手断开连接
2.4.1 三次握手
由下图可知;标志位中的SYN为1,说明就是一个同步报文段。
为什么需要建立连接以及建立连接的意义是什么?
因为TCP是全双工通信的,因此建立连接的目的是投石问路——检查当前的网络情况是否是畅通的,需要注意的是三次握手建立的连接并不传输任何业务数据。
但是会传输一些重要数据,如窗口大小,下文会提到。
三次握手同时是可以检查通信双方的 发送能力 以及 接收能力 是否是正常的。
为什么是三次握手?两次握手行不行?
很显然,三次握手才是最优解,因为只有三次握手后,才能验证通信双方可以正常的接收和发送数据(同时三次握手恰好是验证双方通信信道的最小次数)。
- 在服务器端收到来自客户端的SYN时,服务器就明白客户端的发送数据能力是没有问题以及自己的接收能力没有问题,
- 于是立即向服务器发送ACK和SYN(两者都是系统内核触发的,因为时间相同,直接就合并为一个报文发送了),在客户端收到这第二次握手的时候时(ACK+SYN),客户端就明白自己的发送能力没有问题,接收能力没有问题,服务器的接收能力没有问题,服务器的发送能力没有问题。
- 第二次握手后(客户端已经知道双方可以正常通信了,但是因为服务器不知道,所以需要继续进行第三次握手)
- 这时需要客户端返回ACK(针对服务器发送的SYN做出应答),当服务器收到客户端的ACK后,服务器才能明白自己的发送能力是没有问题的,客户端的接收能力没有问题。
- 完成三次握手后服务器才知道双方通信可以正常进行。
另外,三次握手其实还有一个好处,就是可以让连接异常时,异常挂在客户端这里,通过上述描述我们可以得知,服务器和客户端双方各自建立连接的时间点是不同的,
如果客户端发出的第三次握手丢包了,那么服务器就不会与其建立连接,而客户端就要对这个现象进行维护(需要耗费时间成本和空间成本),如果出现丢包,难免会耗费时间和空间成本,但是如果发生在客户端就会好点,因为如果是服务器的话,一旦出现多个失效情况,那么成本可能就会翻倍。
三次握手时的状态:
- 通信未开始的时候,客户端与服务器都是处于CLOSED状态,服务器为了能够与客户端建立连接,需要从CLOSED状态变成LISTEN状态(相当于给对方打电话,需要对方的手机处于正常开机状态)。
- 这时客户端才能开始进行三次握手,当客户端发起第一次握手后,状态转为SYN_SENT
- 处于LISTEN状态的服务器收到客户端的连接请求后,将该连接放入内核等待队列中,并向客户端发起第二次握手,此时服务器也变成SYN_SENT
- 当客户端收到服务器的第三次握手后,紧接着向服务器发送最后一次握手,此时客户端的连接已经建立,状态转为ESTABLISHED,当服务器收到客户端发来的第三次握手时,状态也变成ESTABLISH。
2.4.2 四次挥手
通信双方断开连接(取消相互之间的认同关系),
通信双方各自向对方申请断开连接,再各自给对方回应。
需要注意的是,这里从服务端发送的ACK和FIN没有像三次握手中的ACK和SYN一样是合并到一个报文中发送的,因为在四次握手中从 服务端发送出去的ACK 是操作系统内核收到来自 客户端的FIN 后立即触发的,而 服务端发送的FIN 则是显示的调用 应用程序中的socket的close 方法触发的。
四次挥手时的状态
- 通信双方建立完连接后,都处于 ESTABLISHED 状态
- 客户端向服务器提出断开连接的请求FIN(调用close方法),此时客户端的状态变为 FIN_WAIT_1 。
- 服务器收到客户端发来的断开请求后,会立马做出响应(ACK),此时服务器变为 CLOSE_WAIT状态,而客户端收到服务器对结束报文段的确认,则进入FIN_WAIT_2,开始等待服务器的结束报文段;
- 服务器进入CLOSE_WAIT后说明服务器准备关闭连接(但是需要处理完之前的未处理的数据),当服务器调用close方法真正准备关闭连接时,会向客户端发送FIN,这时服务器进入 LAST_ACK状态(等待最后一个ACK到来)。
- 当客户端收到服务器发来的第三次挥手的时候,会向服务器发送最后一个响应报文,此时客户端进入TIME_WAIT状态。tip:上述客户端并没有立马进入close状态的原因是;对服务器发出FIN请求做出回应的ACK可能会出现丢失,所以这里先进入TIME_WAIT状态,如果出现丢包的情况,需要重传操作。
- 当服务器收到来自客户端的ACK(对自己提出FIN请求的回应)后,就会彻底关闭变为CLOSED状态。
以上就是四次挥手的全部状态。
2.5 滑动窗口
回顾一下刚才的确认应答机制,其内容是发送方对自己每一个发送的数据段,都需要接收端返回一个ACK确认应答后才能继续发送下一条数据。
这种做法虽然在一定程度上保证了数据的可靠性,但是呢,却有一个缺点,就说性能较差,尤其是返回的ACK需要等待很长时间的时候。
设计TCP协议的人当然想到了这一点,所以引入了滑动窗口这一机制来提高发送的效率,但是其实有点属于亡羊补牢,其效率再高也没用UDP协议高。
既然一条一条发送数据效率太低,发送端先发送一波信息,然后等待接收端回应一波ACK,以此循环:
需要注意的是发送方并不需要等待所有的ACK都传回来之后再进行发送,而是传回来一个ACK后,就继续发送下一条数据。
现在对上图进行说明:
- 窗口的大小指的是:无需等待接收端确认应答就发送的数据量(在TCP三次握手时,接收端就会将这个窗口大小发送给发送方),以此图为例,窗口大小为4000(以字节为单位换算的)
- 主机A最开始发送的前4个数据段无需等待ACK回应,直接发送。
为了更好的理解窗口这一概念,请看下图;
- 序号1中窗口表示的是,主机A给主机B发送了4条数据 1001~5000(这些数据需要等待ACK的响应),序号2中2001这个ACK到达主机A的时,主机A就立即发送5001~6000, 此时A等待ACK的范围就是2001~6000。
- 在2001这个ACK到达主机A之前,主机A上需要等待确认的数据是1001~5000,
- 在2001这个ACK到达之后,意味着1001~2000的数据段发送成功了,于是就把1001~2000这个记录从白色的框移除。
- 紧接着,发送5001~6000这个数据段,因此就需要等待5001~6000的ACK(白色的框中增加这个数据段的原因)
此时等待的数据段仍是4条,在视觉上,看起来窗口的大小没变,但是却向右边移动了。因此我们把这种现象叫做 滑动窗口。
Tips:窗口越大TCP传输的效率就越高,反之则越小,假设窗口是无穷大,那么此时的发送方就完全不需要等待接收端传回的ACK,这时TCP的效率就跟UDP一样了,但是假设终究是假设,实际上是不可能完成的。
上述我们讨论的情景都属于正常情况下,网络情况错综复杂,当出现丢包/乱序的情况下应该如何处理呢?
丢包分为两种情况;
- 数据包已到达,ACK丢了。
- 数据包丢了
针对情况1,ACK丢了,其实完全不需要作任何事,因为就算ACK丢包率达到了百分之50,也无需担心,不用每一个丢失的ACK都进行重传(如果刚好是最后一个ACK丢了,那么正常进行超时重传操作就好),因为2001这个ACK代表的是接收了1~2000字节的数据,已经涵盖了刚刚1001这个ACK所包含的内容,6001这个ACK代表的是接收了1~6000字节的数据,也涵盖了在这之前丢失的ACK。
针对情况2,传输的数据包丢了;
- 如上图所示,1001~2000这个数据丢失时,主机B会向主机A索要1001的数据段
- 此时的主机A并未意识到数据丢失了,仍然在向主机B发送后续的数据段
- 当主机A发现主机B连续索要多次数据时,才反应过来,1001的数据段丢失了
- 于是主机A重传了1001这个数据段,主机B收到主机A发送的1001后,由于主机A已经把1~7000的数据段发送过来了,所以紧接着只需要放回7001的ACK即可
前面丢失的是1001~2000,而2001~7000的数据并未丢失(主机B都受到了),当A重传之后,主机B的7000之前的数据就补齐了,后续主机B只返回了7001这个ACK,并无返回2001,3001这些ACK,这种不拖泥带水的操作称之为快速重传——搭配滑动窗口的超时重传
并不是有了快速重传之后,超时重传就毫无用武之地,如果传输的数据很多,当然是遵守快速重传的方式,如果传输的数据很少(只有一条),那么就需要按照超时重传的方式进行。
例如刚刚传输多条数据的过程中,刚好最后一个数据段丢失了,那么就按照超时重传的方式进行,
可以这样理解:只要出现丢包的情况,就要进行超时重传,快速重传其实只是超时重传的一种特殊情况。
2.6 流量控制
我们知道,窗口越大,发送的速度越快,但是如果窗口太大,导致接收方没有能力接收这些数据,会起到反效果,可能会造成丢包等一系列操作,
TCP是可靠的,就会对这些数据进行重传,这犹如雪上加霜般,对接收端将是很不好的体验,TCP根据接收端的处理能力(由于应用程序从接收端抽取数据的速率不好衡量,于是是用接收方缓冲区的剩余容量来衡量接收方处理能力),来决定发送端的发送速度,也就是说其实流量控制是对发送方的速度进行控制。
主机A第一次向主机B发送数据的时候如何得知窗口大小?
回忆TCP首部中,有一个16位窗口字段,就是存放窗口大小信息的,在TCP三次握手的阶段,除了验证双方通信是否畅通之外,还会进行其他信息的交互:接收端就会在ACK中将窗口大小反馈给发送方,因此当我们第一次发送数据的时候,就不会出现窗口溢出的情况。
tips:窗口的大小根据网络状况的优劣以及其他因素是实时变化的。
当缓冲区满了,主机A会停止发送数据,那后续主机A是如何得知可以继续发送数据呢?
- 等待告知,当主机B的上层将缓冲区的数据拿走后,会主动发送一个TCP报文给主机A,告知其现在的窗口大小。
- 主动询问,主机A每过一个时间段,就会发出一个探测报文给主机B,该报文不携带任何业务数据(不携带载荷),只是探测一下窗口是多少。
窗口字段是16位大小,是否意味着窗口最大就是65535呢?
当然不是这样,在TCP报文的”选项“字段中,包含了一个窗口大小扩展因子M,而实际的窗口大小是窗口字段的值左移M位得到的。
2.7 拥塞控制
拥塞控制的存在意义?
为了防止和流量控制混淆,以下做出区分:
- 流量控制:考虑的是接收端缓存区的接收能力,进而反馈给发送方,调整发送窗口的大小,避免接收端的缓冲区溢出。
- 拥塞控制:考虑的是通信双方所处的网络环境,如果发送的数据超过拥塞窗口(拥塞控制下的窗口大小)的大小,可能会引起网络阻塞。
当流量控制和拥塞控制下产生的窗口大小不同时,听命于谁?
:听命于窗口小的那一方。
拥塞控制
虽然TCP协议中有滑动窗口这么一个厉害的方法能一次性发送大量数据,但是现实中可能没有那么美好,因为网络环境的错综复杂,时好时坏的表现,可能会让我们出现大量丢包的情况,
为了避免这种情况,TCP引入了 慢启动 这一概念,先发少量的数据,探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据,如果后续再出现丢包的情况,那么窗口就再变小,反复调整。
由图中所示:
- 一开始传输的数据特别小,然后以指数形式增长(这种做法可以在短时间内,摸清网络传输承载的底线)
- 到达阈值后(如果不设置阈值的话,那么就会一下子就超过窗口上限了,此处的阈值是一般是系统配置的),再以线性模式增长——以此来缓慢接近上限。
总结:
少量的丢包,我们仅仅是触发超时重传;大量的丢包,我们就认为网络拥塞;
当TCP通信开始后,网络吞吐量会逐渐上升;随着网络发生拥堵,吞吐量会立刻下降;
拥塞控制,归根结底是TCP协议想尽可能快的把数据传输给对方,但是又要避免给网络造成太大压力的折中方案。
2.8 延迟应答
概念:这是一种基于 流量控制 而提高效率的机制,简单来说,延迟应答是流量控制的延申,流量控制的目的是为了使发送方不要发送的太快,而延迟应答在此基础上,想让窗口的大小尽量大一些。
- 如果接收数据的主机在收到发送端发送的数据后立刻返回ACK应答,这时返回的窗口可能比较小。
- 假设接收端缓存区剩余空间为1M,一次收到了400k的数据;如果立刻应答,返回的窗口就是600k。
- 但实际上可能处理端处理的速度很快,10ms之内就把400k的数据从缓冲区提取走了
- 在这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一点,也能处理过来
- 如果接收端稍微等一会再应答,比如等待200ms再应答,那么这个时候返回的窗口大小就是1M。
延迟应答的出现,就是为了让传输的效率变高,因为窗口越大,对应着网络吞吐量越大,我们的目的就是在保证网络不堵塞的情况下提高传输的效率。
那么所有的包都可以延迟应答么?肯定也不是。
- 数量限制:每隔N个包就应答一次;
- 时间限制:超过最大延迟时间就应答一次;
2.9 捎带应答
在延迟应答的基础上,我们发现,很多情况下,客户端服务器在应用层也是 "一发一收" 的。意味着客户端给服务器说了 "How are you",服务器也会给客户端回一个 "Fine, thank you";
那么这个时候ACK就可以搭顺风车,和服务器回应的 "Fine,thank you" 一起回给客户端(所以在一定条件下,是可以让四次挥手变成三次挥手的)。
2.10 粘包问题
为什么会出现粘包问题?
- 简单来说:虽然传输层(TCP)是一个个报文发送,然后按照序号排在缓冲区,之后发送给应用层的,但是应用层去取数据的时候(从其角度来看,这只是一串连续的字节数据),
- 不知道从哪里到哪里是一个完整的应用层数据报(TCP的协议报头中没有跟UDP一样的”报文长度“这样的字段)。
- 其实不止TCP传输存在这类问题,所以面向字节流读文件都会有这种问题。
需要注意的是:
首先要明确,这里面所说的包是指应用层的数据包。
该如何避免这种粘包的问题呢?
归根结底:明确包的长度。
- 对于定长的包,保证每次都按固定大小读取即可;例如上面的Request结构,是固定大小的,那么就从缓冲区从头开始按sizeof(Request)依次读取即可;
- 对于变长的包,可以在包头的位置,约定一个包总长度的字段,从而就知道了包的结束位置;
- 对于变长的包,还可以在包和包之间使用明确的分隔符(应用层协议,是程序猿自己来定的,只要保证分隔符不和正文冲突即可);
对于UDP协议来说,是否也存在 "粘包问题" 呢?
- 对于UDP,如果还没有上层交付数据,UDP的报文长度仍然在。同时,UDP是一个一个把数据交付给应用层。就有很明确的数据边界。
- 站在应用层的站在应用层的角度,使用UDP的时候,要么收到完整的UDP报文,要么不收。不会出现"半个"的情况
2.11 TCP异常情况
主要分为以下几种情况,进程崩溃,正常关机,主机掉电,网络断开
进程崩溃
进程崩溃也称进程的异常退出,操作系统会回收进程的资源,其中包括释放文件描述符,这样的操作就相当于调用socket的close方法,执行close就会触发FIN报文,进一步的开始四次挥手。
需要注意的是:这种情况和普通的四次挥手没有什么区别。
正常关机/机器重启(通过 开始菜单 这种方式来关闭主机)
关机的时候或者计算机重启的时候,系统会强制结束所有的用户进程,和上述的那个进程崩溃类似,系统内核会进行文件描述符的释放操作,进一步的进行四次挥手。
主机掉电
分为两种情况,一种为掉电的是接收方,另一种为掉电的是发送方。
当掉电是接收方的时候,发送方是不知道接收方断电了,还会继续发送数据,此时发的数据没有,接收方无法返回ACK了。发送方此时会触发超时重传,重传了几次之后,仍然无法连接。
此时发送方会重置连接(发送复位报文段,RST为1),发现还是无法连接时,发送端就放弃连接了。
当掉电的是发送方的时候,此时接收方并没有干等着,会在一个时间周期后,发送一个“心跳包”。(心跳包是周期性触发的,只是一个简单的不携带任何业务数据的包,该包存在的意义就是确认一下对方是否还在)
tip:这里的心跳包虽然和探测报文的功能很像,但是两者并非同一个东西。
如果对方不反回一个心跳包的话,说明对方就挂了,于是便放弃了连接。
网线断开
该情况与主机掉电是相同的,只不过这时通信的双方分别按照上述(发送方,接收方)进行着。
另外,应用层的某些协议,也有一些这样的检测机制。例如HTTP长连接中,也会定期检测对方的状态。例如QQ,在QQ断线之后,也会定期尝试重新连接。
三、TCP小结
为什么TCP这么复杂?因为要保证可靠性,同时又尽可能的提高性能。
可靠性:
- 校验和
- 序列号(按序到达)
- 确认应答
- 超时重发
- 连接管理
- 流量控制
- 拥塞控制
提高性能:
- 滑动窗口
- 快速重传
- 延迟应答
- 捎带应答
基于TCP应用层协议
- HTTP
- HTTPS
- SSH
- Telnet
- FTP
- SMTP
当然,也包括你自己写TCP程序时自定义的应用层协议。
下面来看一道常见面试题:
如何使用UDP来实现可靠传输
其实考察的是TCP,只要引入TCP中的可靠机制即可,如校验和,序列号(按序到达),确认应答等等。