当前位置: 首页 > news >正文

【Linux】网络基础之TCP协议

目录

  • 🌈前言
  • 🌸1、基本概念
  • 🌺2、TCP协议报文结构
    • 🍨2.1、源端口号和目的端口号
    • 🍩2.2、4位首部长度
    • 🍪2.3、32位序号和确认序号(重点)
    • 🍫2.4、16位窗口大小
    • 🍬2.5、常见的6位标记位
      • ✨2.5.1、SYN和FIN标记位(三次握手和四次挥手)
      • ✱2.5.2、ACK标记位(确认标记位)
      • ✴2.5.3、PSH标记位(数据推送标记位)
      • ✵2.5.4、URG标记位(紧急指针标记位)和16位紧急指针
    • 🍺2.6、三次握手和RST标记位
      • ✶2.5.5、RST标记位(复位标记位)
  • 🌺3、TCP机制
    • 🍨3.1、确认应答机制
    • 🍩3.2、超时重传机制
    • 🍪3.3、连接管理机制
      • ✨3.3.1、四次挥手
      • ✨3.3.2、连接管理机制状态的变化
      • ✨3.3.3、CLOSE_WAIT状态
      • ✨3.3.3、TIME_WAIT状态

🌈前言

这篇文章给大家带来传输层中TCP协议学习!!!


🌸1、基本概念

[TCP协议 – 百度百科]

  • TCP是隶属于传输层的协议,它的主要功能是实现是让应用程序之间可以相互通信

  • TCP全称为 “传输控制协议(Transmission Control Protocol”)主要对数据的传输进行一个详细的控制

  • TCP协议是一个可靠的(确认应答机制、超时重传等等)面向连接的(通信前先建立连接)基于字节流(流式IO)进行网络数据传输的网络通信协议

  • TCP在传输过程中可以正确的处理丢包、数据包乱序的异常状况;还能有效的利用宽带,缓解网络拥堵

在这里插入图片描述


🌺2、TCP协议报文结构

TCP协议是如何封包的呢?

  • 封包的本质是:将TCP报头对象拷贝到应用层协议报文的前面即可,这样就完成了封包了

  • 后续添加新的报头时,只要将缓冲区的指针移动到TCP头部然后加上新报头长度的大小,最后填充就行了

在这里插入图片描述

TCP是如何解包的呢?

  • 解包的本质是:将TCP报头和TCP选项在数据包中去除即可

  • TCP报头中有一个"4位首部长度"字段,它标识了这个报头的长度(报头 + 选项),只要拿到首部长度然后根据首部长度去掉选项,剩下的就是有效载荷了

  • 有效载荷包含了上层应用所需传输的数据,比如HTTP请求或响应内容

TCP协议是如何进行分用的呢?

  • TCP报头的前32位属性字段名是跟UDP一样,都是源端口号和目的端口号

  • 分用的本质是:通过TCP报头里面的目的端口号字段找到应用层的具体协议,并且传递给上层应用程序进行处理

  • 注意:这里没有说IP地址,因为IP地址是用来找网络中唯一主机的,现在已经找到了,通过目的端口号就能标定主机中唯一的进程了


下图为TCP报文的组成结构,里面包含了不同的属性字段:

在这里插入图片描述

🍨2.1、源端口号和目的端口号

  • 源/目的端口号: 表示数据是从哪个进程来,到哪个进程去

  • 16位源端口号:标识发送端主机上进行网络通信的某个进程(具有唯一性)

  • 16位目的端口号:标识接收端主机上进行网络通信的某个进程(具有唯一性)


🍩2.2、4位首部长度

