【Linux】网络基础之TCP协议

news2024/12/23 22:11:27

目录

  • 🌈前言
  • 🌸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;
}

在这里插入图片描述


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/791102.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

解决rosdep网络问题

众所周知&#xff0c;想要使用rosdep&#xff0c;需要两个步骤&#xff1a; sudo rosdep init rosdep update其中&#xff0c;第一步就是下载了一个文件&#xff0c;第二步是从服务器下载一些数据。 但是因为国内的网络的原因&#xff0c;这两步都有一点困难。但是可以使用tun…

P3611 [USACO17JAN] Cow Dance Show S

思路&#xff1a;二分K&#xff0c;查看当前K能否满足总时间不超过最大时间 ACcode: #include<bits/stdc.h> using namespace std; #define int long long const int N1e410; int n,tmax,a[N]; bool check(int x) {priority_queue < int, vector < int >, gre…

ASCII码、UniCode码、字符转换、中文、英文、二进制、十进制、十六进制

文章目录 效果图htmlJavaScript 效果图 html <div class"w_680 p_t_20 p_b_20 p_l_6 p_r_6"><div class"w_100_ d_f jc_c"><textarea class"w_97_ h_86 fs_16 resize_none outline_none" oninput"oninputF(event)">…

OpenAI重磅官宣ChatGPT安卓版本周发布,现已开启下载预约,附详细预约教程

7月22号&#xff0c;OpenAI 突然宣布&#xff0c;安卓版 ChatGPT 将在下周发布&#xff01;换句话说&#xff0c;本周安卓版 ChatGPT正式上线&#xff01; 最早&#xff0c;ChatGPT仅有网页版。 今年5月&#xff0c;iOS版ChatGPT正式发布&#xff0c;当时OpenAI表示Android版将…

中缀表达式转后缀表达式,使用逆波兰计算。可以计算小数

1、使用方法 传递一个分开保存符号与数字的List即可&#xff1a;List SumNumber; 获取参数的构造方法如下&#xff1a; public ReversePolish(List<String> sumNumber) {SumNumber sumNumber;}要求的List保存数据的方式如下&#xff1a; 例如&#xff1a;123 然后使用…

【C++】详解多态的底层原理

文章目录 前言1. 虚函数表指针与虚函数表2. 子类的虚函数表&#xff08;单继承&#xff09;3. 多态的实现原理3.1 多态是如何实现的3.2 多态调用与非多态调用的区别3.3 为什么父类的对象不能实现多态 4. 其它多态相关问题的理解4.1 虚函数是存在哪里的&#xff1f;4.2 子类新增…

手机照片误删除?无需担忧,点击这里,即可轻松恢复

手机照片误删除&#xff1f;无需担忧&#xff0c;点击这里&#xff0c;即可轻松恢复 开头&#xff1a;在数字化时代&#xff0c;手机已成为我们生活中不可或缺的伙伴。随着手机摄影的普及&#xff0c;我们记录了许多珍贵的瞬间和回忆。然而&#xff0c;有时候我们不小心误删除…

项目经理必备的5种管理能力

作为中层管理者&#xff0c;需要同时完成上级的任务安排和照顾下属的情绪&#xff0c;这是职场中最具挑战性的管理能力。项目经理必备能力中&#xff0c;计划制定、有效授权、高效沟通、化解冲突、项目跟踪是至关重要的。 1、计划制定是项目管理的关键。 作为项目经理&#…

Tribon二次开发- tbbatchjob

在Tribon安装目录下C:\Tribon\M3\Bin里面有许多未知用途的exe,有的双击后时一个DOS终端,有的一闪而过,有的需要按照提示输入信息,有的需要提前在指定的目录配置文件,该如何使用呢? 这些exe大多可以在Tribon以外通过.NET来使用,有的可以通过添加.NET项目引用来使用,有的…

聊聊spring中bean的作用域

前言 今天分享一下spring bean的作用域&#xff0c;理解bean的作用域能够在使用过程中避免一些问题&#xff0c;bean的作用域也是spring bean创建过程中一个重要的点。 Spring bean的作用域类型 singleton&#xff08;单例模式&#xff09;&#xff1a;在整个应用程序的生命周…

