文章目录
- TCP协议
-
- [1. TCP协议段格式](#1. TCP协议段格式)
-
- [(1) 2个核心问题(解包与分用)](#(1) 2个核心问题(解包与分用))
- [(2) TCP的可靠性](#(2) TCP的可靠性)
- [2. 确认应答(ACK)机制](#2. 确认应答(ACK)机制)
- [3. 16位窗口大小](#3. 16位窗口大小)
- [4. 6位标志位](#4. 6位标志位)
-
- [(1) ACK](#(1) ACK)
- [(2) SYN](#(2) SYN)
- [(3) FIN](#(3) FIN)
- [(4) PSH](#(4) PSH)
- [(5) RST](#(5) RST)
- [(6) URG](#(6) URG)
- [5. 超时重传机制](#5. 超时重传机制)
- [6. 连接管理机制](#6. 连接管理机制)
-
- [6.0 TCP协议通讯流程](#6.0 TCP协议通讯流程)
- [6.1 三次握手](#6.1 三次握手)
-
- [(1) 前置问题](#(1) 前置问题)
- [(2) 细节问题](#(2) 细节问题)
- [(3) 为什么要是3次握手呢](#(3) 为什么要是3次握手呢)
- [(4) 其他问题](#(4) 其他问题)
- [(5) 3次握手时的状态变化](#(5) 3次握手时的状态变化)
- [6.2 四次挥手](#6.2 四次挥手)
-
- [(1) 为什么要是4次挥手呢](#(1) 为什么要是4次挥手呢)
- [(2) 4次挥手时的状态变化](#(2) 4次挥手时的状态变化)
- [(3) CLOSE_WAIT 与 TIME_WAIT](#(3) CLOSE_WAIT 与 TIME_WAIT)
-
- [<1> CLOSE_WAIT](#<1> CLOSE_WAIT)
- [<2> TIME_WAIT](#<2> TIME_WAIT)
- [<3> 解决TIME_WAIT状态引起的bind失败的问题](#<3> 解决TIME_WAIT状态引起的bind失败的问题)
- [7. 流量控制](#7. 流量控制)
-
- [(1) 细节问题](#(1) 细节问题)
- [8. 滑动窗口](#8. 滑动窗口)
-
- [(1) 前提知识](#(1) 前提知识)
- [(2) 滑动窗口](#(2) 滑动窗口)
-
- [<1> 一般情况](#<1> 一般情况)
- [<2> 深入理解](#<2> 深入理解)
- [<3> 特殊情况](#<3> 特殊情况)
- [<4> 快重传](#<4> 快重传)
- [9. 拥塞控制](#9. 拥塞控制)
- [10. 延迟应答](#10. 延迟应答)
- [11. 捎带应答](#11. 捎带应答)
- [12. 面向字节流](#12. 面向字节流)
- [13. 粘包问题](#13. 粘包问题)
- [14. TCP异常情况](#14. TCP异常情况)
- 15.TCP小结
- [16. 基于TCP应用层协议](#16. 基于TCP应用层协议)
- [17. TCP/UDP对比](#17. TCP/UDP对比)
- [18. TCP实验:理解listen的第二个参数](#18. TCP实验:理解listen的第二个参数)
TCP协议
TCP全称为 "传输控制协议(Transmission Control Protocol")。要对数据的传输进行一个详细的控制;
1. TCP协议段格式
- 源/目的端口号: 表示数据是从哪个进程来, 到哪个进程去;
- 32位序号/32位确认号: 后面详细讲;
- 4位TCP报头长度: 表示该TCP头部有多少个32位bit(有多少个4字节); 所以TCP头部最大长度是15 * 4 = 60
- 6位标志位:(其实就是6个比特位)
- URG: 紧急指针是否有效
- ACK: 确认号是否有效
- PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走
- RST: 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段
- SYN: 请求建立连接; 我们把携带SYN标识的称为同步报文段
- FIN: 通知对方, 本端要关闭了, 我们称携带FIN标识的为结束报文段
- 16位窗口大小: 后面再说
- 16位校验和: 发送端填充, CRC校验。接收端校验不通过, 则认为数据有问题。此处的检验和不光包含TCP首部, 也包含TCP数据部分。
- 16位紧急指针: 标识哪部分数据是紧急数据;
- 40字节头部选项: 暂时忽略;
(1) 2个核心问题(解包与分用)
前置知识:
- 4位首部长度:4比特位,二进制取值范围
0000~1111
,转成10进制0~15
;首部长度计算的基本单位:4字节,即首部长度 * 4 = 最终长度 ,故最终长度取值范围0~60
- TCP报头标准长度20字节 ,故选项最多
60-20=40
字节;TCP报头的非标准长度是20字节+选项 - 如果TCP报头是标准报头,4位首部长度的填充:
20 / 4 = 5
,转成二进制就是0101
- 报头和有效载荷如何分离(解包)
- 提取报文的前20字节和4位首部长度
- 4位首部长度 * 4 - 20 == 0
- 等于0(标准报头),报头读取完毕了;
- 不等于0(非标准报头),计算结果就代表有几个字节的选项,继续读取选项即可。
- 读取完报头后,剩下的就是有效载荷了
为什么没有有效载荷的长度呢?
因为TCP是面向字节流的,即以字节流的形式传输数据的,TCP收到数据就直接交给上层了,数据怎样用怎么解析,是你上层的工作,与我TCP无关
扩展,关TCP选项字段是如何实现的呢?
C语言中柔性数组的概念,帮助我们定义变长的结构体;前面的字段用结构体(位段)定义好,后面跟上柔性数组,你需要多少选项直接malloc空间,malloc出来的空间+20字节标准报头填到4位首部长度处,就知道了整个报文的大小。
-
有效载荷是如何做到交付给应用层的哪一个进程呢(分用)?
根据目的端口号向上交付给应用层,绑定该端口号的进程,即分用的过程
(2) TCP的可靠性
网络传输中不可靠的情况:
丢包(少量,大量),乱序,重复,校验失败,发送太快/太慢,网络出现问题
为什么会出现这么多不可靠的问题呢?
单纯的就是因为通信双方之间距离变长了
上面不可靠情况中最关键:丢包问题,你怎么知道这个报文丢包了呢?
这就引出了可靠性中最核心的问题:正确理解确认应答机制
客户端给服务器发送消息,服务器给客户端ACK应答,无法保证对ACK应答的确认。即使存在对ACK的应答,也无法保证对ACK应答的应该。换句话说,在长距离传输中总有最新的报文无法被确认。
所以我们认为:
- 在时间窗口内,只要C收到应答,C->S发送的数据对方就100%收到了;C收不到应答,C直接认为报文丢失了。即可靠性是通过收到应答保证的。
- S->C的可靠性是怎么保证的呢?TCP中Server和Client双方地位是对等的,S->C发送数据,S收到C的应答,则S->C就是可靠的。
- 我们无法保证任何报文都是可靠送达,但是我们能局部保证可靠性
误区:不要仅仅认为对方把数据收到了就是可靠性。当对方收到数据时,我知道了;当对方没有收到数据时,我也知道了;这才叫做可靠 。即对于我们所做的动作都要有反馈,这套机制称为可靠性。
但是真实的情况,并不是向上面一样,你发一个数据我给你一个应答,这样做整个发送过程是串行,效率低,所以真实的发送过程是C->S一次性发送多份报文(并发),S再对这些报文处理(并发响应)
这样做:发送多个报文时间上就重叠了,提高了发送效率
但是有2个问题:
- 多个报文经过网络发送,发送顺序不一定是收到的顺序
- 当C收到多个确认的时候,C是如何知道哪一个应答对应哪一个报文呢?
所以,就注定了,我们要想办法给报文带上编号,即序号和确认序号。
故,我们最终呈现出来的样子:
无论请求还是响应,双方在进行交互时,通信发送的都是TCP报头+有效载荷(如果有的话)
2. 确认应答(ACK)机制
TCP将发送出去的每个字节数据都进行了编号,这个编号叫做序列号。
TCP是怎么做到对每个字节数据都进行了编号呢?
在通信的过程中,TCP并不关心自己缓冲区中的数据类型;TCP是面向字节流的,缓冲区中的数据可以把它整体看待成 char sendBuffer[N]
类型,它仅仅表示uint8_t
按字节为单位划分成一个一个的小格子,即发送缓冲区是一个大数组,数组中是以字节为单位来呈现的。上层把对应数据拷贝到缓冲区时,由于发送缓冲区是一个大数组,每个字节的数据就天然地带上了编号,即该数组的下标。
每一个ACK都带有对应的确认序列号。
对于任意一个确认序号X 表示:X-1之前的报文应经全部收到了,下次发请从X编号开始发送
以上图为例,用确认序号规则,收到确认序号1001表示:1000之前的序号已经全部收到;收到确认序号2001表示:2000之前的序号已经全部收到;如果只收到确认序号2001没有收到1001,同样也能表明:2000之前的序号已经全部收到(当然包含1000),只是确认序号1001丢失了,这种确认序号的概念定义:可以允许少量的ACK丢失,更细粒度的确认丢包原因
这里就衍生出一个问题,为什么不把序号和确认序号表示成一个字段呢?
TCP是全双工的,C可能在给S发消息也可能收到S到来的数据请求,对于S也是如此;以C->S发信息,就出现了一种情况:S->C的ACK确认和S->C发送数据,这时就会把两个应答/请求压缩成一个 ,就称为捎带应答。
这里确认和发送数据是同时存在的:
- 32位序号:S->C数据序号
- 32位确认序号:S->C历史数据的确认
所以,就注定了序号和确认序号必须是不同的字段
3. 16位窗口大小
TCP协议是全双工的,本身具有接收缓冲区和发送缓冲区
-
在应用层调用write,read等系统接口时,并不是直接把数据从本主机发送到目标主机,而是把数据向下交给传输层,由传输层来处理
-
所以write,read等系统接口的本质是:拷贝,把应用层缓冲区的数据拷贝给传输层的缓冲区
-
发送过程:把我发送缓冲区数据拷贝到对方的接收缓冲区,把对方发送缓冲区数据拷贝到我的接收缓冲区。整个过程由TCP协议来控制。
我们以上图C->S发数据为例:
C->S发送消息太快,S来不及接收,一旦S的接收缓冲区被打满了,就无法接收来自C的消息,那么后面的数据将会直接丢弃,看似很正常,因为TCP协议存在超时重传机制,所以这些丢弃的报文可以重新发送。但是C与S通信过程中,经历长距离传输,这些要丢弃的报文已经消耗了很多网络资源了,直接丢弃是比较低效的表现,同时站在C的角度:既然S已经来不及接收了,为啥不早点告诉我呢,让我控制一下自己的发送速度。问题解决方案:S->C做ACK确认应答时,告诉C自己的接收能力,即S接收缓冲区的大小,把这个字段填入TCP报头的,称此字段为16位窗口大小。16窗口的存在实现了对方交换接收能力的作用,这种策略就称为流量控制(Flow Control)。
16位窗口大小:
TCP的报头中有一个称为窗口大小的字段,它占用了16个比特位。用来表示接收端的接收能力,即接收端接收缓冲区的大小。
注意:凡是我要构建TCP报文必定是我向对方发送消息,16窗口大小就填自己的窗口大小,表示我自己接收缓冲区的大小
4. 6位标志位
客户端与服务端的通信的过程中,以服务端为例,它会在同一时刻/时间段收到各种各样的报文请求,比如有的请求是建立连接的,有的请求是发送消息的,有的请求是断开连接的。这些请求报文都是不同的,就注定了报文是有类型的,不同的请求报文要提供不同的处理动作。这就回答了标志位的一些前提问题。
标志位的本质是什么?
标识不同类型的报文。
为什么要有标志位?
标志位的不同反应了报文的不同,从而提供不同的处理动作
具体标志位,是怎么设计的呢?
前提:这6个标志位全部都是一个一个的比特位,为1表示该标志位被设置,为0则表示不被设置
(1) ACK
ACK(acknowledgement)
:此标志表示应答域是否有效。
-
报文中的ACK被设置为1时,表明该报文是一个确认应答报文
-
报文中的ACK被设置为1同时该报文携带数据,称为捎带应答。表明既是对对方上一个报文的确认也包含了我所给对方发送的消息。
(2) SYN
SYN(synchronous)
:此标志表示建立连接
- 报文中的SYN被设置为1时,表明该报文是一个连接请求的报文。双方进入3次握手状态
(3) FIN
FIN(finish)
:此标志表示断开连接
- 报文中的FIN被设置为1时,表明该报文是一个连接断开的请求报文。双方进入4次挥手状态
(4) PSH
PSH(push)
:此标志为1表示提示接收端应用程序立刻从TCP缓冲区把数据读走。
-
当两台主机通信时,发送方不断向接收方发送数据,接收方此时也会给发送方做ACK应答同时通过16窗口大小告诉发送方自己接收缓冲区的大小,一旦窗口大小为0,表明接收方没有能力再接收数据了。此时窗口大小为0可能有两种原因:
- 发送方发送太快,接收方来不及接收,接收方的接收缓冲区很快被打满
- 应用层读取数据太慢了,导致下层缓冲区被打满了
-
窗口大小为0后,发送方会定期向接收方发送询问报文(不携带数据只含有报头),接收方做ACK应答后告诉发送方自己的窗口大小还是0,几次询问后发送方会再发送询问报文将自己的PSH标志位置为1,表示提示(催促)接收端应用程序立刻从TCP缓冲区把数据读走(向上交付)。
(5) RST
RST(reset)
:此标志表示要求对方重新建立连接
-
比如,客户端和服务器在正常通信时,突然拔掉服务器的网线,客户端与服务器并没有进行四次挥手,此时客户端依然认为与服务端建立着连接。重启服务器后,服务器会收到客户端发来的TCP报文。服务端会发现没有与客户端建立连接的情况下直接通信,此时服务端就会把RST设置为1,把报文发送给客户端,让客户端与服务端进行重新连接。
-
RST标志位表示通信双方建立连接成功后,由于某些原因导致双方认为连接状态不一致,无法进行正常通信,需要进行连接重置。
我们在访问某些网站时,就可能出现下面连接重置的情况:
(6) URG
URG(urgent)
:此标志表示紧急指针字段是否有效
16位紧急指针:标识哪部分数据是紧急数据
- TCP协议通信时会保证数据的按序到达,就相对于把缓冲区当做一个队列,在上层取数据时按序取走。TCP报文中包含了32位序号,通过序号排序后按顺序入队列。
- 可是如果我们此时想发送一些"紧急数据", 这些数据需要让对方上层快速处理。这时候就需要设置URG为1,表示紧急指针生效。
- 16位紧急指针可以找到紧急数据数据是谁,即紧急数据在有效载荷中的偏移量。找到紧急数据后,常规非紧急数据直接入队列,紧急数据尽快交给上层(通过特殊接口,直接让上层获取)
- 紧急数据只有一个字节,只需要一个偏移量就可以标识
实际上现在的TCP协议中,紧急指针的使用情况很少几乎用不到
recv/send函数就有设置URG的参数
recv函数第四个参数flags有一个叫做MSG_OOB的选项,其中OOB是带外数据(out-of-band)的简称,带外数据就是一些比较重要的数据,因此上层如果想读取紧急数据,就可以在使用recv函数进行读取,并设置MSG_OOB选项
5. 超时重传机制
- 主机A发送数据给B之后, 可能因为网络拥堵等原因, 数据无法到达主机B;
- 如果主机A在一个特定时间间隔内没有收到B发来的确认应答, 就会进行重发;
但是, 主机A未收到B发来的确认应答, 也可能是因为ACK丢失了;
因此主机B会收到很多重复数据. 那么TCP协议需要能够识别出那些包是重复的包,并且把重复的丢弃掉。这时候我们可以利用前面提到的序列号, 就可以很容易做到去重的效果(序号相同的报文直接丢弃)。
细节:主机A已经把数据发到网络中了,不能立即在自己的发送端把数据清掉,需要把对应数据暂时维护一段时间,直到收到应答或超时情况,即把数据维护到结果确定的情况下,以支持超时重传机制。
一个已经发送出去的数据在没有收到应答时,TCP必须把此数据维护一段时间(保存起来),以支持超时重传
那么, 如果超时的时间如何确定?
- 最理想的情况下, 找到一个最小的时间, 保证 "确认应答一定能在这个时间内返回"。
- 但是这个时间的长短, 随着网络环境的不同, 是有差异的。
- 如果超时时间设的太长, 会影响整体的重传效率;
- 如果超时时间设的太短, 有可能会频繁发送重复的包;
TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间。
- Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时时间都是500ms的整数倍。
- 如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传。
- 如果仍然得不到应答, 等待 4*500ms 进行重传。依次类推, 以指数形式递增。
- 累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接。
6. 连接管理机制
6.0 TCP协议通讯流程
服务器初始化:
- 调用socket, 创建文件描述符;
- 调用bind, 将当前的文件描述符和ip/port绑定在一起; 如果这个端口已经被其他进程占用了, 就会bind失败;
- 调用listen, 声明当前这个文件描述符作为一个服务器的文件描述符, 为后面的accept做好准备;
- 调用accecpt, 并阻塞, 等待客户端连接过来;
建立连接的过程:
- 调用socket, 创建文件描述符;
- 调用connect, 向服务器发起连接请求;
- connect会发出SYN段并阻塞等待服务器应答; (第一次)
- 服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"; (第二次)
- 客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段; (第三次)
这个建立连接的过程 , 通常称为三次握手;
数据传输的过程:
- 建立连接后,TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时刻, 通信双方可以同时写数据; 相对的概念叫做半双工, 同一条连接在同一时刻, 只能由一方来写数据;
- 服务器从accept()返回后立刻调 用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等待;
- 这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期间客户端调用read()阻塞等待服务器的应答;
- 服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等待下一条请求;
- 客户端收到后从read()返回, 发送下一条请求,如此循环下去;
断开连接的过程:
-
如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);
-
此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);
-
read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN(第三次);
-
客户端收到FIN, 再返回一个ACK给服务器(第四次);
这个断开连接的过程 , 通常称为四次挥手。
在学习socket API时要注意应用程序和TCP协议层是如何交互的:
- 应用程序调用某个socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段
- 应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段
6.1 三次握手
强调:
3次握手 中发送的可不是简单的SYN,ACK。发送的是TCP报头,就是上面TCP协议数据只不过把对应标志位置为1
(1) 前置问题
3次握手是建立连接的过程。
什么是连接呢?
-
作为一款服务器,它可能随时随地收到来自多个客户端的连接请求,它们都想要连接服务器,连接服务器就会触发3次握手。3次握手就是要建立连接,整个过程是在TCP层,由TCP协议帮我们做的,即OS帮我们做的,就意味着一定在OS内部同时存在多个建立好的连接。
-
那么OS要不要把这些已经建立好的连接管理起来呢?当然要
-
所以OS怎么对这些已经建立好的连接管理呢?先描述,再组织。所谓的连接即OS为了管理连接维护的数据结构!先描述:
struct tcp_link{管理连接的字段(源ip,目的ip,源端口,目的端口,缓冲区....)}
,再组织:这个结构体里面包含struct tcp_link* next
以双向链表或其他数据结构组织起来。所以OS就把对连接的管理转化成对某种数据结构对象的管理 -
管理连接必须先描述,描述成结构体;建立连接成功时OS要根据连接类型帮我们new/malloc出来对应的连接对象,连接对象中包含了各种属性。所以创建并维护连接是有成本的 。创建连接对象并设置数据到对象中,一定要new/malloc连接对象,此过程一定要消耗内存资源 ;维护定时器(保证超时重传),维护连接的状态,设置各种字段值,维护管理连接的算法等此过程,一定要消耗CPU资源
结论:连接即OS内维护的数据结构对象。维护连接是有成本的,会消耗内存和CPU资源
(2) 细节问题
-
客户端只要把SYN报文发送出去,客户端的状态立马变成SYN_SENT(同步发送);
-
只要服务器收到SYN报文,服务端的状态立马变成SYN_RCVD(同步收到);
-
客户端收到SYN + ACK报文,并且发出ACK时,客户端的3次握手就完成了,它认为连接就建立好了;服务端收到ACK时,服务端的3次握手就完成了,它认为连接也建立好了。只有双方都认为自己的3次握手完成时,3次握手的过程才完成。
<1> 3次握手过程中,客户端到服务器的线为什么是斜着向下的呢?
这张图中存在时间维度,报文发出和对方收到这两个时间点绝对不一样,斜着向下表明时间流逝。
<2> 3次握手中,客户端到服务器从来不担心第1次,第2次丢失
3次握手不一定能握手成功。
-
第1个报文丢失:服务端不会收到任何影响,因为它从未收到握手请求即服务端不会为我们维护管理连接的数据结构,客户端可以给我们超时重传
-
第2个报文丢失:双方3次握手过程照样没有成功,双方没有建立起来连接,没有成本。同时报文是否丢失我们还存在应答
第3个报文丢失:
3次握手中客户端只要把最后的ACK报文发出,客户端就认为连接建立好了。无论服务端是否收到
即最后一个ACK是否被服务器收到,客户端并不知道到。如果最后一个ACK报文丢失怎么办呢?
-
客户端已经在最后一个ACK报文发出时认为连接就建立好了。此时客户端就会维护连接对应的结构体;只有服务器收到最后一个ACK报文,才对它来说接连建立好了,才会维护连接对应的结构体,双方就会正常通信。
-
客户端认为连接建立好了,服务器没有收到最后一个ACK报文,就出现了客户端与服务器之间连接建立状态不一致。此时客户端就直接给服务器发送消息,服务器会判定双方连接建立状态不一致,服务器立马给客户端发送RST连接重置响应,客户端收到响应后会关掉连接然后重新建立连接。
在网络中最后一个ACK能否被收到是不知道的,那我们可不可以给最后一个ACK应答呢?
只要你不断在应答就注定了永远有最新的报文无法应答,如果你对ACK的应答丢失了呢,服务端此时也无法知道对ACK的应答是否被客户端收到。
(3) 为什么要是3次握手呢
2次握手可以吗?
- 2次握手的情况下,服务器认为只要把第2个SYN+ACK报文发出就认为建立连接完成,无法知道客户端是否能收到。如果此时此报文丢失,客户端照样认为连接没有建立好,客户端与服务器之间出现连接了建立状态不一致的情况。
- 此时我们假设一台服务器,不断地向它发送SYN请求,服务器发送完第2个SYN+ACK报文就认为连接建立好了,会不断地维护连接对应的数据结构。对于服务器SYN+ACK报文我们直接丢掉不做任何响应,我们甚至拿上一个单片机不断向服务器发送SYN请求,一瞬间就能把服务器上的连接资源打满,一旦内存资源没有,无法创建对应的连接结构体,那么正常的用户想要连接时也无法连上。发送海量SYN的方式我们就叫做SYN洪水
即2次握手非常容易受到攻击。
注:TCP即便是3次握手也会受到攻击,缓解TCP受到攻击的问题不是TCP本身该解决,防止攻击本身和TCP无关,但是TCP本身设计有明显漏洞被攻击就和TCP有关系了。
4次握手可以吗?不建议
-
如果是主动发起连接建立的一方,最后一定会存在最后一个ACK是没有对应的应答的。
-
最后一个ACK由谁发起,谁一定会先完成指定次数的握手过程。奇数次握手一定是客户端来主动发起对应的ACK,偶数次握手一定是服务端来主动发起对应的ACK。奇数次握手 一定是客户端先建立好连接,服务器有没有建立好连接客户端不知道,把不确定性留给客户端 ;偶数次握手 一定是服务器先建立好连接,客户端有没有建立好连接服务器不知道,把不确定性留给服务器。
-
连接握手可能会发生异常。
- 偶数次握手时,承担连接建立异常的成本就嫁接到服务器。服务器面临多个客户端,几乎导致服务器维护异常连接成为必然。服务器因为维护异常连接而导致无法为其他连接提供服务,这对服务器资源是一种浪费。
- 奇数次握手时,承担连接建立异常的成本就嫁接到客户端。客户端永远只会与服务器建立连接而且客户端维护连接数相比服务器非常少,承担连接建立异常的成本对客户端影响很小。
3次握手为什么行,其他次握手不行呢
除了上面其他次握手会存在漏洞的问题外,3次握手本身就够了
-
3次握手是奇数次握手,没有明显的设计漏洞,一旦建立连接出现异常,成本嫁接到客户端,服务端成本较低
-
双方进行通信的前提是信道通畅 ,3次握手是验证全双工通信信道通畅的最小成本(验证客户端和服务器必须得能收和能发。客户端:第1, 2次验证;服务器:第1, 3次验证)
(4) 其他问题
3次握手也可以叫4次握手,原因如下:
- 第2次握手时,服务器收到客户端的SYN报文后,向客户端发送一个SYN+ACK(同步+确认)报文,这个SYN/ACK 报文也可以分两次发送给客户端。
- 其实在第2次握手时,服务器向客户端发送一个SYN+ACK报文本身就是一种捎带应答
我们以前在写套接字代码时并没有对服务器进行响应,所以发数据是把数据拷贝到对应的TCP缓冲区,发送数据是由自己的OS自动发送的,确认也是由对端的OS自动应答的,发送和应答本身是由OS完成的。即整个3次握手的过程本身是由两端OS自动完成的。
(5) 3次握手时的状态变化
服务端:
- [
CLOSED -> LISTEN
] 服务器端调用listen后进入LISTEN
状态,等待客户端连接 - [
LISTEN -> SYN_RCVD
] 一旦监听到连接请求(同步报文段),就将该连接放入内核等待队列中,并向客户端发送SYN确认报文 - [
SYN_RCVD -> ESTABLISHED
] 服务端一旦收到客户端的确认报文,就进入ESTABLISHED
状态,可以进行读写数据了
客户端:
- [
CLOSED -> SYN_SENT
] 客户端调用connect,发送同步报文段 - [
SYN_SENT -> ESTABLISHED
] connect调用成功,则进入ESTABLISHED
状态,开始读写数据
6.2 四次挥手
(1) 为什么要是4次挥手呢
- 因为客户端和服务器双方通信的地位是对等的,所以断开连接时要征得双方的同意,即在双方都有请求和双方都要应答的情况下才叫征得双方同意。
- 4次挥手是双方建立连接断开的最小成本。
4次挥手也可能变成三次挥手,原因如下:
- 第2次挥手:服务器接收到客户端的FIN报文后,向客户端发送一个ACK报文,表示已经收到客户端的断开请求。
- 第3次挥手:服务器向客户端发送一个FIN报文,表示服务器也不再发送数据
- 这两个报文可能会合在一起发,即ACK+FIN,因为FIN和ACK都是不同的标志位,不会影响双方
(2) 4次挥手时的状态变化
客户端状态转化:
- [
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
状态
服务端状态转化:
- [
ESTABLISHED -> CLOSE_WAIT
] 当客户端主动关闭连接(调用close),服务器会收到结束报文段,服务器返回确认报文段并进入CLOSE_WAIT
- [
CLOSE_WAIT -> LAST_ACK
] 进入CLOSE_WAIT
后说明服务器准备关闭连接(需要处理完之前的数据);当服务器真正调用close关闭连接时,会向客户端发送FIN,此时服务器进入LAST_ACK
状态,等待最后一个ACK到来(这个ACK是客户端确认收到了FIN) - [
LAST_ACK -> CLOSED
] 服务器收到了对FIN的ACK,彻底关闭连接
注:4次挥手是上层双方各自调用close(sock),调用一次close(sock)就是2次挥手。
(3) CLOSE_WAIT 与 TIME_WAIT
- 主动断开连接的一方,最终状态是
TIME_WAIT
- 被动断开连接的一方,两次挥手完成,进入
CLOSE_WAIT
状态
<1> CLOSE_WAIT
-
被动断开连接的一方 ,收到别人发送来的FIN,在给对方ACK时,不调用close(sock),最终会进入
CLOSE_WAIT
状态。 -
CLOSE_WAIT
状态:主动断开连接的一方断开连接,被动断开连接的一方不断开连接,它此时就会维持CLOSE_WAIT
状态
我们此时注释代码中的:close函数
开始测试:
启动服务器,用telnet作为客户端连接服务器,连上后发现两者ESTABLISHED
状态
此时我们立马关掉客户端,客户端进入FIN_WAIT_2
状态,服务器进入CLOSE_WAIT
状态
过了一会,继续查看,此时客户端已经退出了,服务器会维持很长时间的CLOSE_WAIT
状态
如果服务器出现了大量的CLOSE_WAIT
状态,说明服务器:
- 服务器没有正确的关闭 socket, 导致四次挥手没有正确完成。
- 服务器的连接资源没有释放,对于服务器压力很大
<2> TIME_WAIT
-
主动断开连接的一方 ,会发送最后一次ACK,最终会进入
TIME_WAIT
状态。 -
TIME_WAIT
状态:发送方发送完最后一次ACK后,等待一段时间尽可能保证最后一次ACK被对方收到。
放开close函数,演示正常的四次握手
启动服务器,用telnet作为客户端连接服务器,连上后发现两者ESTABLISHED
状态
主动关掉客户端,客户端进入TIME_WAIT
状态
TIME_WAIT
状态持续一段时间后才进入真正的关闭,可见TIME_WAIT
状态是一种临时性状态,过一会就没了
<3> 解决TIME_WAIT状态引起的bind失败的问题
还是上面的演示,此时我们主动关掉服务器,服务器进入TIME_WAIT
状态
在次启动这个服务器发现会bind失败
绑定失败现象:
客户端连着服务端,服务端主动退出,紧接着服务端再次启动绑定相同的端口就会出现绑定失败的现象
为什么会出现上面的情况呢?
TIME_WAIT
状态,是一种临界状态,对于连接资源数据结构还是维护的,与此连接相关联的,进程所绑定的端口号依旧在被使用
在服务器的TCP连接没有完全断开之前不允许重新监听,某些情况下可能是不合理的。来进一步回答这个问题
是否会存在服务器主动关闭连接的场景呢?
存在,比如双11期间服务器上有大量的连接,服务器压力越来越大。直到来了某个连接,服务器实在撑不住挂掉了,连接断开,服务器进入TIME_WAIT状态。想要立马恢复服务,可是此时状态还是TIME_WAIT,无法立即重启成功,就会造成巨大的金钱损失。
上面的问题如何解决呢?如果服务器不想等待或更换端口?
使用setsockopt()
设置socket描述符的选项SO_REUSEADDR
为1,表示允许创建端口号相同但IP地址不同的多个socket描述符
直接修改代码
继续测试,bind绑定失败问题没有了
查看TIME_ WAIT状态的时间
cpp
cat /proc/sys/net/ipv4/tcp_fin_timeout
-
等待两个MSL(maximum segment lifetime)
-
MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同, 在Centos7上默认配置的值是60s;
为什么是TIME_WAIT的时间是2MSL?
- MSL是TCP报文的最大生存时间, 因此TIME_WAIT持续存在2MSL的话
- 就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启, 可能会收到来自上一个进程的迟到的数据, 但是这种数据很可能是错误的);
- 同时也是在理论上保证最后一个报文可靠到达(假设最后一个ACK丢失, 那么服务器会再重发一个FIN。这时虽然客户端的进程不在了, 但是TCP连接还在, 仍然可以重发LAST_ACK);
7. 流量控制
前面16窗口大小学习时,我们已经引入了流量控制。这里详细介绍一下。
接收端处理数据的速度是有限的。如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送,就会造成丢包, 继而引起丢包重传等等一系列连锁反应。
因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度。这个机制就叫做流量控制(Flow Control);
- 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 "窗口大小" 字段, 通过ACK端通知发送端;
- 窗口大小字段越大, 说明网络的吞吐量越高;
- 接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;
- 发送端接受到这个窗口之后, 就会减慢自己的发送速度;
- 如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数
据段, 使接收端把窗口大小告诉发送端。
接收端如何把窗口大小告诉发送端呢?
回忆我们的TCP首部中, 有一个16位窗口字段, 就是存放了窗口大小信息;
那么问题来了, 16位数字最大表示65535, 那么TCP窗口最大就是65535字节么?
实际上, TCP首部40字节选项中还包含了一个窗口扩大因子M, 实际窗口大小是 窗口字段的值左移 M 位;
(1) 细节问题
三次握手过程中可以携带数据吗?
第一次、第二次握手不可以携带数据,第三次握手可以携带数据
理由:
第一次握手不可以放数据,其中一个简单的原因就是会让服务器更加容易受到攻击了。而对于第三次的话,此时客户端已经处于 ESTABLISHED 状态。对于客户端来说,他已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以能携带数据也没啥毛病。
针对上面的补充问题,我们来回答一下流量控制中一个细节问题:
首次发送的时候,发送方咋知道接收方的窗口大小,即保证第一次发送数据不会出现发送太快太慢的问题?
-
TCP协议通信时,并不是双方在进行第一次报文交换的时候,双方在3次握手期间已经有过至少一次报文交换
-
客户端建立连接的时候不会携带数据只发送报头,不会使用服务器的接收缓冲区,不存在把数据给发多的情况;客户端发送连接建立请求时,除SYN被置1,客户端一定会将自己16位窗口大小通告给服务器,第2次握手时,服务器给客户端响应,即SYN+ACK同时被置1,同时服务器的窗口大小也会告诉客户端
-
于是双方在 1,2次握手时不携带任何数据只发送TCP报头也就不会存在报文发送快慢的问题,即不使用双方的接收缓冲区的情况下, 交换了双方的接收能力
注:其实发送数据就是不断把自己发送缓冲区的数据填入对方的接收缓冲区中。由应用层调用read,recv接口把数据往上拿
8. 滑动窗口
(1) 前提知识
基于确认应答策略, 对每一个发送的数据段, 都要给一个ACK确认应答。收到ACK后再发送下一个数据段(串行)。收发效率非常低下
既然这样一发一收的方式性能较低,那么我们一次发送多条数据(并行), 就可以大大的提高收发效率(其实是将多个段的等待时间重叠在一起了)
实际通信时上面两种方案都会存在,从上面两种方案对比,可以看到:
TCP协议,可靠性是主要研究的问题,但是不是全部。效率问题,也是TCP考虑的问题。
学习滑动窗口的前提:
- 即使在并行发送的前提下,接收方也是有接收能力上限的
- 所以发送方并发发送时,一定要在对方能够接收的前提条件下,进行并发发送。
- 我们发送方,目前最大一次可以向对方发送多少数据量呢?由对方的窗口大小决定(目前这样认为)
(2) 滑动窗口
<1> 一般情况
发送方数据速度会受到接收方接收能力的约束。如果接收方接收能力有限,就注定了发送方只能把发送缓冲区中的一部数据给对方。
所谓的滑动窗口,其实是发送缓冲区的一部分。
于是根据滑动窗口,可以把发送缓冲区分割为3部分
- 滑动窗口左侧:已经发送已经收到确认的数据区域,此区域可以被覆盖,是无效数据。(计算机中的数据清空就是数据覆盖)
- 滑动窗口右侧:尚未发送的数据区域
- 滑动窗口中:可以直接发送,但是尚未收到应答的数据区域
目前我们认为:滑动窗口的大小?和对方的接收能力有关,应答报文中win窗口大小
滑动窗口,可以看做用户往滑动窗口中尚未发送的数据区域放数据,发送时发送滑动窗口中的数据,是一个基于队列的生产消费者模型。
<2> 深入理解
我们前面在确认应答时的确认序号说过:缓冲区中的数据可以把它整体看待成 char sendbuffer[N]
类型数组,将上层数据拷贝下来时每个字节数据就天然有编号。
- 滑动窗口就是在数组中由两个数组下标维护的一段区间
- 滑动窗口的滑动就是修改两个数组下标,滑动的本质就是重新框住一段区间
<3> 特殊情况
- 滑动窗口只能向右滑动吗?向左可以吗?
只能向右滑动,不能向左滑动。滑动窗口左侧是已经发送已经收到确认的数据区域,再把重复的数据发送给对方,没有意义。
- 滑动窗口能变大,能变小吗,能变0吗?变0之后,表示什么意思?
-
滑动窗口的变化是取决于对方的接收能力的,即对方给我通告的win的大小
-
可以变大,变大即
winend+=某些值
-
可以变小,变小即
winstart+=某些值
-
可以变0,变0表示接收方已经不能再接收数据了,它对应的接收缓冲区剩余空间大小为0,滑动窗口的两指针winstart,winend指向同一位置,就会进入流量控制的窗口探测和通知过程
-
基于以上几点,表明滑动窗口的大小是浮动的
- 滑动窗口能一直滑动吗?越界怎么办?
将发送缓冲区设置成环状结构即可,winstart和winend不可能越界,只有打满或为空的情况
- 滑动窗口的大小,怎么更新的?依据是什么?
- 目前是根据对方的接收能力,即响应报头中win大小
- TCP要保证可靠性,所以应答也要按序到达。应答的seq(序号),表示seq前数据已经收到了,下次发送从seq发,即
winstart = seq
(起始位置); 应答的win,winend = winstart + win
(结束位置)
- 滑动窗口内部的报文既然可以直接发送多个报文,如果第一个丢失了呢?中间的丢失了呢?最后一个丢失了呢?
-
第一个丢失:
- 应答丢了:丢失了1001,后续又收到了2001应答,表示2001之前的数据一定被收到了,那么1001之前的一定被收到了
- 数据丢了:报文1~1000, 1001~ 2000,2001~ 3000,3001~ 4000, 1001~2000 的报文丢失了。此时即使收到1~1000 2001~ 3000,3001~ 4000,也只能ACK1001,因为1001~ 2000之间的报文丢失了。此时滑动窗口不滑动,等待数据确认补发成功后才会向右滑动。
- 数据要支持超时重传,就必须被暂时保存起来!保存在滑动窗口中。
-
中间丢失:
- 代表第一个没有丢失(左侧没有丢失),,即左侧收到了,左侧收到了窗口向右滑动,中间的数据也会变成左侧数据。与上面第一个丢失相同
-
最后一个丢失:
- 第一个和中间全部收到了,一旦收到窗口向右滑动,最后一个丢失也变成了最左侧丢失
即,在滑动窗口中所有的丢失情况,全部会变成最左侧丢失问题
<4> 快重传
如果出现了丢包, 如何进行重传?
情况一: 数据包已经抵达, ACK被丢了。
情况二: 数据包就直接丢了。
- 当某一段报文段丢失之后, 发送端会一直收到 1001 这样的ACK, 就像是在提醒发送端 "我想要的是 1001"一样;
- 如果发送端主机连续三次收到了同样一个 "1001" 这样的应答, 就会将对应的数据 1001 - 2000 重新发送
- 这个时候接收端收到了 1001 之后, 再次返回的ACK就是7001了(因为2001 - 7000)接收端其实之前就已经收到了, 被放到了接收端操作系统内核的接收缓冲区中;
这种机制被称为 "高速重发控制"(也叫 "快重传")。
超时重传 VS 快重传
-
超时重传:决定重传的下限,侧重于帮助重传兜底。
-
快重传:决定重传的上限,连续收到3个以上的重复确认序号就会快重传,侧重于重传时的效率问题
9. 拥塞控制
两台主机在通信时如果出现少量的丢包,是很正常的现象,此时可以通过超时重传或快重传来补发丢失的数据;可是如果出现大量的丢包情况,就是不正常的现象了。
举一个例子:
现在大家要考试,班级里有30个同学。如果这30个人中只有1,2个挂科那么可以说是这1,2个同学自己的问题 ;如果这30个人中只有1,2个通过,那么挂科的这些同学会认为不是自己的问题,而是试卷的问题,是不是考试卷子出难了...
同样对于TCP来说也是如此:
- 发送10000个报文,有几个丢包了,是正常的丢包。
- 发送10000个报文,只收到几个,就不是自己的问题(不会再进行超时重传或快重传了),而是网络问题
那么出现大量丢包时,发送方的发送策略是什么呢?
出现大量丢包时,说明处于网络拥塞状态,此时我们的策略要保证两点:
- 保证网络拥塞不能加重
- 在网络拥塞有起色的情况下,尽快恢复网络通信
既然发生了网络拥塞,发送方就要基本得知网络拥塞的严重程度,所以必须要进行网络状态的探测,此处引入一个概念称为拥塞窗口。
- 拥塞窗口:发送方主机,衡量网络健康状态的指标
- 由于网络状态是变化的,衡量网络健康状态也是变化的,即拥塞窗口大小一定也是变化的,一定时时刻刻都会存在一个拥塞窗口的大小
- 作为主机的我,要知道网络的健康状态,需要尝试与探测。所以一直都要想办法得到当前网络的拥塞窗口
TCP引入 慢启动 机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据;
拥塞窗口:
- 发送开始的时候,定义拥塞窗口大小为1
- 每次收到一个ACK应答,拥塞窗口加1
- 每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小做比较,取较小的值作为实际发送的窗口
即自己滑动窗口大小 = min(对端主机的接收能力win,网络的拥塞窗口)
winstart = seq
, winend = min(seq_win,拥塞窗口)
,
像上面这样的拥塞窗口增长速度, 是指数级别的。 "慢启动" 只是指初使时慢, 但是增长速度非常快。
- 为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍。
- 此处引入一个叫做慢启动的阈值
- 当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长
- 当TCP开始启动的时候, 慢启动阈值等于窗口最大值;
- 在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1;
少量的丢包, 我们仅仅是触发超时重传; 大量的丢包, 我们就认为网络拥塞;
当TCP通信开始后, 网络吞吐量会逐渐上升; 随着网络发生拥堵, 吞吐量会立刻下降;
拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案。
10. 延迟应答
-
两台主机在通信时,接收方会给发送方应答,告诉发送方自己的接收能力,即win
-
我们只要给发送方通告更大的win大小,发送方可能就会更新出更大的滑动窗口大小,就可以一次性并发给我们发送更多数据,就能在较大概率上提高传送效率
-
那么如何更新出一个更大的接收窗口呢?只有让上层尽快交付,才有可能更新出更大的接收能力,即给上层更多的时间来进行读取:
- 让上层在这段时间内尽快读取
- 如果一直在读,读取更多
为了给上层更多的时间来读取,我们引入延迟应答的概念
如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小。
- 假设接收端缓冲区为1M。 一次收到了500K的数据; 如果立刻应答, 返回的窗口就是500K;
- 但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了;
- 在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来;
- 如果接收端稍微等一会再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M;
一定要记得, 窗口越大, 网络吞吐量就越大, 传输效率就越高。我们的目标是在保证网络不拥塞的情况下尽量提高传输效率;
那么所有的包都可以延迟应答么? 肯定也不是;
-
数量限制: 每隔N个包就应答一次;
-
时间限制: 超过最大延迟时间就应答一次;
具体的数量和超时时间, 依操作系统不同也有差异; 一般N取2, 超时时间取200ms;
11. 捎带应答
- 主机A给主机B发送了一条消息,当主机B收到这条消息后需要对其进行ACK应答,但如果主机B此时正好也要给主机A发生消息,此时这个ACK就可以搭顺风车,而不用单独发送一个ACK应答,此时主机B发送的这个报文既发送了数据,又完成了对收到数据的响应
- 这叫做捎带应答,捎带应答也是为了提高传输效率
12. 面向字节流
创建一个TCP的socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;
- 调用write时, 数据会先写入发送缓冲区中;
- 如果发送的字节数太长, 会被拆分成多个TCP的数据包发出;
- 如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去;
- 接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;
- 然后应用程序可以调用read从接收缓冲区拿数据;
- 另一方面, TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可
以写数据。这个概念叫做 全双工。支持全双工的本质:通信双方的接收缓冲区和发送缓冲区是分离的。
由于缓冲区的存在, TCP程序的读和写不需要一一匹配,例如:
- 写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节;
- 读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次read一个字节, 重复100次;
对于TCP来说,它并不关心缓冲区里的数据类型,在它看来都是一个一个的字节数据,它只负责把这些数据准确无误地发送对接收方的缓冲区中,至于如何读这些数据完全交给上层处理,这就叫做面向字节流。
13. 粘包问题
首先要明确, 粘包问题中的 "包 " , 是指的应用层的数据包。
- 在TCP的协议头中, 没有如同UDP一样的 "报文长度" 这样的字段, 但是有一个序号这样的字段。
- 站在传输层的角度, TCP是一个一个报文过来的。按照序号排好序放在缓冲区中。
- 站在应用层的角度, 看到的只是一串连续的字节数据。
- 那么应用程序看到了这么一连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是一个完整的应用层数据包。
那么如何避免粘包问题呢?
归根结底就是一句话, 明确两个包之间的边界。
- 对于定长的包, 保证每次都按固定大小读取即可;
- 对于变长的包, 可以在包头的位置, 约定一个包总长度的字段, 从而就知道了包的结束位置;
- 于变长的包, 还可以在包和包之间使用明确的分隔符(应用层协议, 是程序猿自己来定的, 只要保证分隔符不和正文冲突即可);
思考: 对于UDP协议来说, 是否也存在 "粘包问题" 呢?
- 对于UDP, 如果还没有上层交付数据, UDP的报文长度仍然在。同时, UDP是一个一个把数据交付给应用层。就有很明确的数据边界。
- 站在应用层的站在应用层的角度, 使用UDP的时候,要么收到完整的UDP报文,要么不收。不会出现"半个"的情况
因此UDP是不存在粘包问题 的,根本原因就是UDP报头当中的16位UDP长度记录的UDP报文的长度 ,因此UDP在底层的时候就把报文和报文之间的边界明确了,而TCP存在粘包问题就是因为TCP是面向字节流的,TCP报文之间没有明确的边界。
14. TCP异常情况
进程终止:
-
双方通信的进程,一方出现了进程崩溃,该进程曾经打开的文件描述符会被OS自动关闭,自己看来是进程崩溃,实际OS看来就是进程的正常退出,双方OS在底层会正常完成四次挥手,然后释放对应的连接资源。
-
即进程终止会释放文件描述符, 仍然可以发送FIN。和正常关闭没有什么区别。
机器重启:
关机重启之前OS在底层会正常完成四次挥手,然后释放对应的连接资源,对应的进程会被杀掉,和进程终止的情况相同。
机器掉电/网线断开:
接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行reset。即使没有写入操作, TCP自己也内置了一个保活定时器, 会定期询问对方是否还在。如果对方不在, 也会把连接释放。
另外, 应用层的某些协议, 也有一些这样的检测机制。例如HTTP长连接中, 也会定期检测对方的状态。例如我们用QQ时长时间挂着不退出,也不发消息,如果使用长连接就决定腾讯内部一定要维护大量的连接;所以一定是在本地检测到用户不活跃了,就把连接断开了;活跃时,再重新建立连接。
15.TCP小结
为什么TCP这么复杂? 因为要保证可靠性, 同时又尽可能的提高性能。
-
可靠性:
- 校验和
- 序列号(按序到达)
- 确认应答
- 超时重发
- 连接管理
- 流量控制
- 拥塞控制
-
提高性能:
- 滑动窗口
- 快速重传
- 延迟应答
- 捎带应答
-
其他:
- 定时器(超时重传定时器, 保活定时器, TIME_WAIT定时器等)
16. 基于TCP应用层协议
- HTTP(超文本传输协议)
- HTTPS(超文本传输安全协议)
- SSH(安全外壳协议)
- Telnet(远程终端协议)
- FTP(文件传输协议)
- SMTP(电子邮件传输协议)
当然, 也包括你自己写TCP程序时自定义的应用层协议;
17. TCP/UDP对比
我们说了TCP是可靠连接, 那么是不是TCP一定就优于UDP呢? TCP和UDP之间的优点和缺点, 不能简单, 绝对的进行比较
- TCP用于可靠传输的情况, 应用于文件传输, 重要状态更新等场景;
- UDP用于对高速传输和实时性要求较高的通信领域, 例如, 早期的QQ, 视频传输等。另外UDP可以用于广播;
归根结底, TCP和UDP都是程序员的工具, 什么时机用, 具体怎么用, 还是要根据具体的需求场景去判定。
用UDP实现可靠传输(经典面试题)
参考TCP的可靠性机制, 在应用层实现类似的逻辑;
例如:
-
引入序列号, 保证数据顺序;
-
引入确认应答, 确保对端收到了数据;
-
引入超时重传, 如果隔一段时间没有应答, 就重发数据;
18. TCP实验:理解listen的第二个参数
我们修改之前的代码,不进行accept获取listensock
套接字新连接,什么也不干,只进行监听连接的到来,backlog
设置为1
编译运行服务器,开启2个客户端,发现连接没有问题;一旦启动第3个客户端,客户端状态正常, 但是服务器端出现了SYN_RECV
状态, 而不是ESTABLISHED
状态
这是因为, Linux内核协议栈为一个tcp连接管理使用两个队列:
- 半连接队列(用来保存处于SYN_SENT和SYN_RECV状态的请求)
- 全连接队列(accpetd队列)(用来保存处于established状态,但是应用层没有调用accept取走的请求)
而全连接队列的长度会受到 listen 第二个参数的影响。
全连接队列满了的时候, 就无法继续让当前连接的状态进入 established 状态了。
通过上述实验可知, 全连接队列的长度 = listen 的第二个参数 + 1。
上述实验,我们设置的全连接队列大小是2,前2次连接正常,但是到了第3次连接的处于半连接队列,处于了SYN_RECV
状态,在客户端看来,连接已经建立好了,但是在服务端看来没有建立连接成功,因为服务端对于第三次握手的ACK进行了忽略。
过一段时间去看,SYN_RECV
状态已经没了,服务器长时间不受理ACK,为了防止服务器压力过大,直接关掉连接
误区:
在上面全连接队列长度为2,是不是意味着服务器一次最多可以处理两个连接?
不是,这个队列表示已经处于连接成功状态但是还没来得及被上层读取的,不是说上层一共能处理多少连接,这些连接一旦被accept被拿走,就会在此队列中被移走。就像你去餐厅吃饭,餐厅门口在排队,可是里面有上百号人在吃饭,能说餐厅只能供这几个排队的人吃饭吗
全连接队列长度问题:
举一个例子:
比如你在就餐高峰期去某个饭店吃饭,你到门口先问服务员是否有位置,他说没有后你直接离开,去下一家饭店;到这家饭店你还是问服务员是否有位置,他说位置满了,但是在5分钟内就有人要离开,你可以排队等待。对比这两家饭店:在就餐高峰期时间段内,总会有几分钟桌椅上是没有人的,第二饭店通过在外部排队的方式时时刻刻保证了自己饭店的内部资源(桌椅)使用率是100%,提高了盈利。
老板看到了这种方式,于是买了大量的椅子供用户在饭店外排队,以保证自己饭店的盈利。可是他这样做真的有用吗?(1) 一旦这个队伍过长,那么处于队尾的人就不太愿意排队了,他有排队的时间不如去另一家饭店吃饭 (2) 老板本人也是比较愚蠢的,他有买椅子的钱,为啥不用来扩张一下店面,以供更多的用户就餐。
所以,我们在全连接队列不能没有的前提下,保证这个队列不能太长:
- 队列太长,会消耗过多OS本身的资源,本来是可以给server使用的
- 队列太长,客户端不愿意等待