Bootstrap

【Linux网络编程】传输层协议(1)--TCP/UDP--原理

目录

前言:

UDP协议

UDP协议的特征 

UDP协议端格式

UDP的缓冲区

分离与交付(封装与解包)

UDP的注意事项 

TCP协议 

 TCP协议段格式

确认应答机制(ACK) 

捎带应答机制 

超时重传机制

连接管理机制 

建立连接/三次握手简介

断开连接/四次挥手简介

详谈三次握手 

详谈四次挥手 


前言:

传输层主要负责数据能够从发送端传输接收端,在socket编程中我们写的套接字就是传输层中的协议

常见的传输层协议有两个:

  • UDP协议
  • TCP协议

这两个协议中有相同的特征,如下:

  • UDP和TCP都需要通过IP地址和端口号标识互联网中的一个进程
  • UDP和TCP在进行数据传输时,它们都是全双工的

注意:全双工指的是通信过程中数据可以在两个方向上同时传输,即实现信号的双向同时传输(A→B且B→A)

支持全双工的原因是内核的传输层中维护了两个缓冲区,发送缓冲区和接收缓冲区。这两个缓冲区互不影响!这一点在应用层中详细说明过

  • 图中,主机A从发送缓冲区中拿取数据发送给主机B的同时,主机B也可以从自己的发送缓冲区中拿取数据发送给主机A,这两个步骤是不会相互影响的! 

端口号与进程的关系 

一个进程可以bind多个端口号

  • 实际上,我们需要的是客户端可以使用ip地址和端口号唯一标识一个进程,而反过来成不成立都可以 

一个端口号不可以被多个进程bind 

实际上,我们要使用UDP/TCP时server都需要手动进行bind

一般地、内核中维护了一个端口号和PCB之间的kv结构,所以我们bind的时候是把这个端口号和进程PCB之间构建好映射以后,插入到哈希表之类的数据结构中!就能很快的根据端口号找到对应进程        


UDP协议


UDP协议的特征 

  • 无连接
  • 不可靠
  • 面向数据报 

无连接主要说明UDP的通信就像是寄信一样,你写好了一封信以后不需要经过收信人的同意可以直接送到他家去。UDP在发送数据之前不需要建立连接。发送方可以随时随地发送数据,接收方也无需事先同意接收。这种特性使得UDP的通信过程更加简单、高效,但同时也意味着UDP不提供像TCP那样的可靠传输机制。

网络传输的过程中是可能会丢包的,即传输过去的数据对方接收不到,如果是使用UDP进行传输,那么发生丢包什么都不管,这也是说明UDP是不可靠的原因!不可靠仅仅是UDP的特征,不是它有BUG。正因为它不需要保证数据的可靠,所以它才能简单、高效。TCP是可靠的,但TCP的可靠是付出了很多代价的

应用层交给UDP多长的报文, UDP原样发送, 既不会拆分, 也不会合并。假设用UDP传输100个字节的数据,如果发送端调用一次sendto, 发送100个字节, 那么接收端也必须调用对应的 一次 recvfrom, 接收100个字节; 而不能循环调用10次recvfrom, 每次接收10个字节。总得来说,面向字节流比起面向数据报更加灵活一些

UDP协议端格式

  • 图中除了数据以外的所有字段,它们共同组成了UDP通信时的传输层报头
  • 数据实际上就是有效载荷
  • UDP是可以进行双向通信的,它的传输层报头中包含了源端口和目的端口,这样就可以确定把数据交到哪个应用层进程

实际上,所谓的协议,也就是一个双方都定义出来的结构化的数据,在内核中UDP的协议的伪代码如下:

struct XXX
{
    uint16_t src_port;//源端口
    uint16_t dst_port;//目的端口
    uint16_t udp_len;//UDP长度
    uint16_t check;//校验和
};
  • 这个结构体被定义在通信双方OS内核中。协议就是结构体,这就是大多数协议的本质,不仅仅UDP

UDP中有两个字段是相对陌生的:

  • 16位UDP长度
  • 16位UDP校验和

尽管UDP本身是一个无连接的、不可靠的传输协议,但校验和机制为数据传输提供了一定程度的可靠性保障。通过检测并丢弃可能损坏的数据报,校验和有助于减少上层协议接收到错误数据的可能性。

UDP长度表示的是报头+有效载荷的长度,其中报头的大小是固定的8字节

UDP长度 = 有效载荷长度 + 8

正因为UDP长度的存在,提供给UDP面向数据报的能力


UDP的缓冲区

  • 发送缓冲区存在的意义是发送方可以进行等待接收方,当接收方速度过慢时,先把数据放入发送缓冲区作为缓冲 
  • 由于UDP不需要保证数据可靠性,所以对于UDP来说不需要内核中的发送缓冲区,因为我根本不需要临时保存数据,不需要管对方是啥状态!sendto直接拷贝数据给OS即可
  • UDP具有接收缓冲区. 但是这个接收缓冲区不能保证收到的UDP报的顺序和发送UDP报的顺序一致; 如果缓冲区满了, 再到达的UDP数据就会被丢弃;

分离与交付(封装与解包)

UDP的分离:实际上根据UDP长度即可得到有效载荷的长度,就能把UDP报头进行封装/分离

UDP的交付:根据目的端口即可知道交付给哪个进程 


系统角度理解封装和解包

  • 实际上,报文的传输并不是互斥的,即主机A向主机B发送报文时可能还存在主机C也同时在发送报文给主机B。 
  • OS内可以同时存在很多个收到的报文,这个报文还没来得及处理,这个报文可能只到了接收缓冲区甚至是网络/链路层
  • 由于OS内可能存在大量的还没来得及处理的报文,所以OS需要对这些大量的报文进行管理
  • OS对报文的管理方式就是先描述、后组织。

所以OS内一定存在着一个描述报文的内核结构体,这个结构体叫做sk_buffer。并且也一定会给报文提供内存空间!

示例图:

  • 图中,对报文的管理就变为了对链表的增删查改 

封装:所谓的封装就是给报文添加上对应层的报头

  • 所谓的封装报头就是把sk_buffer中的head指针左移,左移的字节数要看封装的是什么报头。不管封装的是什么报头,它都是结构体类型,都能sizeof算出它的大小
  • 通过head指针填完对应报头的信息

 解包:所谓的解包就是对报文去掉对应层的报头

  • 所谓的解包就是sk_buffer中的head指针右移,右移的字节数要看封装的是什么报头。
  • 指针右移完以后交付给上层就完成了一层解包

所以所谓的封装和解包,在内核中只需要进行指针移动即可控制 


UDP的注意事项 

在UDP报头中,UDP长度是用16位的一个整形来存储的,换句话来说,你发送的数据的字节数不能超过这个整形能表示的字节数,这个整形能表示64KB。若你一次发送的数据超过64KB,那么就需要在发送方应用层手动分包进行分批发送!分批发送完以后接收方再把收到的数据手动拼接起来 


TCP协议 


 TCP协议段格式

  • 图中,我们已知的字段有两个:源端口号、目的端口号
  • 源/目的端口号:表示数据是从哪个进程来, 到哪个进程去
  • 标准TCP协议端(没有选项的TCP报头)的大小是20个字节
  • 除此之外,对于TCP协议段,我们本篇文章不谈的内容:16位校验和、保留的6位、选项

4位首部长度 

TCP协议可以携带选项,并且选项个数是可变的。所以TCP协议是没有固定大小的,所以TCP中携带了4位首部长度

4位首部长度:表示了一个报文中报头的大小,它能表示的范围是[0,15],单位是4字节,所以能表示的报头大小是[0,60]字节

其中,标准TCP协议格式(不携带选项)的大小是20字节=首部长度:5,二进制:0101


确认应答机制(ACK) 

TCP协议最经典的一个特征:是可靠的

所谓的可靠,也就是说它能保证发送方发送的数据,接收方一定能收到

实际上,TCP的可靠性策略是有很多机制配合完成的,但其中最关键的一个机制就是确认应答机制