概念

  • TCP报头的标准长度是20个字节(不包括选项字段长度

  • 那么我们如何确定选项的大小呢? 答案就是:4位首部长度

  • 4位首部长度:表示该TCP头部有多少个32位bit(4字节);4位的的取值范围是[0, 15](0000 - 1111)可以得出TCP报头最多有15 * 4 = 60个字节的数据

  • TCP报头标准长度为20字节,所以4位首部长度的值至少为5(0101)

  • 我们解包时,就是通过四位首部长度来提取选项字段的!!!

在这里插入图片描述


🍪2.3、32位序号和确认序号(重点)

TCP可靠性问题

  • 什么是不可靠:网络数据传输过程中导致丢包、数据包乱序、校验失败等问题

  • 什么是可靠性:网络传输过程中不会出现丢包、乱序、校验失败等异常问题,确保数据包完整的到达对端

  • 那么TCP是如何解决丢包问题的呢? 怎么确定一个报文是丢了还是没丢呢?

确认应答机制

  • TCP中的32位序号和32位确认序号就是为了防止丢包准备的

  • 确认应答机制:数据在网络传输过程中没有发生丢包,完整的被对端收到,并且得到对端的应答

  • 确认应答机制不能保证双方的应答,因为这是不可能的,永远只有一条消息是没有应答的,只能保证单向的应答

在这里插入图片描述

32位序号和确认序号

首先建立一个共识:TCP进行通信时,发出去的报文一定携带TCP报头,哪怕不携带数据

  • TCP是如何实现确认应答机制的呢? 答案是通过序号和确认序号实现的

  • 实现原理:发送端给对方发送消息时会携带序号字段,接收端收到后回复消息会更新确认序号,回复给发送端后,发送端根据自己的序号和对方的确认序号可以判断数据是否应答(发送端到接收端的数据)

  • 通俗的话说就是:网络传输时,发送端发送的数据,根据自身的序号和对方回复的确认序号可以确保单端的数据是否应答

  • 比如:发送端要发送100个字节数据,携带的序号是1,接收端收到数据后,会更新自己的确认序号为101,回复给发送端后,发送端看到确认序号比自己的序号大于100,表示没有丢包

在这里插入图片描述

在这里插入图片描述

为什么TCP报文中需要二个不同的序号来完成确认应答机制呢?⭐⭐⭐⭐⭐

  • 按照上面的说法,一个也能实现确认应答机制,为什么出现了二个不同的序号

  • 我们都知道TCP在通信时是全双工的,意味着双方都能够进行发消息和收消息

假设如下图:

  • 如果发送端发送消息,接收端应答消息并且发送新的消息呢???

在这里插入图片描述

总结:

  • 只有单个序号只能保证一端到另外一端的数据应答

  • 如果对端应答并且携带了新的消息,那么就不能保证对端到发送端的应答了

  • 可以得出:发送端序号和对端确认序号可以保证发送端到对端数据的应答对端序号和发送端确认序号可以保证对端到发送端的应答

  • 对端给发送端发送消息,也要更新响应报文的序号,发送端收到报文更新确认序号然后回复给对端,对端比较自己的序号和发送端序号是否符合就能判断是否应答


🍫2.4、16位窗口大小

发送缓冲区和接收缓冲区

首先建立一个共识:这里的TCP协议发送缓冲区和接收缓冲区都是在内核中定义的

  • TCP需要保证报文的可靠性,需要发送缓冲区做各种可靠性的策略,而UDP不需要保证数据完整到达,所以没有发送缓冲区

  • 缓冲区本质是一段连续的内存,它可以集中的处理数据刷新减少I/O次数,从而达到提高整机的效率~!

  • 发送缓冲区:当我们在应用层调用write、send系统函数向套接字写入数据时,进程会从用户态变为内核态,并且会将数据拷贝到内核的发送缓冲区中(用户的数据拷贝到内核中)

  • 接收缓冲区:当我们在应用层调用read、recv系统函数向套接字接收数据时,进程会从用户态变为内核态,并且将内核中接收缓冲区的数据拷贝到用户所设置的buffer中存储着(内核数据拷贝到用户中)

  • 数据被拷贝到内核的缓冲区中,就归OS管了(OS实现了传输层和网络层),用户不会再过问,什么时候传输是根据指定传输层协议来确定的

  • 注意:write、send、read、recv都是面向字节流的,他们本身是不携带缓冲区的函数,但是它们向指定的套接字写入或接收数据,函数会根据传输层不同的协议放到缓冲区或从缓冲区中读取数据

在这里插入图片描述


16位窗口大小

  • 缓冲区是有大小的,如果发送的数据太快或数据太大超过缓冲区大小,都会导致数据被丢弃(丢包),那么我们就要有个字段来获取对端的缓冲区接收能力(剩余空间大小)

  • 16位窗口大小:用于填充缓冲区剩余空间大小的属性字段;可以让发送端智能的根据对端的接收能力动态的调整发送的速度或数据的大小

  • 如果服务器给客户端发报文,那么只能由服务器填充自己的窗口大小,对方就知道自己的缓冲区接收能力了

  • 流量控制:不管是服务器给客户端发信息还是客户端给服务器发信息,只要有窗口大小存在,就能解决发送数据太快或太大,导致对端缓冲区已经接收不了数据还一直发的问题!!!


🍬2.5、常见的6位标记位

在这里插入图片描述
概念

  • 我们都知道TCP协议在通信前要建立连接(三次握手)后才能正常通信,通信完后要进行断开连接(四次挥手)

  • 服务器每次要处理那么多报文(连接报文、通信报文、断开连接报文等),需要对报文进行类别的(根据不同类型的报文用不同的逻辑处理它)

  • 6位标记位:就是来标记报文类型的!!!


✨2.5.1、SYN和FIN标记位(三次握手和四次挥手)

概念

  • SYN:只要是建立连接请求的报文SYN就要被设置为1携带SYN标记位的报文称为同步报文段(连接请求报文)

  • 只要是断开连接请求的报文FIN就要被设置为1携带FIN标记位的报文称为结束报文段(断开连接请求报文)

  • SYN和FIN标记位不可能被同时设置为1,因为不可能同时进行连接和断开连接


✱2.5.2、ACK标记位(确认标记位)

概念

  • ACK:确认标记位,表示该报文是对历史报文的确认(根据确认序号来进行确认),表示发送端发送的报文已经被对端收到,应答报文携带ACK标记位

  • 历史报文:发送端发送数据给对端,对端应答发送端的报文就是”历史发送的报文“

  • 一般在大部分正式通信情况下,ACK都是1


✴2.5.3、PSH标记位(数据推送标记位)

概念

  • PSH:提示接收端应用程序立刻从缓冲区把数据读走

  • 应用层中的write、recv系统函数,在读取内核缓冲区时会自动判断是否存在数据,如果没有数据会一直阻塞,反之读取数据到用户设置的缓冲区中

  • TCP缓冲区中有一个接收数据的低水位线(比如有100字节,低水位线为20字节),只要传输的数据超过20字节,就会被上层给读取

假设

假设应用层一直非常的忙,没有时间读取TCP接收缓冲区里面的数据

  • 如果TCP接收缓冲区满了,并且应答了一个窗口大小为0的报文,那么发送端只能等待对端读取完数据才能发送

  • 如果等待了很久对端还是没有读取数据,那么发送端可以发送一个带有PSH标记位的报文给对端催促对端应用层赶紧把缓冲区数据读取完

在这里插入图片描述


✵2.5.4、URG标记位(紧急指针标记位)和16位紧急指针

前言

  • 报文在传输的过程中,是可能乱序到达的,它是不可靠其中的一种行为,TCP必须让我们发的报文按序到达

  • 如果数据必须在TCP中进行按序到达,那么如果有一部分TCP报文优先级更高(PSH报文),但是序号比较晚,就无法做到报文被优先紧急处理

  • TCP是根据16位序号来实现按序达到的,因为序号可以确定每个报文中数据发送了多少字节(比如第一个报文发送了100字节,那么第二个报文序号就是101)

概念

  • URG:只要是发送紧急数据,就要把URG标记位置1URG标志设置为1时,TCP首部紧急指针字段才有效,默认为0时,紧急指针无效

  • 紧急指针:该字段中保存着一个正的偏移量,通过这个偏移量和序号相加,可以找到数据(有效载荷)中的紧急数据

  • 紧急报文发送给对端是不会经过缓冲区保存按序等待读取的,会直接交付上层读取紧急数据只有一个字节,其余数据要进入接收缓冲区

在这里插入图片描述


🍺2.6、三次握手和RST标记位

概念

Server是服务器,Client是客户端

  • TCP是面向连接的协议,通信前需要建立连接(三次握手)

  • 原理:Client向Server发送连接请求报文(携带SYN标记位),Server收到报文后应答连接请求报文(携带SYN+ACK标记位),Client收到报文后创建连接对象并且发送应答报文(携带ACK),Server收到应答报文也创建连接对象,到此,就完成了三次握手

  • 只要最后一次握手Server端收到Client的应答报文,Server就会创建连接对象,表明建立连接成功了!!!

注意

  • TCP是保证数据完整的被对端收到,但是三次握手不一定会成功

  • 第三次握手时Client发送的应答报文可能会丢包,没有被Server收到,Client已经处于连接成功状态,但是Server还未完成连接


原理

  • Client与Server建立好连接后,Server需要对连接进行管理,因为如果来了成千上万个连接,Server就会分不清谁是谁了

  • 管理的本质就是先描述(连接的属性结构体),再组织(高效数据结构进行增删查改)

  • 可以得出双方建立连接,需要花费时间和内存的,特别是Server还要管理连接对象,而Client只需创建好连接对象就行了

在这里插入图片描述

为什么要进行三次握手呢?一次二次不行吗?

一次握手

  • 一次握手:一次握手是完全不行的,因为极其容易受到服务器攻击(SYN泛洪攻击),因为一次握手只要Client给我Server发送一个连接请求报文就能完成连接的建立了

  • 但是如果Client发送完连接请求报文后就不管了,也不创建连接对象,但是Server会创建连接对象并且管理起来

  • 如果Client循环式的发送SYN报文,那么Server的内存就会一下子被填满了,最后发生崩溃(创建连接对象要消耗内存)


二次握手

  • 二次握手:跟一次握手一样不行,因为二次握手是由Server端最后应答连接报文给Client(意味着要先创建连接对象

  • 如果Client无视Server发送的应答连接报文或者直接丢弃,那么Server端维护的连接对象也就没有意义了,白白浪费资源

  • 如果Client还是循环的发送SYN报文,那么服务器内存也会被一下子填满,导致崩溃


三次握手

  • 前面的一、二次握手都不行,是因为Server端先认为自己建立连接成功(创建连接对象并且管理),只要Client不建立连接或忽略应答连接报文,那么Server就浪费资源了

  • 三次握手:第三次握手由Server来结束握手,意味只要Server收到Client的应答报文,Server才会认为自己连接成功,但是Client在之前就认为自己建立连接成功了

  • 好处就是Client如果想对Server进行攻击,循环发送连接报文,那么Client会先建立连接,而Server最后建立连接,双方都消耗了OS的内存资源

  • 三次握手在Server收到ACK报文之前,都维持着一个半连接的方式只有Client认为自己连接成功),只要Server收到ACK报文,那么就完成了一个完整的连接


✶2.5.5、RST标记位(复位标记位)

概念

  • RST:对方要求重新建立连接;携带RST标识的称为复位报文段

  • 作用:发送带有RST标记位的报文,表示叫对方关闭连接,并且进行重新建立连接

  • 用途:双方在建立连接失败或出现严重丢包等等时,就会给对端发送带有RST标记位的报文,表明要求重新建立连接或连接复位

假设

  • 如果Client在第三次握手中发送的ACK报文没有被Server收到,但是Client认为自己建立连接成功了,但是Server认为还未建立连接

  • Client认为自己建立连接成功,向Server发送数据报文,Server收到报文后,想着自己还没有建立连接成功,说明最后一次ACK报文丢包了

  • Server会给Client发送的数据报文进行应答,并且把应答报文中的RST标记位也置为1,表示要求Client断开当前连接,并且重新建立连接


🌺3、TCP机制

🍨3.1、确认应答机制

概念

  • 确认应答机制:主机A向主机B发送数据,主机B给主机A应答并且设置ACK标记位为1

  • ACK标记位设置为1的意思是对历史接收的报文的应答

在这里插入图片描述

序号问题

  • 缓冲区是一段线性的内存,发送端将发送的数据拷贝到缓冲区中,也就意味着可以像数组一样用下标来进行访问

  • TCP每次发送报文都会设置序号(数据从哪里开始发送,也就是从哪个下标开始发送),每一个ACK应答报文都有对应的确认序号(已经接收到了哪些数据,[序号, 确认序号]),发送端会根据对端应答报文中确认序号来判断下一次从哪里开始发送

  • 如下图所示:一开始序号为1(从第一个字节开始发送),对端收到了1k字节,应答报文中确认序号更新1001(收到了1k字节,1000+1就是后续发送端从1001下标继续发送的位置)。发送端看到应答报文中确认序号为1001,就知道从下标为1001的数据开始再次发送

在这里插入图片描述


🍩3.2、超时重传机制

概念

  • 主机A发生数据给主机B,在网络传输过程中可能因为网络拥堵等原因,数据无法到达主机B,说明数据丢包了

  • 如果主机A在特定的时间间隔内没有收到主机B的应答ACK报文,就会进行超时重发报文

在这里插入图片描述

如果主机B收到了报文,但是发送的应答ACK报文丢包了怎么办?

  • 因此得出主机B会收到很多重复数据,那么TCP协议需要能够识别出哪些包是重复的包,并且把重复的丢弃掉

  • 这时候我们可以利用前面提到的序列号(判断序号下标是否出现冗余),就可以很容易做到去重的效果


超时重传时间如何确定?

  • 最理想的情况下,找到一个最小的时间,保证 “确认应答报文一定能在这个时间内返回

  • 但是这个时间的长短,随着网络环境和状态的不同,是有差异

  • 如果超时时间设的太长,会影响整体的重传效率

  • 如果超时时间设的太短,有可能会频繁发送重复的包

解决方案

  • TCP为了保证无论在任何环境下都能比较高性能的通信,将动态计算这个最大超时时间

  • Linux中(BSD Unix和Windows也是如此),超时以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍

  • 如果重发一次之后,仍然得不到应答,等待 2*500ms 后再进行重传

  • 如果仍然得不到应答,等待 4*500ms 进行重传. 依次类推,以指数形式递增(2N * 500ms)

  • 累计到一定的重传次数,TCP认为网络或者对端主机出现异常,强制关闭连接


🍪3.3、连接管理机制

在正常情况下,TCP要经过三次握手建立连接,四次挥手断开连接

在这里插入图片描述


✨3.3.1、四次挥手

概念

注意图中的时间轴走向,图中Client先提出断开连接,却是在Server断开之后才断开的,反之Server先提出的也是一样的

  • TCP首先要进行三次握手建立连接,随后正常数据通信,双方断开连接时要进行四次挥手

  • 四次挥手:不管是Client还是Server,都要与对方断开连接(双方都要调用close),也就是Client要跟Server断开连接Server也要跟Client断开连接

  • 比如:我和女朋友分手了,是女方先提出的,我同意了. 但是我还没提出跟她分手,我还可以一直骚扰她(发消息),直到我给她提出分手,女方也同意后,双方才真正的分手了!!!

原理

CLOSED状态就是断开连接状态

  • Client向Server发送断开连接报文并且将状态设置为FIN_WAIT_1. Server收到报文,向Client发送应答报文并且将状态设置为CLOSE_WAIT. Client收到报文将状态由FIN_WAIT_1设置为FIN_WAIT_2

  • Server向Client发送FIN报文,状态从CLOSE_WAIT变成LAST_ACK. Client收到报文并且应答,从FIN_WAIT_2状态变成TIME_WAIT(等待一段时间变成CLOSED状态),Server收到应答后,由LAST_ACK状态变成CLOSED


✨3.3.2、连接管理机制状态的变化

在这里插入图片描述

服务端状态的变化(三次握手)

  • [CLOSED, LISTEN]:服务器端调用listen系统函数后进入LISTEN状态(监听Client连接请求状态),等待客户端连接

  • [LISTEN, SYN_RCVD]:一旦监听到Client连接请求(同步报文段 – SYN),就将该连接放入内核等待队列中,并向客户端发送SYN确认报文

  • [SYN_RCVD, ESTABUSHED]:服务端收到客户端的确认报文,就进入ESTABLISHED状态,可以进行读写数据

服务端状态的变化(四次挥手)

  • [ESTABLISHED, CLOSE_WAIT]:当客户端主动关闭连接(调用close),服务器会收到结束报文段(FIN),服务器返回确认报文段并进入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)

  • [SYN_SENT, ESTABLISHED]:connect调用成功,则进入ESTABLISHED状态, 开始正常读写数据

