目录
[(一) 端口号范围划分](#(一) 端口号范围划分)
[(二) 认识知名端口号](#(二) 认识知名端口号)
[(三) netstat](#(三) netstat)
[(四) pidof](#(四) pidof)
[(一) UDP协议端格式](#(一) UDP协议端格式)
[(二) UDP的特点](#(二) UDP的特点)
[(三) 面向数据报](#(三) 面向数据报)
[(四) UDP的缓冲区](#(四) UDP的缓冲区)
[(五) UDP使用注意事项](#(五) UDP使用注意事项)
[(六) 基于UDP的应用层协议](#(六) 基于UDP的应用层协议)
[(一) TCP协议的段格式](#(一) TCP协议的段格式)
[1. 报头和有效载荷如何分离,如何交付给上层](#1. 报头和有效载荷如何分离,如何交付给上层)
[(二) 16位窗口大小](#(二) 16位窗口大小)
[(三) 32位序号和32位确认序号](#(三) 32位序号和32位确认序号)
[(四) 6位标志位](#(四) 6位标志位)
[(五) 确认应答(ACK)机制](#(五) 确认应答(ACK)机制)
[(六) 超时重传机制](#(六) 超时重传机制)
[(七) 连接管理机制](#(七) 连接管理机制)
[(八) 流量控制](#(八) 流量控制)
[(九) 滑动窗口](#(九) 滑动窗口)
[(十) 延迟应答](#(十) 延迟应答)
[(十一) 捎带应答](#(十一) 捎带应答)
[(十二) 拥塞控制](#(十二) 拥塞控制)
[(十三) TCP异常情况](#(十三) TCP异常情况)
[(十四) TCP小结](#(十四) TCP小结)
传输层和网络层是在Linu内核中实现的,Linux提供了一系列的系统调用接口,可以实现TCP/UDP通信。
一、再谈端口号
运行一个程序就是一个进程,一个进程有一个专属的进程id,而端口号标识了一个主机上进行通信的不同的应用程序,前面写的TCP/UDP通信程序中,当服务端启动之后,客户端需要通过ip:port的方式来找到服务端。port代表的就是服务器中的哪一个进程。
在TCP/IP协议中, 用 "源IP", "源端口号", "目的IP", "目的端口号", "协议号" 这样一个五元组来标识一个通信(可以通过netstat -n查看);
(一) 端口号范围划分
0 - 1023: 知名端口号, HTTP, FTP, SSH等这些广为使用的应用层协议, 他们的端口号都是固定的
1024 - 65535: 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的.
(二) 认识知名端口号
有些服务器是非常常用的, 为了使用方便, 人们约定一些常用的服务器, 都是用以下这些固定的端口号
ssh服务器,使用22端口号
ftp服务器,使用21端口号
telnet服务器,使用23端口号
http服务器,使用80端口号
https服务器,使用443端口号
我们自己写程序要使用端口号的时候,要避开这些知名端口号。
查看知名端口号 cat /etc/services
(三) netstat
netstat可以查看网络状态的重要工具。
语法:netstat [选项]
功能:查看网络状态
常用选项:
n:拒绝显示别名,能显示数字的全部转化为数字
l:仅列出有在Listen(监听)的服务状态
p:显示建立相关链接的程序名
t:仅显示tcp相关选项
u:仅显示udp的相关选项
a:显示所有选项,默认不显示Listen相关
(四) pidof
再查看服务器的进程id时非常方便。
语法:pidof [进程名]
功能:通过进程名,查看进程id
二、UDP协议
(一) UDP协议端格式
在使用UDP进行通信的时候,16位的源端口号和目的端口号谁填写?客户端在发送数据的时候会形成随机端口,目的端口号是在进行通信的时候用户自己写的。'
现在我们要发送数据,当A向B使用UDP发送数据的时候,B怎么知道数据在传输的过程中没有丢失,接受到的是完整的数据呢?数据有没有可能出问题?
16位UDP长度,可以表示整个数据报(UDP首部 + UDP数据)的最大长度。最大长度减去8字节就是有效载荷的长度。
16位UDP检验和,用于检测数据传输过程中是否发生了错误。通过对UDP头部和有效载荷部分的所有16字的二进制求和,然后取反得到最终的检验和值。接收端在接受数据包时会重新计算检验和,并将计算结果与数据包中的检验和进行比较。如果不一样,就说明数据包可能已经损坏或被篡改,接收端可以选择丢弃该数据包。UDP本身不提供任何机制来纠正错误。
(二) UDP的特点
无连接:知道对端的IP和端口号就直接进行传输,不需要建立连接
不可靠:没有确认机制,没有重传机制;如果因为网络故障该段无法发送到对象,UDP协议层也不会给应用层返回任何错误信息
面向数据报:不够灵活的控制读写数据的次数和数量
(三) 面向数据报
应用层交给UDP多长的报文,UDP原样发送,既不会拆分,也不会合并;
用UDP传输100个字节的数据;
如果发送端调用一次sendto,发送100个字节,那么接收端也必须调用对应的一次recvfrom,接受100个字节;而不能循环调用10次recvfrom,每次就收10个字节。
(四) UDP的缓冲区
UDP没有真正意义上的发送缓冲区,调用sendto会直接交给内核,由内核将数据传给网络层协议进行后续的传输动作
UDP具有接受缓冲区,但是这个接受缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致;如果缓冲区满了,再达到的UDP数据就会被丢弃。
UDP的socket既能读,也能写,这个概念叫做全双工。
(五) UDP使用注意事项
UDP协议首部中有一个16位的最大长度. 也就是说一个UDP能传输的数据最大长度是64K(包含UDP首部).但是64K在当今的互联网环境下, 是一个非常小的数字.
当客户端向服务端发送了10个报文,分别是1......10,但是服务端接收到的报文一定是从1接收到......10吗?不一定,服务端接收到的报文也可能是乱序,UDP不保证可靠性,所以会直接把报文给丢弃。
(六) 基于UDP的应用层协议
NFS: 网络文件系统
TFTP: 简单文件传输协议
DHCP: 动态主机配置协议
BOOTP: 启动协议(用于无盘设备启动)
DNS: 域名解析协议
当然, 也包括你自己写UDP程序时自定义的应用层协议
三、TCP协议
TCP全称为 "传输控制协议(Transmission Control Protocol"). 人如其名, 要对数据的传输进行一个详细的控制。
TCP在操作系统内部,会存在一个发送缓冲区和接收缓冲区,每建立一个链接就会创建这两个缓冲区。其实我们平时在应用层写的数据,调用上层的接口read,write等,本质并不是把数据发送到网络当中,是把数据拷贝到网络当中。数据什么时候发送,发送多少,出错了怎么办,由TCP协议自主决定。
以前在学习文件的时候,每个文件都有对应的文件缓冲区,往文件里写数据其实就是将数据拷贝到文件缓冲区里面,缓冲区什么时候刷新数据这些都不需要用户关心,是由操作系统和磁盘交互的。文件是IO,网络也是IO,一个是将数据写到磁盘上,一个是将数据写到网络里面。
数据写到网络里面之后,会从发送缓冲区里面发送到另一个用户的接收缓冲区里面。本质也是拷贝。从宏观上来讲就是将数据拷贝到对方的接收缓冲区里面,发送过程中可能会出现一些问题,这些问题会有一些方案来解决。当然数据并不是直接发送给对方的,会交给下层的协议。
(一) TCP协议的段格式
每一种数据在协议的不同层有不同的名称,一般在应用层把数据叫做请求和响应;在传输层一般把数据叫做数据段;在IP层一般叫做数据报;在数据链路层一般叫做帧,所以同样数据在不同层可能会有不同的名字。
1. 报头和有效载荷如何分离,如何交付给上层
TCP报头的前20字节是TCP的标准报头,对于选项(40字节)先忽略,数据为TCP的有效载荷。
前两为字段为16位源端口号和16位目的端口号,根据目的端口号可以决定将报文交给上层的哪个协议,根据端口号可以找到上层的哪一个进程,这就交付给了上层。
对于这个TCP的数据段,其实是二进制的,因为在制定协议的时候已经约定好了TCP的标准报头是20字节,所以可以先把前20字节拿出来,选项有没有不确定,但是标准报头中有一个4位首部长度,表示的是我们的总长度是多少,包含了选项的大小。4位首部长度的范围就是0000 - 1111->[0,15],它只能表示从0到15,怎么可能表示数据段的总长度?4位首部长度在做计算的时候,有基本的大小单位:4字节。数字是[0,15],字节则是[0, 60]。
如果我们不谈选项,4位首部长度该写多少?
设4位首部长度位x,当我们的报头长度为20的时候,x * 4(基本单位) = 20,x = 5,即首部长度为0101.
如何将报头和有效载荷分离?固定长度(前20字节) + 自描述字段(4位首部长度)。
(二) 16位窗口大小
上面说过,TCP协议有自己的接收缓冲区和收发缓冲区,对于客户端是这样,对于服务端也是这样。在本地,应用层构建了一个http请求,通过write接口拷贝到了发送缓冲区,在发送之后,会添加上本层协议的报头,这个过程叫做封装,然后发送到对方的接收缓冲区中。对方的传输层通过4位首部长度 - 20字节,将tcp报头和有效载荷进行分离,再根据目的端口号找到上层的进程,来完成整个通信的过程。在这个通信的过程中,client和sesrver发送的是完整的TCP报文,即一定携带完整的TCP报头。
假如client向server发送信息,直到server的接收缓冲区满了,client还在发送,就直接导致server的接收缓冲区满了,进而导致大面积的丢包。这就导致了不可靠,所以要在client控制发送速度,让server来得及接收,从而避免大面积的丢包,这种策略叫做流量控制。如果你之前学过计算机网络,那么应该知道,TCP可靠性里面是有丢包重传,超时重传的,如果我们的报文丢失了,是可以补发的,如果sever来不及接收,是可以通过重传来解决的,虽然有重传机制,但是对于正确的报文做丢弃,很显然是不合理的,因为一个报文在占用了网络里面的带宽资源等其他资源来到server,却把他丢掉了,对于正确的报文做丢弃,是对网络资源的浪费。所以client发送的慢一点,是合理的方案。
TCP凭什么保证可靠性?
最基本的一个特点:确认应答机制。client向server发送数据,server要向client确认,发送一个确认一下,这样的话,就可以保证,client刚才发送的数据,server收到了这个数据。
当client向server发送数据之后,server接收到了数据,会给client发送一个确认应答。
如果client发送的太快,收不到server的确认应答了,client要发送的慢一点点,依据是什么?对于发送方来讲,由对方接收缓冲区剩余大小决定。
client如何知道server接收缓冲区剩余大小多少?
别忘了有确认应答机制,server会向client发送一个确认应答,这个确认应答也是一个完整的TCP报文,报文中有16位窗口大小,16位窗口大小中填的就是server端的接收缓冲区大小。双方互发消息,互相得知对方的接收缓冲区大小。所以双方就可以互相进行流量控制。
16位窗口大小中填写的一定是自己接收缓冲区剩余空间的大小。
这个16位的窗口大小就是进行流量控制的
(三) 32位序号和32位确认序号
TCP最基本,最原始的通信过程。当client向server发送数据的时候,server会向client发送一个应答,server也会向client发送数据,client会向server发送一个应答。这就保证了两个方向上的可靠性。
当client向server发送信息后,server发送应答,server不知道client有没有收到应答。其实也不需要关心这个,如果client没有收到应答,在一段时间内,如果没有收到应答,client认为数据丢失了,需要重传。只有收到了应答,client才知道刚发送的信息server收到了。
其实可以将c->s和s->c相互发送的4条消息变为3条。
像这种client发送一个消息需要server应答一次,效率就太低了,实际上client是一次性向server发送一批消息,server在原则上是要对这一批消息进行每一条的应答。
但是client将一批数据按照顺序发送给server的时候,server在接收的时候可能并不是按照顺序接收的,这就出现了乱序的问题。如果不解决这个问题,按照乱序交给应用层,解析出来的数据也是乱的。所以乱序本身就是不可靠的一种。如果张图片是由编号位1234的四个报文发送给server的,接收到的是2314,按照乱序解析后,这个图片会是啥样?传输层的TCP协议就要解决这个问题。
别忘了发送的tcp报文是有完整的tcp报头的,给每一个报头带上序号。server接收之后,就算是乱序的,也可以按照序号来给报文排序。
序号的核心作用之一:将数据按需到达。
序号是什么?
因为数组的下标本来就是连续的,所以按照下标排序就ok了。
TCP也不可能按照字节去发送,这样太慢了。所以发送的时候是按照一个一个的数据块发送的,每一个数据块是以数据块中最后一个元素的下标来发送的(可以这样理解)。
如图所示,client;连续向server发送了多个报文,server怎么知道是哪个报文?
确认序号:收到的报文序号+1,这是它的约定。
client发送1000,2000,3000,4000,server收到后会发送1001,2001,3001,4001作为应答。
确认序号的意思表示:确认序号之前的数据全部收到了。比如说,我收到了2001,那么2001以前的报文我全部收到了;如果是4001,那么4001以前的报文我全部都收到了。下一次发送请从序号指定的数字开始发送。
为什么这样规定? 如下图所示,当1000......4000的报文发送给server,server全部收到了,但是在确认应答的时候,只有4001发送过去了,client收到应答后,发现只有4001,没有其他的,按照规定,我们已经收到了4001前面的所有数据,尽管没有收到1001,2001,3001的确认应答。通过这样的规定,应答允许有少量的丢失。
为什么要有一个序号一个确认序号呢?
server向client发送确认应答的时候,是没有携带数据的,那么复用序号不行吗?
有个场景是可以解决这个问题的。
- client向server发消息,server要做出应答,做出应答的同时可能会应答纯报头,也可能会给client发消息,server向client做出应答的过程可能即是应答,也是发送过去的数据。所以对于server对client的应答就使用确认序号,而这个tcp数据本身就有数据,也需要client对server做出应答,通信时候的数据段可能是双重身份,所以必须把序号和确认序号分开,不能复用。
(四) 6位标志位
TCP通信的时候,是要建立连接,正常的数据通信,TCP断开连接一定要发送TCP报文,TCP收到的报文中一定是有各种类型的。不同的类型决定了server要做不同的动作。
接收方如何得知报头的类型各自是什么呢?报头中存在了6位标志位。
- ACK:确认号是否有效
- SYN:请求建立连接,我们把携带SYN标识的称为同步报文段
- FIN:通知对方,本端要关闭了
- PSH:提示接收端应用程序立刻从TCP缓冲区把数据读走(后面说事件就绪的时候再谈)
- RST:对方要求重新建立连接; 我们把携带RST标识的称为复位报文段
- URG:紧急指针是否有效
TCP虽然可靠,但是TCP允许连接建立失败。client在三次握手中,认为只要把三次握手中第三次报文发出去,就认为连接建立好了。
如果说,三次握手当中第三次报文发出去之后,server没有接收到这个报文,client认为三次握手已经成功,并且开始发送了数据,此时server会怎么办?server给client进行应答,并设置RST标志,告知连接是有问题的,需要重新进行三次握手。
TCP在发送数据的时候是按序发送,也是按序到达,但在某些情况下,想让上层优先处理某些数据,此时可以设置URG标志位,设置为1的时候,16位紧急指针有效,为0,16位紧急指针无效。
(五) 确认应答(ACK)机制
TCP将每个字节的数据都进行了编号. 即为序列号
每一个ACK都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下一次你从哪里开始发.
(六) 超时重传机制
主机A发送数据给B之后,可能因为网络拥堵等原因,数据无法到达主机B;如果主机A在一个特定的时间间隔内没有收到B发来的确认应答,就会进行重发。
如果主机A没有收到B发来的确认应答,也可能是因为ACK丢失了。
因此主机B会收到很多重复数据. 那么TCP协议需要能够识别出那些包是重复的包, 并且把重复的丢弃掉.去重的效果. 这时候我们可以利用前面提到的序列号
超时的时间如何确定呢?
最理想的情况下, 找到一个最小的时间, 保证 "确认应答一定能在这个时间内返回".
但是这个时间的长短, 随着网络环境的不同, 是有差异的.
如果超时时间设的太长, 会影响整体的重传效率;
如果超时时间设的太短, 有可能会频繁发送重复的包;
TCP为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间。
Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时时间都是500ms的整数倍.
如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传.
如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增.
累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接
(七) 连接管理机制
TCP会通过三次握手来进行链接的建立
客户端首先向服务器发送一个带有SYN(同步)标志的数据包,指明客户端希望建立连接。此时客户端进入SYN_SENT状态。
客户端 --------SYN-----> 服务器
服务器收到客户端的SYN请求后,如果同意建立连接,会发送一个带有SYN和ACK(确认)标志的数据包作为相应,同时,服务器会为这次连接分配资源。此时服务器进入SYN_PECV状态。
客户端 <------SYN-ACK------ 服务器
客户端收到服务器的SYN-ACK后,会发送一个带有ACK标志的数据包给服务器,表示连接已经建立。此时客户端和服务器都进入ESTABLISHED状态,连接建立完成
客户端 --------ACK-------> 服务器
通过四次挥手,来完成连接的释放。
客户端首先向服务器发送一个带有 FIN(终止)标志的数据包,表示客户端不再发送数据。此时,客户端进入 FIN_WAIT_1 状态。
客户端 --------FIN-------> 服务器
服务器收到客户端的 FIN 请求后,会发送一个带有 ACK 标志的数据包作为确认,并同时关闭对客户端的发送。此时,服务器进入 CLOSE_WAIT 状态。
客户端 <--------ACK--------- 服务器
服务器在准备好关闭连接后,向客户端发送一个带有 FIN 标志的数据包,表示服务器不再发送数据。此时,服务器进入 LAST_ACK 状态。
客户端 <--------FIN--------- 服务器
客户端收到服务器的 FIN 请求后,发送一个带有 ACK 标志的数据包作为确认。此时,客户端进入 TIME_WAIT 状态,等待可能出现的延迟的 ACK 包。
客户端 --------ACK---------> 服务器
客户端在 TIME_WAIT 状态等待一段时间,确保服务器收到 ACK,并且在这段时间内,如果服务器端还有没有被传递完全的数据,可以重传。完成等待后,客户端关闭连接,整个四次挥手过程完成。
下面是TCP状态转换的一个汇总:
较粗的虚线表示服务端的状态变化情况,较粗的虚线表示客户端的状态变化情况,CLOSED是一个假想的起始点,不是真实情况。
在讲网络套接字的时候,写了一个简单的tcp通信代码,现在用一下tcp的代码,做一些演示,当服务端启动的时候,在还没有进行accept的时候,通过 neetstat -nltp
可以看到tcpserver端处于Listen状态。
现在在不同的远程服务器中,启动客户端,将客户端与服务端进行连接。
客户端连接之后,处于ESTABLISHED状态。
当连接的个数超过了listen函数的第二个参数的时候,客户端还在继续请求连接。
server端就会进入SYN_RECV状态
,但是客户端是会进入ESTABLISHED状态,用图来表示就是
client向server发送第一次请求的时候,server端进行了恢复,server进入到SYN_RECV状态并向client端回复,client端进入到ESTABLISHED状态,并向server端发送回复。此时server端没有接收到这个回复。所以会处于SYN_RECV状态。对于client端来说,他认为已经完成了三次握手,对于server端来说,根本没有接收到第三次握手的消息。操作系统也不会让这个进程长时间的去占用资源,所以不会太长时间的去维护这个SYN_RECV,被建立连接的一方处于SYN_RECV,这是一种办连接状态,会将这个半连接放在半连接队列里面。
这就是服务端和客户端的建立连接不一致关系。
如果说用处于上面所描述的状态,client处于ESTABLISHED状态,server处于SYN_RECV状态,client向sesrver发送信息,因为client认为已经握手完成,所以是可以发送信息的,每一次发送信息都会重新进行三次握手,但每一次都不一定不会成功。
客户端和服务端连接不一致的情况挺多的。listen第二个参数代表的就是全连接队列的个数(listen的第二个参数 + 1)。这个参数不能太长,但又不能没有。
当全连接队列太长的时候,上层来不及处理队列中的数列,但是操作系统还要维护这个队列,占用内存资源。
为什么不能没有 ?防止服务器的资源得不到充分的利用,当上层处理完事务之后,下层立即向上层提供链接,主打一个让上层不能闲着。
在写tcp的时候,有时候会出现这样的情况,第一次用8080的端口号连接之后,在断开连接,8080的端口号就不能用了,得停一会才可以继续使用---bind失败,主动断开连接的一方,在四次挥手完成之后,要进入time_wait状态,等待若干时长之后,自动释放。在TIME_WAIT期间不能再次监听同样的server端口 。可以通过setsockopt函数
来解决这个问题,完成复用。
(八) 流量控制
接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送,就会造成丢包, 继而引起丢包重传等等一系列连锁反应.因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow Control);
接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 "窗口大小" 字段, 通过ACK端通知发送端;
窗口大小字段越大, 说明网络的吞吐量越高;
接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;
发送端接受到这个窗口之后, 就会减慢自己的发送速度;
如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端.
接收端如何把窗口大小告诉发送端呢? 回忆我们的TCP首部中, 有一个16位窗口字段, 就是存放了窗口大小信息;那么问题来了, 16位数字最大表示65535, 那么TCP窗口最大就是65535字节么?实际上, TCP首部40字节选项中还包含了一个窗口扩大因子M, 实际窗口大小是 窗口字段的值左移 M 位;
(九) 滑动窗口
- 对于已经发出去,但是暂时没有收到应答的报文,要被TCP暂时保存起来。
- 已经发出去,但是暂时没有收到应答,可能会在发送方存在多个
这两个问题合在一起就是,对于已经发出去,但是暂时灭有收到应答多个报文,会被保存在哪里?
tcp会有一个接收缓冲区和发送缓冲区,对于发送方来说,把数据发出去就是把发送缓冲区中的数据拷贝到底层,让底层去发送,换句话说,就算我发出去了,数据还是在发送缓冲区当中,不需要保存。我要做的无非就是把发送缓冲区做一个划分为几个区间。假设划分为三个区间,区间1:已发送已确认;区间2:已发送未确认;区间3:待发送部分+未知部分
如果说发送缓冲区中处于区间1的状态,上层在向发送缓冲区中写入数据的时候,就可以覆盖区间1的数据---就叫做从TCP缓冲区中移除它了。
区间2的含义就是可以发,但是尚未收到应答的区域。在收到应答之后,就将已发送已确认的部分扔到区间1中 --- 对于已发送未确认的区域,叫做滑动窗口 。在滑动窗口区域中的内容是可以直接推送给接收方的,因为有滑动窗口区域,我们才可以一次向对象发送大量的tcp报文。
到目前为止对于滑动窗口的理解
- 滑动窗口在哪里? 是发送缓冲区的一部分
- 滑动窗口的范围大小,是对方的接收窗口
- 如何理解区域划分?通过指针/下标来进行区分即可
发送前四个段的时候, 不需要等待任何ACK, 直接发送;
收到第一个ACK后, 滑动窗口向后移动, 继续发送第五个段的数据; 依次类推;
操作系统内核为了维护这个滑动窗口, 需要开辟 发送缓冲区 来记录当前还有哪些数据没有应答; 只有确认应答过的数据, 才能从缓冲区删掉;
窗口越大, 则网络的吞吐率就越高;
目前认为:滑动窗口的大小,不能超过对方的接收缓冲区的剩余空间的大小,即应答报文的窗口大小。
- 如果丢包了如何理解滑动窗口?
假设滑动窗口中的2001到3001的ACK丢失了,滑动窗口要往后移动吗?
我们对于确认序号的定义是:确认序号是x,x之前的报文我们全部收到了。如果我们收到5001的ACK,那么就代表我们已经收到了5001之前的所有确认应答。所以tcp是是允许少量的ACK丢失的。如果是5001的ACK丢失了,那么不用担心,滑动窗口左边直接更新到5001,然后进行重传就行了。如果全部报文丢失,滑动窗口不进行更新即可。只要收到了报文,根据确认序号,可以保证滑动窗口现行的连续的向后更新,不会出现跳跃的情况。
- 数据包直接丢失了
所有数据包都收到了,只有1001~2000的数据包没有收到。原则上剩下的报文要确认应答,他的确认序号里面必须填的是1001,当主机A连续收到同样的ACK赢啊的的时候,就会将对应的数据1001~2000进行重新发送。这个时候接收端收到了1001之后,再次返回的ACK就是7001了。这种机制被称为高速重发控制,也叫快重传。快重传是为了提高效率的,假设丢失的数据是8001~9000,此时后面的数据没有那么多了,就不会触发快重传机制,而是会进行超时重传机制。
快重传是为了提高效率的,超时重传是兜底的。
- 滑动窗口会不会向左移动?向右移动?移动的时候大小会变化吗?怎么变化?
原则上是不会向左移动的,左指针和右指针只会进行++操作。只会进行向右移动,滑动窗口的大小肯定不是固定的,如果说接收方的上层不取走数据,那么滑动窗口的大小会变小,如果接收方上层取走数据的速度非常快,那么滑动窗口会变大。这个窗口的大小是处于动态变化的。
(十) 延迟应答
如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小.
假设接收端缓冲区为1M. 一次收到了500K的数据; 如果立刻应答, 返回的窗口就是500K;
但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了;
在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来;
如果接收端稍微等一会再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M;
一定要记得, 窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率;
那么所有的包都可以延迟应答么? 肯定也不是;
数量限制: 每隔N个包就应答一次;
时间限制: 超过最大延迟时间就应答一次;
具体的数量和超时时间, 依操作系统不同也有差异; 一般N取2, 超时时间取200ms;
(十一) 捎带应答
在延迟应答的基础上, 我们发现, 很多情况下, 客户端服务器在应用层也是 "一发一收" 的. 意味着客户端给服务器说了 "How are you", 服务器也会给客户端回一个 "Fine, thank you";那么这个时候ACK就可以搭顺风车, 和服务器回应的 "Fine, thank you" 一起回给客户端
(十二) 拥塞控制
双方主机进行通信的时候,不仅仅是在两台主机上通信,还会再网络上进行传输。TCP还替我们考虑了网络。
如果发送数据,出现问题,不仅仅是对方主机出现问题吧,也可能是网络出现了问题。
- 如果通信的时候,出现了少量的丢包?可能会认为是常规情况
- 如果通信的时候,出现了大量的丢包?TCP本身是可靠传输协议,这个时候可能会考虑网络出现了问题,比如硬件设备问题,数据量太大,引起阻塞等。
出现了大量的丢包的时候(大量的数据都超时了),TCP会判断网络出问题了(网络拥塞了)。
这个时候我们发送发我们不能立即对报文进行超时重发。为什么?如果是因为硬件设备出现问题,在怎么重发也没有作用。如果是因为数据量太大,引起阻塞,此时补发报文不是一个好的选择,会加重阻塞。这个时候应该等一等或者发送少量的数据。
首先网络资源是共享的,网络不会因为你这你台主机因为发送了少量的数据或者不发数据而造成阻塞加重。但是不能光考虑CS两端,这个时候不止你一台主机是这样,其他主机也是这样。所有主机使用的都是TCP/IP协议。用TCP协议实现了多主机面对网络出现拥塞的共识。
那么所有主机都能判断出来网络拥塞了吗?如果说你只发送了一两个数据,而另一个发送了几十条数据,此时,你的主机不会认为网络拥塞,而另一个主机会认为是网络拥塞。
拥塞控制的策略 --- 每台识别主机拥塞的机器,都要做的策略。
TCP引入 慢启动机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据;
此处引入一个概念程为拥塞窗口
发送开始的时候, 定义拥塞窗口大小为1;
每次收到一个ACK应答, 拥塞窗口加1;
每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口;
滑动窗口大小 = min(窗口大小,拥塞窗口);拥塞窗口考虑的是动态的,网络的接受能力。窗口大小是对方主机的接收能力。
在TCP中有三个窗口,滑动窗口,接收窗口,拥塞窗口。
主机判断网络健康成都的指标,超过拥塞窗口,会引发网络拥塞,否则不会。
像上面这样的拥塞窗口增长速度, 是指数级别的. "慢启动" 只是指初使时慢, 但是增长速度非常快
为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍.
此处引入一个叫做慢启动的阈值
当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长
当TCP开始启动的时候, 慢启动阈值等于窗口最大值;
在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1
少量的丢包, 我们仅仅是触发超时重传; 大量的丢包, 我们就认为网络拥塞;
当TCP通信开始后, 网络吞吐量会逐渐上升; 随着网络发生拥堵, 吞吐量会立刻下降;
拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案
(十三) TCP异常情况
进程终止:进程终止会释放文件描述符,仍然可以发送FIN,和正常关闭没有什么区别。
机器重启:电脑关机的时候,会自动杀死所有的进程。跟进程终止情况相同。
机器掉电/网线断开:网线断了之后,报文是发不过去的,服务端会认为链接还正常,一旦服务端有写入操作,此时发现客户端链接已经不在了,就会进行reset。
另外, 应用层的某些协议, 也有一些这样的检测机制. 例如HTTP长连接中, 也会定期检测对方的状态. 例如QQ, 在QQ断线之后, 也会定期尝试重新连接.
(十四) TCP小结
为什么TCP这么复杂? 因为要保证可靠性, 同时又尽可能的提高性能
可靠性
- 校验和
- 序列号
- 确认应答
- 超时重发
- 连接管理
- 流量控制
- 拥塞控制
提高性能
- 滑动窗口
- 快速重传
- 延迟应答
- 捎带应答