目录
UDP协议
如何向上交付
如何解包
UDP协议的特点
UDP是全双工的
UDP面向数据报
TCP协议
如何向上交付
如何解包
4位首部长度
TCP的可靠性
序号与确认序号
16位窗口大小
6位标记
为什么需要6位标记
ACK
SYN
三次握手
RST
URG 16位紧急指针
PSH
FIN 四次挥手
TIME_WAIT
TCP的可靠性
确认应答
超时重传
滑动窗口
ACK丢包
快重传
理解滑动窗口
流量控制
拥塞控制
拥塞窗口
滑动窗口大小
捎带应答
延迟应答
TCP报头及周边知识总结
TCP面向字节流
TCP的粘包问题
TCP异常问题
在前面,我们写的套接字代码,或者是 http 服务,这些都是应用层,而我们今天看的是传输层协议。
传输层协议是在操作系统中实现的,而我们一般使用的传输层协议就是 UDP 和 TCP 协议。
我们前面说了,网络是分层的,所以我们每一层解决的问题是不同的,所以需要分工完成,但是完成后,需要将处理后的数据交付给上层,而且我们每一次层都会加上自己层的报头,所以我们后面的每一层,我们都需要知道,如何向上交付,还有就是如何解包,同样UDP和TCP也是一样的,也需要解决这两个问题,同时我们还会了解传输层协议是为了解决什么问题。
UDP协议,在我们学习的过程中,我们就说了UDP协议是比较简单的,而且UDP协议的特点就是无连接,面向数据报,不可靠。
我们先看一下UDP的报头是什么样子的!
这就是UDP协议格式。
我们现在先回答第一个问题:如何向上交付?
对于UDP而言,它向上交付就是给应用层,也就是我们之前写的代码,都属于应用层代码,那么如何向上交付呢?
我们知道,当我们准备网络通信的时候,我们首先会创建一个sock,对于然后我们就需要bind,将进程与端口号bind在一起,所以每一个网络通信的进程都会有一个或者是多个端口号,那么也就是进程和端口号的比就是1:n的,所以一个端口号能对应一个唯一的进程。
那么既然唯一的端口号可以找到唯一的一个进程,所以我们知道要向上交付给哪一个进程我们只需要找到接收主机的端口号即可。
而UDP协议里面就有接收方主机的端口号,也就是目的端口号!
所以我们只需要找到与目的端口号bind的进程,然后交付给这个进程,那么我们就可以完成向上交付。
但是我们向上交付前,需要去掉我们当前层的报头,也就是解包,那么我们应该如何解包呢?这就是我们需要讨论的下一个问题!
我们可以观察上面的UDP协议,我们发现上面的UDP协议由两部分组成:
1.UDP的报头
2.有效载荷
我们发现,UDP的报头是固定的,8字节,那么我们只需去掉接收到的UDP协议的数据,然后去掉前面的8字节的数据,那么就可以去掉UDP协议的报头了。
但是这就结束了吗?没有,因为我们还需要将有效载荷交付给上层,那么我们知道有效载荷的长度吗?我们是知道的,在UDP协议里面由一个UDP长度,而UDP长度就是表示的是这个UDP报文的长度,所以我们只需要将UDP长度减去8字节,我们就知道该UDP报文的有效载荷的长度了。
所以我们就可以将有效载荷交付给解包出来,然后交付给上层!
但是在UDP协议里面还有一个字段,就是UDP的校验和,这个字段是什么意思呢?
当UDP报文到达的时候,我们是需要验证UDP报文的,这就是需要UDP的校验和,所以UDP的校验和就是为了验证UDP报文。
我们说了UDP的特点好多次,其中我们还说了UDP是全双工的,以及UDP是面向数据报的,那么我们如何理解呢?
首先从结论来看,我们在写关于UDP的代码的时候,我们发现在写UDP代码的时候,我们不论是读还是写,我们都是使用的是一个 socket 但是当我们在管道的时候,我们使用文件描述符的时候,我们只能读或者写,但是当我们在写UDP代码的时候,我们是既可以读也可以写的,也就是在读的时候也可能是在写的,而这就是全双工。
这里只是为了介绍全双工是什么,而TCP也是全双工的,我们到了TCP的时候我们在说全双工记忆为什么可以全双工。
UDP面向数据报是什么?我们前面说了面向数据报就是当一个主机发送了一个数据报的时候,那么对端主机就需要读一次,而不能多次读,还有就是当对端主机发送了十个UDP的数据报,那么对方主机就必须接收十次,而这就是面向数据报。
那么UDP为什么是面向数据报呢?我们在看到UDP协议的时候,我们发信UDP是定长的。
首先是UDP协议的报头是定长的,UDP报头是固定的8字节,还有就是UDP的正文数据,UDP报文的正文数据也是可以计算出来的,就是UDP的长度减去8字节,就是UDP的有效载荷长度,所以我们读取UDP报文的时候,需要一个报文一个报文的读取。
UDP协议是比较简单的,而且当UDP报文丢包了,那么双方主机也是不知道的,所以UDP是不可靠的。
那么TCP的特点是什么呢?
1.可靠
2.面向连接
3.面向字节流
那么我们先看一下TCP协议的格式是什么样子的!然后我们在谈TCP协议的可靠性是靠什么维持的。
首先关于TCP我们还是需要解决两个问题:
1.如何向上交付
2.如何解包
其实如何向上交付,这个问题我相信是可以回答的,首先TCP的前面的4字节和UDP的前面是一样的,而如何i选哪个上交付已经在UDP那里已经明确说明了。
因为是传输层,所以向上交付就是给应用层,也就是所谓的进程,所以我们需要找到对应的进程,而对应的进程一定bind了一个端口号,而目的端口就是bind的端口号,所以我们只需要找到bind目的端口号的进程即可,这样就可以完成向上交付。
如果我们想要解包,那么我们就是需要去掉TCP的报头数据,那么在UDP那里我们是怎么去掉的呢?
因为UDP是固定长度的,UDP的报头只有8字节,所以我们只需要去掉前面的8字节也就可以去掉UDP报头了,就可以将有效载荷交付给上层了,那么TCP呢?
TCP并没有说它的报头的长度是多少,那么我们如何知道它的长度呢?
这就需要TCP协议里面的一个字段了:4位首部长度
4位首部长度是一个4个比特位的一个数据,可以从 0000 -> 1111 也就是15。
而TCP前面我们是可以知道是多少大小的,也就是 20 字节,而4位首部产长度就代表了TCP报头的长度,但是4位首部长度最高只有15,而TCP前面的一些字段就已经20字节了,明显是不够的!
其实4位首部长度是有单位的,而单位就是4字节,所以我们就可以计算一下TCP的报头最大时多少字节:4 * 15 = 60(字节),所以TCP的报头最大长度就是60字节,我们也可以计算一下4位首部长度的基本大小,x = 20 / 4 ,所以我们计算得到4位首部长度的在正常情况下就是5,所以实际上4位首部长度就是 0101 -> 1111。
那么现在我们知道4位首部长度后,我们就可以知道TCP报头的大小吗?知道的!
我们可以先读取TCP报文的前20个字节,然后我们在读取4位首部长度,我们经过计算,我们可以得到TCP报头的总共大小,然后我们在读取总共大小减去20字节,这样我们就可以将TCP报头全部读取出来了,后面就剩下读取有效载荷了。
但是我们现在知道TCP报文的总共大小吗?不知道!这个问题先遗留下来。
既然我们可以向上交付,解包了,那么我们在谈一下TCP的特新,可靠性。
为什么说TCP时可靠的呢?那么它的可靠又时通过什么来维护的呢?为什么会不可靠呢?不可靠的原因是什么呢?
现在我们讲一个故事:
现在有一个小美,还有一个小帅,他们两个距离比较远然后在聊天。
小美问小帅说你在干嘛呢?
小帅只能看到小美在动,但是听不清楚在说什么,所以小帅问你说什么?而小美也没有听到!
而小美这边,它不清楚小帅是否听到了她的话,所以小美不知道该激素说还是将之前的话再问一遍。
那么对于计算机而言为什么会不可靠呢?因为当两台主机距离够远的时候,那么两台计算机的数据就可能会丢失,所以发送端主机并不能确定对端主句是否收到了消息,所以不可靠的原因就是因为距离太远了。
那么我们需要怎么做呢,怎么做才能让对方知道我们收到了对方的消息。
还是刚才那个故事:
现在小美问小帅你在做什么?
小帅听到后,回了一句,收到!
此时当小美听到收到后,小美就知道小帅听到了她的消息,所以小美此时可以继续发问。
但是小帅也可以回答说:”我现在在写作业“!
此时小美听到后,小美也就知道了小帅听到了她的问题,所以在回答的时候是可以确认收到对方的数据的。
那么此时小美收到数据后,小帅不知道小美是否收到了数据。
但是此时小美回答到,那你在写什么作业?
小帅听到这个回答后,小帅也就知道小美收到了数据,所以就可以继续对话。
但是如果是这样的话,那么最后一句消息,双方不知道对方是否收到了没有。
而主机也一样,可以通过确认对方的数据,来表示已经收到了对方的数据。
而主机就是通过序号与确认序号来表示是是否收到对方的数据!
当对方主机发送了数据,该数据的序号位 1000,此时当对端主机收到后,对端主句会在给发送端主机发送数据的时候的时候TCP的报头里面的确认序号就会写 1001 表示1001序号之前的数据都收到了。
所以就可以通过序号与确认需要来表示是否收到了对方的数据。
而这种机制称为确认应答机制!
而实际中在发送数据的时候,其实不是一次发一条数据,而是一次可能发一批数据!
也就是一次可能会发送 2000 3000 4000 5000 等数据,而对端主机也是需要一一确认应答的,所以此时接收端主机就会发送来 2001 3001 4001 5001。
但是在再接的时候,可能由于网络问题,接收的顺序是不一样的。
那么如果接收顺序和发送数据不一致,那么我们认为这样是否是可靠的呢?我们认为这样也是不可靠的!
例如:现在要布置寒假作业。
你们老师说,你们现在把滕王阁序抄100遍,然后又说了一句算了,不要抄了,过了一会又说了一句你们把出师表抄100遍。
但是此时到你们微信里面收到的是 1.你们把滕王阁序抄一百遍 2.你们把出师表抄100遍 3.算了不要抄了
那么等开学的时候,你们什么都没有写,此时是不是就是有问题的,所以即使是顺序不一致,那么也不是可靠的,所以TCP还需要解决这个问题!
那么如何解决呢?
还是序号,因为在发送数据的时候,是有序号的,所以当我们在收到数据的时候,听过序号对收到的数据排序,当排序之后,那么我们就可以得到正确的顺序了,所以就可以解决顺序不一致问题。
既然我们可以确认可靠性了,那么我们就有疑问了,我们为什么一定需要两个序号呢?我们不能只有一个序号吗?
当我们发送数据的时候,这个序号表示序号,当我们返回数据的时候,这个序号表示的是确认序号不可以吗?
我们知道TCP是全双工的,那么也就是TCP可以一边收数据,一边发送数据,那么如果只有一个序号的话,那么在收数据的时候,发数据该怎么办呢?所以我们是需要两个序号的,一个序号一个确认序号,就是为了让我们在发数据的时候,也可以收数据!而且在发送数据的时候,也可以确认应答。
那么下面我们在回答一个问题,为什么TCP可以是全双工的?
我们在UDP的时候说了在TCP这里回答这个问题,那么是为什么呢?
在回答这个问题之前,我们先做一点背景知识!
我们之前学的 write/send read/recv 这样的网络里面使用的接口,我们看起来好像当我们调用这两个接口的时候,我们的数据就直接发送到网络里面了,然后被对方主机使用 read/recv 这样的接口从网络里面读取到了,但是实际上并不是这个样子的。
实际上,write/send read/recv 这样的接口其实是拷贝接口,为什么要这样说呢?
其实在传输层是有自己的缓冲区的,而且并不是一个缓冲区,而是有两个缓冲区:
1.发送缓冲区
2.接收缓冲区
假设现在应用层有数据需要发送到对端主机,此时我们调用 write/send 接口,我们并不是将数据直接发送到网络了,而是我们将数据拷贝到发送缓冲区,当我们将数据拷贝完毕后,那么什么时候发送,一次性发送多少,那么就不需要我们来操心了,后面就需要操作系统的TCP协议来关心了,当拷贝到发送缓冲区后,我们就认为我们将数据发送了。
同样接收也是一样的,对方主机调用 read/recv 接口的时候,并不是从网络中读取数据,而是从接收缓冲区中读取数据,我们的操作系统发现有数据发送给自己的时候,那么就会将数据保存到接收缓冲区,等待应用层来读取数据。
那么如果现在是两台主机呢?如果是两台主机那么就是有两对发送缓冲区,和接收缓冲区的,如果是这样的话,说明这两台主机是可以一边向发送缓冲区里面拷贝数据的,还可以从接收缓冲区里面向应用层拷贝数据,所以就是全双工的。
现在我们问一个问题,当我们发送数据太快的时候,如果对方接收不过来怎么办?
那么如果对方接收不过来,那么是不是可以直接丢弃报文呢?因为TCP有可靠性的保证,所以就可以直接丢弃报文这是正确的吗?
当一个报文千里迢迢到达了对端主机,但是没有什么原因然后就被丢弃了,那么这个报文有错吗?没有错,那么我们要怎么解决呢?我们是不是可以控制我们自己的发送速度!那么我们怎么知道当前到底是需要快一点还是慢一点?
这就需要16位窗口大小了。
现在又一个问题,那就是是什么决定了我们的发送速度快一点还是慢一点呢?是对端主机的接收缓冲区!
那么我们又如何知道对方主机的接收缓冲区的大小呢?当我们每发送一个报文的时候,我们就需要将自己的接收缓冲区的大小填到接收缓冲区中,然后当对方主机解析了我们的TCP报文的时候,对方就知道我们的接收缓冲区的大小,然后就可以控制发送的速率,如果我们当前的接收缓冲区的剩余大小还有很多,那么我们当前就可以发送速率快一点,如果当前的接收缓冲区的大小以及剩下一点了,那么我们就不能发送数据,或者是发送速率要慢一下。
16位窗口大小简而言之就是谁发送报文,那么就把自己的接收缓冲区的剩余大小填进去即可。
TCP的报头里面还有其他字段,而16位校验和我们就不说了,就是当报文到达的时候,会根据16位校验和做检验,如果没有问题的话,那么就可以继续解析向上交付,如果又问题的话,那么就可以直接丢弃!
还有就是选项,选项也是报文里面的一部分,但是选项的大小是需要看4位首部长度的,选项我们也不说了。
还有就是6位保留,6位保留是为了后面的扩展,所以6位保留也不说了。
关于TCP报头里面,我们还剩下几个字段,其中我们打算将后面的几个字段连着讲,因为单独讲的话并不好说明!
首先我们先说这个6位标记,在说6位标记的时候,我们就会穿插着说其他的字段。
其实在不同的版本中,这些标记的个数是不同的,但是6位是最标准的,所以我们就采用6位来说明!
我们知道在一台服务器中是有很多的TCP的连接的,那么这些连接不一定是相同的,为什么这样说呢?因为在服务器中,有一些连接可能是刚到,所以这个连接可能是需要连接,那么还有的连接时早就以及连接上了,所以就是普通的报文,那么还有一下连接是需要断开的,因为数据以及发送完了,所以需要断开连接,那么这些都是属于TCP的报文,其中由于连接想要做的事情是不同的所以一定是需要不同的处理,所以我们就需要区分不同的连接,所以就需要这些标记来区分连接来的目的!
6位标记就是为了区分报文的目的!
下面我们打算先说几个比较可以理解的标记,然后我们在说标记的时候在穿插的说其他的,这样也可以根号的理解标记!
ACK:该报文时用来确认应答的,所以该报文时确认上面的报文收到了。
FIN:FIN其实就是finish,表示结束的意思,所以这个字段就是表示该报文时断开连接的。
SYN:由于TCP时面向连接的,所以在通信之前时需要连接的,该字段表示该报文是请求连接的。
前面这三个是比较简单的,我们先说一下这三个字段。
ACK 其实是不需要我们多说的,因为我们前面已经说了TCP是需要可靠性的,而TCP保证可靠性的最终要的一个手段就是确认应答机制,而ACK就是用来确认之前的报文已经收到了。
而在这里我们需要理解一个点:当虽然我们这里说的是ACK是确认对方发送的数据是已经被收到了,但是我们这里发送的数据,并不是只有一一个ACK,或者是其他的标记位,而是我们每一次发送数据都发送的是一整个TCP报文。
而ACK标记位也不是只需要ACK标记为,ACK是需要和32位确认序号一起使用的,当ACK标记被设置的,我们才关心32位确认序号,但是在TCP通信的过程中,ACK标记基本都是被设置的,因为在不断的通信,所以TCP也是需要不断的确认收到了之前的数据,所以ACK基本就是都被设置了。
下面我们说SYN这个标记位,在说这个标记位的时候,我打算就是简单的说一下这个标记位是做什么的,我们想说TCP在连接的时候的细节!
我们知道TCP通信的时候是需要建立连接的,那么是什么时候建立连接呢?
首先我们知道一般服务器是需要设置监听套接字,然后 accept 的,而客户端时需要 connect 的,那么也就是说客户端时需要主动向服务器发起连接请求的!
那么在服务器 listen 的时候连接建立好了吗?没有!为什么因为 listen 套接字是为了接收在网络中谁来连接服务器的,所以在 listen 的时候是没有建立好连接的。
当客户端调用 connect 的时候,客户端向服务器发起连接请求,此时服务器就需要来接收这个连接!
在TCP建立连接的时候,并不是说只要客户端给服务器发送了SYN那么连接就建立好了,而是需要双方主机都完成”三次握手“。
首先我们说什么是三次握手!
首先我们可以看一下三次握手,其实我们看这个图我们也可以明白为什么较三次握手!
首先我们知道客户端是需要主动向服务器发起连接请求的,此时客户端调用 commect 函数,然后客户端性=向服务器发送了一个报文,此时报文里面的 SYN 标记位被设置,当服务器接收到的时候看到是 SYN 那么服务器就明白了,是有客户端向想连接我了,此时服务器就同意建立连接,然后也向客户端发送了一个 ACK + SYN 首先是 ACK 表示是确认上面的报文收到了,然后还有 SYN 表示服务器也想建立连接,等客户端收到服务器发送的数据的时候,客户端就会发送 ACK 表示收到了服务器的数据,也就是同意建立连接,等最后一条 ACK 被服务器接收到的时候,此时服务器也就认为连接建立好了。
其中TCP是有状态的,当客户端第一次发起 SYN 的时候,此时客户端就会进入 SYN_SENT,此时服务器在 accept ,服务器发现了一个连接想建立连接,然后此时就会发送 SYN + ACK 想客户端发送过去,此时客户端接收到后,客户端看到服务器也想建立建立,然后发送出去 ACK 此时 connect 函数返回,然后客户端认为连接建立好了,最后当服务器看到客户端的 ACK 达到后服务器调用的 accept 函数返回,此时服务器也就认为连接建立好了。
这就是三次握手,但是这时候就又有疑问了:
为什么是三次握手?不能是一次吗?两次不可以吗?四次不可以?
首先TCP三次握手是有原因的,首先我们需要理解一下连接!
连接是什么?我们知道在服务器中一定需要给大量的客户端提供服务,那么也就是意味着一定是有大量的TCP服务的,既然有大量的TCP服务,那么是不是也就一定有大量的连接,那么这么多连接要不要操作系统管理呢?需要!
那么如何管理?先描述,在组织!
但是我们今天并不像知道如何描述,如何组织,我们只需要知道操作系统是需要管理这些连接,那么既然是管理那么需要消耗资源吗?需要消耗资源(内存 + CPU)。
现在我们得出一个结论,就是奥做系统管理大量的连接是需要消耗资源的!
现在我们开始回答为什么是三次握手:
首先我们先回答为什么不是一次握手?我们可以想一下,如果是一次握手,那么是什么样子的?如果是一次握手的话,那么就是客户端向服务器发送了一个 SYN 然后服务器就认为连接建立好了,那么如果此时一个黑客拿着一台主机向服务器在一秒中发送了以一亿个SYN报文呢?此时服务器就瘫痪了,所以我们知道,如果是一次握手,那么客户端就可以向服务器发送一个SYN然后服务器就认为连接建立好了,然后客户端可以不管这些连接,然后就可以轻松的攻击服务器!
那么为什么不能是两次呢?首先如果是两次的话,那么其实和一次是没有什么区别的,为什么这样说呢?还是客户端向服务器发送 SYN ,那么当服务器接收到 SYN后,然后服务器就会发送出 ACK 确认收到了这个报文,那么客户端可以对服务器发送来的 ACK不管吗?可以的,所以此时客户端依然可以发送大量的 SYN,然后对服务器发送的 ACK不管,此时还是服务器上挂满了大量的连接,最后当服务器撑不住的时候就会瘫痪,所以对于两次握手和一次握手最后的成本都会被嫁接到服务器身上,而客户端没有成本!
那么四次可以吗?其实是偶数次的话,最后的成本都是会嫁接到服务器身上的,我们还是以上面的例子为例,当客户端给服务器发送 SYN 的时候,服务器发送了 ACK + SYN ,然后客户端在发送 ACK 此时服务器收到后,继续发送一个 ACK给客户端,但是这又是最后一个报文,所以客户端即使收到了也可以对这个报文不管,所以此时连接还是对于客户端来说没有挂接上,但是对于服务器来说连接已经建立好了,此时对于服务器来说,还是需要消耗资源来管理这些没有客户端的连接!
那么三次可以预防这种情况吗?三次是可以的!首先还是客户端发送数据给服务器,此时服务器收到 SYN 后,服务器发送 ACK + SYN 后,被客户端收到了,此时客户端还需要发送一个 ACK ,当客户端发送出去1 ACK后客户端认为连接建立好了,而服务器需要收到 ACK 后才认为连接建立好了,此时客户端身上就可以嫁接上和服务器相同的成本,而且如果 ACK丢失的话,那么服务器还可以不用建立连接,所以三次是可以的!
那么还有为什么是三次握手呢?
由于TCP是全双工的,所以当通信的时候是需要验证一下双方主机是都是全双工的,如果不是全双工,那么一定是不能完成三次握手的,所以三次握手还哟一个原因就是验证双方主机是全双工。
还有就是验证主机是健康的,如果双方主机有一台或者是双方都是有问题的,那么很有可能接收不了报文,所以还就是为了验证双方主机是健康的。
三次握手总结:
预防 SYN 洪流
验证双方主机的全双工
验证双方主机是健康的
上面已经基本把三次握手的细节说完了,那么我们下面提问,三次握手一定会成功吗?不一定!
不是说TCP协议是可靠的吗?那么三次握手为什么不一定会成功呢?TCP的可靠性是基于连接建立号之后才是可靠的,而三次握手的时候连接还没有建立好,所以三次握手是可能会失败的,那么如果三次握手失败了怎么办呢?
我们要知道三次握手如果没有建立成功的话,那么服务器是不会主动发送数据给客户端的,所以当连接建立失败,服务器是不会发送数据的,那么客户端可能给发送数据吗?可能!
当客户端第一次给服务器发送 SYN 的时候,服务器收到了 SYN 后,给客户端返回了 SYN + ACK ,然后客户端收到了该数据,此时客户端会给服务器继续发送 ACK,当该 ACK 发送出去后,客户端就认为连接建立好了,但是此时服务器还没有收到 ACK,所以服务器不认为连接建立好了,只有当服务器收到 ACK 后,服务器才会认为连接建立好了,那么当ACK丢包了呢?丢包了说明服务器认为连接还没有建立好,但是此时客户端认为建立好了,然后此时如果服务器给客户端发送数据,此时服务器就有疑问,连接不是好没有建立好吗?为什么直接发送数据呢?所以此时服务器并不会给客户端服务,而是给客户端返回一个 RST 的报文,该标记位表示重置连接,当客户端收到该报文后,就知道前面服务器没有建立好连接,所以就会重新建立连接!
RST 是reset 的缩写,也就是重新设置,一般当连接断开后,然后直接发送数据,就会收到该报文,并不是说只有当建立连接失败后才能设置,有些服务器,会因为客户端太久没有响应,所以就会断开连接,所以此时客户端如哦在断开连接后发送数据,那么此时客户端就会收到 RST 标记位,所以此时客户端就知道,需要重新建立连接。
该单词是 urgent 的缩写,表示紧急的,由于TCP是需要可靠的,而顺序也是需要是有序的,所以当使用TCP发送数据的时候,是按序到达的,那么如果现在有一个紧急需要处理的任务呢?
此时就需要用到 URG 标记位了,而该标记位是需要和 16 位紧急指针一起使用的。
当设置了该标记位,那么此时收到数据后就会先处理 16 位紧急指针指向的数据,但是 16位紧急指针只能指向一个字节的数据,所以设置了紧急指针,也只能提前处理一个字节的数据!
前面我们提过,TCP是可靠的,那么当对端的接收缓冲区的剩余大小很少呢?
如果对端主机的剩余接收缓冲区的大小很少,那么此时TCP就需要控制发送的速度,那么还有问题,如果对端主机甚至不接收数据呢?那么服务器主机就是不发送数据了吗?当然不是,如果对端主机的接收缓冲区的大小变小后,或者是已经满了怎么办呢?那么此时发送端主机就需要发送 PSH 标记,表示催促对端主机快点读取数据, PSH 表示 PUSH,表示就是让快点读取数据,如果对端主机没有读取数据,所以服务器也可以发送只有报头的报文,此时可以携带 PSH 的报头,而这样也可以让接收端主机也发送一个报文,让服务器知道接收缓冲区的大小!
FIN前面我们已经说了,是在关闭连接的时候需要携带的标记,那么在连接关闭的时候我们称之为“四次挥手”那么”四次挥手“的细节是什么呢?
如果是客户端向断开连接的话,那么首先客户端就会发送 FIN 给服务器,那么此时服务器可以就会接收到这个数据,然后发送回去 ACK。
此时服务器可以选择断开连接,也可以不断开,但是一般都是会同意断开连接的,因为这都是操作系统自动的,所以此时服务器也就会发送 FIN 表示我们也想和你断开连接,然后此时客户端收到数据后,就会响应回去ACK表示同意断开连接。
那么这就完了吗?没有,TCP是有状态的,那么此时的状态时什么呢?
当客户端第一次发送 FIN 的时候,客户端就会进入 FIN_WAIT_1, 当服务器接收到后,此时服务器就会进入 CLOSE_WAIT 的状态,那么当服务器给客户端发送 FIN的时候,服务器又会进入 LAST_ACK,也就是最后的确认阶段,然后当客户端收到 FIN 后客户端进入 FIN_WAIT_2 状态,然后响应一个 ACK 此时客户端就任务连接已经断开了,客户端就会进入 TIME_WAIT 状态,然后当服务器收到ACK后,服务器的连接就正式关闭!然后等过一段世间后(2MSL),客户端也就关闭了连接。
在之前我们做实验的时候,我们服务器有时候断开连接后,此时立马连接就会显示 bind 错误,为什呢?
当断开连接的时候,如果客户端先发起的 FIN 那么客户端就会进入 TIME_WAIT 状态 TIME_WAIT 状态的时候,其实连接没有断开,知识客户端认为连接已经断开了,但是在 TIME_WAIT 的时候,客户端时不会发送数据给服务器的,那么为什么不能立马所以我们也就是知道进入 TIME_WAIT 状态为什么不能立马连接了,因为此时连接还没有断开,所以一旦 bind 就会失败!
那么为什么需要进入 TIME_WAIT 状态呢?
根据官方说的是,进入 TIME_WAIT 状态是为了网络中的历史数据消散,所以需要进入 TIME_WAIT 世间,而TIME_WAIT 的世间事 2MSL 那么什么事 MSL 呢?MSL 世间就是当一个报文从这段发送到另一端的最大时间,而 2MSL 就是数据报可以发送两次,但是我们可以理解一下这个 TIME_WAIT时间,当服务器没有收到最后的 ACK 怎么办呢?服务器就会继续发送 SYN 给客户端,此时客户端就会想,我不是给你发送了 ACK 吗?此时客户端就明白了 丢包了,所以客户端就会重新发送 ACK 给服务器,而这刚好事 2MSL 时间!
上面就是TCP 的报头,我们下面看一下TCP的可靠事靠什么来维护的!
TCP最大的可靠性就是依靠序号机制来维护的,而确认应答就是基于序号机制的一种可靠性的处理方法! 我们在前面已经说过了确认应答机制的,当发送端主机发送一个报文后,此时接收端主机必须ACK。
但是不管是数据报,还是发送端主机的ACK都可能会丢失,那么丢失了怎么办呢?这个我们后面说,我们在看一下TCP的确认应答机制!
TCP的确认应答机制,首先事发送端主机在报文中的32位序号里面写入该报文的序号,当接收端主机在ACK的时候,必须写接收到报文的序号+1 也就是确认序号,这里的确认序号表示的是在确认序号之前的序号都收到了!
我们一直在说序号,那么我们又应该如何理解序号呢? 我们前面说了,在传输层是有自己的缓冲区的,而缓冲区实际上就是一段 char 类型的数组,既然是数据,那么天然就是有下标的,那么这些下标也就是序号了!
TCP通过确认应答机制来实现最主要的可靠的,那么如果当报文丢失了呢? 如果报文丢失了,那么不论是发送端主机的报文丢失,还是接收端主机的 ACK 丢失,那么此时发送端主机就会收不到 ACK,如果收不到的话,就会考虑是不是报文丢失了,如果是自己的报文丢失了,那么就会重传,然后被对端主机收到,对端主机就会响应 ACK。 那么如果是对端主机的ACK丢失了呢? 如果是对端主机的ACK丢失,那么就会因为超时重传机制导致了接收端主机的数据重复问题,那么这样有问题吗?其实是没有问题的,因为TCP是有序号机制的,既然是有序号机制,那么那个报文先到的,那个报文后到,哪一个报文没有到达,TCP都是可恶意通过排序知道的,所以无需担心TCP报文重复问题,当对端主机的ACK丢失了,那么此时会收到一个相同的报文此时接收端主机就会知道是自己的ACK丢失了,所以此时就会再次响应一个ACK。
那么超时重传的时间是多少呢? 首先问一个问题,超时重传的时间是固定的吗?一定不是固定的!为什么? 因为超时重传是有很多原因的,首先就是因为网络问题,有一些时候网络好,所以超时重传的时间短一些,那么如果网络很差,说不准是因为数据报还没有到达对端主机,那么就触发了超时重传,所以超时重传的时间一定不是固定的。
超时重传的时间一般是 500毫秒位倍数来发送,第一次是500毫秒,第二次就是500毫秒乘2,一般在重传一定次数后,发送端主角及就会默认连接断开了,所以发送端主机也就会断开连接。
前面我们说过了,TCP发送数据并不是一次只能发一条,而是一次可以发送一批数据,而这一批数据是不需要确认应答就可以发送出下一条数据的,那么发送多少呢?
也就是说在滑动窗口内,是可以不需要确认应答的,就可以发送下一条数据,而这也就是TCP位了提高效率的一种方式!
在之前的说的都是发送一条数据,然后 ACK 一条,如果不ACK 的话,那么就会超时重传,但是这样做效率太低了,所以就有了提升效率的方案,滑动窗口,它可以一次性发送一批数据而无需确认应答!
但是服务器还是会对每一个报文进行 ACK,那么这里一次性发送这么多数据,丢包了会不会有什么问题呢?
丢包这里会有两种情况:ACK丢了、数据报丢了,那么我们看一下会不会有什么问题呢!
如果是ACK丢包,那么要看是中间或者前面的ACK丢包,还是最后一个ACK丢包! 如果是中间或者前面的ACK丢包其实并没有什么问题,后面的ACK确认就可以了,因为ACK确认应答的含义是在确认序号之前的数据包都收到了!
最后一个ACK到达,那么就可以表示在 4001之前的报文全部到达了,下一个序号就从 4001 开始!
那么如果是数据丢了呢? 数据丢包其实也没有什么事,数据丢包此时确认应答就不能应答下一个,例如:现在 1001~2000丢了,即使是2001~3000到达了,也不能进行ACK 3001,因为前面有数据没有到达,所以下一个ACK的数据还是 2001。
就像上图一样,是数据报丢了,那么有什么影响吗?
这里需要在引入一个新的概念“快重传”,快重传就是收到三次相同的ACK,那么就会触发快重传机制,然后重新发送ACK的报文!
这里是1001~2000的报文丢失了,那么此时它接收到,3001~4000的报文时候,是不可以ACK 4001 的,而是还需要ACK 2001 的报文,然后当收到3个重复的ACK的时候,那么就触发了快重传,然后就会讲1001~2000的爱护据重新发送,接收端在ACK就是8001了。
快重传的特点是什么呢?
快
重传
既然快重传的特点是快,还可以重传,那么为什么还需要超时重传呢?超时重传虽然也可以重传,但是需要等一定的时间才可以,所以为什么既要有快重传还要有超时重传?
前面我们说了触发快重传是需要什么的?是需要收到三次相同ACK 的报文,也就是说快重传是需要条件的,那么条件不满足的话,就不会触发快重传,但是超时重传不一样,超时重传是一定会满足条件的,如果一个数据报没有被ACK,那么在一定的时间过后,就可以触发超时重传,所以快重传只是为了在加快一定效率,而超时重传是用来兜底的,也就是有超时重传一定就是可靠的!
那么前面我们在说滑动窗口里面的可以不需要ACK然后就发送下一条数据,那么滑动窗口是什么呢? 我们又应该如何理解滑动串口呢? 其实这个并不难理解,前面我们理解了序号,序号也就是因为又发送缓冲区的存在,而发送缓冲区就是一个 char 类型的数组,所以数组是天然有下标的,所以序号也就是下标,那么滑动窗口呢?我们下面先看一下滑动窗口的示意图。
首先当我们看到这个图之后,我们其实就可以明白,滑动窗口就是属于发送缓冲区的一部分! 而这里所谓的窗口,也就是发送缓冲区上的两个指针,分别指向发送缓冲区的下标,也有可能下标相同,而在滑动窗口里面的表示发送了不需要ACK的数据报,而在滑动窗口之前的表示已经发送,并且已经得到ACK的报文,后面的就是还没有发送的数据!
既然我们现在知道了滑动窗口是什么,在哪里,那么我们是不是就可以提出疑问了,滑动窗口的大小是多少呢? 现在我们先回忆一下,前面我们在说TCP报头的时候,说了一个16位窗口大小,也就是对端接收缓冲区的剩余大小就在16位窗口大小里面,我们发送的数据不能超过16位窗口大小的数据,否则会因为对端接受数据不了,而丢弃报文! 那么滑动窗口就是一次向对方发送数据不需要ACK的大小,那么你觉得这个大小可以超过16位窗口大小吗?不可以,所以我们目前就认为滑动窗口的大小就是16位窗口大小!(后面还有)
那么滑动窗口又如何滑动呢? 我们先想一下,在滑动串口之前的表示已经发送且ACK了,后面的表示还没有发送。 那么如果对方ACK的话,滑动窗口怎么移动呢?滑动窗口的左侧需要向右移动! 那么如果对方一直在ACK但是对方的接受能力一直没有增加呢?反而一直在减小,甚至接受能力为0呢? 那么此时滑动窗口的右侧可以向右移动吗?不可以的,为什么?因为对端的接受能力为0,说明对方已经无法在接受数据,那么滑动窗口的大小是与16位窗口大小有关的,如果对端的接受能力为0了,那么继续发送还有意义吗?所以当对端的接受能力为0时,滑动窗口的右侧也就不会在继续滑动!
那么滑动窗口的大小可以为0吗?也就是说,滑动窗口的左侧和右侧相等处于同一下标? 可以的,因为当对方将所有的数据报都ACK了,但是对方的接受缓冲区为0,那么此时滑动窗口的大小也就一定为0,不能向对方发送数据,此时滑动窗口的大小就是0。
那么滑动窗口会越界吗? 一定是不会的,虽然看起来是滑动窗口一直向右移动,但是滑动窗口到了最大的时候,一定是会回到最开始的位置的,所以不需要担心滑动窗口越界的问题!
既然有了确认应答以及超时重传和排序就可以保证报文可以可靠的到达了,那么如果发送的报文对方接受不了了丢包了呢?这有没有问题呢?如果由于一次性发送太多数据,导致对端主机接收不了数据而发送丢八篇,此时发送短主机就会触发超时重传,那么这样有没有问题呢?其实是有问题的,为什么?因为超时重传是超出一定的时间后重传,也就是需要等待一定的时间,那么如果发送的报文丢失了,没有得到响应,但是发送端还需要等待一定的时间才能重传,那么是不是有效率问题呢?不光是因为重传需要等待一定的时间,还有就是如果发送太多的数据导致对方丢包,那么这些发送过去的数据是不是还不如不发,还可以节省网络资源,那么此时TCP需要控制发送的速度吗?需要控制!
那么流量控制是控制什么的呢?流量控制就是控制发送数据的速度的! 因为对端主机的接受能力是有限的,而TCP根据对端主机的接受能力来控制发送数据的速度,这个就叫做流量控制!
那么流量控制是如何做到的呢?其实我们前面说的也就是流量控制的几种手段之一!
接收方主机在返回报文的时候,会将自己的接受能力写到16位窗口大小里面。
窗口大小越大,说明网络的吞吐量就越高!
接收端发现自己的接收缓冲区快慢了,就会立马设置一个小的窗口大小,通知发送端主机。
发送端主机接受到后,就会调节发送数据的速度。
如果接收端缓冲区满了,那么就会将窗口大小设置为0,表示无法接受数据。
此时发送端就会停止发送数据,然后隔段时间发一个探测报文,看接收端的缓冲区大小。
为什么既然窗口跟新会通知,那么为什么需要发送端还一直探测呢? 因为报文可能会丢失,如果丢失了,那么发送端不探测的话,就导致无法继续通信,所以发送端需要一直向接收端发起窗口探测,如果接收端接收到报文就必须回一个报文给发送端,此时发送端就可以看到接收端的接收缓冲区的大小了!
还有一个问题,就是TCP的报头中有一个16位窗口大小,那么16位最大就是65535字节,也就是说TCP一次最多发送65535字节吗? 一般来说一次性就是可以发送65535字节,但是在TCP中还有一个就是选项,在选项中是有控制发送大小的字段的窗口扩大因子M,也就是窗口大小左移M位置。
前面我们一直在说TCP的可靠性,但是我们说的一直都是一台主机到另一台主机之间的可靠性,那么有没有其他的因素也来干扰可靠性呢?有的!网络。
一般而言在两台主机通信的过程中,丢失一两个报文时很正常的,但是在两台主机中突然丢失了一大批报文呢?那么这是不是还是两台主机的问题呢?
现在我们可以想一个问题,如果现在学生在考试,有一两个学生考的比较差,那么时这一两个学生的问题,那么如果1000名学生考试其中900名学生没有及格,那么还是不是这1000名学生的问题呢?显然不是,这就是其他的问题了,也就是试卷的问题,同理,当一大批报文只有一两个丢包是正常的,那么当一大批报文只有一两个没有丢包,那么这还是主机的问题吗?不是这就是网络的问题,说明此时的网络是拥塞的。
我们知道当报文丢失了后,此时是需要重传的,那么当出现网络拥塞导致的丢包,那么可以立马将这丢了的一大批报文全部重传吗?不可以,为什么? 我们要知道,此时我们就不能以两台主机的视角来看待报文丢失了,一般的网络拥塞导致的报文丢失可不是仅仅两台主机的报文丢失,而是在这个网段内的所有主机的报文都可能出现大面积的数据报丢失问题,那么此时如果所有的主机在立马重传这一批数据,此时网络中顿时塞满了大量的数据,那么网络的情况只会根糟糕,所以当出现网络拥塞导致的大面积丢包问题,此时主机可以立马将数据报重传吗?不可以。
那么此时主机应该怎么做呢?此时主机就应该试探性的在网络里面发送少量的数据,如果这些数据立马得到回应,说明此时的网络状态已经变好了,此时就需要快速的增加发送的数据量。如果此时发送数据对方响应很慢,那么此时还是需要慢慢的增长发送的数据量。
所以这里就引入了一个”慢启动“,而慢启动就是我们刚才说的那样,先发送少量的数据,看网络的状况如何,然后再决定后续的操作!
所以为了控制发送的数据量,此时还需要一个拥塞窗口来表示此时发送的数据量。 但是拥塞窗口可并不是像滑动窗口,而拥塞窗口实际上就是一个数字,代表现在最多可以发送的数据量。
在最开始发送的时候,拥塞窗口为1
当对方ACK后,拥塞窗口会以指数级别增长。
但是并不是说拥塞窗口越大发送的数据就越多,还与窗口大小有关。
但是这里是并不能单纯的以指数级增长的,因为越到后面,指数级增长就越恐怖,所以到达一定的阈值后,就会改为线性增长,只要网络一直不拥塞,那么拥塞窗口也就会一直变大,但是当网络再一次拥塞,那么拥塞窗口立马变为1,再一次开始这个指数级增长!
下面我们在谈一下滑动窗口的大小。 前面我们说滑动窗口的大小与16位窗口大小有关,但是当我们知道了拥塞窗口后,我们知道了还要与拥塞窗口的大小有关,也就是滑动窗口的大小实际上就是16位窗口大小与拥塞窗口大小的最小值。
滑动窗口 = min(16位窗口大小,拥塞窗口大小)
在TCP的报头中,不仅有32位序号,还有32位确认序号,32位确认序号是为例可靠性是为了确认应答的。 那么如果此时接收方还想要发送数据给发送方呢? 那么此时就可以将数据和确认应答一起发送过去,这就是捎带应答!
在TCP通信的过程中,每一次发送数据的多少取决于两个因素: 1.对端主机的接收缓冲区的剩余大小 2.网络是否阻塞 其中网络因素我们并不能改变,但是主机的接收缓冲区的剩余大小我们是大概率可以让其变大一点的。
所以只要对端主机的接收缓冲区的剩余大小比较大的话,那么大概率我们发送方一次可以发送更多的数据的,所以接收方主机每一次响应的报文里面的16位窗口大小如何可以变得大一点呢? 让接受缓冲区的数据被上层应用多读取走一些就可以了,那么如何让上层应用多读取走一些呢?等!
当接收方一旦接收到数据,立马返回数据报,那么此时接收缓冲区的数据大概率是没有被读取走的,但是假设接收方每一次接收到数据并不是立马返回,而是等多接收到几个报文或者稍微等一下后再返回,那么是不是大概率接收缓冲区的数据是被上层读取走一下的,那么此时再返回数据报,是不是可以在16位窗口大小里面填入的数据更大呢?也就是说当对方发送数据的时候,可以让它的滑动窗口更大呢?既然滑动窗口更大,那么一次性发送的数据,并且无需应答的数据是不是也就是更多呢。
上面我们学习到了关于TCP报头的知识,其中TCP是传输控制协议,而TCP也是就保证可靠性的。
TCP的封装和解包: TCP的报头是 固定长度+子描述字段,固定长度就是20字节,而子描述字段就是4位首部长度。
TCP的向上交付: TCP的报头中有源端口号,以及目的端口号,目的端口号就是TCP要交付给那一个进程。
TCP的可靠性保证: TCP的报头中有32位序号和32位确认序号,以及16位校验和。
TCP可靠性基于序号的其他保证:
TCP的可靠性基本是基于序号来的,那么可靠性有哪些呢: 1.序号机制,也就是TCP报头中的序号 2.基于序号的确认应答机制 3.当到达的时候顺序不正确的时候,还有基于序号的排序 4.当数据报丢失后,发信一直没有确认应答,那么就启动超时重传机制
虽然说TCP是可靠的,但是TCP也并不是不考虑效率的问题,那么TCP关于效率方面做出了哪些呢: 1.因为TCP是需要确认应答的,但是每次发送一条确认一条效率太低,所以就有滑动窗口 2.因为加入了滑动窗口,所以如果数据包丢失,连续收到三次连续相同的确认应答,那么就启动快重传 3.为了每一次发送数据更多,TCP还有延迟应答策略 4.如果每一次确认应答都需要重新发送,那么太浪费了,所以TCP还有捎带应答机制
前面一直再说TCP是面向字节流的,以及再UDP的报头里面有16位总长度,但是TCP里面并没有16位总长度,这是为什么?下面我们解释一下面向字节流! 因为TCP是有缓冲区的,再写的时候,将数据拷贝到发送缓冲区,在读的时候,从接收缓冲区中读,所以TCP是全双工的,既可以读,也可以写。 正是因为TCP有缓冲区的存在,所以TCP如果在发送数据的时候一次发送不完的话,可以拆封成多次发送所以TCP并不会关心发送的数据是多少,也不会关心发送的数据是什么。 发送的时候是不关心的,同时当数据发送到了之后,就会拷贝到对方的接收缓冲区中,而接收缓冲区也是一样,如果一次拿不完的话,就拆分成多次拿数据,所以也不关心到底是多少数据,所以发送的次数和接收的次数也并不一定是一样的,而这就是面向字节流!
所以对于TCP而言,如果现在有100字节的数据,那么TCP可以一次将它发送完毕,也可以将它一次发送1字节,发送一百次,同时接收也一样,现在又100字节的数据,接收方可以一次将100字节的数据全部拿完,也可以一次拿10字节,拿10次!
正因为TCP是面向字节流的,所以接收方并不知道一次将数据拿了多少,拿了一个报文,还是一个半报文,那么当拿了一个半报文的时候,上层在解包的时候是将另外的半个报文是解析不出来的,所以上层需要自己将数据分开,可以使用特定的分隔符,也可以使用子描述字段来区分。
既然TCP是面向连接的,而管理连接也是需要成本的,那么现在有问题了,TCP会不会有连接泄露的风险呢?因为TCP的连接实际上就是文件描述符,所以也就是文件描述符泄露的风险呢?
首先我们考虑一下TCP的异常有几种:
1.使用TCP协议的进程奔溃了: 如果是第一种情况,我们知道TCP的连接实际上就是文件文件描述符,而文件描述符的生命周期随进程,如果当该进程奔溃了,那么相应的文件描述符也就会被释放掉,文件描述符的释放实际上就是关闭文件描述符,而文件描述符被关闭也就是客户端在4次挥手,4次挥手就是在断开连接,所以当进程奔溃的这种情况下TCP的连接释放时安全的!
2.电脑重启: 电脑重启其实是和第一种是相同的,因为当操作系统在关闭的时候,首先操作系统需要将自己的子进程全部释放掉,而操作系统的子进程就是使用TCP协议的进程,当使用TCP协议的进程释放了,而TCP连接的生命周期是随进程的,所以操作系统重启也是没有问题的!
3.网络断开: 如果当网络断开了,那么此时上面运行TCP协议的进程并没有死掉,也就是还在运行,而网络突然断开也就是文件描述符并没有关闭,所以也就没有进行4次挥手,对端主机认为连接还是连接的,那么这样会有问题吗? 实际上TCP是会每隔一段时间问一下对端主机的,如果问了几次没有回答,那么TCP认为连接已经断开,所以服务器也就会默认断开连接,所以即使是网络断开也是没有问题的。