客户端状态的变化(四次挥手)

  • [ESTABLISHEDFIN, WAIT_1]:客户端主动调用close时,向服务器发送结束报文段,同时进入FIN_WAIT_1状态

  • [FIN_WAIT_1, FIN_WAIT_2]:客户端收到服务器对结束报文段的确认ACK报文,则进入FIN_WAIT_2,开始等待服务器的结束报文段(FIN)

  • [FIN_WAIT_2, TIME_WAIT]:客户端收到服务器发来的结束报文段,进入TIME_WAIT状态,并发出LAST_ACK(最后的确认应答报文)

  • [TIME_WAIT, CLOSED]:客户端要等待一个2MSL(Max Segment Life,报文最大生存时间)的时间,才会进入CLOSED状态


✨3.3.3、CLOSE_WAIT状态

概念

  • CLOSE_WAIT状态:先发出断开连接报文(FIN)的一端先调用了close,对端收到了FIN报文,但是自己没有真正的调用close,所以会一直处于CLOSE_WAIT状态,但会给先断开的一端进行ACK应答,让他进入FIN_WAIT_2状态

  • 只要调用了close函数,关闭套接字,就会进入下一个LAST_ACK状态

使用Server基本通信程序 和 telnet指令测试

#include <iostream>
#include <string>
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <cassert>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>using std::cout;
using std::endl;class TcpServer
{
public:TcpServer(uint16_t port, std::string ip = ""): _listensockfd(-1), _ip(ip), _port(port){}~TcpServer(){if (_listensockfd > 2)close(_listensockfd);}void InitInetData(){assert(_port > 1025);_listensockfd = socket(AF_INET, SOCK_STREAM, 0);if (_listensockfd < 0)exit(1);sockaddr_in serverData;socklen_t len = sizeof(serverData);memset(&serverData, 0, len);serverData.sin_family = PF_INET;serverData.sin_port = htons(_port);serverData.sin_addr.s_addr = _ip.empty() ? (INADDR_ANY) : inet_addr(_ip.c_str());if (bind(_listensockfd, (const sockaddr *)&serverData, len) < 0)exit(3);if (listen(_listensockfd, 2) < 0)exit(4);}void StartServer(){while (true){int ServerSockfd = accept(_listensockfd, nullptr, nullptr);if (ServerSockfd < 0)exit(5);}}private:int _listensockfd;uint16_t _port;std::string _ip;
};int main(int argc, char *argv[])
{if (argc < 2){std::cout << "Format: ./可执行程序 [ip] port" << std::endl;exit(8);}std::string ip;uint16_t port;if (argc == 3){ip = argv[1];port = std::stoi(argv[2]);}else{port = std::stoi(argv[1]);}TcpServer tps(port, ip);tps.InitInetData();tps.StartServer();return 0;
}