日常生活中的确认应答机制

  • 在日常生活中,我们对朋友说话的时候,是不能完全保证他听到了的,特别是距离变远时更是如此!
  • 那么我们有什么方法得知你的朋友听到了你的信息呢?是有的,当你朋友对你说出的话做出反应时比如说他说了一句听到了,我们就可以得知你的朋友确实听到了你说话
  • 但现在问题又来了,你的朋友怎么确定你听到了他的回应呢?很简单,当你对他的回应做出反应时,他就能得知你确实收到了回应

上述,就是日常生活中的确认应答机制,而计算机中的确认应答机制也相同 


计算机中的确认应答机制 

实际上,在通信过程中,若我们对发送的每一个消息(包括数据和应答)都做出应答,是无法做到的

  • 假设发送方发送了一条数据,接收方给出了应答,如果要保证发送方接收到了接收方的应答,则发送方需要对接收方的应答作出应答
  • 同理,接收方要保证收到了发送方的应答,则接收方需要对发送方的应答作出应答
  • 以此往复,死循环了 

所以实际场景中,TCP协议的可靠性保证不是靠的上述策略,而是只对数据进行应答,不对应答做出应答

  • 假设发送方发送了一条数据,接收方做出应答
  • 当接收方做出应答以后尽管无法保证这条应答一定到达发送方,但当这条应答到达发送方以后,发送方能知道我的数据一定被接收方收到了 

当然,上述过程中可能会出现接收方的应答丢失了,发送方没收到应答,此时发送方能判断出消息的状态有两种情况

  • 发送的数据没有到达接收方,接收方没发出应答
  • 发送的数据到达了接收方,接收方给出了应答,但应答半路丢失了
  • 但不管是上述情况的哪一种,发送方只需要当成是发送的数据没到达接收方处理即可

  • 若发送方收到了应答,则数据一定没有丢失
  • 若发送方没收到应答,则当成数据已经丢失处理。 此时发送方再重新发送一条数据给接收方

上述机制,我们称之为确认应答机制(ACK)

需要注意的是:发送应答和发送数据,一般是双方OS自动完成的,通信细节由OS自动解决了 


序号和确认序号 

在上述我们说的TCP的确认应答机制是发送方发送一条数据之后等待接收方的确认应答

这种方法当然是没错,但效率不高,所以实际TCP的确认应答如下示例图

  • 发送方可以连续发送多条数据,这几条数据的发送不需要等待确认应答
  • 这种确认应答的设计模式是TCP协议的真实情况,它使得发送方发送数据的时间和接收方发送应答的时间高度重合了起来,提高了效率

序号和确认序号

上图中的问题是:当发送方收到多条应答时,发送方无法得知接收方是对哪条数据进行的应答

所以发送方在发送报文时一般会带上这条报文的序号,这个序号一般是递增的

而当接收方接收到发送方的报文时,会给出应答,应答中会填写上确认序号

确认序号=序号+1

确认序号的含义是,确认序号之前的报文我已经全部收到了,下一次序号从确认序号开始!

发送方看到携带了确认序号的应答报文,就可以知道它是对哪条报文做出的应答


TCP的按序到达

上图中,是一种理想的情况,即序号的发送顺序与确认序号的到达顺序相同

但在实际场景中,是可能出现乱序情况的

所以到发送方收到多份应答以后一般会对应答按确认序号排序,保证收到确认序号的顺序和发送序号的顺序相同

这称为TCP的按序到达策略 


捎带应答机制 

对于为什么要有序号,其实上文已经给出了明确的答案,但在上述场景中为什么要带有确认序号呢?好像只有一个序号也能完成上述的这些动作吧?

例如发送方发送了一个序号为1000的报文,那么接收方只需要发送一个序号为1001的应答不就能完成应答吗?

在上述场景中,我们所聊的是一台主机作为发送方,一台主机作为接收方的通信

但实际当中,我们通信的主机即可以作为发送方,又可以作为接收方,进行全双工通信

