1.TCP协议的报文结构
TCP的全称为:Transmission Control Protocol
。
特点:
- 有连接
- 可靠传输
- 面向字节流
- 全双工
下面是TCP的报文结构:
- 源端口和目的端口:
源端口表示数据从哪个端口传输出来,目的端口表示数据传输到哪个端口去。
- 32位序号和32位确认序号
用来区分应答报文是针对发送方的哪条消息进行的应答。
- 4位TCP报头长度
表示TCP头部有多少个32bit(4个字节), 范围:0~15
所以报头的最大长度为:15 * 4 = 60字节
- 6位标志位:每位标志位的值为0或者1,0代表标志位无效,1代表标志位有效。
URG:表示紧急指针是否有效
ACK:应答报文段,如果为1,表示这个报文是一个应答报文
PSH:提示接受端程序从TCP缓冲区把数据读走。
RST:复位报文段,表示重新建立连接
SYN:同步报文段,表示请求建立连接
FIN:结束报文段,通知对方本端要关闭了
- 16位窗口大小
表示接收缓冲区的空闲空间
- 16位校验和
发送端填充,CRC校验。接收端校验不通过,则认为数据有问题。此处的检验的数据不光
包含TCP首部,也包含TCP数据部分
- 16位紧急指针:
表示哪部分数据是紧急数据
- 40个字节头部选项:
提供一些特殊的功能或者选项
比如:
MSS (Maximum Segment Size): 最大分段大小
Windows Scale Factor: 滑动窗口的规模因子
Timestamps: 时间戳
NOOP (No Operation): 空操作选项
TCP设置那么多报文段的主要原因就是为了保证数据传输的可靠性。
可靠性是TCP协议的核心,而且TCP不仅想保证可靠性,还想让传输效率更高一点。
都说:”鱼和熊掌不可兼得“,可TCP全都要,可想而知这TCP肯定是个硬骨头。
那么TCP是如何保证可靠性并且提高效率的呢?下面就介绍TCP的一些特性。
2.确认应答
确认应答,简单点说就是,发送方发了一条数据,接收方要确认一下是否收到数据。确认应答
是保证可靠性的核心机制。
根据具体的例子会更好理解,这还得从小明和小红的故事说起…
有一天,小明联系小红,请她看电影:
小红答应了,并且回复了:好呀好呀。
小红的回复就相当于应答报文
,ACK报文
(Acknowledgement)。
通过小红的应答,小明就知道小红收到了消息。
两个人看了电影后,感情进一步提升,小明觉得机会来了,过了几天,又发消息给小红。
小红原本的回答:
但是在网络中,先发的消息很可能后到,结果小红的回答变成了:
这给小明开心坏了,虽然不能看电影,但是可以做自己的女朋友,这不是血赚吗?
为了解决这个问题就要引入序号
和确认序号
。
这样小明就知道,小红什么意思了。
小明已经哭晕在厕所里了。
但是TCP里的序号
和确认序号
还跟上面的例子不一样。
下面来看看,TCP的序号
和确认序号
是怎么回事?
TCP为每个字节都进行了编号。
TCP发送端发送带序号
的报文,接收端返回带确认序号
的报文。
- ACK应答报文的
确认序号
跟发送过来的序号
是不一样的。 - ACK应答报文的序号是:发送方发送过来的所有数据,最后一个字节的下一个字节序号。
就拿上面的例子来说,发送方总共发了100个字节,这一百个字节最后一个字节的序号是100,所以应答报文返回的是第100字节后的一个字节,确认序号就是101了。
上面例子中,应答报文确认序号
的含义:
-
小于101序号之前的数据已经收到
-
向发送方索要从101序号开始的数据
3.超时重传
在讲超时重传之前还得知道为什么要超时重传,原因是丢包。
像平时打游戏,丢包就会变得一卡一卡的,那为什么会丢包呢?
网络中的数据是要经过很多设备的,各个设备不断转发,数据最终才传到目标主机那里去。
某一个时刻,某个设备,流量达到峰值的时候,数据再传过来,就可能导致丢包了。
1.接收方没收到发送方的数据。
发送方在一定的时间间隔没有收到ACK应答报文,就会重新发送数据。
2.接收方收到了数据,但是发给发送方的ACK应答报文,丢了!
接收方收到了数据,但是ACK应答报文丢了,发送方由于收不到ACK应答报文,然隔了一段时间又重新发数据给接收方。
注意:
在第2种情况下,接收方收到了两次数据,TCP会在接收缓冲区里根据收到数据的序号,把数据去重。
如果发生了多次丢包,多次重传,每次重传的时间间隔都会变大,而且累计到一定的次数就会认为接收端异常,直接关闭连接。
这里拿具体一点的数字举例子,比如:
从间隔1S重传,到间隔2S重传,到间隔4S重传…
重传的次数累计到了10次,直接关闭连接。
4.连接管理(三次握手,四次挥手)
在进行数据传输之前,TCP还必须进行连接,进行三次握手。
握手(Handshake)
指的是通信双方,进行一次网络交互。
三次握手指的是,主机与主机之间进行三次交互,互相记录对方的信息,这就建立了连接。
TCP协议报头中,有这六个标志位,一个标志位占1位。
这六个报文段默认为0,如果为1,则有特殊含义。
SYN这一位为1,则表示当前TCP数据报是同步报文,表示申请连接
ACK这一位为1,则表示当前TCP数据报是应答报文
三次握手:
- A向B发出SYN报文,第一次握手
- B收到A发来的SYN同步报文,然后返回SYN+ACK报文,这个报文中SYN和ACK的标志为都为1。这是第二次握手
- A收到SYN + ACK报文,再返回ACK应答报文,第三次握手
这三次握手的过程确定了双方的接收能力和发送能力,是否正常。
如果都没问题,接下来就可以传输数据了。
注意:
三次握手的过程全都由内核来完成,应用程序干预不了。
SYN报文和ACK报文都是由内核自动发送的。
当我们在进行TCP网络编程时,还没socket.accept()
时,连接早已建立好了。
执行socket.accept()
的时候才从操作系统内核把连接拿出来。
断开连接的时候要进行四次挥手。
- A向B发送FIN报文,第一次挥手
- B收到A的报文,并且回复一个ACK报文,第二次挥手
- 过了一段时间后,B向A发送FIN报文,第三次挥手
- A收到B的报文,并且回复一个ACK报文,第四次挥手
注意:
1.四次挥手中发送FIN报文是由应用程序来控制的,
~~~ 当执行socket.close()
的时候,才会发送FIN报文。
2.ACK报文则是由内核自动发送的。
5.滑动窗口
TCP不仅要保证可靠性,还要保证效率。
为什么需要滑动窗口?且看下图:
A发送数据,然后等B端的ACK, 假如等了0.3s,0.3s过后发送端收到了ACK了,再发送下一个数据。在此期间,A端白白等了0.3秒。是不是可以利用这0.3秒的时间,再发几条数据呢?
所以就有了滑动窗口,就是批量传输,批量传输不是无限地发送数据,而是发送到一定的程度,就等待ACK。而且是发送方收到一个ACK就立即发送下一条数据,发送方批量等待的数据是保持不变的。发送方批量等待的数据的数量,就称为窗口大小
。
举个例子:
1.A向B发送四个段的数据,每段1000个字节。
第一段:第1-第1000字节
第二段:第1001-2000字节
第三段:第2001-3000字节
第四段:第3001-4000字节
这四段数据无需等待ACK,直接发送。
2.B收到数据后,返回第一个ACK。
3.A收到ACK后,继续发送数据。
4.B发送第二个ACK。
5.A收到第二个ACK,继续发送下一条数据。
一直下去如此往复:
注意:
- 在此过程中,发送方等待应答的数据一直都是4000个字节,即窗口大小一直保持在4000个字节。
- 操作系统内核为了维护这个滑动窗口,需要开辟 发送缓冲区 来记录当前还有哪些数据没有应答;只有确认应答过的数据,才能从缓冲区删掉;
- 窗口越大,则网络的吞吐率就越高
如下图:
当B发给A一个ACK,A收到ACK之后,就会继续发送数据,期间这个窗口的大小是不变的,而且是滑动的。
继续滑动:
批量传输的过程中,出现丢包该咋办?毕竟TCP还是以可靠性为主的,保证可靠性是TCP至始至终的目标。
在这里,丢包可分为两种情况:
1.接收方收到了数据,但是返回给发送方的ACK丢了。
这种情况下无所谓,等到下一次ACK成功发送的时候,发送方根据确认序号就知道数据有没有发送成功。
虽然确认序号为1001的ACK丢了,但是确认序号为2001的ACK发送方收到了。
发送方根据这个确认序号就知道第2001字节之前的数据
,已经发送成功了。
这里假设发送方窗口大小为4000字节,发送方继续发送数据,
上图中一次发了两条数据:4001-5000字节和5001-6000字节。
发送方原本的窗口:
滑动之后的窗口:
2.发送方向接收方发送的包丢了。
接收方所做的处理。
如上图,发送方发送的1001-2000字节的数据丢了。
这里接收方不会因为接收到的是2001-3000字节就返回确认序号为3001的ACK。
而是继续返回确认序号为1001的ACK。
当返回了三次确认序号为1001的ACK之后,发送方就知道了,1001-2000字节的数据可能丢了。然后,发送方继续发送1001-2000字节的数据。
接收方返回确认序号为7001的ACK。
这里不是返回确认序号为2001的ACK,因为接收缓冲区里已经有了第7001字节之前的数据。
6.流量控制
在滑动窗口中,我们说,窗口越大效率越高。但是,如果一下子发太多了,把接收方缓冲区直接干满了。接下来如果继续发送数据,就会丢包。所以流量控制就是为了,防止数据发送太快,而导致丢包。
在TCP报文结构中有16位窗口大小
的报文段。
当ACK这个标志位为1的时候,窗口大小就会生效,这个窗口大小表示的是接收方缓冲区里还剩的大小,0~65535字节。
下面具体说明:
1.发送方批量发送数据,接收方把数据放到接收缓冲区中。
2.接收方返回ACK,窗口大小为1000,发送方继续发送数据,接收方再把数据放到缓冲区中。
3.此时缓冲区满了,返回的ACK中窗口大小为0,发送方不会发送数据了。
4.一段时间过后,发送方会发送窗口探测报文,然后接收方继续返回缓冲区中的窗口大小。
5.缓冲区一部分数据被取走了。
6.发送方继续发送探测报文,得知缓冲区有空位了。
7.继续发送数据。
7.拥塞控制
滑动窗口的大小取决于流量控制和拥塞控制。
流量控制衡量了接收方的处理能力。拥塞控制衡量了传输路径的处理能力。
在网络中数据是要经过很多节点的。
如果中间任何一个设备达到处理能力上的瓶颈,都会影响整个网络的传输。
拥塞控制要做的就是衡量中间经过的路径,这些路径上有多少个节点,每个节点的处理能力情况等等。
这里要清楚两个概念,拥塞窗口和流控窗口。
这里所说的窗口不是TCP报文中的那个窗口大小,而是发送方发送完数据,发送方等待等待ACK的数据的数量。拥塞窗口是在拥塞控制试出来的窗口,流控窗口是流量控制中得出的窗口。
实际发送方的滑动窗口大小=min(拥塞窗口,流控窗口)
。
下面是拥塞控制的具体过程,也是拥塞窗口的变化过程:
1.刚开始传输,会给一个非常小的窗口,也叫做慢开始。
2.拥塞窗口指数级增长,可以短时间到达一个比较大的值,快速接近当前网络传输路径的能力瓶颈。
3.指数级增长变为线性增长,避免一下子超过网络能力上限,可以使得传输速度逐渐接近上限。
4.增长到一定程度,出现丢包,就认为当前窗口的大小已经达到了路径的传输上限。
5.立即把窗口大小降到一个很小的值,继续上述过程。
8.延时应答
延时应答是啥呢?就是字面上的意思,等一会再应答,即ACK不会立即发,等一会再发。
TCP中决定传输效率的关键元素就是发送方的窗口大小,而流量控制和拥塞控制共同决定了这个窗口的大小。就可以从这两个方面进行优化。而延时应答就是对流量控制做了优化。
- 发送方不停地发数据,发到接收方的接收缓冲区里,同时应用程序也在不停地从接收缓冲区里取数据。
假设接收缓冲区的大小是128KB,发送方发送了64KB的数据,那么缓冲区里还剩64KB的数据。
如果立即返回ACK,ACK报文里的窗口大小就是64KB。
发送方最多发送64KB的数据,发送完就又要等下一个ACK了。
如果等一会返回ACK,因为应用程序一直在从缓冲区里拿数据,所以这次返回的ACK中的窗口大小,大概率比64KB要大,发送方就可以发多一点数据。
9.捎带应答
捎带应答,顾名思义:应答的时候带了一点东西,就叫捎带应答。
而且捎带应答也是会延时一点时间再应答的。
如下图:客户端之间的通信一般都是一问一答的。
本来ACK和应用程序的应答是不同时机的,但是如果ACK延时一点发送就有可能和应用程序的应答合成一个报文,这样子效率就比分两次发要高一点。
10.面向字节流
因为TCP是面向字节流的,这会导致粘包问题。要明白粘包问题是啥,还得看具体的例子。
如下图所示:
TCP会根据序号把数据在缓冲区里拼起来,缓冲区里的数据是一长串。
从小红的角度看的话,他是知道小明要干什么的。因为每次小明发的消息都是以小红,小红
开头的,而小明就蒙了,这一长串到底是啥意思。这就是粘包问题。
应用层拿到接收缓冲区的数据时,无法区分一个完整的应用层数据报。很容易就会拿错应用层数据报。
解决方法:
1.定义分隔符
2.约定一个应用层数据报的长度。
采用定义分隔符法,拿上面的例子说明,如果小明和小红每次发送数据是都会加一个\n
,那么就很好区分一个应用层数据报了。
小明看到\n
就知道,当前这个数据报到哪里结束了,也就明白对方什么意思了。
平常看到的json
、xml
是用分隔符来实现的。
11.异常情况
1.进程关闭/进程崩溃。
进程没了socket是文件,随之也被关闭,但是连接还在,仍然可以进行四次挥手。
2.主机正常关机。
正常流程关机会杀死所有的用户进程。也会触发四次握手,如果发送端的FIN报文发过来了,但是还没来得及返回ACK报文就关机了,那么发送端会重新发送FIN报文,发送几次之后都没有ACK返回,发送方就会重置连接,如果还不行,干脆就释放连接了。
3.主机突然断电。
-
断电的是接收方。
发送方:收不到ACK=>重传=>重置连接=>释放连接。 -
断电的是发送方。
接收方不知道发送方发生什么情况了。是没来得及发送新的数据还是其他情况。
TCP内置了心跳包。这个心跳包是周期性发送的。接收方会定期发送一个心跳包,如果发送方没有回应,接收方就能及时判断发送方是不是挂了。
4.网线断开。
网线断开和断电的情况是一样。