测试

  • Server:./test 8080
  • telnet:telnet 127.0.0.1 8080

在这里插入图片描述

总结

  • 对于服务器上出现大量的 CLOSE_WAIT 状态, 原因就是服务器没有正确的关闭 socket套接字, 导致四次挥手没有正确完成

  • 这是一个 BUG. 只需要加上对应的 close函数 即可解决问题


✨3.3.3、TIME_WAIT状态

模拟TIME_WAIT状态,Client先关闭,随后关闭Server,代码不变还是上面的

在这里插入图片描述

模拟TIME_WAIT状态,Server先关闭,随后关闭Client,代码不变还是上面的

在这里插入图片描述


大家应该都遇到过先关闭服务器,然后关闭客户端,服务器再重启启动就会bind出错,这是为什么呢???

  • 现在做一个测试,首先启动server,然后启动client,然后用Ctrl-C使server终止,然后用Ctrl-C使Client终止,这时马上重新运行server

  • 运行结果是:bind函数出错

#include <iostream>
#include <string>
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <cassert>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>using std::cout;
using std::endl;class TcpServer
{
public:TcpServer(uint16_t port, std::string ip = ""): _listensockfd(-1), _ip(ip), _port(port){}~TcpServer(){if (_listensockfd > 2)close(_listensockfd);}void InitInetData(){assert(_port > 1025);_listensockfd = socket(AF_INET, SOCK_STREAM, 0);if (_listensockfd < 0)exit(1);sockaddr_in serverData;socklen_t len = sizeof(serverData);memset(&serverData, 0, len);serverData.sin_family = PF_INET;serverData.sin_port = htons(_port);serverData.sin_addr.s_addr = _ip.empty() ? (INADDR_ANY) : inet_addr(_ip.c_str());if (bind(_listensockfd, (const sockaddr *)&serverData, len) < 0){std::cout << "bind error: " << strerror(errno) << std::endl;exit(3);}if (listen(_listensockfd, 2) < 0)exit(4);}void StartServer(){while (true){int ServerSockfd = accept(_listensockfd, nullptr, nullptr);if (ServerSockfd < 0)exit(5);}}private:int _listensockfd;uint16_t _port;std::string _ip;
};int main(int argc, char *argv[])
{if (argc < 2){std::cout << "Format: ./可执行程序 [ip] port" << std::endl;exit(8);}std::string ip;uint16_t port;if (argc == 3){ip = argv[1];port = std::stoi(argv[2]);}else{port = std::stoi(argv[1]);}TcpServer tps(port, ip);tps.InitInetData();tps.StartServer();return 0;
}

