(一)UDP协议
1.协议格式:
源端口,目的端口我们不多做解释,我们来说一下16位 UDP长度,因为长度只有两个字节,所以UDP的长度被限制到64kb以内,而16位检验和是确保我们传输的数据正确,如果检验和出错就会直接丢弃(我们可以用crc和md5算法来生成校验和,一般我们使用md5算法)
2.UDP特点:
1.无连接:我们客户端向服务器发起请求,不需要服务器同意就可以发送
2.不可靠传输:客户端向服务器发送请求,客户端无法知道服务器是否收到了请求
3.面向数据报:客户端向服务器发送请求的最小单元是一个数据报,不能够灵活的控制读写数据的次数和数量
4:全双工(由于和TCP一样都是全双工,所以不多做介绍)
3.UDP注意事项
我们注意到,UDP协议⾸部中有⼀个16位的最⼤⻓度.也就是说⼀个UDP能传输的数据最⼤⻓度是 64K(包含UDP⾸部). 然⽽64K在当今的互联⽹环境下,是⼀个⾮常⼩的数字. 如果我们需要传输的数据超过64K,就需要在应⽤层⼿动的分包,多次发送,并在接收端⼿动拼装
(二)TCP协议
1.TCP协议格式
源端口和目的端口标记了我们的客户端和服务器
32位序号和32位确认序号我们会在之后的一些TCP特点中讲到
4位首部长度和保留位是用来控制TCP报头长度的大小的
6位标志位:
◦ URG:紧急指针是否有效
◦ ACK:确认号是否有效
◦ PSH:提⽰接收端应⽤程序⽴刻从TCP缓冲区把数据读⾛
◦ RST:对⽅要求重新建⽴连接;我们把携带RST标识的称为复位报⽂段
◦ SYN:请求建⽴连接;我们把携带SYN标识的称为同步报⽂段
◦ FIN:通知对⽅,本端要关闭了,我们称携带FIN标识的为结束报⽂段
16位窗口大小我们会在滑动窗口,流量控制和拥塞控制时候涉及到
16位校验和:发送端填充,CRC校验.接收端校验不通过,则认为数据有问题.此处的检验和不光包含 TCP⾸部,也包含TCP数据部分.
16位紧急指针标识着那一部分数据是紧急数据
选项我们会在后面特点中说到
2.TCP的特点
1.有连接:客户端发送给服务器请求,需要服务器主动接收
2.可靠传输:客户端发送给服务器请求,能够准确知道服务器收没收到,如果没收到会进行一系列处理
3.面向字节流:客户端向服务器发送请求是通过字节流,能够灵活的控制读写数据的次数和数量
4.全双工
那TCP协议是如何保障这几个特点的呢?
1.确认应答
TCP将没有字节进行了编号即为序号,在将数据传到服务器时也会将序号传过去,而服务器会告诉客户端我收到了这个数据,返回一个ack里面有一个确认序号来告诉客户端下一个数据的序号应该是什么
2.超时重传
在我们向服务器传输数据的时候,我们可能会发生丢包的现象,导致我们长时间没有收到ack,那这时我们为了保证可靠性,我们就会进行超时重传,即重新发送数据,但是也不一定是我们的数据丢了,也可能是服务器给我们返回的ack数据报丢了,那我们仍然会重新发送数据给服务器,就会导致服务器中有很多重复的数据。
但是我们有内置的缓冲区,我们可以将缓冲区中的按照序号的顺序排序,并且根据序号去除重复的数据
我们刚说到触发超时重传是因为我们长时间没有收到ACK,那我们怎么判断这个长时间
• 最理想的情况下,找到⼀个最⼩的时间,保证"确认应答⼀定能在这个时间内返回".
• 但是这个时间的⻓短,随着⽹络环境的不同,是有差异的.
• 如果超时时间设的太⻓,会影响整体的重传效率,时间太短,会导致我们多次重复的发送同一个包
所以我们默认为500ms,在500ms以内一般情况下是足够我们接收到返回ack的,如果超出这个时间,我们认为丢包了,如果我们进行超时重传后,仍然没有收到ack那么我们就会将500ms的时间延长,延长的时间是500ms*2的指数倍,如果多次重传后仍然没有应答,我们就会与服务器断开连接。
3.连接管理
在正常情况下,我们与服务器建立连接需要进行三次握手,断开连接需要进行四次挥手
这时我们就会有疑问,为什么连接只需要握手三次,ack和syn可以合并成一个数据报发回,挥手为什么不可以合并?
实际上,挥手也可以合并,我们先了解一下为什么syn和ack可以合并,因为我们服务器在收到客户端发送的syn建立连接请求时,同一时刻我们服务器就会向客户端传回ack和syn的数据包,为了节省我们封装分用的开销,我们就把两个数据包合并成一个数据包,本质原因还是因为她们时同一时刻向客户端返回的(这里涉及到捎带应答,我们过一会会说到)
而四次挥手,我们在接收到客户端传来的fin之后,我们会立刻返回ack,但是fin数据包需要等待我们服务器执行完他对应的流程之后才能发送,那这个时间我们是无法确定的,所以在大部分情况下,我们无法合并这两个数据包
说完了这个我们再来说一下为什么要进行三次握手:
1.我们要确保连接通路的流畅
2.我们要保证客户端和服务器能够正常的接收和发送数据
3.协调一些必要的参数
我们再来看一下TCP的一些状态
CLOSE_WAIT:我们服务器在等待socket的close时的状态,⼀般⽽⾔,对于服务器上出现⼤量的CLOSE_WAIT状态,原因就是服务器没有正确的关闭socket,导致 四次挥⼿没有正确完成.这是⼀个BUG.只需要加上对应的close即可解决问题
TIME_WAIT:MSL是TCP报⽂的最⼤⽣存时间,因此TIME_WAIT持续存在2MSL的话 就能保证在两个传输⽅向上的尚未被接收或迟到的报⽂段都已经消失(否则服务器⽴刻重启,可能会 收到来⾃上⼀个进程的迟到的数据,但是这种数据很可能是错误的);
同时也是在理论上保证最后⼀个报⽂可靠到达(假设最后⼀个ACK丢失,那么服务器会再重发⼀个 FIN. 这时虽然客⼾端的进程不在了,但是TCP连接还在,仍然可以重发LAST_ACK)
4.滑动窗口
我们刚刚说TCP为了可靠性每发送一个数据包就会传回一个ack,但是这种方式在频繁的传输中非常的浪费时间
所以我们就想可不可以一次发送多个数据再由服务器传回ack(本质上就是用等待服务器发送ack的时间发送数据)
• 窗⼝⼤⼩指的是⽆需等待确认应答⽽可以继续发送数据的最⼤值.上图的窗⼝⼤⼩就是4000个字节 (四个段).
• 发送前四个段的时候,不需要等待任何ACK,直接发送;
• 收到第⼀个ACK后,滑动窗⼝向后移动,继续发送第五个段的数据;依次类推;
• 操作系统内核为了维护这个滑动窗⼝,需要开辟发送缓冲区来记录当前还有哪些数据没有应答;只 有确认应答过的数据,才能从缓冲区删掉;
• 窗⼝越⼤,则⽹络的吞吐率就越⾼(但是窗口大小不是越大越好,我们要保证可靠性,不能只关注速度)
但是如果此时发生了丢包,我们该如何进行超时重传呢?
仍然分两种情况:
1.数据包到了,但是ack丢了,此时我们不需要进行处理,在下一个ack到时,我们可以观察他的确认序号,在这个确认序号之前的所有数据包,都准确的到达了
2.数据包丢了,此时我们需要触发超时重传,当某⼀段报⽂段丢失之后,发送端会⼀直收到1001这样的ACK,就像是在提醒发送端"我想要的是 1001" ⼀样;
• 如果发送端主机连续三次收到了同样⼀个"1001"这样的应答,就会将对应的数据1001-2000重新 发送
• 这个时候接收端收到了1001之后,再次返回的ACK就是7001了(因为2001-7000)接收端其实之前就 已经收到了,被放到了接收端操作系统内核的接收缓冲区中;(这种机制被我们叫做快重传)
5.流量控制
上述我们说到利用滑动窗口可以提高我们程序的效率,但是窗口也不是越大越好,我们要保证可靠性,这才是我们使用TCP的根本,所以i我们引入了流量控制,一是为了保证不让滑动窗口太大影响可靠性,二是为了防止我们数据包发送的过快,导致缓冲区满了,这时候会发生丢包
根据接收方处理数据的能力,来控制发送数据包的速度,就叫做流量控制
服务器会将自己缓冲区剩余空间的大小放到窗口控制中,这时我们客户端就会根据这个数据来控制窗口的大小,如果缓冲区剩余空间小,我们就会将窗口变小,如果接收端缓冲区满了,就会将窗⼝置为0;这时发送⽅不再发送数据,但是需要定期发送⼀个窗⼝探 测数据段,使接收端把窗⼝⼤⼩告诉发送端
6.拥塞控制
TCP滑动窗口可以一次性传输大量数据,但是即使有流量控制,也不能保证没有问题,我们连接通路的环境可能导致数据传输缓慢,此时我们就需要控制我们传输的速度
这时我们引入了慢启动的概念,我们会先发较少的数据来探测网络通路的环境,再决定我们要传多大的数据
起初我们滑动窗口大小为一,再收到一次ack时我们会以指数倍增长,但是到之后我们增长速度过快,所以我们会设定一个阈值,一旦达到这个阈值我们就会变为线性增长。当我们发生快重传时我们之前是会重新进行慢启动重复上述过程,再之后我们进行了优化,我们会设定一个新的阈值,再从这个阈值进行线性增长
这时候我们可能会产生疑问,拥塞控制和流量控制都能控制窗口大小,那我们该遵循哪一个呢?答案是我们要遵循窗口最小的那个
7.面向字节流
创建TCP的socket时我们会在内核中创建发送缓冲区和接收缓冲区
在调用write时我们会将数据写到缓冲区中,如果数据太长就会被拆分,如果太短,我们就会进行等待,等到缓冲区长度够了再一起发送,所以在我们之前写的代码要进行手动的冲刷
在接收数据时,我们仍然时先发送到接收缓冲区,程序在通过read方法来从接收缓冲区拿数据
因为有缓冲区在,所以我们TCP的读写并不需要一一匹配,这时就可能会导致粘包问题
那什么是粘包?我们说的包就是应用层数据包,在tcp中没有UDP中的报文长度,所以站在传输层来看,我们的数据是一个报文一个报文传来的,按照序号排在缓冲区中,那在应用层角度,我们看到的只是一整串字节数据,所以我们不知道那个是一起传来的数据包。
那我们该如何避免呢?也就是分清两个包的边界即可,对于固定长度的包,我们就可以固定读取一定大小,对于不固定长度的包,我们可以在包头位置浪费一定长度来写上我们包的长度,或者包和包之间用一个明确的分隔符即可
可能还会有疑问,那UDP会不会发生粘包问题?答案是不会的,因为我们UDP会传输报文长度,我们根据长度,就可以知道我们该读取到哪里,而且站在应用层角度,使用UDP时,要么收到完整的报文,要么不接受
8.延时应答
如果我们一接到数据就立刻应答返回ack那就会导致,我们传输的很快但是服务器从缓冲区拿数据处理的也很快,就会导致一次返回的窗口大小很小,我们在滑动窗口时说,窗口越大我们进行传输的效率就越快,⼀定要记得, 窗⼝越⼤, ⽹络吞吐量就越⼤, 传输效率就越⾼. 我们的⽬标是在保证⽹络不拥塞的情况下尽量提⾼传输效率;
但是我们什么情况下不可以继续延时应答了呢?
1.我们超过了最大延时时间没应答
2.我们隔了n个包都没进行应答
9.捎带应答
我们在同一时刻或者间隔很短的时间发送多个数据包,这时我们多次发送就会导致我们封装分用的开销很大,这时我们就可以用一个包,把其他包的数据带着,一起返回,就比如我们的三次握手就是将syn和ack进行了捎带应答
10.异常情况
进程终⽌: 进程终⽌会释放⽂件描述符, 仍然可以发送FIN. 和正常关闭没有什么区别.
• 机器重启: 和进程终⽌的情况相同.
• 机器掉电/⽹线断开: 接收端认为连接还在, ⼀旦接收端有写⼊操作, 接收端发现连接已经不在了, 就会进⾏reset. 即使没有写⼊操作, TCP⾃⼰也内置了⼀个保活定时器, 会定期询问对⽅是否还在. 如果对⽅不在, 也会把连接释放
(三)TCP和UDP的对比
我们上述说了很多TCP的优点,但也不是TCP就比UDP好,因为tcp要实现可靠传输,所以势必会损耗一定的效率,这就导致了TCP和UDP有不同的使用场景。
• TCP⽤于可靠传输的情况, 应⽤于⽂件传输, 重要状态更新等场景;
• UDP⽤于对⾼速传输和实时性要求较⾼的通信领域, 例如, 早期的QQ, 视频传输等. 另外UDP可以⽤于⼴播;