假设我们今天的场景是主机A发送数据给主机B,主机B作出应答以后立马想发送数据给主机A。如下图:

  • 可以看到,上述这套模型是有点奇怪的,不要忘记了,我们所说的数据和应答,对于通信双方拿到的本质上都是一个报文,只不过对于应答来说不需要有效载荷
  • 换句话来说对于主机B来说,原本可以一条报文即发送了应答(带上确认序号),又发送数据(带上有效载荷)。他却分成了两条报文来发送,从发送成本和发送效率的角度来说都不合理
  • 所以对于连续的应答+数据,TCP协议把它们合成一条报文

发送应答+数据的报文,我们称之为捎带应答

 而要实现捎带应带,就必须有确认序号,因为一台主机捎带应答时既要确认报文到达,又要填写我这条带数据的报文的序号


理解序号

我们之前说序号其实是TCP为每一个报文编号,那么TCP是如何编号的呢?怎么理解呢?

假设主机A向主机B一次发送1000字节的数据,示例图:

实际上,我们说过每台主机都有发送和接收缓冲区,若我们把发送缓冲区想象成是一个char类型的数组,那么此时发送缓冲区就天然的有了序号了,序号就是每个字节所对应的下标


超时重传机制

之前说过,若发送方收到接收方的应答,那么发送方可以知道,上一条消息接收方一定已经收到了。

若发送方没有收到接收方的应答,那么发送方当成数据没有到达接收方处理。但实际情况可能有以下两种:

  • 发送方的数据确实没有到接收方
  • 发送方的数据接收方收到了,但接收方的应答丢了

所以发送方若没有收到应答,那么发送方会重新发送一条与丢失报文相同的一条新的报文。这种行为也称之为重传

那么问题来了,发送方如何判断接收方的应答丢了呢?

所以发送方只能等待接收方的应答,当一段时间过后没有收到接收方的应答则发送方会重传

而这种等待一段时间过后若没有收到应答则重发的策略我们称之为超时重传策略

当然,若数据到达了接收方,但接收方的应答可能丢失,重传以后导致接收方有大量相同的报文,所以当重传时,TCP协议会根据报文的序号对相同的报文进行去重

那么超时重传等待的时间具体是多少呢?

实际上,这个时间段是不太好固定的,因为如果你超时重载等待的时间设置的比较长,那么长时间等待会导致效率不足。若你超时重载等待的时间设置的比较短,那么可能一个正常的ACK报文你也会来不及接收。导致浪费资源

所以这个时间段是根据你的网络情况,动态分布超时时间的!

  • Linux 中(BSD Unix 和 Windows 也是如此), 超时以 500ms 为一个单位进行控制, 每次判定超时重发的超时时间都是 500ms 的整数倍.
  • 如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传
  • 如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增
  • 累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接.

连接管理机制 

TCP协议的另一大特点是它是面向连接的,也就是若需要客户端和服务端用TCP通信,则要保证客户端和服务端已经建立连接。当客户端要与服务端去关联,则需要客户端和服务端关闭连接


TCP的标志位 

TCP协议段格式中有6个标志位,分别是URG、ACK、PSH、RST、SYN、 FIN

从宏观的角度来说,TCP为什么需要这些标志位?

实际上,使用TCP通信需要建立连接,不再使用时需要断开连接,而这些动作也是要求通信双方协商共同完成的。

而既然要协商,对于双方来说只能通过发送报文的方式进行协商,所以对于一条报文来说,它需要完成的工作是不同的,总的来说划分为如下:

  • 建立连接的报文
  • 携带数据的报文
  • 断开连接的报文

同时,对于一台服务器来说,最常见的情况是非常多的客户端发送报文给他,这些客户端的需求不同,报文也是各式各样的,所以服务器要区分这些报文的类型。换句话来说报文其实是有类型的!

而这也就说明了TCP中标志位的意义。即标识各种报文的类型


建立连接/三次握手简介