在这里插入图片描述

原理解析

  • 虽然server的应用程序终止了,但TCP协议层的连接并没有完全断开,因此不能再次监听同样的server端口

  • 上图运行结果中使用netstat指令可以看到Server还处于TIME_WAIT未关闭连接的状态

  • TCP协议规定,主动关闭连接的一方要处于TIME_ WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态

  • MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同,在Centos7上默认配置的值是60s

  • 可以通过 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看MSL的值

[root@Linux_Study]$ cat /proc/sys/net/ipv4/tcp_fin_timeout
60

为什么是TIME_WAIT的时间是2MSL?

  • 服务器需要处理非常大量的客户端的连接,每个连接的生存时间可能很短,但是每秒都有很大数量的客户端来请求

  • 这个时候如果由服务器端主动关闭连接(比如某些客户端不活跃, 就需要被服务器端主动清理掉),就会产生大量TIME_WAIT连接

  • 由于我们的请求量很大,就可能导致TIME_WAIT的连接数很多,每个连接都会占用一个通信五元组(源ip、源端口、目的ip、目的端口、协议)

  • 其中服务器的ip和端口和协议是固定的,如果新来的客户端连接的ip和端口号和TIME_WAIT占用的链接重复了,就会出现问题


解决方案

  • 使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符
    在这里插入图片描述
