传输层协议:负责数据能够从发送端传输接收端。
目录
- 1.UDP协议
-
- 1.1 UDP协议端格式
- 1.2 UDP的特点
- 1.3 基于UDP的应用层协议
- 2. TCP协议
-
- 2.1 TCP协议端格式
- 2.2 TCP原理
-
- 2.2.1 确认应答机制(安全机制)
- 2.1.2 超时重传机制(安全机制)
- 2.1.3 连接管理机制(安全机制)
- 2.1.4 滑动窗口(效率机制)
- 2.1.5 流量控制(安全机制)
- 2.1.6 拥塞控制(安全机制)
- 2.1.7 延迟应答(效率机制)
- 2.1.8 捎带应答(效率机制)
- 2.1.9 面向字节流 缓冲区 大小限制
- 3 粘包问题
- 4.TCP异常情况
- 3. TCP小结
- 4.基于TCP应用层协议
- 5. 经典面试题:基于UDP如果实现可靠传输
1.UDP协议
1.1 UDP协议端格式
UDP报头:
对于UDP报头,大多数资料上给出的都是这种形式:
但这其实是为了排版方便才这样描述的,真正的UDP报头应该是如下形式:
- 16位(bit位)UDP长度,表示整个数据报(UDP首部+UDP数据)的最大长度;
- 如果校验和出错,就会直接丢弃;
1.2 UDP的特点
UDP传输的过程类似于寄信
-
无连接
知道对端的IP和端口号就直接进行传输,不需要建立连接;
-
不可靠
没有任何安全机制,发送端发送数据报以后,如果因为网络故障该段无法发到对方,UDP协议层也不会给应用层返回任何错误信息;
-
面向数据报
应用层交给UDP多长的报文,UDP原样发送,既不会拆分,也不会合并;
用UDP传输100个字节的数据:
如果发送端一次发送100个字节,那么接收端也必须一次接收100个字节;而不能循环接收10次,
每次接收10个字节。
-
缓冲区
UDP只有接收缓冲区,没有发送缓冲区:
UDP没有真正意义上的 发送缓冲区。发送的数据会直接交给内核,由内核将数据传给网络层协议
进行后续的传输动作;
UDP具有接收缓冲区,但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一
致;如果缓冲区满了,再到达的UDP数据就会被丢弃;
-
全双工
UDP的socket既能读,也能写
-
大小受限
UDP协议首部中有一个16位的最大长度。也就是说一个UDP能传输的数据最大长度是64K(包含UDP首
部)
1.3 基于UDP的应用层协议
- NFS:网络文件系统
- TFTP:简单文件传输协议
- DHCP:动态主机配置协议
- BOOTP:启动协议(用于无盘设备启动)
- DNS:域名解析协议
- 当然,也包括你自己写UDP程序时自定义的应用层协议。
2. TCP协议
2.1 TCP协议端格式
- 源/目的端口号:表示数据是从哪个进程来,到哪个进程去;
- 32位序号/32位确认号:基于TCP协议是可靠传输,用于确认数据是否传输到接收方
- 4位TCP报头长度:表示该TCP头部有多少个32位bit(有多少个4字节);所以TCP头部最大长度是
15 * 4 = 60
- 6位标志位:
URG:紧急指针是否有效
ACK:确认号是否有效
PSH:提示接收端应用程序立刻从TCP缓冲区把数据读走
RST:对方要求重新建立连接;我们把携带RST标识的称为复位报文段
SYN:请求建立连接;我们把携带SYN标识的称为同步报文段
FIN:通知对方,本端要关闭了,我们称携带FIN标识的为结束报文段
- 16位窗口大小:
- 16位校验和:发送端填充,CRC校验。接收端校验不通过,则认为数据有问题。此处的检验和不
光包含TCP首部,也包含TCP数据部分。
- 16位紧急指针:标识哪部分数据是紧急数据;
- 40字节头部选项:暂时忽略;
2.2 TCP原理
TCP对数据传输提供的管控机制,主要体现在两个方面:安全和效率。
这些机制和多线程的设计原则类似:保证数据传输安全的前提下,尽可能的提高传输效率。
2.2.1 确认应答机制(安全机制)
TCP将每个字节的数据都进行了编号。即为序列号。(以字节为单位对数据进行编号)
每一个ACK都带有对应的确认序列号,意思是告诉发送者,我已经收到了哪些数据;下一次你从哪里开
始发
2.1.2 超时重传机制(安全机制)
- 主机A发送数据给B之后,可能因为网络拥堵等原因,数据无法到达主机B;
- 如果主机A在一个特定时间间隔内没有收到B发来的确认应答,就会进行重发;
但是,主机A未收到B发来的确认应答,也可能是因为ACK丢失了;
因此主机B会收到很多重复数据。那么TCP协议需要能够识别出那些包是重复的包,并且把重复的丢弃
掉。
这时候我们可以利用前面提到的序列号,就可以很容易做到去重的效果。
2.1.3 连接管理机制(安全机制)
在正常情况下,TCP要经过三次握手建立连接,四次挥手断开连接
- syn是该协议中的一个标志位。如果该位被置为1,则表示这个报文是一个请求建立连接的报文(同步报文段)。
- ack也是该协议的一个标志位。如果该位被置为1,则表示这个报文是一个用于确认的报文(确认报文段)。
- 如果ACK这一位是1,表示这个报文段就是一个“确认报文段”
三次握手建立连接:
这个过程是一个双向奔赴的过程,类似于表白,双方都像对方表白,才算真正建立了关系
而且,中间的这两次(ACK+SYN)是会合二为一的,因为每次要传输的数据,都要经过一系列的封装和分用,才能完成传输,为了提高传输的效率,这里会把二次封装合为一次封装发送
这个过程也很容易,在tcp报文段中同时将SYN+ACK设置为1进行传输即可:
所以三次握手建立连接就是这样:
对于TCP的状态,很多我们是不需要也没必要去关注的,我们只需要知道这两个即可:
- LISTEN:表示服务器启动成功,端口绑定成功,随时可以有客服端来建立连接(类似于手机开机,信号良好,随时可以有人打电话过来)
- ESTABLISHED:表示客服端已经连接成功,随时可以进行通信了(类似于电话接通,随时可以说话了)
四次挥手断开连接:
三次握手,就让客服端和服务器之间建立好了连接;建立好了连接之后,操作系统内核中,就需要使用一定的数据结构来保存连接相关的信息;其中保存的信息其实最重要的就是“五元组”,并且客服端服务器都得保存五元组~ (源IP+源端口+目的IP+目的端口+协议(TCP/UDP)),既然保存了信息就需要占用系统资源(内存),如果之后连接断开了(连接不复存在了),此前保存的连接信息就没有意义了,对应的空间也就可以释放了;
四次挥手断开连接图示:
双方各自向对方发生了FIN(结束报文段)请求,并且各自给对方一个ACK确认报文;
FIN设置为1,就表示这是一个结束报文段
- 三次握手,一定是客服端主动发起的(主动发起的一方才叫客服端)
- 四次挥手,可能是客服端主动发起的,也可能是服务器主动发起的
- 三次挥手,中间两次能合并
- 四次挥手,中间两次有时候合并不了(有时候是能合并的),不能合并的原因,在于B发生ACK和B发生FIN的时机是不同的;四次挥手中,B给A发生的ACK,是操作系统内核负责的,B给A发的FIN是用户代码负责(B中的代码调用了socket.close()方法,才会触发FIN,收到FIN,内核立刻返回ACK)
- 而三次握手建立连接中,B发送的ACK和SYN是同一时机的,就能够合并,此时B给A发送的ACK和SYN都是由操作系统内核负责进行的
对于断开连接中的状态,有两个需要我们重点理解:
-
CLOSE_WAIT :四次挥手了两次之后出现的状态,这个状态就是在等待代码中调用socket.close()方法,来进行后续的挥手过程;正常情况下,一个服务器上不应该存在大量的CLOSE_WAIT,如果存在,大概率是代码bug,close没有被执行到
-
TIME_WAIT:谁主动发起FIN,谁就进入到TIME_WAIT,起到的效果,就是给最后一次ACK提供重传的机会
因为表面上看起来,A发送完ACK之后,就没有A啥事了,按理说,A就应该销毁连接,释放资源,但是实际情况A并没有直接释放,而是会进入到TIME_WAIT状态等待一段时间,再来释放连接(等待这一个过程,就是担心最后一个ACK丢包,如果最后一个ACK丢包了,就意味着B过一会就会重传FIN)
-
TIME_WAIT的持续设定时间一般是2*MSL(MSL表示网络上任意两点之间,传输需要的最大时间,这个时间也是系统上可以自行配置的参数,一个典型的设置就是60s(经验值))
2.1.4 滑动窗口(效率机制)
滑动窗口存在的意义就是在保证可靠性的前提下,尽量提高传输效率
刚才的确认应答策略,对每一个发送的数据段,都要给一个ACK确认应答。收到ACK后再发送
下一个数据段。这样做有一个比较大的缺点,就是性能较差。尤其是数据往返的时间较长的时候,数据传输过程中大量的时间都浪费在等待ACK的过程中,为了提高传输效率,引入了滑动窗口机制
既然这样一发一收的方式性能较低,那么我们一次发送多条数据,就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了)
- 窗口大小指的是无需等待确认应答而可以继续发送数据的最大值。上图的窗口大小就是4000
个字节(四个段)。
- 发送前四个段的时候,不需要等待任何ACK,直接发送;
- 收到第一个ACK后,滑动窗口向后移动,继续发送第五个段的数据;依次类推;
- 操作系统内核为了维护这个滑动窗口,需要开辟 发送缓冲区 来记录当前还有哪些数据没有
应答;只有确认应答过的数据,才能从缓冲区删掉;
- 窗口越大,则网络的吞吐率就越高;
如果出现了丢包,如何进行重传?这里分两种情况讨论:
情况一:数据包已经抵达,ACK被丢了:
这种情况下,部分ACK丢了并不要紧,因为可以通过后续的ACK进行确认(因为ACK代表的是前面的所有数据都成功收到了,假如2001丢了,但是4001收到了,也可以代表1001-2000数据成功收到了)
情况二:数据包就直接丢了:
- 当某一段报文段丢失之后,发送端会一直收到 1001 这样的ACK,就像是在提醒发送端 “我想
要的是 1001” 一样;
- 如果发送端主机连续三次收到了同样一个 “1001” 这样的应答,就会将对应的数据 1001 -
2000 重新发送;
- 这个时候接收端收到了 1001 之后,再次返回的ACK就是7001了(因为2001 - 7000)接收端
其实之前就已经收到了,被放到了接收端操作系统内核的接收缓冲区中;
在A重传1001-2000之前,B的接收缓冲区如图:
当A重传了1001-2000之后,B的接收缓冲区,就把这个数据缺口给补上了,后续的2001-7000这些数据都是之前已经传输过的了,这些数据就不必再重传了,接下来B就向A索要7001的数据
这样的重传就只是把丢了的那一块数据给重传了即可,其他已经收到了的数据就不必再重传了,这种机制被称为 “高速重发控制”(也叫 “快重传”),传输效率是比较高的
2.1.5 流量控制(安全机制)
接收端处理数据的速度是有限的。如果发送端发的太快,导致接收端的缓冲区被打满,这个时候如果发
送端继续发送,就会造成丢包,继而引起丢包重传等等一系列连锁反应。
因此TCP支持根据接收端的处理能力,来决定发送端的发送速度。这个机制就叫做流量控制(Flow Control);
- 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 “窗口大小” 字段,通过ACK端通知
发送端;
- 窗口大小字段越大,说明网络的吞吐量越高;
- 接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值通知给发送端;
- 发送端接受到这个窗口之后,就会减慢自己的发送速度;
- 如果接收端缓冲区满了,就会将窗口置为0;这时发送方不再发送数据,但是需要定期发送
一个窗口探测数据段,使接收端把窗口大小告诉发送端。
这样的数据传输过程,可以理解为“生产者消费者模型”
A就是生产者,B的应用程序就是消费者,B的接收缓冲区,就是交易场所,
接收缓冲区有一个总大小,随着A发生数据,接收缓冲区就会逐渐放入一些数据,剩余空间就会逐渐缩小
- 如果剩余空间比较大,就认为B的处理能力是比较强的,就可以让A发的快一些
- 如果剩余空间比较小,就认为B的处理能力是比较弱的,就可以让A发的慢一些
接收端如何把窗口大小告诉发送端呢?在TCP首部中,有一个16位窗口字段,就是存放了窗口
大小信息;
那么问题来了,16位数字最大表示65535,那么TCP窗口最大就是65535字节么?
实际上,TCP首部40字节选项中还包含了一个窗口扩大因子M,实际窗口大小是 窗口字段的值左移 M
位;
2.1.6 拥塞控制(安全机制)
拥塞控制,是滑动窗口的延伸,也限制滑动窗口发送的速度
拥塞控制衡量的是,发送方到接收方,这整个链路之间,拥堵情况(处理能力)
虽然TCP有了滑动窗口这个大杀器,能够高效可靠的发送大量的数据。但是如果在刚开始阶段就发送大
量的数据,仍然可能引发问题。
因为网络上有很多的计算机,可能当前的网络状态就已经比较拥堵。在不清楚当前网络状态下,贸然发
送大量的数据,是很有可能引起雪上加霜的。
TCP引入 慢启动 机制,先发少量的数据,探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据;
A开始的时候以一个比较小的窗口来发送数据,如果数据很流畅的到达了B,就逐渐增大窗口大小
如果加大到一定程度之后,出现了丢包;(丢包就意味着通信链路出现拥堵了),这个时候再减少窗口
通过反复的增大,减小窗口,逐渐就摸索到了一个合适的范围,拥塞窗口在这个范围不断变化中,达到了“动态平衡”
- 此处引入一个概念程为拥塞窗口
- 发送开始的时候,定义拥塞窗口大小为1;
- 每次收到一个ACK应答,拥塞窗口加1;
- 最终滑动窗口的大小=min(拥塞窗口,流畅控制窗口)
像上面这样的拥塞窗口增长速度,是指数级别的。“慢启动” 只是指初使时慢,但是增长速度非常快
-
为了不增长的那么快,因此不能使拥塞窗口单纯的加倍。
-
此处引入一个叫做慢启动的阈值
-
当拥塞窗口超过这个阈值的时候,不再按照指数方式增长,而是按照线性方式增长
-
当TCP开始启动的时候,慢启动阈值等于窗口最大值;
-
在每次超时重发的时候,慢启动阈值会变成原来的一半,同时拥塞窗口置回1;
少量的丢包,我们仅仅是触发超时重传;大量的丢包,我们就认为网络拥塞;
当TCP通信开始后,网络吞吐量会逐渐上升;随着网络发生拥堵,吞吐量会立刻下降;
拥塞控制,归根结底是TCP协议想尽可能快的把数据传输给对方,但是又要避免给网络造成太大压力的
折中方案,理想的效果就是 窗口大小在 阈值和丢包窗口 之间
2.1.7 延迟应答(效率机制)
如果接收数据的主机立刻返回ACK应答,这时候返回的窗口可能比较小。
延时应答相当于流量控制的延伸,流量控制是踩了下刹车,使发送方,发送数据不要太快
延时应答就在这个基础上,能够尽量的再让窗口再大一些
一定要记得,窗口越大,网络吞吐量就越大,传输效率就越高。我们的目标是在保证网络不拥塞的情况
下尽量提高传输效率;
所有的包也不是都可以延迟应答的:
- 数量限制:每隔N个包就应答一次;
- 时间限制:超过最大延迟时间就应答一次;
具体的数量和超时时间,依操作系统不同也有差异;一般N取2,超时时间取200ms;
2.1.8 捎带应答(效率机制)
捎带应答又是延时应答的延伸
- 因为ACK是内核相应的,收到数据之后就立即执行;
- 而真正的相应数据是应用程序返回的,执行到相关的代码时候
- 这两东西不是同时触发的,不应该合并
- 但是因为延时应答的存在,导致ACK不一定是立即返回的,如果当前的延时应答导致ACK的返回时机和代码中返回的相应时机重合了,就可以捎带一起把这个ACK和响应数据合二为一了
- 对于捎带应答来说,ACK已经和数据合为一个包了,ACK丢了也意味着数据丢了,就走正常的丢包重传就行了
2.1.9 面向字节流 缓冲区 大小限制
创建一个TCP的socket,同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;
- 调用write时,数据会先写入发送缓冲区中;
- 如果发送的字节数太长,会被拆分成多个TCP的数据包发出;
- 如果发送的字节数太短,就会先在缓冲区里等待,等到缓冲区长度差不多了,或者其他合适
的时机发送出去;
- 接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区;
- 然后应用程序可以调用read从接收缓冲区拿数据;
- 另一方面,TCP的一个连接,既有发送缓冲区,也有接收缓冲区,那么对于这一个连接,既
可以读数据,也可以写数据。这个概念叫做 全双工
由于缓冲区的存在,TCP程序的读和写不需要一一匹配,例如:
- 写100个字节数据时,可以调用一次write写100个字节,也可以调用100次write,每次写一
个字节;
- 读100个字节数据时,也完全不需要考虑写的时候是怎么写的,既可以一次read 100个字
节,也可以一次read一个字节,重复100次;
3 粘包问题
- 首先要明确,粘包问题中的 “包” ,是指的应用层的数据包。
- 在TCP的协议头中,没有如同UDP一样的 “报文长度” 这样的字段,但是有一个序号这样的字
段。
- 站在传输层的角度,TCP是一个一个报文过来的。按照序号排好序放在缓冲区中。
- 站在应用层的角度,看到的只是一串连续的字节数据。
- 那么应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分,是一个
完整的应用层数据包。
避免粘包问题,归根结底就是一句话,明确两个包之间的边界。
- 对于定长的包,保证每次都按固定大小读取即可;例如上面的Request结构,是固定大小
的,那么就从缓冲区从头开始按sizeof(Request)依次读取即可;
- 对于变长的包,可以在包头的位置,约定一个包总长度的字段,从而就知道了包的结束位
置;
- 对于变长的包,还可以在包和包之间使用明确的分隔符(应用层协议,是程序猿自己来定
的,只要保证分隔符不和正文冲突即可);
对于UDP协议来说,是不存在 “粘包问题” 的
- 对于UDP,如果还没有上层交付数据,UDP的报文长度仍然在。同时,UDP是一个一个把数
据交付给应用层。就有很明确的数据边界。
- 站在应用层的,使用UDP的时候,要么收到完整的UDP报文,要么不收。不会出现"半个"的情况。
4.TCP异常情况
- 进程终止:进程终止会释放文件描述符,仍然可以发送FIN。和正常关闭没有什么区别。
- 机器重启:和进程终止的情况相同。
- 机器掉电/网线断开:接收端认为连接还在,一旦接收端有写入操作,接收端发现连接已经不在了,就会进行reset。即使没有写入操作,TCP自己也内置了一个保活定时器,会定期询问对方是否还在。如果对方不在,也会把连接释放。
(相当于,把A比作服务器端,B比作客服端,如果B断电,意味着A发送的数据不再会有ACK了,A进入超时重传,重传几次之后,A认为这个连接已经出现了严重故障了,尝试重新建立连接,如果重连失败后,就会放弃连接;
如果A断电,B不知道A是已经挂了,还是休息会再继续发数据,B就会时不时的给A发送一个小的报文,这个报文不带有数据,只是为了触发ACK,称为探测报文或者心跳包,通过这个探测报文,发现A不再返回ACK了,因此B就认为A出现了问题,就会放弃连接)
- 另外,应用层的某些协议,也有一些这样的检测机制。例如HTTP长连接中,也会定期检测对方的状
态。例如QQ,在QQ断线之后,也会定期尝试重新连接。
3. TCP小结
因为TCP即要保证可靠性,同时又尽可能的提高性能,所以相较于UDP复杂的多
可靠性:
- 流量控制(动态调整发送速度)
- 拥塞控制(动态放缩找到合适的窗口大小)
提高性能:
- 滑动窗口
- 快速重传
- 延迟应答(尽最大努力让窗口大一些)
- 捎带应答(让服务器相应捎带着内核发送的ACK一起发送,提高传输效率)
4.基于TCP应用层协议
- HTTP
- HTTPS
- SSH
- Telnet
- FTP
- SMTP
- 自己写TCP程序时自定义的应用层协议;
5. 经典面试题:基于UDP如果实现可靠传输
这个问题其实在考TCP,回答时候本质上就是在应用层基于UDP复刻TCP机制
就是在应用层引入实现这些TCP保证可靠传输的机制就可以了: