1.UDP协议
UDP的特点为无连接、不可靠传输、面向数据报、全双工。
- 无连接:不需要进行连接就可以发送数据,通过UDP协议可以完成一个主机同时向多个主机发送消息。
- 不可靠传输:发送端并不能知道接收端是否收到了消息。
- 面向数据报:以数据包为单位进行传输。
- 全双工:A和B可以同时向对方发送接收数据
UDP的报文格式如下图:
上图为教科书上呈现的格式,其实际格式如下图:
以下为对其报文中的一些组成的解释:
- 源端口号:发送发的端口号
- 目标端口号:接收方的端口号
- 包长度(报文长度):表示一个UDP数据包的大小,单位为字节
- 校验和:用来验证网络传输的数据是否正确
在上述UDP报文中,报文长度用两字节存储,其范围为0~65535,如果有一个较大的数据报需要传输时,包长度就超出了最大范围,无法表示一个比较大的数据报,于是就有了TCP这种协议来更好的处理这种情况。
2.TCP协议
2.1 TCP头部格式
TCP头部格式如下图:
源端口号、目的端口号、校验和:其作用与UDP中的作用相同
序列号:在建立连接时由计算机生成的随机数作为其初始值,通过SYN包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用于解决网络包乱序问题。
确认应答号:指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后,可以认为在这个序号以前的数据都已经被正常接收。用于解决不丢包的问题。
首部长度:表示TCP报文长度,一共4比特,能表示0-15,单位为4字节,也就是说能表示TCP报头长度为0-60字节,如果不够用,还有后边的保留位。
控制位(TCP报文的核心字段):
- ACK:该位为1时,「确认应答」的字段变为有效,TCP规定除了最初建立连接时的SYN包之外该为必须设置为1.
- RST:该位为1时,表示TCP连接中出现异常必须强制断开连接。
- SYN:该位为1时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。
- FIN:该位为1时,表示今后不会再发送数据,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换FIN位为1的TCP段。
其余报头中暂未提及的部分在文章后续慢慢介绍。
TCP的特点是有连接,可靠传输,面向字节流,全双工。
- 有连接:⼀定是「⼀对⼀」才能连接
- 可靠传输:发送端能够知道接收端是否收到了消息
- 面向字节流:以字节流的方式进行传输,可以自行定义发送的字节多少,比较灵活
- 全双工:A和B可以同时向对方发送接收数据
在TCP的这些核心机制中,除了可靠传输之外,其他性质都可以在代码中体现出来,虽然可靠传输不能直接在代码中体现出来,但是这个却是TCP最核心的机制,下边我们就来了解TCP都是通过怎样的方式来完成可靠传输的。
2.2 TCP可靠性实现机制
2.2.1 确认应答
该机制为保证可靠传输的核心机制。
在发送方发出去数据之后,在接受方接收到消息后返回给发送方一个应答报文(ACK) 来表示自己已经收到了。
下边我们来通过我请女神吃牛排的例子来更好的理解这个问题:
但如果发了多条消息,可能就会出现后发先至的情况:
如果出现这种情况,将会引起不必要的误会,我们可以通过对消息进行编号的方法来解决上述问题:
上述的序号与确认序号便对应TCP报头中的序列号与确认应答号,通过编号的方式来针对哪个消息进行确认应答。
在实际的TCP传输中,并不是按“消息条数”来进行编号的,而是按照字节来进行编号
A给B发送了1000个字节,序号为1-1000,那么B给A返回的应答报文(ACK)就会带有一个确认序号——1001(表示小于1001的数据都被主机B收到了,接下来主机A应该从1001这个序号开始,往后进行传递)
2.2.2 超时重传
在网络一切正常的情况下,上述确认应答可以正常完成,但如果由于网络问题出现了丢包的问题,超时重传机制就要其效果了(超时重传是确认应答的补充)
在一个数据发送出去之后,一段时间过去并没有收到应答报文,这时发送方将会重新发送数据(当然不会无限制的重发,如果由于网络问题将会停止重发)。
触发重传的情况有以下两种:
一种是数据丢了,那么发送方在等待一段时间后重发就可以了。
另一种情况是ACK丢了,对方虽然收到了消息,但是我却收不到ACK,对于发送方的我来说,并不知道是哪种原因导致我没有收到ACK,所以我以最坏情况打算,就认为是对方没有收到数据,在一段时间过后我会重发一次。但这样对方实则会收到重复的消息,在TCP内部会进行去重操作,并返回一个和之前相同的ACK。
图解TCP/IP 超时重传示图:
2.2.3 连接管理(三次握手、四次挥手)
2.2.3.1 三次握手
TCP是面向连接的协议,所以使用TCP前必须先建立连接,而建立连接是通过三次握手来进行的。
客户端和服务器之间通过三次交互,完成了建立的过程,“握手”是一个形象的比喻。
在上图中的两侧也表明了TCP的状态,我们需要主要了解的如下:
LISTEN:表示服务器启动成功,端口绑定成功,随时可以有客户端来建立连接(手机开机,信号良好,随时可以有人给我打电话)
ESTABLISHED:表示客户端已经连接成功,随时可以进行通信了(有人给我打电话我接听了,接下来就可以说话了)
通过三次握手,可以检查当前网络的情况是否满足可靠传输的基本条件,例如我们在给人打电话时,打通电话后第一句是“喂,可以听到吗”,如果接收方听到了会回一句“听到了,你能听到我说话吗”,这一句回复就说明了我的麦克风和喇叭都是正常的,我听到后会回复“可以听到”,这一句回复验证了接收方的麦克风和喇叭是正常的,这个过程就是在验证通信双发的发送和接收能力是否都正常。
那如果两次可以吗?
两次意味着缺少最后一次,此时客户端这边发送接收能力是正常的,但是服务器这边是残缺的,服务器不知道自己的发送能力是否正常,也不知道客户端的接收能力是否正常,所以第三次握手不可少。
如果四次可以吗?
四次意味着SYN和ACK需要分开传输,降低了效率
2.2.3.2 四次挥手
通过三次握手,就让客户端和服务器之间建立好了连接,建立好连接后,就需要占用一定的系统资源来保存连接相关的信息,如果连接断开,此时之前保存的连接信息就没有意义了,对应的空间也就可以释放了。
双方各自向对方发送了FIN(结束报文段)请求,并且各自给对方一个ACK确认报文。
在三次握手中,一定是客户端主动发起的,但在四次挥手中,可能是客户端主动发起,也可能是服务器主动发起。
三次握手中,中间两次可以合并,但是四次挥手的中间两次有时候合并不了(有时候是可以合并的),不能合并的原因在于B发送ACK和B发送FIN的时机不同,B给A发的ACK是由内核负责的,而B给A发的FIN是用户代码负责的(代码中调用了socket.close()方法),如果这两个操作之间的时间差比较大就不能合并了,如果时间差比较小是又可能合并的(延时应答和捎带应答,在后边详细解释)。
我们也来认识四次挥手中两个重要的状态:
CLOSE_WAIT : 四次挥手挥了两次之后出现的状态,这个状态就是在等待代码中调用socket.close()方法,来进行后续的挥手过程
TIME_WAIT :谁主动发起FIN,谁就会进入TIME_WAIT状态。其是为了给最后一次ACK提供重传机会。
由于最后一次主动方发送ACK后可能丢包,如果丢包了被动方就会以最坏情况,重传FIN(超时重传),所以主动方需要预留一段时间来等待被动方重发FIN,预留的时间为2MSL,MSL表示报文最大生存时间。如果在等待过程中没有接收到FIN,服务器就与客户端断开连接了。
2.2.4 滑动窗口
TCP虽然可靠性是最高的机制,但是TCP也会尽可能的提高传输效率
由于确认应答机制的存在,导致了当前每次执行一次发送操作,都需要等待上个ACK的到达,大量的时间都花在了等待ACK上了。
滑动窗口的本质就是在“批量的发送数据”,一次发送一波数据,然后一起等一波ACK
比如客户端一次发送了4组数据,然后等ACK的到达(这样一份的时间就等待了多份ACK,把多份ACK的时间压缩成一份了),如果一次批量发送数据为N,统一等待一波,此时这里的N称为“窗口大小”,“滑动”的意思是,并不用把N组数据的ACK都等到了才继续往下发送,而是收到一个ACK就往下发送一组。随着ACK接连到来,接连发送新的数据,此时这个“窗口”,就在逐渐往后“滑动”。
但如果在滑动窗口的背景下出现了丢包问题,该如何进行重传呢?
丢包分为两种情况:
一种是ACK丢了,这种情况不需要处理,这是因为ACK确认序号的含义是该序号以前的数据都已经收到了,也就是说在发送方收到5001的时候,意味着1-5000的数据都确认收到了,及时ACK(确认应答号)3001、4001都丢包了,只要收到5001,就涵盖了3001和4001表达的信息。
另一种情况是数据丢了,这就必须要进行处理了,如下图,由于1001-2000这个数据丢了,B就在反复索要1001这个确认应答号,即使A已经在给B发后边的数据了,但仍然再索要1001的确认应答报文,当索要若干次以后,A就会触发重传。
在A重传1001-2000之前,B的接收缓冲区如下图所示,数据一直在接收但是有缺口,在A完成重传后把接口给补上就行了(其他已经到了的数据就不必再进行重传),接下来B就向A索要7001开始的数据就行,这种机制也被称为高速重发控制。
2.2.5 流量控制
在滑动窗口中,窗口越大,传输效率就越高,但是我们不仅要考虑发送方的传输速度,还需要考虑接收方的处理速度。
流量控制的关键,就是能够衡量接收方的处理速度,根据接收方接收缓冲区的剩余空间大小,来衡量当前的处理能力。
如果剩余空间比较大,就认为接收方的处理能力比较强,就可以让A发的快点
如果剩余空间比较小,就认为接收方的处理能力比较弱,就可以让A发的慢点
在TCP头部中,有窗口大小一栏,接收方在ACK报文中通过窗口大小来告知发送方剩余空间的大小。
《图解TCP/IP》流控制示意图:
其中的窗口探测报文中不含任何数据,只是为了触发ACK,来知道当前窗口的大小是多少。
2.2.6 拥塞控制
也是滑动窗口的延申,用于限制滑动窗口发送的速率
拥塞控制衡量的是发送方到接收方的链路之间的拥堵情况(处理能力)
A能够发多快,不光取决于B的处理能力,也取决于中间链路的处理能力。
A一开始以一个比较小的窗口来发送数据,如果数据很流畅的就到达了,那就逐渐加大窗口大小;如果加大到一定程度之后,出现了丢包的情况(丢包意味着通信链路出现拥堵了),这个时候就需要减小窗口。
通过反复的增大/减小窗口,就逐渐摸索到了一个比较合适的范围,拥塞窗口就在这个范围中不断变化,达到“动态平衡”。
拥塞窗口的具体变化如下图:
2.2.7 延时应答
延时应答相当于流量控制的延申,流量控制的目的是为了使发送方不要发送的太快,而延时应答在此基础上,希望窗口能更大一些。
在发送方询问接收方窗口大小的时候,不立即做出回答,而是稍等一下再回答,这样接收方又可以多处理一部分数据,窗口就又大一些,这个操作就是在有限的情况下,又尽可能的提高了一点传输速度。
2.2.8 捎带应答
捎带应答又是延时应答的延申。
因为延迟应答的存在,导致接受方的ACK不一定是即时返回的,如果延时应答导致ACK的返回时机和应用代码中返回的响应时机重合了,就可以把这个ACK和响应数据合二为一(就像四次挥手中第二三次挥手过程,ACK和FIN就有可能同一时机返回)
2.2.9 粘包问题
TCP粘包粘的是应用层数据包(不仅是TCP存在粘包问题,其他面向字节流的机制比如读文件,也存在粘包问题)
在TCP接受缓冲区中,若干个应用层数据包混在一起就分不出来谁是谁了,如果想要解决这个问题就需要在应用层中加入包的边界,比如:约定每个包的结尾以;结尾。
2.2.10 TCP异常处理
1.进程终止
在进程毫无准备的情况下,突然结束进程。
TCP连接是通过socket来建立的,socket本质上是进程打开的一个文件,文件其实就存在于进程PCB里的文件描述符表
每次打开一个文件(包括socket),都会在文件描述符表里增加一项
每次关闭一个文都会在文件描述符表里删除一项
如果直接杀死进程,PCB也就没有了,里面的文件描述符表也就没有了,此处文件相当于“自动关闭”了,这个过程和手动调用socket.close()一样,都会触发四次挥手
2.机器关机
如果按照操作系统约定的正常流程关机,会让操作系统杀死所有进程然后关机,四次挥手的过程依旧会执行。
3.机器断电/网线断开
当电源或者网线直接断开时,操作系统根本来不及反应,那么四次挥手也无法执行
当接收方断电或者断网时,发送方会尝试重新连接,重连失败一定次数,就会放弃连接。
当发送方断电或者断网时,接收方会发送一个探测报文,触发发送方的ACK,如果没有反应,接收方就认为发送方出现了问题。
3.TCP/UDP总结
如果开发对可靠性有一定要求时,使用TCP(日常开发中的大多数情况都是基于TCP);如果开发对可靠性要求不高,对于效率要求更高,使用UDP(机房内部的主机之间的通信)
UDP如何实现可靠性呢?其实UDP实现可靠性是TCP的复刻,只是按照相同的思路在应用层完成就可以了。