#include <sys/types.h>
#include <sys/socket.h>int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
  • sockfd:套接字的文件描述符(socket的返回值)

  • level:选项定义的层次;支持SOL_SOCKET、IPPROTO_TCP、IPPROTO_IP和IPPROTO_IPV6

  • optname:需要设置的选项,选项有SO_REUSEADDRSO_REUSEPORT,一般设置第一个即可

  • optval:指针,指向存放选项待设置的新值的缓冲区

  • optlen:optval缓冲区长度

  • 作用:主要用来禁止OS的判断和算法,比如服务器处于TIME_WAIT状态,重新启动就要判断状态

#include <iostream>
#include <string>
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <cassert>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>using std::cout;
using std::endl;class TcpServer
{
public:TcpServer(uint16_t port, std::string ip = ""): _listensockfd(-1), _ip(ip), _port(port){}~TcpServer(){if (_listensockfd > 2)close(_listensockfd);}void InitInetData(){assert(_port > 1025);_listensockfd = socket(AF_INET, SOCK_STREAM, 0);if (_listensockfd < 0)exit(1);sockaddr_in serverData;socklen_t len = sizeof(serverData);memset(&serverData, 0, len);serverData.sin_family = PF_INET;serverData.sin_port = htons(_port);serverData.sin_addr.s_addr = _ip.empty() ? (INADDR_ANY) : inet_addr(_ip.c_str());int opt = 1;setsockopt(_listensockfd, SOL_SOCKET, SO_REUSEADDR, (void*)&opt, sizeof(opt));if (bind(_listensockfd, (const sockaddr *)&serverData, len) < 0){std::cout << "bind error: " << strerror(errno) << std::endl;exit(3);}if (listen(_listensockfd, 2) < 0)exit(4);}void StartServer(){while (true){int ServerSockfd = accept(_listensockfd, nullptr, nullptr);if (ServerSockfd < 0)exit(5);}}private:int _listensockfd;uint16_t _port;std::string _ip;
};int main(int argc, char *argv[])
{if (argc < 2){std::cout << "Format: ./可执行程序 [ip] port" << std::endl;exit(8);}std::string ip;uint16_t port;if (argc == 3){ip = argv[1];port = std::stoi(argv[2]);}else{port = std::stoi(argv[1]);}TcpServer tps(port, ip);tps.InitInetData();tps.StartServer();return 0;
}

在这里插入图片描述


相关文章:

【Linux】网络基础之TCP协议

目录 &#x1f308;前言&#x1f338;1、基本概念&#x1f33a;2、TCP协议报文结构&#x1f368;2.1、源端口号和目的端口号&#x1f369;2.2、4位首部长度&#x1f36a;2.3、32位序号和确认序号&#xff08;重点&#xff09;&#x1f36b;2.4、16位窗口大小&#x1f36c;2.5、…...

Java设计模式之装饰器(Decorator)模式

装饰器&#xff08;Decorator&#xff09;设计模式允许动态地将新功能添加到对象中&#xff0c;同时又不改变其结构。 什么是装饰器模式 装饰器&#xff08;Decorator&#xff09;模式通过将对象进行包装&#xff0c;以扩展其功能&#xff0c;而不需要修改其原始类。装饰器模…...

element ui树组件render-content 树节点的内容区的渲染另一种方式

直接上代码吧,不用h的写法。 <el-tree :data"data" node-key"id" default-expand-all :expand-on-click-node"false" :props"defaultProps":render-content"renderContentTree" node-click"handleNodeClick"&g…...

html a标签换行显示

文章目录 用css display属性不用css&#xff0c;可以用<br>标签换行示例 用css display属性 可以使用CSS的display属性来实现多个a标签每行显示一个。 HTML代码&#xff1a; <div class"link-container"><a href"#">Link 1</a>…...