客户端要与服务器建立连接,要经历三次握手

  • 第一次握手:客户端向服务器发送一个SYN标志位为1的报文,这个报文不携带有效载荷
  • 第二次握手:服务器收到客户端的报文后,服务器向客户端发送一个SYN和ACK标志位为1的报文,这个报文也不携带有效载荷。其中ACK标志位为1是为了对客户端的第一次握手做出应答
  • 第三次握手:客户端向服务器发送一个ACK标志位为1的报文,这个报文可以携带有效载荷。这个报文的目的是为第二次握手做出应答 

注意:第一次握手要求服务端必须处于监听状态,即socket设置为listen,否则服务端会直接丢弃掉携带SYN的报文 

 三次握手示例图:

  • SYN:我们称之为同步标志位,当一个报文中这个标志位为1时,说明该报文需要发送连接请求。
  • ACK:我们称之为确认标志位,当一个报文中这个标志位为1时,说明该报文对上一条消息做出了应答
  • 除此之外,标志位可以同时为1(SYN和FIN除外),在上图中服务器发送携带SYN+ACK的报文,说明该报文即要做出应答(ACK),又要发起连接请求(SYN)

断开连接/四次挥手简介

客户端要与服务器断开连接,要经历四次挥手

  • 第一次挥手:客户端向服务器发送一个FIN标志位为1的报文
  • 第二次挥手:服务器收到客户端的报文后,向客户端发送一个ACK标志位为1的报文,即对第一次挥手的应答
  • 第三次挥手:服务器向客户端发送一个FIN标志位为1的报文
  • 第四次挥手:客户端向服务器发送一个ACK标志位为1的报文,即对第三次挥手的应答

四次挥手示例图:

  •  FIN标志位:我们称之为断开连接标志位,当一个报文中这个标志位为1时,说明该报文需要发送断开连接的请求。

理解连接 

所谓的捎带应答,本质上其实就是一个报文中ACK标志位为1,且这个报文携带了有效载荷

一个TCP服务可能会建立很多个连接,这要求OS能管理好这些连接。所以内核需要对连接进行先描述后组织,即内核中一定会有描述连接的结构体,并且这个结构体被放在了一个内核数据结构中进行管理。操作系统对连接的管理转化为了对这个数据结构的增删查改

所以所谓的建立连接是有成本的,这个成本主要体现在OS维护连接所需的内存空间和时间!


详谈三次握手 


 三次握手与TCP系统调用接口

系统调用接口connect用于客户端向服务器发起连接请求,所以三次握手的发起者是调用connect的执行流

系统调用接口listen用于设置服务器套接字为监听状态,服务器监听到客户端的连接请求,是发生在第二次握手之前,第一次握手之后!

系统调用接口accept用于服务器响应客户端的连接请求,调用accept时,此时双方连接已经建立,是反生在第三次握手之后!所以accept不参与三次握手的过程


状态变化

这里的状态变化主要指的是TCP连接建立过程中,客户端和服务器双方的状态变化。

  • 在发起第一次握手之前,客户端会把自己的状态设置为SYN_SENT状态
  • 第一次握手完之后,服务器收到客户端的连接请求,会把自己的状态设置为SYN_RECV状态
  • 第二次握手完之后,客户端收到服务器的应答及连接请求,此时它会把自己的状态设置为ESTABUSHED状态并建立好自己的连接
  • 第三次握手完之后,服务器收到客户端的应答,此时它会把自己的状态设置为ESTABUSHED状态并建立好自己的连接

三次握手示例图:

  • 对于客户端来说,它的连接建立时间是第二次握手之后
  • 对于服务器来说,它的连接建立时间是第三次握手之后
  • 所以客户端和服务器连接建立的时间是不同的
  • 当客户端建立连接时它就得认为服务器也能建立好连接,由于第三次握手没有应答,所以事实上服务器有没有建立好连接客户端不知道
  • 从本质上来说,客户端其实是在赌发送的ACK不会丢失!由于已经经历了两次握手,所以大概率第三次握手能成功

所以,依据上述,我们得对三次握手有一个全新得认识:不是说三次握手100%建立好了连接,而是经历了三次握手以后,客户端和服务器都认为连接建立好了,但事实上对方有没有建立好连接,它们互相并不知道! 


