网络基础2(三)

1.传输层

1.1再谈端口号

下面进入传输层(本质在学操作系统网络部分):

首先再谈一下端口号:作为一台主机,上面会绑定各种各样的应用层服务:

底层协议是TCP, 每种应用层服务都绑定一个确定的端口号。在TCP/IP协议中,用"源IP", "源端口号", "目的IP", "目的端口号", "协议号"五元组组来标识一个通信。以上说的不谈应用层如何序列/反序列化,作HTTP还是HTTPS等,单纯站在两主机要通信角度,是需要这五元组的,这五元组分别由不同层的不同协议来给我们提供对应的字段来保证通信正常,否则通信不正常定协议等也是没有什么意义。下面谈一下端口的划分:端口号不是随便被我们去使用的:

还可认识一些知名端口号:

再看看netstat 命令:

还有个小工具叫pidof:

xargs是把标准输入转为后面命令行参数。

1.2 UDP协议

下面学UDP 协议,先看UDP协议的格式:

UDP协议的报头是这个样子,整个报文宽度是0~31,也就是4个字节。 UDP报头是操作系统自动给我们加的,发的'你好'消息在数据那里。这个报文怎么知道把报头加上后怎么做分离呢?可看到上面四个字段是UDP 8字节的报头,所以UDP采用定长报头做到报头和有效载荷做分离。所以收到一个UDP报文,前8个字节拿走就是报头,剩下的是有效载荷。收到了对应的报文,怎么知道收到的UDP要交给上层的哪个协议呢?根据目的端口号标定目标进程。继续说UDP协议,UDP协议中对应源端口号,作为客户端在发数据时会随机形成端口,目的端口号是我们自己传的。有了这两个字段,可进行客户端和服务器双方进程的识别。16位UDP长度表示整个报文长度,减去8就是对应数据的长度。16位UDP检验和可检验数据是否出现偏差在传输过程。UDP特点是:

单独聊一下面向数据报:面向数据报特点是你发几次对方就必须收几次(类似发信),每个报文之间有边界性。 UDP严格意义上讲只有接收缓冲区,暂时保存来不及其处理的报文,它没发送缓冲区,因为把报文交给UDP后直接添加报头向下交付,不需要发送缓冲区:

先出发不一定先到,乱序UDP不保证可靠性,直接上交。 UDP长度是16位,表明单个UDP报文最大是2的16次方,大概64KB, 若发的报文超出长度了就得自己在应用层进行拆分。

下面再理解一下UDP示意图,UDP协议本质是双方操作系统所做的约定, Linux系统是用C语言写的、数据就是自己写的字符串、协议字符串等,这些报头字段是采用结构体构建的报头。所谓的 UDP可叫做struct udp_header:

语言上叫位段类型,协议上叫UDP报头。语言上这是自定义类型,是类型就可以定义变量,开辟空间:

这就是把报头信息填好了。用户想用UDP传helloworld, 很多人向对方发UDP报文, UDP没发送缓冲区,但报文要在内核流动交付。注定了网络通信期间存在大量UDP报文,所以操作系统要对同时存在的多个UDP报文进行管理(先描述再组织),管理UDP报文的结构体叫sk_buff:

构建个UDP报文在内核层面定义一段缓冲区,再开始把udp_header拷贝进来,把用户空间内容拷贝进来。start指向缓冲区头部,end指向结尾,pos指向报文有效部分,这样一个报文有了。未来还能存在大量报文还没有发出去,所以next把每个用链表形式连接起来:

对UDP报文管理转为对sk_buf对应的描述结构体的管理。

1.3TCP协议

下面来谈TCP协议,它是传输层中最重要的一个协议,全称是传输控制协议。下面来理解一下:

黑线以上是应用层,黑线以下是传输层,最典型的叫TCP协议。应用层协议里典型的比如说之前写的网络版计算器, HTTP/HTTPS,我们用到的应用层协议底层用的就是TCP协议的功能。TCP协议在操作系统内会存在自己的发送缓冲区和接收缓冲区,也就是操作系统内只要支持TCP协议的每建立一个链接,此时在操作系统内就要为该链接创建发送缓冲区和接收缓冲区。在应用层我们自己是要定义一些缓冲区来接收用户输入或把输出结果给用户的,自己定义的缓冲区叫用户级缓冲区。我们在上层调那些接口的本质不是把数据发送到网络中,比如发送其实是把数据从用户缓冲区拷贝到TCP的发送缓冲区。到这有应用层把数据交给了操作系统,至于数据什么时候发送,发送多少,出错了怎么办完全由操作系统即TCP协议自主决定。文件也是类似的,如我们往文件里写也用 read接口,每个文件有对应的文件缓冲区,应用层把数据写到文件本质是把数据从应用层拷贝到内核文件的缓冲区里,至于数据什么时候刷等是由操作系统和磁盘交互的。所以磁盘和网络没区别,无非就是把磁盘换成网卡就可完成数据的IO。所以应用层只负责对收上来的数据进行协议处理,如反序列化等,处理完后把数据再写给操作系统,应用层任务就完成了,数据什么时候发等由TCP 协议决定。同信双方都支持TCP协议,客户如此,对方服务器亦是如此,对于TCP协议来讲双方地位是对等的。发送是把传输层发送缓冲区的数据写到对方接收缓冲区里,这个工作本质也是拷贝,只不过这个拷贝要通过网络。 read本质是从接收缓冲区读数据,经常说read会有阻塞情况,本质是接收缓冲区没数据。这里所谓的发送或接收缓冲区其实是内存空间,操作系统为了管理内存把整个内存想成一个个4KB空间,操作系统通过每个内存块的struct配置来管理内存块使用情况,所以这的缓冲区本质是由多个内存块即struct配置构成的(TCP协议的控制体现在数据什么时候发等由 TCP协议决定)。

下面谈谈TCP协议段格式:

每种数据在协议的不同层有不同的名称,应用层把数据叫请求和响应,传输层把数据叫数据段。这是TCP的报文,那报头和有效载荷如何分离?如何交付给上层?TCP基本结构分三部分,前20个是 TCP标准报头,选项我们忽略不管,剩下的是TCP协议的有效载荷。如果收到了一个TCP报文,由前两个字段源和目地端口号找到上层的目标进程就可向上交付。把前20个字节拿出来就全拿到报头信息了,标准报头里有个字段是4位首部长度,描述的是整个报头长度(包含选项)。4位首部长度范围是0,15,它计算的时候,有基本的大小单位:4字节,所以表示范围是0,60,因此选项最多是 40个字节。如果不谈选项,首部长度应填0101,所以4位首部长度可准确帮我们把报头从整个报文里准确的去了。报头和有效载荷分离是先读取前20个字节,根据4位首部长度算出报头总大小,再减20剩下的就是选项大小。再从原始报文读选项,剩下的就是数据。所以分离做法是,固定长度+自描述字段。下面聊一下16位的窗口大小:

应用层构建了HTTP请求,把数据通过read写到本地的发送缓冲区。发送给对方时要给发送的数据添加TCP报头,封装后通过底层把数据交到了对方的接收缓冲区里。对方将报头和有效载荷进行分离,根据16位端口号把数据交给对方接收缓冲区:

所以client和服务器基于TCP协议进行通信的时候,互发消息的时候,发送的可是完整的TCP报文,即一定携带完整的TCP报头。TCP协议是要保证可靠性的,发送过程本质是把数据拷贝给对方,数据被拷进来因为接收缓冲区是固定大小的所以剩余空间就小了。如果接收方应用层因为一些原因不读取接收缓冲区的数据,通信时发送方不清楚接收方的接收能力,所以发送方不断给接收方发的时候有可能对方已经满了,进而导致数据出现大面积丢包。这是不可靠的,因此通信时接收方接收能力很弱的时候要让发送方发慢一些或干脆不发,通过控制发送速度让对方来得及接收避免丢包的做法称为流量控制。 TCP可靠性有一种叫丢包重传,发送的报文丢了发送方仍可以做补发,那是否可不做流量控制,丢了重发就行?虽然可以,但不合理。一个报文经过正规途径,消耗了网络资源到了对方那没什么错被丢了,这样浪费很大,效率不高。 TCP保证可靠性策略非常多,凭什么保证可靠性,最基本的一个特点是确认应答机制。比如直播课时老师说一堆话老师不确定远方同学有没有听到,于是老师说听到的扣1,同在扣1进行响应的过程叫确认应答。所以客户端给服务器发任何消息的时候,服务器都要对收到的消息进行响应。因此客户端给服务器发了个消息,即便服务器没任何话给客户端说,服务器也要对这个消息确认应答,服务器给客户端发也一样:

有了应答机制可以保证客户端到服务器方向的可靠性。下面谈一下通行过程:客户端想发一条消息给服务器,服务器收到消息后 TCP协议要立即给对方应答:

发送消息、确认应答都是TCP报文。发送方发的数据量可能非常大,对方也要不断进行应答,发送太快可能导致来不及接收,所以要流量控制。客户端发送数据慢一些,依据是什么?对于发送方来讲,发送速度由对方的接收缓冲区中剩余的空间的大小决定。那客户端怎么知道服务器接收的剩余大小呢?因为发一个报文会有对应响应报文,收到响应才发第2个,响应中16位窗口大小填的是接收方剩余接收空间的大小。得知对方接收能力后,发的时候就知道最多发多少数据了。从客户端到服务器和服务器到客户端都要进行流量控制,双方可依据16位窗口大小得知对方接收能力。因此 16位窗口大小,填写的是自己的接收缓冲区中剩余的空间大小。

下面来聊序号和确认序号,为啥确认应答在TCP协议里这么重要?比如两个人相隔100米喊话,A对B喊我们一会去吃饭吧,B回答说好的。此时站在A的角度收到'好的'后可以保证刚A给B发的消息B收到了,B也不知道他喊的'好的'A有没有收到。为了让B知道收到了消息,A又喊收到你的好的了,此时站在B角度认为给A的应答A已经收到了。为了保证让A知道B收到了应答,B又回复收到了......:

通过上述明白:1.只要我收到了应答,可以确认我最近发的消息对方收到了。2.没有应答的数据,我们无法保证可靠性,所以最新的一条消息是没有应答的,所以我们无法保证发出去的消息是100%可靠的。因此站在全局的角度,这个世界上不可能存在100%可靠的网络协议。那局部上呢?A给B发消息,B给A应答,最新的应答对B来说不确定A 是否收到了,但A 收到了应答可保证从A到 B方向上的可靠性。未来A给B了应答,A不确定B是否收到应答,但B收到应答可保证从B到A方向上的可靠性。最新消息之前的消息,可保证双方在两个方向上的可靠性。下面来说TCP真实的方案:客户端给服务器发个消息,服务器合适的时候要对消息做应答。因为是客户端在给服务器发消息,这个过程中最重要的是要保证发的消息被服务器收到了,客户端收到应答后就能保证消息被服务器收到了,不需要再服务器进行响应:

如果服务器给客户端发了消息,服务器是主动的一方,客户端做应答。服务器收到应答后立马意识到它发的消息对方收到了,因此从服务器到客户端的可靠性也可以保证:

所以发送的数据有收到的配套的应答,就可以保证两个朝向上对应的可靠性。一段时间如果没收到应答,客户端认为数据丢了。

下面谈个细节:现实中A说我们去吃饺子吧,B说吃什么饺子。B说的话既带了确认,又带了B想给A说的话。比如让服务器响应又应答是发了两个报文,这样效率很低。此时把应答和给对方发的消息二合一,这种策略叫捎带应答。以上是最基本的通信过程,现在客户端要给服务器发很多消息,那客户端给服务器发了第一条消息,服务器没应答时客户端能不能发第二条消息?从原始过程看收到应答后才能接着发,这样整个TCP通信效率非常低下。客户端此时要向服务器一次并行发一批消息,服务器原则上要对每个消息进行应答:

只要客户端收到了历史上发出去的报文的每个应答,照样可保证客户端向服务器方向上的可靠性。现在问题是,客户端按顺序把报文消息发出去,服务器是否一定按顺序接收呢?不一定,这种情况叫数据包乱序问题。造成乱序,本身就是不可靠的一种,怎么解决?所以每个TCP报头中包含了序号,发的时候给每个报文带上了序号,到了服务端后根据序号对收到的报文排序就可以保证可靠性:

因此,序号保证数据的按序到达。

下面谈一下序号是什么:

上层是用户层,下层是TCP发送缓冲区,可想成一个char outbufferN。用户层拷下来的任何数据在缓冲区里按顺序存在的,把缓冲区当成char类型数组的时候,天然每个字节都有自己的编号(本质是数组下标)。假设把一批数据发出去时,发一个数据块:

整个报文将来被封装的时候填的序号是发送的数据块的最后一个字符的下标。现在问题是,对方发来响应,发送方怎么知道哪个响应对应的谁的?所以TCP协议为了区分响应是对应哪个报文的响应,就有了一个确认序号的概念。确认序号,填充的是收到报文的序号+1:

那为什么这么规定?确认序号的意义是表示是确认序号之前的数据我已经全部收到了,下一次发送数据就从确认序号指定的数字开始指定长度发送。意思是发个1000,对应的响应是1001,客户端读到应答后能确定自己发的1000对方前面收到了,客户端再发送就从1001开始指定长度发送下一个报文。这样好处是应答时如果1001和2001丢了,但是3001还在,这样才能确定服务器把3001 之前的报文收到了,这样包容了应答有少量的丢失。有个问题,客户端给服务器发消息时带了序号 1000,应答时又没数据把序号改为1001应答回来就行,约定用一个序号就行,为啥报头中看到有两个序号?1.客户端给服务器发消息,服务器应答时同时也可能想给客户端发消息,所以服务器给客户端应答用确认序号,它本身也带数据需要客户端响应,此时用序号,所以分开不能复用。2.客户端给服务器发的同时服务器也可能发给客户端,所以序号还是分开。

TCP报头里还携带了6个标记位,作为服务器会收到多个客户端的请求:

TCP通信的时候,建立链接;通信完成后,TCP断开链接;断开和建立之间要进行正常数据通信。虽然不懂客户端和服务器怎么建立链接,但知道一定要发TCP报文,包括正常通信,断开链接要发 TCP报文。所以在通信之前,有些TCP请求是用来建连接的,有的是把数据交给服务器的,有的是断开链接的。注定了服务器收到的报文一定是有各种类型的,不同的类型决定了服务器要做不同动作。那接收方如何得知报头的类型各自是什么呢?所以TCP中存在了6个标记位,标志位存在的意义是区分TCP报头的类型。

下面聊的第一个标记位叫ACK,它的作用是确认序号是否有效。通信时只要三次握手成功了,大部分情况所有报文的ACK是默认被置1的。ACK就是用来确认整体的TCP报文是确认报文,有数据就是携带数据的确认报文(只要有应答属性ACK就要置1)。下面来看SYN:表示双方要通信,首先要建立链接,客户端会给服务器发各种TCP报文,服务器怎么知道哪些客户端想建立链接,哪些想通信?一个报文中标记位要是设置了SYN标记位,代表这个报文是想和服务器三次握手建立链接。下面看FIN:表示我想和对方断开链接。下面说说PSH标志位:TCP报头以及它所配套协议完全由操作系统自主决定,所以PSH、SYN等标记位不会直接暴露给用户让用户直接去修改比特位,会给我们提供系统调用来设置。PSH全称是PUSH,通信时应用层因一些原因导致上层一直不取缓冲区的数据,缓冲区数据会越来越多。下层由操作系统管理,上层读不读由用户决定。对于缓冲区来讲,操作系统一定要把收到的数据放缓冲区,一定要有人从上层把缓冲区的数据取走,这就是系统级的生产者消费者模型,因此所有流量控制本质是对发送过程同步的过程。同步了发送方和接收方的应用层,发送方一直写,接收方不拿,可能导致发送和接收缓冲区都满了,因此发送方应用层会有写阻塞。那接收方开始取数据后发送方怎么知道自己可以发了?1.发送方定期询问接收方。2.接收方缓冲区更新了,此时给发送方发个报文表示更新了。以上两种策略同时存在,哪种先协商成功就用哪一种。假设是第一种,然后接收方应用层就是不拿缓冲区里的数据,此时发送方会不断的问,对方依旧不拿走,此时发送方继续发TCP报文,并且把PSH标志位设置。接收方收到了携带PSH标志位的报文时,他的操作系统就明白要赶紧把缓冲区的数据给上层,PSH作用是催促对方尽快把缓冲区李的数据取走。下面来看 RST标记位:TCP是为了通信保证可靠性的,那是否意味着建立链接必须要成功?TCP可靠性是在建立链接成功通信时有很多保证可靠性的策略,建立连接失败是正常的。所以TCP虽然保证可靠性,但是TCP允许建立链接失败。客户端和服务器通信三次握手期间,三次握手全部完成时双方才认为把链接建立好了。所以作为服务端,操作系统内可能存在多个已被建立好的链接,直接证据是服务端会在应用层让用户获取多个连接所对应的文件描述符。每次三次握手成功要维护一个链接(以服务端为例),一个链接断开要把链接断开。通信时通信双方发送时,起始、确认序号是什么,累计发送或接收了多少数据,这些信息都要由双方操作系统TCP层维护的。服务端存在多个链接,服务端操作系统要对链接进行常规管理,先描述再组织。因此操作系统要为链接创建struct结构体,里面包含链接对应的属性,如链接的起始序号、确认序号等等。起始连接结构体再带上连接指针,对连接管理转为对链表管理:

客户端和服务器维护链接也是有成本的,如对象创建花空间、时间等。大量客户端进行连接的时候,可能出现连接建立异常的情况。如果客户端认为连接建立好了,服务器认为连接没建立好,此时三次握手完成了。因此客户端给服务器发消息,服务器应答时会携带RST,要求对连接重新建立。

