总言
主要内容:传输层UDP、TCP协议基本介绍。UDP报文格式、TCP报文格式、三次握手四次挥手、TCP可靠性策略说明。
文章目录
- 总言
- 8、UDP协议(传输层·一)
- 8.1、传输层预备知识
- 8.1.1、端口号
- 8.1.2、一些指令(netstat、pidof、xargs)
- 8.2、UDP报文
- 8.2.1、基本认识
- 8.2.2、细节理解
- 9、TCP协议(传输层·二)
- 9.1、初步理解
- 9.1.1、首要解决的两个问题(16位目的端口号、四位首部长度)
- 9.1.2、理解什么叫做可靠性
- 9.1.3、32位序号和32位确认序号
- 9.2、其它TCP报头属性
- 9.2.1、16位窗口大小与流量控制(引入篇)
- 9.2.2、6位标志位
- 9.2.3、其余报头简述
- 9.3、TCP的三次握手和四次挥手
- 9.3.1、如何理解连接
- 9.3.2、如何理解三次握手
- 9.3.2.1、基本描述
- 9.3.2.2、细节理解
- 9.3.3、如何理解四次挥手
- 9.3.3.1、基本描述
- 9.3.3.2、细节理解
- 9.4、TCP可靠性的其他策略
- 9.4.1、确认应答(ACK)机制
- 9.4.2、超时重传机制
- 9.4.3、流量控制
- 9.4.4、滑动窗口 && 快重传
- 9.4.5、高速重发机制:快重传
- 9.4.6、慢启动机制 && 拥塞窗口
- 9.4.7、延迟应答
- 9.4.8、捎带应答
- 9.5、其它问题说明
- 9.5.1、面向字节流
- 9.5.2、数据报粘包问题
- 9.5.3、TCP异常
- 9.5.4、 listen 的第二个参数
8、UDP协议(传输层·一)
8.1、传输层预备知识
8.1.1、端口号
1)、什么是端口号
说明:端口号(Port)标识了一个主机上进行通信的不同的应用程序。
在TCP/IP协议中, 用 “源IP”、“源端口号”、“目的IP”、“目的端口号”、“协议号” 这样一个五元组来标识一个通信(可以通过netstat -n
查看:显示所有已建立的有效连接。);
2)、端口号范围划分
0 - 1023
: 知名端口号, HTTP, FTP, SSH等这些广为使用的应用层协议, 他们的端口号都是固定的。
1024 - 65535
: 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作系统从这个范围分配的。
说明:为了使用方便, 人们约定一些常用的服务器, 都是用以下这些固定的端口号。在/etc/services
文件中可查看。
ssh服务器, 使用22端口
ftp服务器, 使用21端口
telnet服务器, 使用23端口
http服务器, 使用80端口
https服务器, 使用443
3)、问题说明
问题一: 一个进程是否可以bind多个端口号?
回答:可以。之前套接字学习中,创建套接字会分配sockefd,相当于网络中的一个连接就是一个文件。因为一个进程可以打开多个文件描述符,所以一个进程可以绑定多个端口号。
问题二: 一个端口号是否可以被多个进程bind?
回答:原则上不可以。如果一个进程先绑定一个端口号,然后再fork一个子进程,这样的话就实现了多个进程绑定一个端口号。但是不同的进程绑定同一个端口号是不可以的,实际上当服务器挂掉不能立即重启(TIME_WAIT
状态)也说明不用进程不能同时绑定同一个端口号。
问题三:为什么不直接使用进程PID来代替端口号识别唯一的进程?
回答:①不是所有的进程都要对外提供网络服务,如果直接使用进程pid,OS还需要筛选到底是哪些进程参与提供网络服务(增添筛选成本)。②为了让系统功能和网络功能解偶联。
8.1.2、一些指令(netstat、pidof、xargs)
1)、netstat
netstat
是一个用来查看网络状态的重要工具。Netstat会显示与IP、TCP、UDP和ICMP协议相关的统计数据,一般用于检验本机各端口的网络连接情况。
语法:netstat [选项]
功能:查看网络状态
常用选项:
-n 拒绝显示别名,能显示数字的全部转化成数字
-l 仅列出有在 Listen (监听) 的服务状态
-p 显示建立相关链接的程序名
-t (tcp)仅显示tcp相关选项
-u (udp)仅显示udp相关选项
-a (all)显示所有选项,默认不显示LISTEN相关
常用:-nltp
、-antp
.
2)、pidof
语法: pidof [进程名]
功能: 通过进程名, 查看进程id
以下为简单演示,通过这种方法,可以很快的查看自己写的进程。
3)、xargs
相关指令介绍见常见指令入门(二)。
一个小演示:文件日期更新。
[wj@VM-4-3-centos http_demo]$ ls -al #原先文件日期
total 36
drwxrwxr-x 3 wj wj 4096 Nov 12 21:59 .
drwxrwxr-x 4 wj wj 4096 Nov 11 20:11 ..
-rw-rw-r-- 1 wj wj 2894 Nov 12 21:40 HttpServer.cc
-rw-rw-r-- 1 wj wj 1667 Nov 11 11:02 HttpServer.hpp
-rw-rw-r-- 1 wj wj 1107 Nov 11 11:02 Log.hpp
-rw-rw-r-- 1 wj wj 98 Nov 11 11:02 Makefile
-rw-rw-r-- 1 wj wj 4043 Nov 11 11:02 Sock.hpp
-rw-rw-r-- 1 wj wj 1334 Nov 11 19:41 Util.hpp
drwxrwxr-x 5 wj wj 4096 Nov 11 18:47 wwwroot
[wj@VM-4-3-centos http_demo]$ ls | xargs touch
[wj@VM-4-3-centos http_demo]$ ls -al #更新后的文件日期
total 36
drwxrwxr-x 3 wj wj 4096 Nov 12 21:59 .
drwxrwxr-x 4 wj wj 4096 Nov 11 20:11 ..
-rw-rw-r-- 1 wj wj 2894 Nov 19 09:37 HttpServer.cc
-rw-rw-r-- 1 wj wj 1667 Nov 19 09:37 HttpServer.hpp
-rw-rw-r-- 1 wj wj 1107 Nov 19 09:37 Log.hpp
-rw-rw-r-- 1 wj wj 98 Nov 19 09:37 Makefile
-rw-rw-r-- 1 wj wj 4043 Nov 19 09:37 Sock.hpp
-rw-rw-r-- 1 wj wj 1334 Nov 19 09:37 Util.hpp
drwxrwxr-x 5 wj wj 4096 Nov 19 09:37 wwwroot
[wj@VM-4-3-centos http_demo]$
8.2、UDP报文
8.2.1、基本认识
1)、UDP协议端格式
问题描述: 根据之前学习,我们知道数据自上而下交付需要层层封装,添加报头。相反,自下而上递交数据时需要层层解包,去掉报头。那么,①UDP协议是如何分离报头(or 封装报头)的?②又是如何将报文交付到某一具体主机的具体进程上?
回答:以下为UDP协议的基本格式。
1、UDP采用固定长度的报头(8字节),可根据此分离报头和有效载荷。
2、每层协议层报头中存储着当前层的首部属性信息。在UDP报头首部,记载着16位的目的端口号,可根据此进行向上交付(PS:无论客服端还是服务端,进程使用bind函数时,绑定了端口号。因此交付报文时可以找到具体的某一进程。)
2)、其它问题说明
问题1:为什么编写代码时,对端口号的类型为uint16_t
?
回答: OS所决定了协议所使用的端口号就是16位的。因此我们使用时也一并使用uint16_t
表示端口号类型。
问题2:udp如何正确提取整个完整的报文
回答: 报头中有记录报文长度的属性,16位UDP长度。
PS:在不考虑丢包情况下,UDP是具有将报文一个一个正确接受的能力的,即面向数据报。
8.2.2、细节理解
1)、如何理解报文本身?
实际报头也是结构化的字段。例如:位段
,以下为一个简易版的理解草图(实际OS会很复杂,如需要处理位段的可移植性等各种问题)。
2)、UDP传输的特点
无连接
:知道对端的IP和端口号就直接进行传输, 不需要建立连接;
不可靠
:没有确认机制, 没有重传机制; 如果因为网络故障该段无法发到对方, UDP协议层也不会给应用层返回任何错误信息;
面向数据报
:不能够灵活的控制读写数据的次数和数量;
对面向数据报的理解
说明: 应用层交给UDP多长的报文,UDP就原样发送该长度的报文。既不会拆分, 也不会合并。
用UDP传输100个字节的数据:
例如: 如果发送端调用一次sendto发送100个字节,那么接收端也必须调用对应的一次recvfrom,接收100个字节,而不能循环调用10次recvfrom, 每次接收10个字节。
3)、UDP缓冲区
理解sent\recvfrom等IO类接口数据
说明:IO函数本质是拷贝函数,我们实际在使用send/write或者recv/read这类函数时,并不是直接把数据发送到网络甚至对方主机,而是把我们的数据拷贝到发送/接收缓冲区中。
至于什么时候发?发多少?出错了怎么办?等等此类问题,不是应用层来考虑的事,而是由由传输层的传输控制协议(TCP、UDP)决定的。(体现该层协议传输控制的特性)
理解UDP缓冲区
1、UDP具有接收缓冲区。 但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致。如果缓冲区满了,后续到达的UDP数据就会被丢弃。
2、UDP没有真正意义上的发送缓冲区。 调用sendto会直接交给内核,由内核将数据传给网络层协议进行后续的传输动作。
UDP的socket既能读, 也能写,即UDP是全双工的。
全双工:同时支持读和写
半双工:读写操作同一时刻只能进行一项
4)、UDP数据长度说明
UDP协议首部中有一个16位的最大长度,也就是说一个UDP能传输的数据最大长度是64K(包含UDP首部)。而64K在当今的互联网环境下是一个非常小的数字,如果我们需要传输的数据超过64K,就需要在应用层手动的分包、多次发送,并在接收端手动拼装。
9、TCP协议(传输层·二)
9.1、初步理解
9.1.1、首要解决的两个问题(16位目的端口号、四位首部长度)
1)、tcp报文格式
说明:TCP首部内容很丰富,如果不计算选项字段,一般来说TCP首部有20字节。
2)、如何交付报文
说明:根据上述TCP协议段格式,报头属性中含有16位源/目的端口号
,可以告知数据是从哪个进程来,到哪个进程去。
3)、如何解包 or 封装
要理解该问题,首先要理解4位首部长度
首部长度,也称数据偏移,占4位。它指出了TCP报文段的数据起始处距离TCP报文段的起始处有多远,即TCP报文的首部长度。应注意,“数据偏移”以4字节为计算单位,由于4位二进制数表示的最大十进制数字是15,因此数据偏移的最大值是60字节,这也是TCP首部的最大字节(即选项长度不能超过40字节)。
要理解该问题,首先要理解4位首部长度
基本步骤如下:
1、提取20字节
2、根据标准报头,提取4位首部长度L
(二进制表示,需要转换为十进制)。检测L× 4
,若为20,代表报头中没有选项属性,报头提取完毕。否则,说明存在选项属性,读取[L× 4 - 20]
字节数据,即选项的长度。
3、将20字节的属性和选项都读完,剩下的就都是有效载荷了。
PS:若观察上述的TCP报文格式,可以发现TCP协议没有记录整个报文大小/或者有效载荷大小的字段。
9.1.2、理解什么叫做可靠性
说明:挑一个重要的属性(可靠性),作为讲解tcp的切入点。
1)、问题引入
问题一:是什么原因造成的数据传输不可靠?
回答:单纯就是数据传输的距离变长了。
问题二:存不存在100%可靠的协议呢?
回答:通信双方都无法保证自己作为 “最新发送数据的一方”,发送出去的 “最新数据 ” 能被对方收到。但是在局部上,对于历史发送的数据,可以做到100%可靠(正是因为接收到对方发送的数据,才有了最新数据的发送,作为对接收到的消息的回应)。
2)、如何做到可靠性?(局部)
TCP协议的确认应答机制
:只要发送方发出的一个报文收到了接收方传回的对应应答,就能保证我发出的数据对方收到了。
9.1.3、32位序号和32位确认序号
1)、问题引入
问题说明:数据包乱序,也是造成数据传输不可靠的因素之一。 例如:
①客户端一次可能向服务端发送多个报文,就有一个问题,发送的顺序不一定是接受顺序。
②同理,服务端可以一次响应多个报文,那么客户端就需要确认哪个应答对应哪个请求。
为了解决上述问题,我们引入了序号和确认序号。
2)、序号和确认序号
基本介绍:
序号
:占4个字节。TCP是面向字节流的,在一个TCP连接中传送的字节流中的每一个字节都按顺序编号,整个要传送的字节流的起始序号必须在连接建立时设置,首部中的序号字段值则是指本报文段所发送的数据的第一个字节的序号。
例如,一个报文段的序号是301,而数据共100字节,这就表明:本报文段的数据的第一个字节的序号是301,最后一个字节的序号是400。显然,下一个报文段(如果还有的话)的数据序号应当从401开始,即下一个报文段的序号字段值应为401。
确认号
:占4个字节。是期望收到对方下一个报文段的第一个数据字节的序号。
例如,B正确收到了A发送过来的一个报文段,其序号字段值是501,而数据长度是200字节(序号501~700),这表明B正确收到了A发送的到序号700为止的数据。因此,B期望收到A的下一个数据序号是701,于是B在发送给A的确认报文段中把确认号置为701。
作用说明:
根据上述描述,可知:
1、序号和确认序号将请求和应答进行一一对应。
2、确认序号,表示在该序号之前的确认序号所对应的数据,对方已经全部收到。并告诉对方,下次发送时,从确认序号指明的序号发送。
3、二者的存在允许部分确认丢失,或者不给应答。例如当前序号为1000,2000,3000。若接收到的确认序号为3001,则说明前两次1000、2000的请求已经被对方接收到(即使代表2001、1001的确认序号丢失/未收到。)
4、起到排序作用。因为任何一方都会收到报文,报文中会携带序号。如此可以避免数据接收顺序不匹配而导致响应操作无法响应的问题。
为什么要分别使用序号、确认序号两个字段?(只存在一个序号不行吗?)
回答:tcp协议的工作方式都是全双工的,任何通信的一方,在发送确认的时候,也可能携带新的数据。
如果server即想给对方确认应答,又想同时给对方进行发送它的消息。这时候就体现了两个字段的用途。相当于四个字段,两个为一组,分别服务通讯的一方。由此保障了数据的收发。
(PS:往往给对方发送消息,本身即为应答。)
9.2、其它TCP报头属性
9.2.1、16位窗口大小与流量控制(引入篇)
数据发送存在的问题举例:①丢包、②乱序、③发送太快或太慢。
对于问题③:与UDP不同,TCP协议既有发送缓冲区,又有接收缓冲区,但需要明确的是,缓冲区的大小是有限的,即若发送/接收端处理数据的速度是有限的。
如果发送端发的太快,导致接收端的缓冲区被打满,这个时候如果发送端继续发送,就会造成丢包,继而引起丢包重传等等一系列连锁反应。同理,如果数据发送太慢,会严重影响效率问题。
为了解决这个问题,tcp协议传输双方需要给对方同步自己的接收能力,而接收能力由接收缓冲区中剩余空间的大小所决定。
TCP这种根据接收端的处理能力,来决定发送端的发送速度的机制,就叫做流量控制(Flow Control)
。
tcp报文格式中,有一个叫做16位窗口大小
的报头字段,其作用就是向对方传送自己的接收缓冲区剩余空间大小的。
9.2.2、6位标志位
基本说明:不同版本可能存在情况不同,这里讲述标准的6位标记位的情况。
1)、是什么和为什么
问题一:为什么需要多个标记位?
回答:服务端会收到大量的不同的报文,例如,常规报文、建立链接的报文、断开链接的报文、确认报文、等等其他类型的报文。不同报文,其所需要做的处理工作各有千秋,为了方便区别管理以及进行后续处理,使用标记位来标记报文的类型。
问题二:各个标记位都是什么含义?
回答:标记位实际上是宏。
2)、相关介绍
SYN
: 请求建立连接。我们把携带SYN标识的称为同步报文段,占1位。
例如:将SYN标志位置为1,服务器收到报文,解析后就能知道当前客户端是想要进行连接,那么后续的处理操作就顺理成章了。
FIN
: 通知对方本端即将关闭,我们称携带FIN标识的为结束报文段,占1位。当FIN=1时,表明此报文段的数据已发送完毕,并要求释放连接。
ACK
: 确认应答标志位。凡是该报文具有应答特征,该标志位都会被设置为1。
说明:大部分网络报文的ACK都是被设置为1的。但是有些报文的ACK不会被置为1,比如第一个链接请求报文(原因:既然是首个请求报文,说明历史上客户端和服务器从来没有法发过数据)
URG
: 紧急标志位,配合16位紧急指针使用。占1位。
说明:当URG=1时,表明紧急指针字段有效。它告诉系统此报文段中有紧急数据,应尽快发送(相当于高优先级的数据),而不要按原来的排队顺序来传送。要与首部中紧急指针字段配合使用。
PSH
: 提示接收端应用程序立刻从TCP缓冲区把数据读走。占1位。
说明:当两个应用进程进行交互式通信时,有时在一端的应用进程希望在键入一个命令后立即就能收到对方的响应。在这种情况下,TCP就可以使用 推送(push) 的操作。这时,发送方TCP把PSH置为1,并立即创建一个报文段发送出去,接收方TCP收到PSH=1的报文段,就尽快地交付接收应用进程,而不用再等到整个缓存都填满了后再向上交付。
RST
: 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段。占1位。
说明:当RST=1时,表明TCP连接中出现了严重错误(如由于主机崩溃或其他原因),必须释放连接,然后再重新建立传输连接。RST置为1还用来拒绝一个非法的报文段或拒绝打开一个连接。
9.2.3、其余报头简述
9.3、TCP的三次握手和四次挥手
9.3.1、如何理解连接
因为有大量的客户端将来可能连接服务器,所以服务端端一定会存在大量的连接。对于这些连接,操作系统也需要对它们进行管理,即【先描述,在组织!】。因此,所谓的连接,本质是内核的一种数据结构类型。建立连接成功的时候,就是在内存中创建对应的连接对象,再对多个连接对象进行某种数据结构的组织。
需要注意的是,维护连接是有成本的(内存+ cpu)。
9.3.2、如何理解三次握手
9.3.2.1、基本描述
1)、总览
相关文章:深入浅出TCP三次握手
TCP三次握手是浏览器和服务器建立连接的方式,目的是为了使二者能够建立连接,便于后续的数据交互传输。
第一次握手:客户端发送带有 SYN
标志的连接请求数据包给服务端
第二次握手:服务端发送带有 SYN+ACK
标志的连接请求和应答数据包给客户端
第三次握手:客户端发送带有 ACK
标志的应答数据包给服务端(可以携带数据)
2)、绘图说明
准备说明:所谓的三次握手即TCP连接的建立。最初两端的TCP进程都处于关闭状态,之后,二者分别创建传输控制块。
对于客户端,TCP连接建立是由客户端主动发起的,因此称为主动打开连接。
对于服务端,属于被动打开的一方,其会进入监听状态等待TCP客户进程的连接请求。
后续:
第一次握手:客户端发送TCP连接请求。 设置SYN=1 ,表示这是SYN握手报文。然后将该 SYN 报文发送给服务端(即向服务端发起连接),之后客户端处于同步已发送状态(SYN-SENT
)。
第二次握手:服务端发送针对TCP连接请求进行确认的报文。 服务端收到客户端的 SYN 报文后,根据序列号和反序列号,设置 SYN=1 和 ACK=1,表示这是一个SYN握手和ACK确认应答报文,最后把该报文发给客户端,之后服务端处于同步已接收状态(SYN-RCVD
)。
第三次握手:客户端发送确认的确认。 ①客户端收到服务端报文后,还要向服务端回应最后一个应答报文,设置ACK=1 ,表示这是一个应答报文,将完整的报文发送给服务端(这次报文可以携带数据),之后客户端处于连接已建立状态(ESTABLISHED
)。②服务器收到客户端的应答报文后,也进入连接已建立状态(ESTABLISHED
)。
PS:三次握手不一定要保证连接成功,客户端只要将ACK发出,就会变为ESTABLISHED状态,但是只有服务端收到了真正的ACK才会建立真正的连接。
3)、注意事项
1、为什么绘制的图解中是斜线的箭头传输?
回答:在三次握手期间存在时间成本问题。
2、发送的只是SYN、ACK这样的报文字段吗?
回答:始终要记住,双方传输的是完整的报文,即包含完整的报头和正文数据(可无)。
9.3.2.2、细节理解
1、是不是三次握手都一定要保证成功?
回答:不一定。tcp的可靠性是在建立连接之后。在三次握手期间,存在丢包概率,前两次由于有应答还能获取反馈,最后一次无法保证。
2、三次握手是对客户端服务端都要起效。 这是保障客户端和服务端状态改变的判断依据。
前两次丢包:因为有ACK应答,可以知晓,会根据情况做出后续处理。
对于第三次丢包:客户端只要发出报文就会改变状态为ESTABLISHED
,但它也有可能在中途丢包,导致服务端没有收到第三次报文。
3、为什么要三次握手?
问题一:一次握手行不行?
如下图描述,这种场景下客服端可以写一个死循环,不断请求连接服务器,导致服务器资源充满,最终挂掉。
问题二:两次握手行不行?
如果只进行两次握手,那么服务器只能确认客户端的请求。由于存在丢包等问题,客户端无法保障每次都能收到应答,同理,服务器也无法得知当前响应在客户端的接收情况。
这种情况下,服务端在返回应答报文后就改变状态进入ESTABLISHED ,假如此处客户端因资源阻塞、恶意攻击等原因不断重复发送 SYN 报文请求,那么服务器在收到请求后就会建立多个冗余的无效链接,浪费TCP服务器的资源,仍旧属于SYN洪水问题。
问题三:为什么三次握手就行?
此外,三次握手还保证了站在双方立场上,都能够保障全双工(能收&&能发)。(一次握手、两次握手达不到此目的。)
问题四:四次握手、五次握手……行不行?
9.3.3、如何理解四次挥手
9.3.3.1、基本描述
相关文章:深入浅出TCP四次挥手
说明:终止一个连接要经过四次握手,这是因为TCP连接是全双工的,因此每个方向必须单独地进行关闭。
第一次挥手:客户端发起FIN报文,此时客户端进入FIN_WAIT_1
状态。
第二次挥手:①服务端接受到FIN 报文后,发送ACK应答报文,此时服务端进入CLOSE_WAIT
状态。②客户端接受到ACK应答报文后,进入FIN_WAIT_2
状态。
第三次挥手:①服务端处理完数据后,向客户端发送FIN报文,此时服务端进入LAST_ACK
状态。②客户端接受到FIN报文后,客户端发送应答ACK报文,进入TIME_WAIT
阶段。
第四次挥手:①服务端接受到ACK报文后,断开连接,处于CLOSED
状态。②客户端过一段时间后,也就是2MSL
后,进入CLOSED
状态。
9.3.3.2、细节理解
CLOSE_WAIT状态介绍
CLOSE_WAIT
:对方主动关闭连接或者网络异常导致连接中断,这时我方的状态会变成CLOSE_WAIT,此时我方要调用close()来使得连接正确关闭。(属于被动关闭)
1、如果我们发现服务器具有大量的CLOSE_WAIT
状态的连接的时候,原因是什么?
主要原因:应用层服务器写得有bug,例如忘了关闭对应的连接sockfd
。 (客服端和服务端只有完成四次挥手后连接才算真正断开,此时双方才会释放对应的连接资源。如果服务器接收到两次挥手后不进行调用close,那么服务器端就会存在大量处于CLOSE_WAIT状态的连接。)
演示代码如下:(借助了先前的代码快速演示)
#include "Sock.hpp"
int main()
{
Sock sock;
int listensock = sock.Socket(); // 创建套接字
sock.Bind(listensock, 8080); // 绑定端口号
sock.Listen(listensock); // 监听
while (true)
{
uint16_t clientport;
std::string clientip;
int sockfd = sock.Accept(listensock, &clientip, &clientport);// 连接
if(sockfd > 0)
{
std::cout << "[" << clientip << ":" << clientport << "]# " << sockfd << std::endl;
}
//这里我们没有关闭套接字
}
return 0;
}
演示结果如下:
说明:close_wait
的危害在于,在一个进程上打开的文件描述符超过一定数量时,新来的socket连接就无法建立了,因为每个socket连接也算是一个文件描述符。(报错:Too many open files
。)
TIME_WAIT状态介绍
1、TIME_WAIT
:我方主动调用close()断开连接,收到对方确认后状态变为TIME_WAIT。TCP协议规定TIME_WAIT状态会一直持续2MSL(即两倍的分段最大生存期),以此来确保旧的连接状态不会对新连接产生影响。
相关演示:
#include "Sock.hpp"
int main()
{
Sock sock;
int listensock = sock.Socket(); // 创建套接字
sock.Bind(listensock, 9090); // 绑定端口号
sock.Listen(listensock); // 监听
while (true)
{
uint16_t clientport;
std::string clientip;
int sockfd = sock.Accept(listensock, &clientip, &clientport);// 连接
if(sockfd > 0)
{
std::cout << "[" << clientip << ":" << clientport << "]# " << sockfd << std::endl;
}
//这里我们没有关闭套接字
std::cout << "即将关闭套接字" <<std::endl;
sleep(10);
close(sockfd);
std::cout << "套接字已经关闭:" << sockfd <<std::endl;
}
return 0;
}
演示结果:
2、细节说明:主动断开连接的一方要维持一段时间的TIME_WAIT状态。在该状态下,连接其实已经释放,但是地址信息ip, port依旧是被占用的(即处于TIME_WAIT状态的连接占用的资源不会被内核立马释放)。所以,当服务器断开后立即重新启动时,经常会看到提示绑定失败。
问题描述: 在上述,服务器的TCP连接没有完全断开之前不允许重新监听,这在某些情况下可能是不合理的。
- 服务器需要处理非常大量的客户端的连接(每个连接的生存时间可能很短,但是每秒都有很大数量的客户端来请求),这个时候如果由服务器端主动关闭连接(比如某些客户端不活跃,就需要被服务器端主动清理掉),就会产生大量TIME_WAIT连接。
- 由于我们的请求量很大,就可能导致TIME_WAIT的连接数很多。而每个连接都会占用一个通信五元组(源ip、源端口、目的ip、目的端口、协议),其中服务器的ip、端口号、协议是固定的,如果新来的客户端连接的ip、端口号和TIME_WAIT状态占用的连接重复了,就会出现问题。
解决TIME_WAIT状态引起的bind失败的方法 :使用setsockopt()
设置socket描述符的选项SO_REUSEADDR
为1,表示允许创建端口号相同、但IP地址不同的多个socket描述符。
int opt = 1;
setsockopt(listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
man setsockopt
:该函数相关使用介绍,setsockopt 函数功能及参数详解。
NAME
getsockopt, setsockopt - get and set options on sockets
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int optname,
void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname,
const void *optval, socklen_t optlen);
在创建套接字时设置。
// 创建套接字:int socket(int domain, int type, int protocol);
int Socket()
{
int listensock = socket(AF_INET, SOCK_STREAM, 0);
if (listensock < 0)
{
logMessage(FATAL, "socket:创建套接字失败。%d:%s", errno, strerror(errno));
exit(2); // 退出
}
//设置setsockopt套接字选项
int opt = 1;
setsockopt(listensock,SOL_SOCKET,SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
logMessage(NORMAL, "socket:创建套接字成功, listensock:%d", listensock);
return listensock; // 将套接字返回给TcpServer中的成员函数_listensock
}
3、回答为什么: 为什么要有TIME_WAIT这一段等待时间?
- ①可靠地终止 TCP 连接:网络是不可靠的,一个TCP报文段在传输过程中可能会丢失或者延迟到达,因此需要等待一段时间,确保双方都已经收到对方的ACK报文段,防止出现重复的连接或者错误的连接。
- ②保证让迟来的TCP报文段有足够的时间被识别并丢弃。数据报文可能在发送途中延迟但最终会到达,因此要等延时的报文段或尚未处理的报文在通讯双方断开连接前来得及接收并做出处理,避免造成数据错乱。
扩展:为什么是2MLS?
9.4、TCP可靠性的其他策略
9.4.1、确认应答(ACK)机制
1)、理解确认应答机制
概念介绍:在 TCP 协议中,发送方发送数据后,接收方需要对数据进行确认应答(ACK,acknowledge
的缩写),以确保数据已经被正确接收。
确认应答机制的基本原理:
- 发送方将数据分割成称为TCP段(TCP segment)的较小单元,并为每个段分配一个唯一的序列号。
- 发送方将这些TCP段发送给接收方,并启动一个定时器来跟踪每个已发送段的确认。
- 接收方收到TCP段后,将按序将它们重新组装成完整的数据流,并发送一个确认(ACK)给发送方。确认中包含接收到的最高序列号,表示该序列号之前的所有数据都已正确接收。
- 发送方在接收到确认后,会停止相应定时器,并继续发送下一个序列号的TCP段。
- 如果发送方在定时器超时之前未收到确认,它将重新发送未确认的TCP段。
2)、理解序号、确认序号的来由
序列号
:发送方发送的每个TCP段都包含一些字节的数据,TCP为每个数据字节分配一个唯一的编号以进行标识,此唯一编号称为TCP序列号。
反序列号
:每一个ACK都带有对应的确认序列号,意思是告诉发送者,目前已经收到了哪些数据,下一次该从哪里开始发。
PS:可以将其想象成字节流式的数组,序号代表位置即数组下标位置。
思考问题:为什么引入需要序列号和反序列号? (相关文章:确认应答机制与超时重发机制)
回答:
1、TCP传输中存在 ①发送端发送的数据丢包、②接收端发送的确认应答ACK丢包或延迟等等各种行为,导致发送方和接收方数据在相应时间段内对接不上,出现数据重发的现象。
2、虽然源主机可以通过重发数据来提供可靠的传输,但对于目标主机而言,反复收到相同的数据既浪费网络资源,还要耗资源对它处理。所以,我们需要一种机制来识别是否已经接收到了这个数据包、又能够判断数据包是否需要接收。
3、因此,TCP协议引入序列号,按顺序给发送数据的每一个字节(8位字节)都标上号码的编号(序列号的初始值并非为0。而是在建立连接以后由随机数生成。而后面的计算则是对每一字节加一) 。接收端查询接收数据TCP首部中的序列号和数据的长度,将自己下一步应该接收的序号作为确认应答返送回去,以此保证数据的顺序和完整性。
9.4.2、超时重传机制
基本说明: 根据上述小节内容可知,TCP在发送数据时会设置一个计时器, 若至计时器超时仍未收到数据确认信息,则会引发相应的超时或基于计时器的重传操作,计时器超时称为重传超时(RTO)。
情况一:数据真实丢包
情况二:ACK应答丢包,导致接收方收到重复数据
问题:如何设置超时重传的时间?
- 最理想的情况下,找到一个最小的时间,保证 “确认应答一定能在这个时间内返回”。
- 但是这个时间的长短随着网络环境的不同是有差异的。如果超时时间设的太长,会影响整体的重传效率;如果超时时间设的太短,有可能会频繁发送重复的包。(PS:由此我们可知超时时间不能固定,在网络好的时候,超时时间应该短一些,网络不好的时候,超时时间应该长一些)
- TCP为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间。
- Linux中(BSDUnix和Windows也是如此),超时以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍。
- 如果重发一次之后仍然得不到应答,等待 2 × 500 m s 2×500ms 2×500ms 后再进行重传。如果仍然得不到应答,等待 4 × 500 m s 4×500ms 4×500ms 进行重传。依次类推, 以指数形式递增。累计到一定的重传次数,TCP认为网络或者对端主机出现异常,强制关闭连接。
9.4.3、流量控制
1)、为什么需要流量控制?
说明:接收端处理数据的速度是有限的,如果发送端发的太快导致接收端的缓冲区被打满,这个时候如果发送端继续发送就会造成丢包,继而引起丢包重传等等一系列连锁反应。因此,TCP支持根据接收端的处理能力,来决定发送端的发送速度,这个机制就叫做流量控制(Flow Control)。
相关视频讲解:流量控制
- 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 “窗口大小” 字段,通过ACK端通知发送端。窗口大小字段越大,说明网络的吞吐量越高。
- 接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值通知给发送端。发送端接受到这个窗口之后,就会减慢自己的发送速度。
- 如果接收端缓冲区满了,就会将窗口置为0, 这时发送方不再发送数据,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端。(当然,接收方也可以主动发送一个窗口更新通知数据段,以此发送方可以发送新的数据了。)
2)、相关问题说明
1、流量控制中,第一次发送数据时,通讯双方如何知道对方的接收能力?
回答: 第一次发送数据
≠
≠
= 第一次交换报文。第一次报文交换是在三次握手期间,这期间通讯双方是不进行数据交换的,但可以在对应报文中附带我方当前的接收能力信息(TCP首部中有一个16位窗口字段,就是存放了窗口大小信息)。
扩展: 16位数字最大表示65535,是否意味着TCP窗口最大就是65535字节?
回答:实际上TCP首部40字节选项中,还包含了一个窗口扩大因子M,实际窗口大小是窗口字段的值左移 M 位
;
2、当接收方缓冲区满时,后续发送方如何知道什么时候可以再次发送数据?
回答:通过窗口探测和窗口更新通知。
扩展:为什么需要窗口更新通知? 假设窗口探测间隔为1s,但是窗口立马更新好了,此时要等待1s后再进行通信,无疑是降低效率的行为。因此,设置窗口更新通知,可以让接收方在更新接收缓冲区的接收能力后,立马通知对方。(双重保障的灵活使用)
9.4.4、滑动窗口 && 快重传
1)、滑动窗口是什么?(基本介绍)
问题引入: 对每一个发送的数据段,都要给一个ACK确认应答,收到ACK后再发送下一个数据段。这样做有一个比较大的缺点就是性能较差,尤其是数据往返的时间较长的时候。
为解决这个问题,TCP引入了窗口这个概念。即使在往返时间较长的情况下,它也能控制网络性能的下降。确认应答不再是以每个分段,而是以更大的单位进行确认时,转发时间将会被大幅度的缩短。也就是说,发送端主机在发送了一个段以后不必要一直等待确认应答,而是继续发送。
相关机制说明:
注意事项:
1、当如果数据发出后如若收到确认应答就可以从缓冲区清除这部分数据。如果没有收到确认应答,必须在缓冲区保留这部分数据。
2、尽管滑动窗口一次能向对方发送多条数据段,但这里一次发送的“多条数据”也是有上限的,而这个上限取决于对方当前的接收能力(由上一次ACK报文中携带首部信息)。换句话说,滑动窗口的大小是动态变化的。
2)、滑动窗口的完善理解(建立一个认知模型)
问题一:滑动窗口的本质是什么?
回答:实际滑动窗口在自己的发送缓冲区中,属于自己的发送缓冲区中的一部分。而根据之前所学,缓冲区是一个类似于数组的结构,因此滑动窗口本质就是数组中的一段范围区域,以指针或下标表示范围。
问题二:滑动窗口一定会右移吗?滑动窗口可以为0吗?
回答:不一定。例如,在一段时间内,若接收方暂未读取接收缓冲区中的数据,而发送方不断发送数据,会导致缓冲区有限大小越来越小,故发送方读取到ACK报文后,下一次的滑动缓冲区会出现左指针/左下标向右偏移,右指针/右下标不变的情况。当接收缓冲区存满时,会出现滑动窗口为零的情况。
问题三:若滑动窗口一直向右滑动,是否存在越界问题?
回答:不存在。TCP的缓冲区实则是环状结构的,会根据实际大小做取模处理。
问题四:发送方一次发送多个数据段,①若接收方最终都接收到了报文数据,但返回时某一个ACK应答报文丢包了,是否存在影响?②若是发送方发送的报文丢包了呢?
回答:
对情况①,数据包已经抵达,ACK丢了。在使用窗口控制时,是不需要进行重发的。
对情况②,数据包丢失。当某段报文丢失的时候,当某一段报文段丢失之后, 发送端会一直收到 1001 这样的ACK, 就像是在提醒发送端 "我想要的是 1001"一样;如果发送端主机连续三次收到了同样一个 “1001” 这样的应答, 就会将对应的数据 1001 - 2000 重新发送;这个时候接收端收到了 1001 之后, 再次返回的ACK就是7001了(因为2001 - 7000)接收端其实之前就已经收到了, 被放到了接收端操作系统内核的接收缓冲区中。这种机制也被称为 “高速重发控制” 。
9.4.5、高速重发机制:快重传
说明:快速重传(Fast Retransmission)是一种提高TCP性能的重传策略。当接收方连续收到三个相同序号的确认报文(Duplicate ACKs)时,发送方会认为对应的数据包发生了丢失。为了尽快补发丢失的数据包,发送方会立即进行重传,而不再等待重传计时器超时。这种方法可以减小因数据包丢失导致的延迟。
思考问题:既然有快重传,为什么还需要超时重传?(快重传与超时重传的区别)
回答:只有收到三个及三个以上相同序号的确认报文时,才会触发快速重传,因此可能存在以下情况,①因各种原因(如接收方当前接受能力少于三个报文)导致当前发送的多个报文段少于三个时,不满足快速重传的条件。②若这三个及其以上的确认应答报文丢了,无法触发快速重传。此时就需要超时重传,二者相辅相成。
9.4.6、慢启动机制 && 拥塞窗口
1)、网络状态带来的问题说明
TCP有了窗口之后,可以连续大量的发送数据包。但要知道网络上有很多的计算机,若当前的网络状态出现拥堵,在不清楚当前网络状态下贸然发送大量的数据,很有可能网络状态引起雪上加霜。因此,TCP引入慢启动机制,先发少量的数据探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据。
问题1: 如何知道网络拥塞? (少量丢包 VS 大量丢包)
少量丢包时:可能是当前通讯双方的主机数据传送的问题。
大量丢包时:可能是网络出现问题,如网络拥塞。
类比课程挂科:
挂科率极低,说明本次考试试卷处于正常水平,极大概率下是学生主观因素造成的;
若几乎全班都挂了,说明本次考试试卷严重超纲,属于教学事故,任课老师和评阅老师都要承担责任。
问题2: 网络拥塞是否还能够重传?为什么?
回答:网络拥塞时若再重传数据,虽然一个主机传送的数据大小看起来无足轻重,但要知道网络中不止一个主机,拥塞是对当前区域内所有主机而言的,假若这些主机都不分情况一律向拥塞的网络中传送数据,就好比交通堵塞的道路不断涌入车辆,不利于疏通。
问题3: 如何解决网络拥塞问题?
慢启动机制和拥塞窗口。
2)、解决方案说明
慢启动机制
:为了防止问题的出现,在通信刚刚开始时就会通过“慢启动”的算法得出数值,来对发送数量来进行控制。
举例:
1、一开始初始化 cwnd = 1,表示可以传一个 MSS 大小的数据。
2、当收到一个 ACK 确认应答后,cwnd 增加 1,于是一次能够发送 2 个
3、当收到 2 个的 ACK 确认应答后, cwnd 增加 2,于是就可以比之前多发2 个,所以这一次能够发送 4 个
4、当这 4 个的 ACK 确认到来的时候,每个确认 cwnd 增加 1, 4 个确认 cwnd 增加 4,于是就可以比之前多发 4 个,所以这一次能够发送 8 个。
5、……如此下去,即一个指数级别的增长。
细节理解:同上述,始终要有一个宏观角度,网络中主机不止一台,一个主机的慢启动机制所带来的效果微乎其微,但所有主机遵守相同的协议机制,网络拥塞时因慢启动机制和拥塞窗口的存在,会减少数据量的发送,于是就可达到“星星之火,可以燎原”的效果。
拥塞窗口
:拥塞窗口(cwnd)是发送方维护的一个状态变量,单台主机一次向网络中发送大量数据时,可能会引发网络拥塞的上限值,它会根据网路拥堵情况动态调节:
- ①发送开始的时候,定义拥塞窗口大小为1;每次收到一个ACK应答,拥塞窗口加1;
- ②每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小做比较,取较小的值作为实际发送的窗口,即
滑动窗口大小 = min(拥塞窗口大小,16位窗口大小[对方接收能力] )
;
问题说明:若拥塞窗口按照上面这样的方式增长,是一种指数级别的增长方式。“慢启动” 只是指初始时慢,但是增长速度非常快。为了不能让拥塞窗口单纯倍增式增长,引入慢启动的阈值,当拥塞窗口超过这个阈值的时候,不再按照指数方式增长,而是按照线性方式增长。
- 当TCP开始启动的时候,慢启动阈值等于窗口最大值;
- 在每次超时重发的时候,慢启动阈值会变成原来的一半,同时拥塞窗口置回1。
说明:拥塞避免算法就是将原本慢启动算法的指数增长变成线性增长,本质还是增长阶段,只是增长速度缓慢了一些。若这样一直增长下去,网络就会慢慢进入拥塞状况,于是会出现丢包现象,这时就需要对丢失的数据包进行重传。当触发了重传机制,也就进入了「拥塞发生算法」(慢启动阈值会变成原来的一半,同时拥塞窗口置回1)。
拥塞窗口变化规则如下:①只要网络中没有出现阻塞,窗口就会增大;②网络中出现阻塞,窗口就会减小。
9.4.7、延迟应答
1)、为什么需要有延迟应答
2)、介绍延迟应答
如果接收数据的主机立刻返回ACK应答,这时候返回的窗口可能比较小:
- 假设接收端缓冲区为1M,一次收到了500K的数据,如果此时立刻应答, 返回的窗口就是500K。但实际上可能处理端处理的速度很快,10ms之内就把500K数据从缓冲区消费掉了。在这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些也能处理过来。
- 如果接收端稍微等一会再应答,比如等待200ms再应答,那么这个时候返回的窗口大小就是1M。而窗口越大,网络吞吐量就越大,传输效率就越高,我们的目标是在保证网络不拥塞的情况下,尽量提高传输效率。
问题:那么所有的包都可以延迟应答么?
回答:肯定也不是,延迟应答也有其规则。 - 数量限制:每隔N个包就应答一次。
- 时间限制:超过最大延迟时间就应答一次。
- 具体的数量和超时时间依操作系统不同也有差异。一般N取2,超时时间取200ms。
延迟应答的另一个便捷之处:比如服务端收到客户端的数据后,不是立刻回ACK给客户端,而是等一段时间(一般最大200ms),这样如果服务端要是有数据需要发给客户端,那么这个ACK就和服务端的数据一起发给客户端了,这样比立即回给客户端一个ACK节省了一个数据包。
向上述这样的机制,即捎带应答。
9.4.8、捎带应答
基本介绍:很多情况下,客户端服务器在应用层是 “一发一收” 的,意味着客户端给服务器发送了一个数据报文,服务器也会给客户端回复相应数据,若此时ACK搭上顺风车和服务器回应一起回给客户端。类似这样TCP的确认应答和回执数据通过一个包发送的方式,叫做捎带应答。
PS:如果接受数据立刻返回确认应答,就无法实现捎带应答,而是将所接受的数据传送应用处理生成返回数据以后在进行发送请求为止。所以捎带应答需要延迟应答一起配合实现提高网络效率,通过这种机制,可以使收发的数据量减少。
由于捎带应答的存在,四次挥手,可以合并成三次挥手。
- 当被动关闭方在 TCP 挥手过程中,「没有数据要发送」 并且 「开启了 TCP 延迟确认机制」 ,那么第二和第三次挥手就会合并传输,这样就出现了三次挥手。此时主动关闭的一方会直接从
FIN_WAIT_1
状态转入TIME_WAIT
状态。
9.5、其它问题说明
9.5.1、面向字节流
1)、UDP、TCP协议说明
UDP协议面向数据报:当用户消息通过 UDP 协议传输时,操作系统不会对消息进行拆分,在组装好 UDP 头部后就交给网络层来处理,所以发出去的 UDP 报文中的数据部分就是完整的用户消息。也就是每个 UDP 报文就是一个用户消息的边界,这样接收方在接收到 UDP 报文后,读一个 UDP 报文就能读取到完整的用户消息。
TCP是一种流协议(stream protocol):这就意味着数据是以字节流的形式传递给接收者的,没有固有的”报文”或”报文边界”的概念。
当用户消息通过 TCP 协议传输时,消息可能会被操作系统分组成多个的 TCP 报文,也就是一个完整的用户消息被拆分成多个 TCP 报文进行传输。这时,接收方的程序如果不知道发送方发送的消息的长度,也就是不知道消息的边界时,是无法读出一个有效的用户消息的(用户消息被拆分成多个 TCP 报文后,并不能像 UDP 那样,一个 UDP 报文就能代表一个完整的用户消息)。
2)、进一步介绍
对应TCP协议,创建一个TCP的socket,OS会在内核中同时创建一个 发送缓冲区 和一个 接收缓冲区。
- 调用write等写入函数时,数据会先写入发送缓冲区中。如果发送的字节数太长,会被拆分成多个TCP的数据包发出;如果发送的字节数太短,就会先在缓冲区里等待,等到缓冲区此时装入的数据长度差不多,或其他合适的时机时,再将数据一次性发送出去;
- 接收数据的时候,数据也是从网卡驱动程序先到达内核的接收缓冲区,然后应用程序可以调用read等函数从接收缓冲区拿数据。
此外,TCP的一个连接既有发送缓冲区,也有接收缓冲区。那么对于这样一个连接,既可以读数据,也可以写数据, 这个概念叫做 全双工。由于缓冲区的存在,TCP程序的读和写不需要一一匹配。
- 写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节;
- 读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次read一个字节, 重复100次;
9.5.2、数据报粘包问题
1)、问题引入
首先要明确, 粘包问题中的 “包” , 是指的应用层的数据包.
由于TCP面向字节流的属性,协议头中没有如UDP一样的 “报文长度” 这样的字段,但是有一个序号这样的字段。站在传输层的角度
,TCP是一个一个报文过来的,按照序号排好序放在缓冲区中。站在应用层的角度
,应用程序看到的只是一串连续的字节数据,并不知道这一串连续字节是否为一个完整的应用层数据包。
2)、解决方法
如何避免粘包问题?
归根结底就是明确两个包之间的边界:
1、对于定长的包,保证每次都按固定大小读取即可。由于数据报是固定大小
S
I
Z
E
SIZE
SIZE, 那么就从缓冲区从头开始按照固定长度
S
I
Z
E
SIZE
SIZE依次读取即可。
2、对于变长的包,可以在包头的位置约定一个包总长度的字段,从而就知道了包的结束位置。
3、对于变长的包,还可以在包和包之间使用明确的分隔符(应用层协议, 是程序员自己来定的,只要保证分隔符不和正文冲突即可);
9.5.3、TCP异常
进程终止
:TCP 的连接信息是由内核维护的,所以当服务端的进程崩溃/进程终止后,内核需要回收该进程的所有资源( 进程控制块中包含文件描述符,文件描述符表中有Socket网卡文件,因此TCP的连接资源也被释放)。内核会发送第一次挥手 FIN 报文,后续的挥手过程也都是在内核完成,并不需要进程的参与,所以即使服务端的进程退出了,还是能与客户端完成 TCP四次挥手的过程。
正常关机
:和进程终止的情况相同,关机时会先强制终止进程,回收进程资源(此时会释放文件描述符),内核发送FIN报文,进行四次挥手。
PS:由于触发四次挥手断开网络连接时,我方主机正在关机,所以可能四次挥手挥完了以后才关机,也有可能没有挥完就关机了。 但是四次挥手有没有挥完是没有问题的,我方关机后不再应答,对方就会触发超时重传,多次重传都没有得到ACK应答,对方就会单方面的断开连接(释放存储网络连接相关信息的内存)。
机器掉电/网线断开
:被拔掉电源的一方是没有机会向对方发送四次挥手的,也就是先有拔网线的行为,再有识别检测到TCP连接异常。
对于我方:识别到网络发生变化,自动关闭连接。
对于对方:由于没有进行四次挥手,会认为连接还在。①此时若将网络立马恢复,发送端发送数据时,接受端会识别到连接异常,并重新reset连接(触发复位报文(RST),尝试重置连接。对客户端拔掉电立马恢复和服务端掉电立马恢复都一样)。②即使没有写入操作,TCP自己也内置了一个保活定时器(TCP心跳包),会定期询问对方是否还在,如果对方不在,也会把连接释放。
PS: 应用层的某些协议也有一些这样的检测机制。例如HTTP长连接中,也会定期检测对方的状态。
举例简述:我们日常使用的微信、QQ,并非长期与服务器保持连接,在我们不打开APP应用或只是占线但长时间不访问资源时,应用层会对这样的长连接进行一定的管理工作,如APP保持登录状态,但将与服务器的连接处于断开状态(典型的表现:QQ头像呈现灰色、离线),用户后续使用时后会自动重建连接关系。
9.5.4、 listen 的第二个参数
1)、问题回顾
对服务端,其TCP套接字一系列历程如下:
初始化服务器:socket创建套接字、bind绑定、listen监听
启动服务器:accept获取连接、开始进行通讯服务
1、accept获取连接,要不要参与三次握手过程?
回答:不需要,accept的作用是直接从底层获取已经建立好的链接。也就意味着是连接先建立好,然后accept才能获取对应的连接。即,即使不使用accept,底层也将连接连接好了。
2、如此,就有一个问题,如果上层来不及调用accept,并且对端还来了大量的连接,难道所有的连接都应该先建立好吗?
回答:并非如此,实际这与listen的第二参数有关。
3、在之前学习TCP套接字时,将listen的第二参数设置如下状态,为什么要这样设置?如何理解listen的第二参数?
const static int gbacklog = 20; //不能太大、也不能太小
NAME
listen - listen for connections on a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
2)、正试介绍
是什么?
文档解释如下,根据文档可知,
在linux 2.2 内核之前,backlog是指半连接队列(syns_queue)的长度。
在linux2.2及之后,backlog是指已经完全建立连接,但是还没有被应用层accetp之前,socket所处全连接队列(accetp_queue)的长度。
具体说明:
Linux内核协议栈为一个tcp连接管理使用两个队列:
1. 半链接队列
(用来保存处于SYN_SENT和SYN_RECV状态的请求)
2. 全连接队列
(accpetd队列)(用来保存那些处于established状态而应用层没有调用accept取走的请求)
全连接队列的长度会受到 listen 第二个参数backlog的影响。 全连接队列满了的时候,就无法继续让当前连接的状态进入 established 状态了。
使用举例
相关演示:
演示结果:
其它说明:为什么该连接队列不能太长,也不能太短?
连接队列太长:会导致请求处理时间过长,甚至超时失败。
连接队列太短:服务端工作不饱和,系统性能没有完全发挥(最大程度利用)。