连接重置 

上文我们说过,第三次握手时客户端已经认为连接被建立好了,但事实上连接并不是100%建立好了,第三次握手发送的ACK可能会丢失,那么丢失的情况如何解决呢?

实际上当客户端认为连接已经建立好了以后,它会立马开始通信开始发送有效数据,但由于ACK丢失,所以服务器的连接并没有建立好。而当服务器收到客户端发送的有效数据时,会根据自己没有建立好连接自动判别客户端发送的ACK丢失,此时它会向客户端发送一个携带RST标志位的报文,客户端收到这个报文以后会重新开始三次握手建立连接!

上述过程我们称之为连接重置

RST标志位:重置标志位,携带这个标志位的报文会要求双方重新进行三次连接!

所谓的连接重置,就是让双方重新进行三次连接,这不仅仅适用于我们今天的场景,只要是当TCP通信双方的连接出现异常时,都可能会发生连接重置!


为什么建立连接之前要进行三次握手?

1、双方确认全双工

  • 客户端和服务器双方要进行通信,首先是要确保网络信道是健康的
  • 三次握手之后,客户端和服务器都会有一次确认的收发
  • 第一次握手后,客户端既没有确认自己的接收健康,又没有确定自己发送健康。服务器仅仅确定了自己是能接收数据的
  • 第二次握手之后,客户端确认了自己是收发健康的。服务器仅仅确定了自己能接收数据
  • 第三次握手之后,双方都确定了自己的收发健康。

2、确保双方TCP愿意进行通信

  • 客户端和服务器要与对方进行通信,都需要确定的是对方是愿意通信的
  • 第一次握手之后,客户端和服务器都不清楚对方的通信意愿
  • 第二次握手之后,服务器确认客户端是愿意通信的,但客户端不确定服务器是否愿意通信。
  • 第三次握手之后,双方都确定对方愿意进行通信  

详谈四次挥手 


四次挥手与系统调用接口 

与四次挥手相关的系统调用就是close(sockfd)

  • 当客户端/服务器调用close关闭自己的套接字时,本质上就是先向对方发送一个FIN标志位为1的报头。并没有真正的关闭自己的套接字文件描述符
  • 这个报头的含义是我不会再给你发送用户数据。可以理解为客户端的发送缓冲区关闭

状态变化 

 这里的状态变化主要指的是TCP断开连接过程中,客户端和服务器双方的状态变化。

以下场景都是假设服务器是被动断开连接的一方,客户端是主动发起断开连接的一方

  • 第一次挥手,客户端会先把自己的套接字状态设置为FIN_WAIT_1,并且客户端向服务器发送FIN。服务器收到了客户端的FIN后,会把自己的套接字状态设置为CLOSE_WAIT,这个状态的含义是表示它已经知道对方不会再发送用户数据
  • 第二次挥手,客户端会收到服务器的ACK,此时客户端会把自己的套接字状态设置为FIN_WAIT_2
  • 第三次挥手,服务器发送FIN给客户端,并且服务器的套接字状态被设置为LAST_FIN,这状态的含义是处于等待最后一个ACK的状态,客户端的套接字状态被设置为TIME_WAIT
  • 第四次挥手,客户端发送ACK给服务器,服务器收到后关闭自己的连接。客户端等待一段时间后也关闭自己的连接

四次挥手示例图:

  • 若客户端发送FIN请求,服务器会切换为CLOSE_WAIT,若服务器不发起FIN请求,那么第三次挥手失败,服务器会一直处于CLOSE_WAIT状态,所以若我们在服务器中发现大量的CLOSE_WAIT状态的连接,那么说明服务器写的有bug,大概率是没调用接口关闭套接字