下面来理解一下:TCP建立连接时要经历三次握手,第一次握手客户端要给服务器发送SYN报文,服务器收到SYN后给客户端发SYN+ACK,ACK表示的是对上一个报文的确认,SYN标记位表示服务器也同意和客户端建立链接。接下来客户端再给服务器发送ACK进行确认:

这三次称为三次握手,其中握手时是经历网络的。客户端到服务器和服务器到客户端的三次握手中,画的线是斜的,因为报文中间是要经过网络发送的,所以客户端发对应报文和服务端收到报文是有时间差的。那对于客户端来讲,三次握手时是把ACK发出去客户端认为建立好了链接还是服务器真正收到了报文客户端才认为链接建立好了?:

最后一个ACK没有应答,短期内客户端不可能知道ACK有没有被服务端收到,所以客户端认为链接建立好策略是只要他把第三个报文发出去了他就认为链接已经建立好了。TCP三次握手前两个报文有应答,第三个报文没有应答,所以TCP进行三次握手时本质是在赌,在赌第三个ACK一定能被服务端收到,如果收到了链接就建立好了。我们说了链接也要被管理,所以客户端在发出最后的时候创建链接的结构体准备通信。假设最后的ACK丢了,服务端认为链接并没有建立好。客户端和服务器在发送和接收最后一个报文时是有一些时间窗口的,所以这个时间窗口内客户端和服务器对于链接是否建立成功的认识不一致。客户端认为链接建立好了就可以开始向服务端发送数据,服务器收到这个报文就会感觉很奇怪,服务器就给客户端进行应答设置RST标志位。客户端收到报文设置了RST就把前面的链接结构体释放了,重新发起三次握手。因此RST标志位是在客户端和服务器认为链接不一致,即建立链接异常时双方连接重置使用的。

下面谈一下URG:我们称它为紧急指针标志位。我们说过TCP是按序到达的,但有些情况下就是想让有些数据进行插队,进行优先处理。在原始TCP语境之下,这种插队情况是绝对不可能存在的,因为TCP必须按序到达。若就想让上层高优先级处理某些特殊数据,此时就可以设置URG标志位。一旦URG被置为1,表示16位紧急指针有效:

16位紧急指针通常表示在当前TCP报文中包含紧急数据的哪个数据的偏移量。比如数据携带了1000,16位紧急指针填500,表示500那个位置包含的是紧急数据。这个数据是要被插队的数据, TCP中紧急数据默认一般只允许携带1个字节。那什么样的场景用URG?:

比如这是机房中的某个硬件服务器,上面载了某种服务,底层网络通信用的是TCP协议。现有客户端想向服务器发起请求完成某种任务,但用着时发现服务不响应了,但机器没有挂。我很好奇不知道怎么回事,所以服务端要支持读取紧急数据和给软件功能提供编号。这时服务器不响应客户端发个紧急数据,这个数据不用排队,服务器读到后把软件状态以紧急数据给客户端返回数字,客户端很快就知道服务器当前怎么了。

下面来说TCP策略问题(补充前面没有说过的),来说一下超时重传:

主机A在向主机B发送消息时,如果是数据本身丢失了,主机B不知道A给他发过数据,所以B也不会给A进行应答。所以A没有收到应答就不清楚自己发的报文是否被B收到,没有应答A也不清楚报文是丢了还是阻塞中。所以我们定出一个特定的时间间隔,在特定的时间间隔内没有对话的应答,主机A就判定数据丢了。此时进行重传,这种策略我们称为超时重传。当然主机A给主机B发消息时,可能数据没有丢,而是应答丢了,也会导致主机A没有收到确认应答:

规定只要特定时间没有收到数据,就立马做补发。通过上述可明白:主机对于发出去的报文是否丢失无法判定,必须通过规定表真是否重传。上面情况二,数据没丢应答丢了,A再次发数据,注定主机B会收到重复报文。为了保证可靠性,需要去重,那主机B如何去重呢?报文有序号,所以可对重复报文进行去重。那超时时间如何确定呢?如果我们网络情况特别好,超时的时间间隔一定不要设置的太长,如果出现小量丢包时要等待好长时间才能去重传,会影响传送效率。如果网络情况特别糟糕,也不能把等待的时间设置的太短,如果太短每发个数据都会超时,会不断进行报文补发。所以这个时间间隔需要是动态的,和网络状况相关。这的时间设置策略是这样:

默认超时时长 500 毫秒即0.5秒,如果判定超时了,需要重发后得不到应答,等待时间就变成了 2×500毫秒。不断增加,重传一定次数后还是得不到应答,TCP就认为网络或对端主机出现了问题,就把链接强制关了。

下面正式说说TCP连接管理机制:

TCP可靠性保证里有一个是TCP在通信前要建建立三次握手。首先客户端向服务端发送SYN,服务端收到SYN认为客户端想和我建立链接。客户端给服务务端推送SYN+ACK,客户端再给服务端发ACK叫做三次握手成功。对于客户端来讲,第三次握手只要发出,链接就建立好;服务端收到第三次的ACK后,认为链接建立好。三次握手成功之后,客户端和服务端双方连接已经建立好了,该维护的连接数据结构就维护好了。往后进行数据通信,一方发数据,另一方ACK应答。三次握手的过程中有两个重要的函数,客户端向服务端发起链接调用connect向对方发起链接请求,其实这个建立的过程是客户端构建SYN报文发给服务端。connect函数只负责发起3次握手,至于三次握手的细节上层系统调用接口实则不过多的参与,握手过程是双方操作系统自主完成的。connect发出SYN之后connect此时就处于阻塞状态,等待三次握手完成后connect返回。accept函数本身不参与三次握手,只会把已经建立好的链接拿上来,底层没有建好的链接accept就会一直阻塞。进行三次握手时双方操作系统对应的连接状态会发生变化,发起三次握手发送方处于SYN_SENT,这个状态我们称为同步发送。服务端收到了SYN,它的状态称为SYN_RCVD,表示同步收到。客户端收到第二个报文后立马发ACK,发完后它的状态变为ESTABLISHED,表示本地链接已经建立。服务端收到最后ACK后,状态变为ESTABLISHED。通信完成后要断开链接,应用层对应的是上层调用close。客户端关闭链接此时向服务端发FIN标记位,因为保证可靠性所以服务端对FIN做ACK应答。服务端到客户端也是一样,这就是四次挥手。TCP通信是基于链接的,建立和断开链接采用三次握手和四次挥手。为什么要三次握手和四次挥手?实际上是客户端发一个SYN, 服务端要ACK,完了后服务端再发SYN,客户端回ACK。以上是正常的,三次是中间两次报文捎带应答了。四次挥手中ACK和FIN也可捎带应答,压缩成三次挥手。从普通角度来看三次握手和四次挥手本质是一来一回的一种可靠性,少了任何一个报文可靠性就无法保证了,它们其实都是4次,只不过被捎带应答了所以变成了一个三次一个四次。

再说个理由:

三次握手能够保证无论是客户端还是服务端,在进行双方通信之前,双方都至少做过一次发,至少做过一次收。这叫做验证通路是否通畅,也就是验证全双工通路是否通畅(双方能发能收)。若是单次握手:

客户端发大量SYN服务端就要把链接建立好,这样服务端资源会被消耗完,这过程称为SYN洪水,显然不可以。若是两次握手:

服务端第二次发出连接就建立好,客户端收到连接也建立好。这过程中服务端先建立好链接,客户端才建立链接。万一客户端发完SYN后崩了,因为ACK不用应答所以服务端仍维持链接一段时间。直到发在客户端长时间不访问这个链接才会关闭。若大量客户端出异常了,服务端上要长时间挂大量异常链接。若是3次握手:

就算第三次ACK丢了,但对客户端讲连接建立成功了,服务端认为连接没建立成功。这里连接失败的成本嫁接到了客户端上,这样可保这服务器本身的稳定性。为啥三次握手上述思路总结:

下面说说为啥4次挥手?断开链接是双方的事情,客户端给服务端说要断开是可以的,但服务端也必须给客户端说要断开链接。断开链接本质是断开链接方没有数据给对方发送了,发送数据时双方都可能发,所以必须断开两次。客户端发一个FIN再收一个ACK,是保证从客户端到服务端没数据发了。同样再两次,保证从服务端到客户端也没数据发了。协商后双方都不再互发消息了,所以连接没有存在的必要了。所以四次挥手本质是可靠的保证双方都能得知道对方不想发消息的意愿,所以需要四次挥手。主动断开链接的一方先发FIN,状态是FIN_WAITE_1,服务端收到后状态变为CLOSE_WAIT。再次进行ACK后客户端变成FIN_WAIT_2,这种状态下客户端不再给服务端发送数据了(但可发管理报文如ACK)。服务端如果也发FIN,状态变为LAST_ACK。客户端收到对方想断开链接后状态变为TIME_WAIT,ACK后服务端状态是CLOSED。客户端状态进入TIME_WAIT, 表示客户端要等待一段时间。