关于Redis-存Long取Integer类型转换错误的问题

背景 最近遇到了两个Redis相关的问题&#xff0c;趁着清明假期&#xff0c;梳理整理。 1.存入Long类型对象&#xff0c;在代码中使用Long类型接收&#xff0c;结果报类型转换错误。 2.String对象的反序列化问题&#xff0c;直接在Redis服务器上新增一个key-value&#xff0c…...

设计模式一:简单工厂模式(Simple Factory Pattern)

简单工厂模式&#xff08;Simple Factory Pattern&#xff09;是一种创建型设计模式&#xff0c;它提供了一个通用的接口来创建各种不同类型的对象&#xff0c;而无需直接暴露对象的创建逻辑给客户端。 简单工厂的三个重要角色&#xff1a; 工厂类&#xff08;Factory Class&…...

如何利用plotly和geopandas根据美国邮政编码(Zip-Code)绘制美国地图

对于我自己来说&#xff0c;该需求源自于分析Movielens-1m数据集的用户数据&#xff1a; UserID::Gender::Age::Occupation::Zip-code 1::F::1::10::48067 2::M::56::16::70072 3::M::25::15::55117 4::M::45::7::02460 5::M::25::20::55455 6::F::50::9::55117我希望根据Zip-…...

ceph集群搭建

文章目录 理论知识具体操作搭建ceph本地源yum源及ceph的安装配置NTP&#xff08;解决时间同步问题&#xff09;部署ceph自定义crush 理论知识 Ceph是一个分布式存储系统&#xff0c;并且提供了文件、对象、块存储功能。 Ceph集群中重要的守护进程有&#xff1a;Ceph OSD、Cep…...

前端密码加密 —— bcrypt、MD5、SHA-256、盐

&#x1f414; 前期回顾悄悄告诉你&#xff1a;前端如何获取本机IP&#xff0c;轻松一步开启网络探秘之旅_彩色之外的博客-CSDN博客前端获取 本机 IP 教程https://blog.csdn.net/m0_57904695/article/details/131855907?spm1001.2014.3001.5501 在前端密码加密方案中&#xff…...

汽车UDS诊断深度学习专栏

1.英文术语 英文术语翻译Diagnostic诊断Onboard Diagnostic 在线诊断 Offboard Diagnostic离线诊断Unified diagnostic service简称 UDS 2.缩写表 缩写解释ISO国际标准化组织UDSUnified diagnostic service&#xff0c;统一的诊断服务ECU电控单元DTC 诊断故障码 ISO14229UD…...

macOS 下安装brew、nvm

1、brew&#xff1a; /bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)" brew -v 查看版本 示例&#xff1a; 安装jdk brew search jdk 查询可用的jdk版本 brew install openjdk11 安装制定版本jdk 更换源&#xff1…...

【云原生】Kubernetes工作负载-StatefulSet

StatefulSet StatefulSet 是用来管理有状态应用的工作负载 API 对象 StatefulSet 用来管理某 Pod 集合的部署和扩缩&#xff0c; 并为这些 Pod 提供持久存储和持久标识符 和 Deployment 类似&#xff0c; StatefulSet 管理基于相同容器规约的一组 Pod。但和 Deployment 不同…...

Java:方法的重载

方法重载 为什么需要方法重载 在使用方法的过程中我们可能会遇到以下如同例子的情形&#xff1a; ​ ​ public class method1 {public static void main(String[] args) {int a1 10;int b1 20;double ret1 add(a1, b1);System.out.println("ret1 " ret1);do…...

7.react useCallback与useMemo函数使用与常见问题

react useCallback与useMemo函数使用与常见问题 useCallback返回一个可记忆的函数&#xff0c;useMemo返回一个可记忆的值&#xff0c;useCallback只是useMemo的一种特殊形式。 那么这到底是什么意思呢&#xff1f;实际上我们在父子通信的时候&#xff0c;有可能传递的值是一…...

Sentinel限流中间件

目录 介绍 Sentinel 的特征 Sentinel 的组成 实战使用 简单实例 配置本地控制台 使用可视化ui配置简单流控 配置异步任务限流 使用注解定义限流资源 SpringCloud整合Sentinel 简单整合 并发线程流控 关联模式 整合openFeign使用 介绍 随着微服务的流行&#xff0…...

使用ajax进行前后端交互的方法

使用ajax进行前后端交互的方法&#xff1a;&#xff08;我只测试通了json对象作为参数的方式&#xff0c;其他方式我没有测试通过&#xff09; 1、前端方法&#xff1a; 传参方式&#xff1a;POST 请求类型&#xff1a;json对象 响应类型&#xff1a;json对象 function test() …...

动手学深度学习——线性回归从零开始

生成数据集synthetic_data()读取数据集data_iter()初始化模型参数w, b定义模型&#xff1a;线性回归模型linreg()定义损失函数&#xff1a;均方损失squared_loss()定义优化算法&#xff1a;梯度下降sgd()进行训练&#xff1a;输出损失loss和估计误差 %matplotlib inline impor…...

Redis缓存击穿