之前我们所说的三次握手的服务器发送的是捎带应答SYN,为什么四次挥手时服务器要把ACK和FIN报文进行分开?

  • 若客户端发送了FIN表示自己不再发送用户数据后,而服务器恰好接收缓冲区中正好没有数据,那么它就会把ACK和FIN合并起来一起发送。即捎带应答
  • 但大多数情况下客户端发送FIN后,服务器的接收缓冲区中还有剩余数据,而服务器就需要对接收缓冲区的剩余数据进行处理,即发送给客户端。
  • 所以服务器发送的ACK和FIN之间服务器可以进行数据发送,所以要把ACK和FIN进行分开成两条报头发送

也就是说客户端发送完FIN后,还能接收数据,那么如何接收呢?

操作系统提供了系统调用接口shutdown

#include <sys/socket.h>  
int shutdown(int sockfd, int how);
  • 功能:用于禁止在一个套接口上进行数据的接收与发送。
  • sockfd:套接口的描述符。
  • how:标志,用于描述禁止哪些操作,可以填的选项有三个, SHUT_RD:表示仅禁用套接字的读取。SHUT_WR:表示仅禁用套接字的写入。SHUT_RDWR:表示禁用套接字的写入和读取,带上这个选项的shutdown与close调用的功能基本一致

为什么断开连接前要进行四次挥手 

实际上四次挥手和三次握手的思想是非常像的:都是双方分别给出请求,双方分别给出应答

所以为什么断开连接前要进行四次挥手的理由可以参考三次握手,其实主要是保证双方都愿意断开连接


状态介绍TIME_WAIT

主动发起断开连接请求的一方在最后需要进入TIME_WAIT状态,这个状态表示的是需要等待一段时间才能退出

TIME_WAIT等待的时长:2倍的MSL

  • MSL是TCP报文在网络中的最大生存时间
  • MSL时间是一个经验数据,所以MSL在不同版本的操作系统之间都可能存在差异
  • 使用命令查看MSL时间:cat /proc/sys/net/ipv4/tcp_fin_timeout

为什么要等待2倍的MSL时间?

  • TIME_WAIT 持续存在 2MSL 的话,就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启, 可能会收到来自上一个进程的迟到的数据, 但是这种数据很可能是错误的)
  • 同时也是在理论上保证最后一个报文可靠到达(假设最后一个 ACK丢失, 那么 服务器会再重发一个 FIN这时虽然客户端的进程不在了, 但是TCP连接还在, 仍然可以重发 LAST_ACK);

服务器主动关闭的情况:若服务器主动关闭,那么服务器会进入TIME_WAIT状态,当立马以同一个端口再次运行服务器时,可能会绑定失败,因为前面你主动关闭的服务器进入了TIME_WAIT状态,并没有退出,那么服务器端口还在被占用。

解决服务器主动关闭后无法立马以同一个端口重启的系统调用:setsockopt

SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname,
              const void *optval, socklen_t optlen);
  • 功能:允许你设置套接字(socket)选项。这些选项用于控制套接字的各种行为,比如缓冲区大小、是否启用端口复用、是否保持连接活跃等。
  • sockfd:要设置选项的套接字的文件描述符。
  • level:选项所在的协议级别。对于大多数套接字选项,这个值将是 SOL_SOCKET,表示套接字级别的选项。然而,有些选项可能属于特定的协议(如 IP或TCP),这时将使用 IPPROTO_IP 或 IPPROTO_TCP 等值。
  • optname:要设置的选项的名称。这个参数是一个整数,表示特定的套接字选项。
  • optval:指向包含新选项值的缓冲区的指针。选项值的具体格式取决于 optname 的值。
  • optlenoptval 缓冲区的大小(以字节为单位)。
  • 返回值:如果成功,setsockopt 返回 0。如果失败,返回 -1,并设置 errno 以指示错误原因。

在服务器程序中,可以通过设置socket选项SO_REUSEADDR来允许端口复用。这个选项允许在同一个端口上启动一个新的监听socket,即使旧的socket仍处于TIME_WAIT状态。这可以通过在bind()调用之前设置socket选项来实现

示例:

int sockfd = socket(AF_INET, SOCK_STREAM, 0);  
int opt = 1;  
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {  
    // 处理错误  
}  
// 接下来是bind()和listen()调用
;