下面说说TCP关于状态的相关介绍,这是我们之前写过的TCP代码:

main.cc这写了一个服务器,然后初始化和启动。用TcpServer.hpp中注释一些内容:

我们重点是研究TCP三次握手和四次挥手的过程,我们最多研究到获取到链接这一层就行。目前对应的服务器在start函数这里相当于什么都没有做,连连接都没有获取,前面只是有创建套接字,绑定和监听。TcpClient.cc就创建套接字,连接,失败后还会做超时重传。继续看:

中间服务器,右边是客户端,启动服务后通过netstat查到了服务端。因此tcp服务没调任何accept,只要套接字处于Listen状态就可以通过netstat命令查到。这份代码没有进行accept,把backlog参数设为1了:

客户端连服务器,查的时候看到服务器所在地方上(阿里云)存在一个远端的47.93.176.120:39414的 ESTABLISHED连接:

虽然服务器上没有accept,但客户端和服务端已经建好链接了。云服务器无法绑定自己的ip地址,Local Address没填47.93.176.120公网IP,是因为其实那个IP是被模拟出来的,显示的才是真的公网。意味着站在阿里云机器上,即服务器角度已经有这个客户端来连我了。连接是双向的:

客户端上netstat -ntp,看到客户端上存在一个远端的47.93.176.120:8888的ESTABLISHED连接。所以连接建立成功这件事和上层没有accept是没有关系的,侧面验证了三次握手是双方操作系统自动完成的。再来一个客户端去连服务器:

左下查的时候看到他们连的是同一个服务器,本地采用随机端口,端口号不同就行了,连接状态是 ESTABLISHED。再增加一个服务器连接:

发现第三次时客户端认为连接建立好了,服务器认为连接没有建立好。继续这样可以发现从第三次往后,除非上一次有连接退出了,否则服务器认为连接是SYN_RECV的。因为listen第二个参数前面设置为了1,所以底层能建立的链接数是2。下面说说:

一个客户端向服务端建立连接时,如果三次握手可以完成,服务端在底层建立好连接,这个链接可暂时不把它拿上去,第二个客户端来后完成三次握手服务器底层也要建立连接。在这样来后,服务端上会存在大量已经建立好的链接,但这些被建立好的链接并没有被上层拿上去,因此操作系统对建立好的链接要有临时的维护,先描述后组织。这里采用队列形式管理已经建立好的链接,accept 就是从队列里把连接取走和特定文件关联。Listen第2个参数1表示底层已经建立好的链接队列的最大长度,这个队列也称为全链接队列。前面来第三个链接时不让入队列了,所以服务端状态是SYN_RECV。上面第三次连接时不能让连接进入到全链接队列,即使3次握手成功也不能放(上层没accept)。从一个状态变成下一个状态必须满足下一个状态的条件,这里客户端一定发了ACK, 因为客户端建立连接成功了。只是因为listen第二个参数的限制,此时全队列已经被打满了,而这里状态没变是由于服务端把最后ACK丢弃了,因为服务端判定全链接队列已经满了,就不允许再建立链接成功了。前两次允许握手,客户端发ACK时发现全连接队列是满的,于是服务端把ACK扔了,服务端3次握手不完整就无法从一个状态变迁到下一个状态,所以是SYN_RECV。过了一段时间:

再查时发现这个SYN_RECV链接从服务端查不到了,说明服务端把它释放了,而客户端认为连接还是成功的。因此服务端不会长时间维护SYN_RECV,被建立链接的一方处于SYN_RECV时,由于握了前两次,这种链接称为半链接。半链接也是要维护队列的,称为半链接队列,半链接的节点不会长时间维护。当用最后一次连接的客户端发消息,服务端上看到又有了SYN_RECV,这里底层又3次握手(因为实际没完成3次握手,重建链接)。上面反映客户端和服务器链接不一致的问题,了解以上后来看两个问题:1.listen 第二个参数为什么不能太长?2.为什么不能没有?1.如果全连接队列太长,可能会导致服务器上有些链接来不及被上层处理,依旧要在系统内长时间维持。服务器此时非常忙来不及消化全链接,维护了长链接非常占资源而且没创造什么价值,所以不能太长。2.(饭店门口凳子的例子)服务器上处理完客户端后闲下来了,就可以有能力再从全链接队列里获取一个链接,否则就要等着客户端发起链接,accept处于阻塞,服务器上资源没被充分利用(如上三次握手状态变化谈完了)。

下面谈一下4次挥手:

先断开链接的一方是FIN_WAIT_1,服务端还没关链接进入CLOSE_WAIT。建立链接后让客户端退出:

再查时看到客户端那什么都没了,服务端处于CLOSE_WAIT状态:

虽然客户端进程退出了,文件描述符也就关了,相当于断开链接给服务端发FIN, 服务端也给对方ACK了。但服务端并没有CLOSE,所以服务端无法从CLOSE_WAIT变迁到LAST_ACK。服务端把节点拿上去:

5秒后把文件描述符关了,服务端先要断开连接。日志信息显示到文件:

再连一下:

查时看到8899建立了,close后连接状态是FIN_WAIT_2(服务端发FIN,客户端回ACK,服务端此时状态)。现在把客户端给退出了:

此时服务端状态是TIME_WAIT, 客户端此时查就没了。根据上述来看(前提是把链接从底层获取上来,否则直接断开看不到状态现象,链接直接释放了)服务端链接关了,FIN后客户端ACK ,服务端进入FIN_WAIT_2。客户端ctrl+c发FIN,服务端再ACK,收到ACK后客户端断开链接,但主动断开链接的一方此时处于TIME_WAIT状态。过了一段时间再看8899的TIME_ WAIT没有了:

上面发现主动断开链接的一方,在4次挥手完成后,要进入TIME_ WAIT状态等待若干时长,之后会自动释放。

下面基于现象聊一下:最后服务端关了,再重新启动:

发现出现绑定失败。正常通信时主动断开链接的一方如果是服务方,服务方就要进入TIME_ WAIT状态,意思是这时链接没有彻底被断开。这样意味着ip和port正在被使用,端口号无法被两个不同进程同时绑定,所以再次启动时无法绑定。这样问题是当有很多客户端和服务端处于正常链接:

但最后一个新链接来的时候导致服务端挂了,这样服务端变成了主动断开链接的一方,此时服务端中一定会充满大量的TIME_WAIT状态,这样服务端无法立即重启在一些场景下会很大损失。如何解决?man setsockopt:

设置套节字属性,设置成允许我们地址复用。参数是文件描述符,层级一般设置到sock这一层。下面用一下:

以后可不断启动。如果ip和端口被使用是因为TIME_WAIT状态,这样就可以立即启动(别的状态不行,客户端没这样问题是因为客户端用的随机端口)。下面说说TIME_WAIT等多长时间,为什么要等以及等这么长时间?TCP规定主动断开链接一方处于TIME_WAIT,一旦TIME_WAIT要等待两个MSL才能进入CLOSED状态。一个报文从客户端到服务端来讲,一个报文在网络中是有存活时间的,一个报文在网络中存活的最大时长叫MSL(从一端到另一端)。等待是因为这个时间相当于一个报文一来一回,这样可保证客户端和服务端上双方历史发了还未收到的报文可能在这个时间被彼此收到。还有个原因是可以保证四次挥手以较大的概率正确挥手完,比如最后ACK丢了可补发FIN, 此时还有回应。一个报文在网络存活时长规定为两分钟,可能遇到阻塞等,存在时长和传输时长两个状态注意区分。ubanto中看见是60秒:

相关推荐
m24tyy2 小时前
常见空间测绘搜索引擎介绍和使用以及子域名的收集
网络·测试工具·搜索引擎
数据安全技术观察2 小时前
数据动态脱敏:让脱敏策略跟着数据目录走
大数据·网络·数据库
dogstarhuang5 小时前
OpenAI GPT-5.6 降价后如何重算 API 账单?多模型路由与成本治理实战
服务器·网络·人工智能·大模型·api·ai应用开发·接口管理
苏宸啊5 小时前
linux网络编程udp服务器和客户端代码编写(echo版本和字典版本)
linux·网络
奇树谦7 小时前
HDD 为什么适合顺序大文件读写:从 RAID 5 到 RAID 50 的原理与性能分析
linux·运维·网络
嵌入式阿蔡8 小时前
Linux_01:交叉编译与开发环境实战——从单片机跨到 Linux 的第一道坎
linux·运维·网络·单片机·嵌入式硬件·嵌入式实时数据库
雷电小将军9 小时前
诡秘之主手游占卜家序列加点 诡秘之主手游占卜家怎么加点
服务器·网络·游戏·电脑·玩游戏
陈皮波比茶9 小时前
Python项目实战-网络机器人(爬虫)
开发语言·网络·爬虫·python·机器人
白猫不黑9 小时前
网络安全/信息安全专业学习路线与入行指南:从大学到岗位的完整规划
网络·学习·安全·web安全·网络安全·黑客技术