UDP标准协议文档-RFC 768
TCP标准协议文档-RFC 793
UDP协议详解
UDP协议的特点:
无连接、不可靠传输、面向数据报和全双工。
UDP协议报文结构:
关于端口号:
端口都是使用2个字节,16个bit位来表示,一个端口号的取值范围在0-65535之间。但是并不是所有的端口都可以被咱们程序猿所使用。我们自己写的程序绑定的端口一般是从1024开始的。0-1023这个范围的端口被称为“知名端口号”,即这些端口号是属于已经分配了一些知名的广泛使用的应用程序。1023以下的端口号一般是不建议使用。
关于UDP报文长度:
对于UDP来说,报头一共就是8个字节,分成四个部分(每个部分两个字节)。UDP报文长度也是用2个字节来表示的,2个字节的范围就是0-65535,换算成单位就是64kb,也就是说一个UDP数据报最大也就能够传输64kb的数据。
这个数值对于目前的传输要求来说显然是很小的,如果应用层数据报超过了64KB,对于UDP来说就需要在应用层通过代码的方式针对应用层数据报进行手动的分包,拆成多个包通过多个UDP数据报进行传输(本来需要send一次,现在需要send多次),或者换成TCP协议,因为TCP协议没有长度的限制。
关于校验和:
校验和的作用就是验证传输的数据是否是正确的。网络传输的本质就是光电信号,在传输过程中可能会受到一些干扰,就可能出现“比特翻转”的情况,即1->0 或 0 -> 1,就会造成数据改变。这样的现象是客观存在的,因此就引入了校验和来进行鉴别。
针对数据内容进行一系列的数学运算,得到一个比较短的结果(2个字节),当接收方收到这个数据之后,也会进行相同逻辑的数学运算,也会得到一个结果。如果两次校验和的结果是一样的,则证明传输的数据是正确的,反之,证明数据在传输过程中发生了改变。当然,在低端情况下,就算数据内容不一致,那么也会得到一个相同的校验和,那么这种是极小概率的,可以忽略不计。
TCP协议详解(10个核心机制)
TCP协议的特点:
有连接、可靠传输、面向字节流和全双工。
确认应答:
实现可靠传输的最核心机制。A给B发一个消息,B收到之后就会返回一个应答报文(ACK),此时A收到之后就知道刚才发送的信息已经顺利到达B了。
但是如果A给B发送了多条消息,网络传输中存在“先发后至”现象,那么B收到的信息的顺序也是错乱的,那么B返回A的应答报文到达的顺序也是可能发生变动的,这就有可能会因为顺序错乱而带来语义上的歧义。为了解决上述问题,给传输的数据和应答报文都进行编号。这样即使顺序错乱了,也可以根据序号来区分当前应答报文是针对哪个数据进行的了。
任何一条数据(包括应答报文)都是有序号的,确认序号是只有应答报文有,一条报文是否是应答报文,取决于ACK这个标志位是否是1。
TCP可靠传输能力,最主要就是通过确认应答机制来保证的。通过应答报文,就可以让发送方清楚的知道传输是否成功,进一步的引入了序号和确认序号,针对多组数据进行详细的区分。
超时重传
当发送的数据丢了或者返回的ack丢了,这种情况叫做丢包。但是对于发送方来说,就是没有收到ack,区分不了到底是哪一种情况造成的。因此,TCP就引入了重传机制,在丢包的时候重新再发一次同样的数据。TCP引入了一个时间阈值,发送方发了一个数据之后,就会等待ack,此时开始计时,如果超过了这个时间阈值还没有收到ack,就视为丢包,就重新传输。
但是会引入一个问题,由于重传,就会导致接受方同样的数据就会被收到多次,这是比较恐怖的,比如支付问题。但是,TCP对于这种情况是有处理的,TCP可以对重复数据去重。TCP存在一个“接受缓冲区”这样的存储空间(接收方操作系统内核里的一段内存),每个TCP的socket对象,都有一个接受缓冲区(其实也有一个发送缓冲区)。根据数据的序号,很容易识别当前接受缓冲区里面的重复数据,如果重复了,就丢弃后来的这份数据,保证了应用程序read读取到的数据一定是不重复的。
由于去重和重新排序机制的存在,发送方只要发现ACK没有按时到达,就会重传数据。即使重复了,即使顺序乱了,都没事,接收方都能很好的处理好(去重和排序都依赖TCP报头的序号)。
重传达到一定次数的时候,就不会再继续重传,会认为网路出现故障。接下来TCP会尝试重置连接(相当于断开重连一样),如果重置还是失败,彻底断开连接了。
小结:可靠传输是TCP最核心的部分。TCP的可靠传输就是通过确认应答+超时重传来进行体现的。其中确认应答描述的是传输顺利的情况。超时重传描述的是传输出现问题的情况。这两者相互配合,共同支撑整体的TCP可靠性。
连接管理
建立连接(三次握手)
建立连接就是通信双方各自要记录对方的信息,彼此之间要相互认同。
客户端主动给服务器发起的建立连接请求,称为"SYN",同步报文段。
所谓的三次握手,本质上是四次交互,通信双方,各自要向对方发起一个“建立连接”的请求,同时,再各自向对方回应一个ack。但是,中间两次交互,是可以合并成一次交互的,因此就构成了“三次握手"过程。中间两次的交互是必须合并的,因为封装分用两次一定比封装分用一次的成本更高。
三次握手的重要作用就是建立彼此之间的认同,验证通信双方各自的发送能力和接收能力是否正常,在一定程序上保证了TCP传输的可靠性。
如果是只有两次握手,则验证不了双方通信都是正常的。举个例子:
如果只有两次握手,那么站在男生的角度,知道了女神同意我请她吃饭。但是站在女神的角度,她不知道男生是否同意她请她吃饭。
在建立连接阶段,主要有两个状态:
LISTEN(listen):为服务器的状态,表示服务器已经准备就绪,随时可以有客户端来连接。
ESTABLISHED(established):客户端和服务器都有这个状态,表示连接建立完成,接下来就可以正常通信了。
断开连接(四次挥手)
四次挥手,和三次握手非常类似,都是通信双方各自向对方发起一个断开连接的请求,再各自给对方一个回应。
四次挥手中间的两次交互是不能合并的,之所以三次握手中间两次能够合并是因为,三次握手的中间两次能够合并是因为他俩是同一时机(SYN 和 ACK),具体来说,三次握手这三次交互过程,是纯内核中完成的(应用程序感知不到,也干预不了),服务器的系统内核收到syn之后,就会立即发送ack 也会立即发送syn。
对于四次挥手来说,FIN的发起,不是由内核控制的,而是由应用程序调用socket的close方法(或者进程退出)才会触发FIN。ACK则是由内核控制的,是收到FIN之后,立即返回ACK,而服务器的应用程序执行到对应的close方法,才会触发的FIN,这两者之间就会隔了一个时间差,具体时间差是多少取决于代码的写法。
所以,基于上述原因,FIN 和 ACK 并不完全是同一时机的,所以四次挥手中的中间两次是不能合并的。
四次挥手涉及到两个重要的状态:
CLOSE_WAIT: 出现在被动发起断开连接的一方(建立连接一定是客户端主动发起请求,断开连接可能是客户端主动发起,也可能是服务器主动发起),等待关闭(等待调用close方法关闭socket)。
TIME_WAIT: 出现在主动发起断开连接的一方,假设是客户端主动发起断开连接,那么当客户端进入TIME_WAIT状态的时候,相当于四次挥手在客户端这边已经结束了,但是此时这里的TIME_WAIT要保持当前的TCP连接状态不要立即释放。TIME_WAIT保留一段时间的连接是因为,此时最后一个ack刚刚发出去,还没有到呢,防止ack丢包。
在三次握手和四次挥手过程中都是存在超时重传的。如果是最后一个ACK 丢包了,站在服务器的视角来看,服务器是不知道是因为ACK丢了,还是自己发的FIN 丢了,所有统一视为FIN丢了,统一进行重传操作。
既然服务器可能要重传FIN,客户端就需要能够针对这个重传的FIN进行ACK响应,很明显,如果刚才彻底把连接释放了,这样的ACK就无法进行了。因此使用TIME_WAIT状态保留一定的时间,就是为了能够处理最后一个ACK 丢包的情况,能够在收到重传的FIN之后,进行ACK响应。
那TIME_WAIT具体保持多长时间,就真正释放呢?
按照一般的经验来说,定义MSL为A传输数据到B所花费的时间(经验值),那么就约定TIME_WAIT保持2 MSL。
滑动窗口
可靠性和传输效率本身来说是矛盾的,在保证可靠性的基础上,来尽可能的提高传输效率,就引入了滑动窗口。
对于基本确认应答的情况来说,每次发一个数据,都需要等,等 ack到了再发下一个。滑动窗口的本质就是不等待的批量发送一组数据,然后使用一份时间来等待着一组数据的多个ACK 。把不需要等待,就能直接发送的数据的最大的量,称为"窗口大小"。
如果出现丢包的情况,就分为两种情况了:
第一种就是ack丢了,这种情况是不需要做任何处理的,关键在于有确认序号的存在,表示从该序号往前的所有数据都已经确认到达了。
第二种就是数据丢了:
由于1001-2000丢包了,接下来2001-3000到达主机B之后,B给A返回的ACK确认序号仍然是1001(此时和你发来的这个数据序号是啥,关系不大了),会一直索要1001开头的数据,直到1001开头的数据传输过来(“快速重传-超时重传在滑动窗口下的变形”),如果下面还有数据丢包的情况,那么还会继续索要下一个,直到没有数据丢包。
流量控制
流量控制是一种干预发送的窗口大小的机制。滑动窗口,窗口越大,传输效率就越高(一份时间,等的ack 就越多)。但是滑动窗口无限大也有以下问题:
1.完全不等待ack,可靠性可能得不得保障
2.窗口太大,也会消耗大量的系统资源
3.发送速度太快,接收方处理不过来
接收方的处理能力,就是一个很重要的约束依据,发送方发的速度,不能超出接收方的处理能力。流量控制要做的工作就是根据接收方的处理能力,协调发送方的发送速率。
流量控制的实现方式就是:
根据接收方接受缓冲区的剩余大小,然后把这个值通过ack报文返回给发送方,然后发送方就根据这个值来决定接下来发送的速率是多少(窗口的大小是多少)。
由于接收方缓冲区剩余空间是一直在动态变化的,所以每次返回ack 带的窗口大小都在变化,发送方也是在动态调整。
当窗口大小为0,发送方A就要暂停发送,暂停发送的等待过程中,会给主机B定期发送窗口探测报文。这个报文不携带具体的业务数据,只是为了触发ack查询窗口大小。
拥塞控制
流量控制和拥塞控制共同决定发送方的窗口大小是多少。流量控制考虑的是接收方的处理能力,而拥塞控制描述的是传输过程中中间节点的处理能力。
根据木桶效应,应该要找出处理能力最差的节点,因为发送方和接收方的处理能力可以被衡量,但是中间节点的处理能力不容易被衡量。把中间所有的节点看作是一个大的节点,通过实验的方式,逐渐找到了一个合适的窗口的大小。
拥塞窗口和流量控制的窗口,共同决定了发送方实际的发送窗口(拥塞窗口和流量控制窗口的较小值)
延时应答
延时应答也是提升效率的机制,也是在滑动窗口的基础之上再完善的一些机制。延时应答就是在收到数据之后,不是立即返回ACK了而是稍微等会再返回。等待的时间里,接收方的应用程序,就能够把接收缓冲区的数据给消费一些。
实际上延时应答采取的方式,就是在滑动窗口下,ack 不再每一条数据都返回了,而是比如隔1条或者多条返回一个ack。实际上剩余空间的大小变化是一个复杂的过程,既取决于发送方的发送也取决于接收方的处理。
捎带应答
也是提高效率的方式,在延时应答的基础之上,引入的捎带应答。服务器客户端程序,最典型的模型,就是"一问一答"。由于延时应答机制,就导致等待ACK的过程中,B就要给A发送业务数据了,就可以让业务数据捎上这个ACK一起发过去就行了。
本来返回ACK和发送业务数据是不同的时机,但是在延时应答下,可能成为相同时机,就合并了。
TCP三次握手,本身就是相同时机,所有三次握手是一定会合并的。但是此处引入了延时应答,就和四次挥手的合并比较像了,有一定概率会被合并。
粘包问题
因为TCP是面向字节流的,所有在接收缓冲区,其实是把收到多个数据都放到一起了,那么应用程序read读取的时候,读到哪里才算是一个完整的应用层数据报呢?这就导致一次读到的数据,可能是半个应用层数据报,可能是一个应用层数据报,也有可能是多个应用层数据报。
所以解决方案其实很简单,在应用层约定好应用层协议,尤其是明确应用层数据报和应用层数据报之间的边界。有两种方式:一是约定好分割符;二是约定每个包的长度。
异常情况
传输过程中出现了不可抗力的因素,比如进程奔溃、主机关机(正常流程关机)、主机掉电、网线断开等等。
对于进程奔溃和主机关机(正常流程关机),就是进程没了,对应的PCB就没了,对应的文件描述符表就释放了。相当于socket.close(),此时内核会继续完成四次挥手。此时其实仍然是一个正常断开的流程,主机关机要先杀进程,然后才正式关机(杀死进程的过程中,也就是和上面一样触发四次挥手)。
对于主机掉电和网线断开,显然是来不及四次挥手了。
假设是接收方掉电了,发送方仍然在继续发数据,发完数据要等待ack.ack ,如果等不到就会超时重传,但是再怎么重传,也收不到ack ,重传几次,还没有应答,尝试重置 tcp连接(复位报文段RST)。显然这个重置也会失败,然后单方面放弃连接了。
如果是发送方掉电,接收方发现没数据了。没数据是发送方挂了?还是发送方要组织下语言,稍等会再发?接收方完全不知道,只能先等。接收方需要周期性的给发送方发送一个消息,确认下对方是否还工作正常,也就是发送心跳包。