成都爱尔蔡裕:泡在“糖”里的脆弱血管,暴露在眼睛深处

糖尿病是一组由多病因引起的以慢性高血糖为特征的终身性代谢性疾病。长期血糖增高&#xff0c;大血管、微血管受损并危及心、脑、肾、周围神经、眼睛、足等。医生临床数据显示&#xff0c;糖尿病发病后10年左右&#xff0c;将有30%&#xff5e;40%的患者至少会发生一种并发症&a…

【TypeScript】对函数类型的约束定义

导读 函数是JavaScript 中的 一等公民 概念&#xff1a;函数类型的概念是指给函数添加类型注解&#xff0c;本质上就是给函数的参数和返回值添加类型约束 文章目录 声明式函数:表达式函数&#xff1a;箭头函数可选参数和默认参数&#xff1a;参数默认值&#xff1a;过剩参数的处…

脚本 打开 cmd 跳转到某个文件夹并执行某些命令

很多时候我们需要启动windows安装的redis、nacos等。 通常我们可以打开安装软件的目录&#xff0c;在文件夹目录那一栏输入cmd,再执行相关启动命令但是这样比较麻烦&#xff0c;现在我们写一个bat脚本&#xff0c;直接启动脚本就可以实现启动程序了。 例如&#xff0c; 1&…

docker入门讲解

目录 第 1 章 Docker核心概念与安装 为什么使用容器&#xff1f; Docker是什么 Docker设计目标 Docker基本组成 容器 vs 虚拟机 Docker应用场景 Linux 安装 Docker 第 2 章 Docker镜像管理 Docker 容器管理 Docker 容器数据持久化 Docker 容器网络 Dockerfile 定制…

JAVA的数据类型与变量

目录 1. 字面常量 2. 数据类型 3. 变量 3.2 长整型变量 3.3 短整型变量 3.4 字节型变量 3.5双精度浮点型 3.6 单精度浮点型 3.7字符型变量 3.8布尔型变量 4.类型转换 4.1自动类型转换(隐式) 4.2强制类型转换(显式) 5.字符串类型 1. 字面常量 字面常量的分类&am…

深度学习之梯度下降算法

0.1 学习视频源于&#xff1a;b站&#xff1a;刘二大人《PyTorch深度学习实践》 0.2 本章内容为自主学习总结内容&#xff0c;若有错误欢迎指正&#xff01; 1 线性模型 1.1 通过简单的线性模型来举例&#xff1a; 1.2 如图&#xff0c;简单的一个权重的线性模型&#xff0c…

透明屏的应用范围广吗?

透明屏是一种新型的显示技术&#xff0c;它可以使屏幕显示的内容透明&#xff0c;让用户可以同时看到屏幕上的图像和背后的物体。 透明屏的应用领域非常广泛&#xff0c;可以用于商业广告、展览展示、智能家居等多个领域。 透明屏的原理是利用透明材料和光学技术&#xff0c;…

通过el-tab切换Echarts图表显示不全问题

一、背景 在让日常开发中很多时候会通过el-tab选项卡方式去分类统计数据&#xff0c;本文我们主要是针对统计中用到了echarts图表&#xff0c;在刚接触时可能会遇到默认选项卡可以正常显示图表数据&#xff0c;但是切换选项卡以后会出现图表大小出现问题&#xff0c;当然原因就…

第2集丨webpack 江湖 —— 创建一个简单的webpack工程demo

目录 一、创建webpack工程1.1 新建 webpack工程目录1.2 项目初始化1.3 新建src目录和文件1.4 安装jQuery1.5 安装webpack1.6 配置webpack1.6.1 创建配置文件&#xff1a;webpack.config.js1.6.2 配置dev脚本1.7 运行dev脚本 1.8 查看效果1.9 附件1.9.1 package.json1.9.2 webpa…

MyBatisPlus之DML编程控制

MyBatisPlus之DML编程控制 1. id生成策略控制&#xff08;Insert&#xff09;1.1 id生成策略控制&#xff08;TableId注解&#xff09;1.2 全局策略配置id生成策略全局配置表名前缀全局配置 2. 多记录操作&#xff08;批量Delete/Select&#xff09;2.1 按照主键删除多条记录2.…