2023/10/17 14:32:49
本章主要讲解传输层协议UDP及TCP相关的内容
16位源端口号:表示数据从哪里来
16位目的端口号:表示数据要到哪里去
16位UDP长度:表示整个数据报(UDP首部+UDP数据)的长度
16位UDP检验和:如果UDP报文的检验和出错,就会直接将报文丢弃
注:端口号大部分都是16位的,其根本原因就是因为传输层协议当中的端口号就是16位的
UDP报头是一种定长报头,UDP在读取报文时读取完前8个字节(报头)后剩下的就都是有效载荷了
获取到一个报文后从该报文的前8个字节中提取出对应的目的端口号,通过目的端口号找到对应的上层应用层进程进行交付
无连接: 知道对端的IP和端口号就直接进行传输, 不需要建立连接
不可靠: 没有确认机制, 没有重传机制,无法保证接收报文的正确顺序; 如果因为网络故障该段无法发到对方, UDP协议层也不会给应用层返回任何错误信息
面向数据报: 不能够灵活的控制读写数据的次数和数量
应用层交给UDP多长的报文, UDP原样发送, 既不会拆分, 也不会合并
用UDP传输100个字节的数据:如果发送端调用一次sendto, 发送100个字节, 那么接收端也必须调用对应的一次recvfrom, 接收100个字节; 而不能循环调用10次recvfrom, 每次接收10个字节
UDP没有真正意义上的发送缓冲区,调用sendto会直接交给内核,由内核将数据传给网络层协议进行后续的传输动作
UDP具有接收缓冲区,但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致; 如果缓冲区满了, 再到达的UDP数据就会被丢弃
如果没有接收缓冲区,那么就要求上层及时将获取到的报文读取上去,如果没有及时被读取,那么新获取的报文数据就会被迫丢弃;UDP会维护接收缓冲区将接收到的报文暂时的保存起来,供上层读取
UDP的socket在读(写)的同时也能进行写(读),也就是说不会像管道这种半双工通信读写是阻塞进行的(无法同时进行)
- UDP协议首部中有一个16位的最大长度,也就是说一个UDP能传输的数据最大长度是64K(包含UDP首部)
- 如果我们需要传输的数据超过64K, 就需要在应用层手动的分包,多次发送,并在接收端手动拼装
- NFS:网络文件系统
- TFTP:简单文件传输协议
- DHCP:动态主机配置协议
- BOOTP:启动协议(用于无盘设备启动)
- DNS:域名解析协议
计算机中的硬件设备是之间的数据传输是依靠“线”进行的,而这些硬件设备都在一台机器上,因此传输数据的“线”是很短的,传输出错的概率也非常低
如果要在网络中进行通信(距离远),此时需要维护传输的“线”就非常长,传输出错的概率增大,要保证传输的可靠性就需要相应的做更多的事情,而TCP就是一种保证可靠性的协议
- TCP协议是可靠的协议,也就意味着TCP协议需要做更多的工作来保证传输数据的可靠,此时需要的成本相比于UDP更高
- UDP协议是不可靠的协议,也就意味着UDP协议不需要考虑数据传输时可能出现的问题,但UDP无论是使用还是维护都足够简单
- 虽然TCP复杂,但TCP的效率不一定比UDP低,TCP当中不仅有保证可靠性的机制,还有保证传输效率的其他机制
- 网络通信时具体采用TCP还是UDP完全取决于上层的应用场景。如果应用场景严格要求数据在传输过程中的可靠性,那么就必须采用TCP协议,如果应用场景允许数据传输出现少量丢包,那么肯定优先选择UDP协议,因为UDP协议足够简单
TCP全称为 “传输控制协议(Transmission Control Protocol”). 人如其名, 要对数据的传输进行一个详细的控制
16位源端口号:表示数据从哪里来
16位目的端口号:表示数据要到哪里去
32位序号:表示发送的TCP报文的编号
32位确认序号:表示对接收到对方曾经发送过的TCP报文的确认
4位TCP报头长度:表示该TCP报头的长度,以4字节为单位(表示范围20-60字节:固定的字段就有20字节,如果带有选项的话,最大报头可以为15*4=60字节)
6位保留字段:TCP报头中暂时未使用的6个比特位
16位窗口大小:保证TCP可靠性机制和效率提升机制的重要字段
16位检验和:由发送端填充,采用CRC校验;接收端校验不通过,则认为接收到的数据有问题(检验和包含TCP首部+TCP数据部分)
16位紧急指针:标识紧急数据在报文中的偏移量,需要配合标志字段当中的URG字段统一使用
TCP报头当中的6位标志位:
URG:紧急指针是否有效
ACK:确认序号是否有效
PSH:提示接收端应用程序立刻将TCP接收缓冲区当中的数据读走
RST:表示要求对方重新建立连接。我们把携带RST标识的报文称为复位报文段
SYN:表示请求与对方建立连接。我们把携带SYN标识的报文称为同步报文段
FIN:通知对方,本端要关闭了。我们把携带FIN标识的报文称为结束报文段
获取到TCP报文后,首先读取报文的前20个字节,并从中提取出4位的首部长度,此时便获得了TCP报头的大小,读取完TCP的基本报头和选项字段后,剩下的就是有效载荷了
- 通过发送缓冲区存储发送的数据,当收到对应的应答后(也就是对应的数据成功被对端接收),对应的数据才会‘取出’,也就保证了发送的可靠性;通过接收缓冲区储存接收到的数据,保证数据能够可靠的被上层取出
- 调用write/send这样的系统调用接口时,实际不是将数据直接发送到了网络当中,而是将数据从应用层拷贝到了TCP的发送缓冲区当中;当上层调用read/recv这样的系统调用接口时,实际也不是直接从网络当中读取数据,而是将数据从TCP的接收缓冲区拷贝到了应用层这样能够使得应用层能够和传输层进行解耦,上层只需要将数据交给TCP或者从TCP中将数据取出,TCP则是负责数据的发送和接收的问题
当发送端要将数据发送给对端时,本质是把自己发送缓冲区当中的数据发送到对端的接收缓冲区当中,但缓冲区是有大小的,不能无限制的接收,所以需要对发送进行控制
而16位窗口大小当中填的是自身接收缓冲区中剩余空间的大小,当对端接收到窗口信息时可以根据这个字段来调整自己发送数据的速度,避免发送速度过快造成缓冲区满足,进而可能引起丢包重传
用read/recv函数从套接字当中读取数据时,会因为套接字当中没有数据而被阻塞住,本质是因为TCP的接收缓冲区当中没有数据了,实际是阻塞在接收缓冲区当中了;调用write/send函数往套接字中写入数据时,会因为套接字已经写满而被阻塞住,本质是因为TCP的发送缓冲区已经被写满了,实际是阻塞在发送缓冲区当中了
TCP报文除了正常通信时发送的普通报文,还需要其他类型的报文来表示其他的一些需求,例如连接,断开连接等等;TCP使用报头当中的六个标志字段来进行区分的报文类型,这六个标志位都只占用一个比特位,为0表示假,为1表示真
SYN:报文当中的SYN被设置为1,表明该报文是一个连接建立的请求报文;只有在连接建立阶段,SYN才被设置,正常通信时SYN不会被设置
ACK:报文当中的ACK被设置为1,表明该报文可以对收到的报文进行确认;一般除了第一个请求报文没有设置ACK以外,其余报文基本都会设置ACK,因为发送出去的数据本身就对对方发送过来的数据的确认
FIN:报文当中的FIN被设置为1,表明该报文是一个连接断开的请求报文;只有在断开连接阶段,FIN才被设置,正常通信时FIN不会被设置
URG:TCP是保证数据按序到达的,对于若干个TCP报文进行发送,最终到达接收端时这些数据也都是有序的,对于发送“紧急数据”就需要让对方上层提取进行加急处理;当URG标志位被设置为1时,需要通过TCP报头当中的16位紧急指针来找到紧急数据,否则一般情况下不需要关注TCP报头当中的16位紧急指针,16位紧急指针代表的就是紧急数据在报文中的偏移量
注:recv函数的第四个参数flags有一个叫做MSG_OOB的选项可供设置,其中OOB是带外数据(out-of-band)的简称,带外数据就是一些比较重要的数据,因此上层如果想读取紧急数据,就可以在使用recv函数进行读取,并设置MSG_OOB选项;send函数的第四个参数flags也提供了一个叫做MSG_OOB的选项,上层如果想发送紧急数据,就可以使用send函数进行写入,并设置MSG_OOB选项
PSH:报文当中的PSH被设置为1,是在告诉对端尽快将缓冲区里的数据进行取出
RST:报文当中的RST被设置为1,表示需要让对方重新建立连接;双方在连接未建立好一方发数据,另一方要求对方重新建立连接,建立好的连接出现了异常也会要求重新建立连接
- 在进行网络通信时,一方发出的数据后,它不能保证该数据能够成功被对端收到,因为数据在传输过程中可能会出现各种各样的错误,只有当收到对端主机发来的响应消息后,该主机才能保证上一次发送的数据被对端成功的收到了,这就叫做真正的可靠,而这种策略在TCP当中就叫做确认应答机制
- 在实际的网络通信中,最后一次的数据传输是无法确认对端是否成功收到,但实际没有必要保证所有消息的可靠性,只要传输的核心数据都有对应的响应就可以,一些无关紧要的数据没有必要保证它的可靠性
注:对端如果没有收到这个响应数据,会判定上一次发送的报文丢失了,此时对端可以将上一次发送的数据进行重传
- TCP将每个字节的数据都进行了编号(TCP具有发送缓冲区,缓冲区的下标即为编号),即为序列号
- 每一个ACK都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下一次你从哪里开始发
- 在双方实际进行网络通信时,为了提高数据传输的效率,允许一方向另一方连续发送多个报文数据,只要保证发送的每个报文都有对应的响应消息就行了
- 在连续发送多个报文时,由于各个报文在进行网络传输时选择的路径可能是不一样的,因此这些报文到达对端主机的先后顺序也就可能和发送报文的顺序是不同的,而TCP报头中的32位序号的作用之一实际就是用来保证报文的有序性的
- 接收端收到多个TCP报文后,就可以根据TCP报头当中的32位序列号对这多个报文进行顺序重排,重排后将其放到TCP的接收缓冲区当中,此时接收端这里报文的顺序就和发送端发送报文的顺序是一样的
- 当进行报文重排时,会根据当前报文的32位序号与其有效载荷的字节数,进而确定下一个报文对应的序号,通过检验序号查看是否有数据丢失,进而进行重发
- 报头当中的确认序号实际就是,接收缓冲区中接收到的最后一个有效数据的下一个位置所对应的下标
- TCP报头当中的32位确认序号是告诉对端,当前已经收到了之前的哪些数据,你的数据下一次应该从哪里开始发
当主机B收到主机A发送过来的32位序号为1的报文时,由于该报文当中包含1000字节的数据,因此主机B已经收到序列号为1-1000的字节数据,于是主机B发给主机A的响应数据的报头当中的32位确认序号的值就会填成1001…
- 一般来说双方通信发送数据和应答数据,只用一套序号就可以了;TCP为了保证效率,在应答对方之前发送的数据的同时可能想给对方发送数据
- 双方发出的报文当中,不仅需要填充32位序号来表明自己当前发送数据的序号,还需要填充32位确认序号,对对方上一次发送的数据进行确认,告诉对方下一次应该从哪一字节序号开始进行发送
- 双方在进行网络通信时,发送方发出去的数据在一个特定的事件间隔内如果得不到对方的应答,此时发送方就会进行数据重发,这就是TCP的超时重传机制
- 超时重传机制实际就是发送方在发送数据后开启了一个定时器,若是在这个时间内没有收到刚才发送数据的确认应答报文,则会对该报文进行重传
此时发送端在一定时间内收不到对应的响应报文,就会进行超时重传
说明:主机A发送数据给B之后, 可能因为网络拥堵等原因, 数据无法到达主机B;如果主机A在一个特定时间间隔内没有收到B发来的确认应答, 就会进行重发
此时发送端也会因为收不到对应的响应报文,而进行超时重传
说明:主机B会收到很多重复数据. 那么TCP协议需要能够识别出那些包是重复的包,并且把重复的丢弃掉,通过序列号就可以很容易做到去重的效果
注:当发送缓冲区当中的数据被发送出去后,操作系统不会立即将该数据从发送缓冲区当中删除或覆盖,而会让其保留在发送缓冲区当中,以免需要进行超时重传,直到收到该数据的响应报文后,发送缓冲区中的这部分数据才可以被删除或覆盖
- 超时重传的时间设置的太长,会导致丢包后对方长时间收不到对应的数据,进而影响整体重传的效率;超时重传的时间设置的太短,会导致对方收到大量的重复报文,可能对方发送的响应报文还在网络中传输而并没有丢包,但此时发送方就开始进行数据重传了,而发送大量重复报文会是对网络资源的浪费
- 超时重传的时间需要保证“确认应答一定能在这个时间内返回”,同时这个时间的长短是与网络环境有关的,会根据网络状况进行相应的变化
- Linux中(BSD Unix和Windows也是如此),超时以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍,如果重发一次之后,仍然得不到应答,下一次重传的等待时间就是2 × 500 2\times5002×500ms,如果仍然得不到应答,那么下一次重传的等待时间就是4 × 500 4\times5004×500ms,以此类推以指数的形式递增
- 当累计到一定的重传次数后,TCP就会认为是网络或对端主机出现了异常,进而强转关闭连接
- TCP是面向连接的,在进行TCP通信之前需要先建立连接,保证传输数据的可靠性
- 面向连接是TCP可靠性的一种,一台机器上可能会存在大量的连接,此时操作系统就需要对这些连接进行管理
- 建立连接,实际就是在操作系统中用该结构体定义一个结构体变量,然后填充连接的各种属性字段,最后将其插入到管理连接的数据结构当中即可;断开连接,实际就是将某个连接从管理连接的数据结构当中删除,释放该连接曾经占用的各种资源
- [CLOSED -> LISTEN] 服务器端调用listen后进入LISTEN状态, 等待客户端连接
- [LISTEN -> SYN_RCVD] 一旦监听到连接请求(同步报文段), 就将该连接放入内核等待队列中, 并向客户端发送SYN确认报文
- [SYN_RCVD -> ESTABLISHED] 服务端一旦收到客户端的确认报文, 就进入ESTABLISHED状态, 可以进行读写数据了
- [ESTABLISHED -> CLOSE_WAIT] 当客户端主动关闭连接(调用close), 服务器会收到结束报文段, 服务器返回确认报文段并进入CLOSE_WAIT
- [CLOSE_WAIT -> LAST_ACK] 进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据); 当服务器真正调用close关闭连接时, 会向客户端发送FIN, 此时服务器进入LAST_ACK状态, 等待最后一个ACK到来(这个ACK是客户端确认收到了FIN)
- [LAST_ACK -> CLOSED] 服务器收到了对FIN的ACK, 彻底关闭连接
- [CLOSED -> SYN_SENT] 客户端调用connect, 发送同步报文段
- [SYN_SENT -> ESTABLISHED] connect调用成功, 则进入ESTABLISHED状态, 开始读写数据
- [ESTABLISHED -> FIN_WAIT_1] 客户端主动调用close时, 向服务器发送结束报文段, 同时进入FIN_WAIT_1
- [FIN_WAIT_1 -> FIN_WAIT_2] 客户端收到服务器对结束报文段的确认, 则进入FIN_WAIT_2, 开始等待服务器的结束报文段
- [FIN_WAIT_2 -> TIME_WAIT] 客户端收到服务器发来的结束报文段, 进入TIME_WAIT, 并发出LAST_ACK
- [TIME_WAIT -> CLOSED] 客户端要等待一个2MSL(Max Segment Life, 报文最大生存时间)的时间, 才会进入CLOSED状态
- 第一次握手:客户端向服务器发送的报文当中的SYN位被设置为1,表示请求与服务器建立连接
- 第二次握手:服务器收到客户端发来的连接请求报文后,紧接着向客户端发起连接建立请求并对客户端发来的连接请求进行响应,此时服务器向客户端发送的报文当中的SYN位和ACK位均被设置为1
- 第三次握手:客户端收到服务器发来的报文后,得知服务器收到了自己发送的连接建立请求,并请求和自己建立连接,最后客户端再向服务器发来的报文进行响应
- 需要注意的是,客户端向服务器发起的连接建立请求,是请求建立从客户端到服务器方向的通信连接,而TCP是全双工通信,因此服务器在收到客户端发来的连接建立请求后,服务器也需要向客户端发起连接建立请求,请求建立从服务器到客户端方法的通信连接
TCP是全双工的,连接建立的核心要务是验证双方的通信信道是否是良好的(从服务端到客户端,从客户端到服务端的信道),而三次握手恰好是验证双方通信信道的最小次数(此时服务端和客户端都进行了数据的发送和接收)
- 我们知道最后一次的数据发送是无法知道是否成功被对端接收的,也就是说最后一次的握手是存在丢包的风险的。一方发起最后一次握手前会进行连接的建立,而另一方在接收到最后一次握手时才会进行连接的建立,如果最后一次握手丢包,那么发起方会存有一个异常的连接
- 而连接的维护是需要时间成本和空间成本的,同时服务端和客户端是一个1:n的数量状况,如果异常连接的维护是在服务端的话,当存在大量的异常连接就会影响服务器的性能;发起连接请求是客户端访问服务端,当奇数次握手时异常连接是挂在客户端的,而不会影响到服务器
注:建立连接失败时的异常连接不会一直维护下去。如果服务器端长时间收不到客户端发来的第三次握手,就会将第二次握手进行超时重传,此时客户端就会重新发出第三次握手;或者当客户端认为连接建立好后向服务器发送数据时,此时服务器会发现没有和该客户端建立连接而要求客户端重新建立连接(设置RST标志位)
- connect函数不参与底层的三次握手,connect函数的作用只是发起三次握手。当connect函数返回时,要么是底层已经成功完成了三次握手连接建立成功,要么是底层三次握手失败
- 如果服务器端与客户端成功完成了三次握手,此时在服务器端就会建立一个连接,但这个连接在内核的等待队列当中,服务器端需要通过调用accept函数将这个建立好的连接获取上来
- 当服务器端将建立好的连接获取上来后,上层就可以通过调用read/recv函数和write/send函数进行数据交互了
- 第一次挥手:客户端向服务器发送的报文当中的FIN位被设置为1,表示请求与服务器断开连接
- 第二次挥手:服务器收到客户端发来的断开连接请求后对其进行响应
- 第三次挥手:服务器收到客户端断开连接的请求,且已经没有数据需要发送给客户端的时候,服务器就会向客户端发起断开连接请求
- 第四次挥手:客户端收到服务器发来的断开连接请求后对其进行响应
- TCP是全双工的,建立的连接是双向的,所以断开连接需要将服务端对客户端的连接以及客户端对服务端的连接都给断开,每两次挥手对应就是关闭一个方向的通信信道,因此断开连接时需要进行四次挥手
- 四次挥手当中的第二次和第三次挥手不能合并在一起,第三次握手是服务器端想要与客户端断开连接时发给客户端的请求,服务器不一定会马上发起第三次挥手,服务器可能还有某些数据要发送给客户端(此时服务端对客户端的连接还没有断开)
客户端发起断开连接请求,对应就是客户端主动调用close函数,服务器发起断开连接请求,对应就是服务器主动调用close函数,一个close对应的就是两次挥手,双方都要调用close,因此就是四次挥手
- 客户端调用了close函数发起两次挥手,服务器接收后就会进入CLOSE_WAIT状态,客户端再接收到服务端的ACK之后则会进入到FIN_WAIT_2状态;但服务端还没有发起两次挥手,只有完成四次挥手后连接才算真正断开,此时双方才会释放对应的连接资源
- 如果服务器接收到两次挥手后不进行调用close,那么服务器端就会存在大量处于CLOSE_WAIT状态的连接,而每个连接都会占用服务器的资源,最终就会导致服务器可用资源越来越少
注:对于服务器上出现大量的 CLOSE_WAIT 状态, 原因就是服务器没有正确的关闭 socket, 导致四次挥手没有正确完成,这是一个 BUG,只需要加上对应的 close 即可解决问题
客户端主动关闭连接时,发送最后一个ACK后,然后会进入TIME_WAIT状态,再停留2个MSL时间(后有MSL的解释),进入CLOSED状态,所以我们可以粗略地理解成在断开连接后主动断开连接的那一方就会进入一个等待状态,过了一定时间后,再真正的关闭,这所谓的状态也就是TIME_WAIT状态,这其中所说的一定时间也就是2MSL(MSL:最长分节生命期maximum segment lifetime,这个状态会持续MSL时长的两倍,所以称为2MSL)
- 可靠地实现TCP全双工连接的终止
假设发起主动关闭的一方(client)最后发送的ACK在网络中丢失,由于TCP协议的重传机制,执行被动关闭的一方(server)将会重发其FIN,在该FIN到达client之前,client必须维护这条连接状态,也就说这条TCP连接所对应的资源(client方的local_ip,local_port)不能被立即释放或重新分配,直到另一方重发的FIN达到之后,client重发ACK后,经过2MSL时间周期没有再收到另一方的FIN之后,说明被动关闭的一方(server)成功收到ACK,这样才能转变成CLOSED状态,正式断开双方建立的连接
如果主动关闭一方不维护这样一个TIME_WAIT状态,那么当被动关闭一方重发的FIN到达时,主动关闭一方的TCP传输层会用RST包响应对方,这会被对方认为是有错误发生,然而这事实上只是正常的关闭连接过程,并非异常
- 允许老的重复分节在网络中消逝
假设在12.106.32.254的1500端口和206.168.112.219的21端口之间有一个TCP连接。我们关闭这个连接,过一段时间后在相同的IP地址和端口之间建立另一个连接。后一个连接称为前一个连接的化身(incarnation),因为它们的IP地址和端口号都相同
TCP必须防止来自某个连接的老的重复分组在该连接已终止后再现,从而被误解成属于同一连接的某个新的化身。为做到这一点,TCP将不给处于TIME_WAIT状态的连接发起新的化身。所以TIME_WAIT状态的持续时间是MSL的2倍,这就足以让某个方向上的分组最多存活MSL秒即被丢弃,另一个方向上的应答最多存活MSL秒也被丢弃;此后,就可以用相同的四元组建立一条新连接而不会发生前后两次连接数据错乱的情况
注:MSL在RFC1122中规定为两分钟,但是各个操作系统的实现不同,比如在Centos7上默认配置的值是60s,可以通过cat /proc/sys/net/ipv4/tcp_fin_timeout命令来查看MSL的值
MSL是TCP报文的最大生存时间,因此TIME_WAIT状态持续存在2MSL的话,就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失,同时也是在理论上保证最后一个报文可靠到达的时间
- 接收端处理数据的速度是有限的,如果发送端发的太快,导致接收端的缓冲区被打满,此时发送端继续发送数据,就会造成丢包,进而引起丢包重传等一系列连锁反应,因此接收端可以将自己接收数据的能力告知发送端,让发送端控制自己发送数据的速度
- TCP支持根据接收端的接收数据的能力来决定发送端发送数据的速度,这个机制叫做流量控制(Flow Control)
- 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 “窗口大小” 字段, 通过ACK端通知发送端;
- 窗口大小字段越大, 说明网络的吞吐量越高;
- 接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;
- 发送端接受到这个窗口之后, 就会减慢自己的发送速度;
- 如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端
主动询问:发送端每隔一段时间向接收端发送报文,该报文不携带有效数据,只是为了询问发送端的窗口大小,直到接收端的接收缓冲区有空间后发送端就可以继续发送数据了
等待告知:接收端上层将接收缓冲区当中的数据读走后,接收端向发送端发送一个TCP报文,主动将自己的窗口大小告知发送端
理论上确实是这样的,但实际上TCP报头当中40字节的选项字段中包含了一个窗口扩大因子M,实际窗口大小是窗口字段的值左移M位得到的
双方在进行TCP通信之前需要先进行三次握手建立连接,而双方在握手时除了验证双方通信信道是否通畅以外,还进行了其他信息的交互,其中就包括告知对方自己的接收能力,因此在双方还没有正式开始通信之前就已经知道了对方接收数据能力
- 在穿行的数据通行时,对每一个发送的数据段都要给一个ACK确认应答,收到ACK后再发送下一个数据段,而这样性能较差,效率不高,尤其是数据往返的时间较长的时候
- 实际在进行TCP通信中,发送方在根据对方的接受能力下可以一次向对方发送多条数据,这样可以将等待多个响应的时间重叠起来,进而提高数据通信的效率
- 可以将发送缓冲区当中的数据分为三部分:已经发送并且已经收到ACK的数据,已经发送还但没有收到ACK的数据,还没有发送的数据
- 窗口大小指的是无需等待确认应答而可以继续发送数据的最大值,滑动窗口存在的最大意义就是可以提高发送数据的效率
- 窗口大小指的是无需等待确认应答而可以继续发送数据的最大值,上图的窗口大小就是4000个字节(四个段)发送前四个段的时候,不需要等待任何ACK直接发送;收到第一个ACK后,滑动窗口向后移动,继续发送第五个段的数据,依次类推
- 操作系统内核为了维护这个滑动窗口,需要开辟发送缓冲区来记录当前还有哪些数据没有应答;只有确认应答过的数据才能从缓冲区删掉(保证数据传输的可靠性,如果丢包可以进行超时重传)
- 窗口越大,则网络的吞吐率就越高
注:实际中滑动窗口的大小等于对方窗口大小与自身拥塞窗口大小的较小值,因为发送数据时不仅要考虑对方的接收能力,还要考虑当前网络的状况
滑动窗口会根据接收到的确认应答来移动左边界的位置,根据对端和网络的情况两者中的最小值计算确定有边界的位置,所以滑动窗口也会随之不断变宽或者变窄
当发送端一次发送多个报文数据时,此时的丢包情况也可以分为两种
示图:
- 在发送端连续发送多个报文数据时,部分ACK丢包并不要紧,此时可以通过后续的ACK进行确认
- 因为确认序号表示的是改序号之前的数据包已经成功的接收到了,下一次从改序号开始发送数据
- 当某一段报文段丢失之后,发送端会一直收到 1001 这样的ACK,就像是在提醒发送端 "我想要的是 1001"一样
- 如果发送端主机连续三次收到了同样一个 “1001” 这样的应答,就会将对应的数据 1001 - 2000 重新发送
- 这个时候接收端收到了 1001 之后,再次返回的ACK就是7001了(因为2001 - 7000)接收端其实之前就已经收到了,被放到了接收端操作系统内核的接收缓冲区中
注:这种机制被称为“高速重发控制”,也叫做“快重传”
- 超时重传需要通过设置重传定时器,在固定的时间后才会进行重传;快重传是在接收者接收到一个
乱序的分组
的话,就返回对前一个正确分组的确认应答,当浏览器连续收到三个冗余ACK
,就会马上快速重传丢失数据
,不必等到超时时间再重传- 超时重传缺点是太慢了,超时时间的设置不好把握;快速重传解决了超时重传的慢速缺点,但是多发了好几个ACK会导致网络更加拥塞
- 虽然TCP有了滑动窗口这个大杀器,能够高效可靠的发送大量的数据,但是如果在刚开始阶段就发送大量的数据,仍然可能引发问题
- 因为网络上有很多的计算机,可能当前的网络状态就已经比较拥堵,在不清楚当前网络状态下,贸然发送大量的数据,是很有可能引起雪上加霜的,此时很可能出现大量丢包的状况
- TCP引入慢启动机制,先发少量的数据探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据
注:一旦出现大量的丢包,此时TCP就不再推测是双方接收和发送数据的问题,而判断是双方通信信道网络出现了拥塞问题
此处引入一个概念程为拥塞窗口,发送开始的时候, 定义拥塞窗口大小为1k;每次收到一个ACK应答, 拥塞窗口加1k;每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口
像上面这样的拥塞窗口增长速度, 是指数级别的,“慢启动” 只是指初使时慢,但是增长速度非常快。为了不增长的那么快,因此不能使拥塞窗口单纯的加倍,此处引入一个叫做慢启动的阈值:当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长 示图:
注:当TCP开始启动的时候, 慢启动阈值等于窗口最大值;在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1
- 少量的丢包,我们仅仅是触发超时重传;大量的丢包,我们就认为网络拥塞
- 当TCP通信开始后, 网络吞吐量会逐渐上升;随着网络发生拥堵, 吞吐量会立刻下降
- 拥塞控制,归根结底是TCP协议想尽可能快的把数据传输给对方,但是又要避免给网络造成太大压力的折中方案
网络拥塞时影响的不只是一台主机,而几乎是该网络当中的所有主机,此时所有使用TCP传输控制协议的主机都会执行拥塞避免算法;拥塞控制看似只是谈论的一台主机上的通信策略,实际这个策略是所有主机在网络崩溃后都会遵守的策略
- 假设对方接收端缓冲区剩余空间大小为1M,对方一次收到500K的数据后,如果立即进行ACK应答,此时返回的窗口就是500K
- 但实际接收端处理数据的速度很快,10ms之内就将接收缓冲区中500K的数据消费掉了
- 在这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些,也能处理过来
- 如果接收端稍微等一会再进行ACK应答,比如等待200ms再应答,那么这时返回的窗口大小就是1M
窗口越大,网络吞吐量就越大,传输效率就越高,延迟应答的目标是在保证网络不拥塞的情况下尽量提高传输效率
- 数量限制:每个N个包就应答一次
- 时间限制:超过最大延迟时间就应答一次(这个时间不会导致误超时重传)
注:延迟应答具体的数量和超时时间,依操作系统不同也有差异,一般N取2,超时时间取200ms
- 很多情况下, 客户端服务器在应用层也是 “一发一收” 的:客户端给服务器发送数据,服务器可能也会给客户端发送数据,那么这个时候ACK可以和数据一起回给客户端,此时发送的这个报文既发送了数据,又完成了对收到数据的响应,这就叫做捎带应答
- 捎带应答实际也是发送数据的效率,此时双方通信时就可以不用再发送单纯的确认报文了
- 调用write时, 数据会先写入发送缓冲区中
- 如果发送的字节数太长, 会被拆分成多个TCP的数据包发出
- 如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去
- 接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区
- 然后应用程序可以调用read从接收缓冲区拿数据
- 另一方面, TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可以写数据,这个概念叫做全双工
- 写100个字节数据时,可以调用一次write写100字节,也可以调用100次write,每次写一个字节
- 读100个字节数据时,也完全不需要考虑写的时候是怎么写的,既可以一次read100个字节,也可以一次read一个字节,重复100次
注:在TCP看来这些只是一个个的字节数据,它的任务就是将这些数据准确无误的发送到对方的接收缓冲区当中就行了,而至于如何解释这些数据完全由上层应用来决定,这就叫做面向字节流
首先要明确,粘包问题中的 “包” ,是指的应用层的数据包,在TCP的协议头中,没有如同UDP一样的 “报文长度” 这样的字段,但是有一个序号这样的字段,站在传输层的角度,TCP是一个一个报文过来的,按照序号排好序放在缓冲区中;站在应用层的角度,看到的只是一串连续的字节数据,那么应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分,是一个完整的应用层数据包
- 对于定长的包,保证每次都按固定大小读取即可
- 对于变长的包,可以在报头的位置,约定一个包总长度的字段,从而就知道了包的结束位置。比如HTTP报头当中就包含Content-Length属性,表示正文的长度
- 对于变长的包,还可以在包和包之间使用明确的分隔符。因为应用层协议是程序员自己来定的,只要保证分隔符不和正文冲突即可
- 对于UDP,如果还没有上层交付数据,UDP的报文长度仍然在,同时UDP是一个一个把数据交付给应用层的,有很明确的数据边界,站在应用层的角度,使用UDP的时候,要么收到完整的UDP报文,要么不收,不会出现“半个”的情况
- 因此UDP是不存在粘包问题的,根本原因就是UDP报头当中的16位UDP长度记录的UDP报文的长度,因此UDP在底层的时候就把报文和报文之间的边界明确了,而TCP存在粘包问题就是因为TCP是面向字节流的,TCP报文之间没有明确的边界
- 进程终止:进程终止会释放文件描述符,仍然可以发送FIN,和正常关闭没有什么区别
- 机器重启:和进程终止的情况相同
- 机器掉电/网线断开:接收端认为连接还在,一旦接收端有写入操作,接收端发现连接已经不在了,就会进行reset,即使没有写入操作,TCP自己也内置了一个保活定时器,会定期询问对方是否还在,如果对方不在会把连接释放;另外, 应用层的某些协议, 也有一些这样的检测机制. 例如HTTP长连接中, 也会定期检测对方的状态. 例如QQ, 在QQ断线之后, 也会定期尝试重新连接
的分隔符。因为应用层协议是程序员自己来定的,只要保证分隔符不和正文冲突即可
- 对于UDP,如果还没有上层交付数据,UDP的报文长度仍然在,同时UDP是一个一个把数据交付给应用层的,有很明确的数据边界,站在应用层的角度,使用UDP的时候,要么收到完整的UDP报文,要么不收,不会出现“半个”的情况
- 因此UDP是不存在粘包问题的,根本原因就是UDP报头当中的16位UDP长度记录的UDP报文的长度,因此UDP在底层的时候就把报文和报文之间的边界明确了,而TCP存在粘包问题就是因为TCP是面向字节流的,TCP报文之间没有明确的边界
- 进程终止:进程终止会释放文件描述符,仍然可以发送FIN,和正常关闭没有什么区别
- 机器重启:和进程终止的情况相同
- 机器掉电/网线断开:接收端认为连接还在,一旦接收端有写入操作,接收端发现连接已经不在了,就会进行reset,即使没有写入操作,TCP自己也内置了一个保活定时器,会定期询问对方是否还在,如果对方不在会把连接释放;另外, 应用层的某些协议, 也有一些这样的检测机制. 例如HTTP长连接中, 也会定期检测对方的状态. 例如QQ, 在QQ断线之后, 也会定期尝试重新连接