Redis缓存击穿是指在使用Redis作为缓存时&#xff0c;某个热点数据过期或不存在&#xff0c;导致大量请求直接打到后端存储系统&#xff08;例如数据库&#xff09;&#xff0c;使得后端系统压力骤增&#xff0c;性能下降的情况。这种情况通常发生在热点数据失效的瞬间。 缓存…...

网络安全(黑客)自学的一些建议

1.选择方向 首先是选择方向的问题&#xff0c;网络安全是一个很宽泛的专业&#xff0c;包含的方向特别多。比如 web安全&#xff0c;系统安全&#xff0c;无线安全 &#xff0c;二进制安全&#xff0c;运维安全&#xff0c;渗透测试&#xff0c;软件安全&#xff0c;IOT安全&a…...

全志F1C200S嵌入式驱动开发(基于usb otg的spi-nor镜像烧入)

【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing @163.com】 前面既然已经搞定了spi-nor驱动,那么下一步考虑的就是怎么从spi-nor flash上面加载uboot、kernel和rootfs。目前spi-nor就是一块白片,上面肯定什么都没有,那么这个时候,我们要做…...

第19节 Node.js Express 框架

Express 是一个为Node.js设计的web开发框架&#xff0c;它基于nodejs平台。 Express 简介 Express是一个简洁而灵活的node.js Web应用框架, 提供了一系列强大特性帮助你创建各种Web应用&#xff0c;和丰富的HTTP工具。 使用Express可以快速地搭建一个完整功能的网站。 Expre…...

Flask RESTful 示例

目录 1. 环境准备2. 安装依赖3. 修改main.py4. 运行应用5. API使用示例获取所有任务获取单个任务创建新任务更新任务删除任务 中文乱码问题&#xff1a; 下面创建一个简单的Flask RESTful API示例。首先&#xff0c;我们需要创建环境&#xff0c;安装必要的依赖&#xff0c;然后…...

React第五十七节 Router中RouterProvider使用详解及注意事项

前言 在 React Router v6.4 中&#xff0c;RouterProvider 是一个核心组件&#xff0c;用于提供基于数据路由&#xff08;data routers&#xff09;的新型路由方案。 它替代了传统的 <BrowserRouter>&#xff0c;支持更强大的数据加载和操作功能&#xff08;如 loader 和…...

【SQL学习笔记1】增删改查+多表连接全解析(内附SQL免费在线练习工具)

可以使用Sqliteviz这个网站免费编写sql语句&#xff0c;它能够让用户直接在浏览器内练习SQL的语法&#xff0c;不需要安装任何软件。 链接如下&#xff1a; sqliteviz 注意&#xff1a; 在转写SQL语法时&#xff0c;关键字之间有一个特定的顺序&#xff0c;这个顺序会影响到…...

React19源码系列之 事件插件系统

事件类别 事件类型 定义 文档 Event Event 接口表示在 EventTarget 上出现的事件。 Event - Web API | MDN UIEvent UIEvent 接口表示简单的用户界面事件。 UIEvent - Web API | MDN KeyboardEvent KeyboardEvent 对象描述了用户与键盘的交互。 KeyboardEvent - Web…...

ETLCloud可能遇到的问题有哪些?常见坑位解析

数据集成平台ETLCloud&#xff0c;主要用于支持数据的抽取&#xff08;Extract&#xff09;、转换&#xff08;Transform&#xff09;和加载&#xff08;Load&#xff09;过程。提供了一个简洁直观的界面&#xff0c;以便用户可以在不同的数据源之间轻松地进行数据迁移和转换。…...

现代密码学 | 椭圆曲线密码学—附py代码

Elliptic Curve Cryptography 椭圆曲线密码学&#xff08;ECC&#xff09;是一种基于有限域上椭圆曲线数学特性的公钥加密技术。其核心原理涉及椭圆曲线的代数性质、离散对数问题以及有限域上的运算。 椭圆曲线密码学是多种数字签名算法的基础&#xff0c;例如椭圆曲线数字签…...

Golang——6、指针和结构体

指针和结构体 1、指针1.1、指针地址和指针类型1.2、指针取值1.3、new和make 2、结构体2.1、type关键字的使用2.2、结构体的定义和初始化2.3、结构体方法和接收者2.4、给任意类型添加方法2.5、结构体的匿名字段2.6、嵌套结构体2.7、嵌套匿名结构体2.8、结构体的继承 3、结构体与…...

【从零开始学习JVM | 第四篇】类加载器和双亲委派机制(高频面试题)

前言&#xff1a; 双亲委派机制对于面试这块来说非常重要&#xff0c;在实际开发中也是经常遇见需要打破双亲委派的需求&#xff0c;今天我们一起来探索一下什么是双亲委派机制&#xff0c;在此之前我们先介绍一下类的加载器。 目录 ​编辑 前言&#xff1a; 类加载器 1. …...

【Elasticsearch】Elasticsearch 在大数据生态圈的地位 实践经验

Elasticsearch 在大数据生态圈的地位 & 实践经验 1.Elasticsearch 的优势1.1 Elasticsearch 解决的核心问题1.1.1 传统方案的短板1.1.2 Elasticsearch 的解决方案 1.2 与大数据组件的对比优势1.3 关键优势技术支撑1.4 Elasticsearch 的竞品1.4.1 全文搜索领域1.4.2 日志分析…...