暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

一文详解TCP/IP协议,从网络协议开始,到TCP拆包粘包结束

炎炎录 2021-10-09
816

一、背景


最近一直在写 Netty 相关的文章,一直讲的是网络编程,考虑到部分小伙伴对网络这块不是特别了解,特意写一篇通讯协议相关的总结,以TCP为主。


本文偏向普及,所以内容会相对简单,浅显易懂一点,高手劝退。


另外,如果对 Netty 感兴趣可以关注公众号,查看历史文章。


闲话到此,开搞。。。


二、网络协议


先思考一个问题,我们访问一个网站,数据是怎么传输的?


下面我们带着这个问题,来分析网络协议分层及传输


1、OSI七层模型与TCP/IP五层模型



说起网络协议分层,那绕不开这两个分层模型

OSI(Open System Interconnection)七层网络模型称为开放式系统互联参考模型,是一个逻辑上的定义和规范。简单来说就是一个理论完善的概念,有指导思想,但是没有具体实现。


TCP/IP 分层模型才是我们真正使用的模型,所以下面的协议分层都是TCP/IP为主。


首先 TCP/IP 分层模型有五层(也有说是四层的,就是最下面两层合并为一)


  • 硬件层: 负责光/电信号的传递方式,决定了最大传输速率、传输距离、抗干扰性等。


  • 网卡层: 负责设备之间的数据帧的传送和识别


  • 网络层: 负责地址管理和路由选择


  • 传输层: 负责两台主机之间的数据传输


  • 应用层: 负责应用程序间沟通,我们的网络编程主要就是针对应用层。


数据传输的过程:



发送的数据是自上往下封装,接收的就是自下而上解析:

2、几个重要的协议


其实TCP/IP协议是泛指利用IP进行通讯时所必须使用的协议群的统称,包含很多协议,如下图:

本文主要讲TCP协议,其它后续有机会再详细说明。


三、TCP协议


1、协议结构


由上面可以知道,TCP协议由头部和数据组成,头部如下:

在TCP层,有个FLAGS字段,这个字段有以下几个标识:SYN(synchronous建立连接) ACK(acknowledgement 确认) PSH(push传送) FIN(finish结束) RST(reset重置) URG(urgent紧急)

  • SYN表示建立连接

  • FIN表示关闭连接

  • ACK表示响应

  • PSH表示有 DATA数据传输

  • RST表示连接重置


2、TCP连接


TCP 提供面向有连接的通信传输。面向有连接是指在数据通信开始之前先做好两端之间的准备工作。

建立连接时进行三次握手,断开连接时进行四次挥手


a、三次握手


所谓三次握手是指建立一个 TCP 连接时需要客户端和服务器端总共发送三个包以确认连接的建立。


  • 客户端–发送带有 SYN标志的数据包–一次握手–服务端

  • 服务端–发送带有 SYN/ACK 标志的数据包–二次握手–客户端

  • 客户端–发送带有带有 ACK 标志的数据包–三次握手–服务端


问:为什么是三次握手呢?

三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。

一次握手


由于TCP是面向连接的,一次很明显是不可能的,因为客户端发出连接消息后,却没有接收到来自服务端的回应,客户端就无法确定服务端接是否收到了连接请求,当然也就不能确定是否连接成功

两次握手


既然一次客户端接收不到服务端的回应,那就连接两次,接收到回应就说明服务端接收到了连接请求,可以连接了。结果并不是这样。会造成资源浪费。

三次握手


在两次握手中服务端不知道当前这个SYN是不是有效的,三次握手就很好的解决了这个问题,第三次握手就是客户端给服务端回复第二次握手,这也就是说服务端会等第三次握手的到来,如果第三次握手迟迟不来,服务端就可以识别这个SYN是无效的,就会将他的资源释放了。还有一种情况就是第三次握手由于网络中的种种原因失败了,这时候客户端认为自己已经连接好了,就会给服务端发送数据,服务端由于没有收到第三次握手,就会以RST包对客户端响应,收到RST的的客户端就知道第三次握手没有成功,就会重新连接。在谢希仁著《计算机网络》第四版中讲“三次握手”的目的是“为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误”。

四次握手和两次握手的情况一样,五次握手和三次握手的情况一样,以此类推,奇数次握手的情况与三次握手相同,同理偶数次握手与两次握手一样,所以为了更快的连接,就使用三次握手最合适。

在Google Groups的TopLanguage中看到一帖讨论TCP“三次握手”觉得很有意思。贴主提出“TCP建立连接为什么是三次握手?”的问题,在众多回复中,有一条回复写道:“这个问题的本质是, 信道不可靠, 但是通信双发需要就某个问题达成一致. 而要解决这个问题, 无论你在消息中包含什么信息, 三次通信是理论上的最小值. 所以三次握手不是TCP本身的要求, 而是为了满足"在不可靠信道上可靠地传输信息"这一需求所导致的. 请注意这里的本质需求,信道不可靠, 数据传输要可靠. 三次达到了, 那后面你想接着握手也好, 发数据也好, 跟进行可靠信息传输的需求就没关系了. 因此,如果信道是可靠的, 即无论什么时候发出消息, 对方一定能收到, 或者你不关心是否要保证对方收到你的消息, 那就能像UDP那样直接发送消息就可以了.”。这可视为对“三次握手”目的的另一种解答思路。

每次握手失败的对应


第一次握手失败:
  如果第一次的SYN传输失败,两端都不会申请资源。如果一段时间后之前的SYN发送成功了,这时客户端只会接收他最后发送的SYN的SYN+ACK回应,其他的一概忽略,服务端也是如此,会将之前多申请的资源释放了。

第二次握手失败:
  如果服务端发送的SYN+ACK传输失败,客户端由于没有收到这条响应,不会申请资源,虽然服务端申请了资源,但是迟迟收不到来自客户端的ACK,也会将该资源释放。

第三次握手失败:
  如果第三次握手的ACK传输失败,导致服务端迟迟没有收到ACK,就会释放资源,这时候客户端认为自己已经连接好了,就会给服务端发送数据,服务端由于没有收到第三次握手,就会以RST包对客户端响应。但是实际上服务端会因为没有收到客户端的ACK多次发送SYN+ACK,次数是可以设置的,如果最后还是没有收到客户端的ACK,则释放资源。


b、四次挥手

以下描述不讨论序号和确认号,因为序号和确认号的规则比较简单。并且不讨论 ACK,因为 ACK 在连接建立之后都为 1。

  1. A 发送连接释放报文,FIN=1。

  2. B 收到之后发出确认,此时 TCP 属于半关闭状态,B 能向 A 发送数据但是 A 不能向 B 发送数据。

  3. 当 B 不再需要连接时,发送连接释放报文,FIN=1。

  4. A 收到后发出确认,进入 TIME-WAIT 状态,等待 2 MSL(最大报文存活时间)后释放连接。

  5. B 收到 A 的确认后释放连接。

问:为什么A在TIME-WAIT状态必须等待2MSL的时间呢?


  • 为了保证A发送的最后一个ACK报文段能够到达B。A发送的这个ACK报文段有可能丢失,如果 B 没收到 A 发送来的确认报文,那么A就会重新发送连接释放请求报文,A 等待一段时间就是为了处理这种情况的发生。

  • 防止“已经失效的连接请求报文段”出现在本链接中。A在发送完最后一个ACK报文段后,再经过时间2MSL,就可以使本连接的时间内所产生的所有报文段都从网络中消失。这样下一个新的连接中就不会出现这种旧的连接请求报文段。


c、TCP状态详解


  • CLOSED: 这个没什么好说的了,表示初始状态。

  • LISTEN: 这个也是非常容易理解的一个状态,表示服务器端的某个SOCKET处于监听状态,可以接受连接了。

  • SYN_RCVD: 这个状态表示接受到了SYN报文,在正常情况下,这个状态是服务器端的SOCKET在建立TCP连接时的三次握手会话过程中的一个中间状态,很短暂,基本 上用netstat你是很难看到这种状态的,除非你特意写了一个客户端测试程序,故意将三次TCP握手过程中最后一个ACK报文不予发送。因此这种状态 时,当收到客户端的ACK报文后,它会进入到ESTABLISHED状态。

  • SYN_SENT: 这个状态与SYN_RCVD遥相呼应,当客户端SOCKET执行CONNECT连接时,它首先发送SYN报文,因此也随即它会进入到了SYN_SENT状 态,并等待服务端的发送三次握手中的第2个报文。SYN_SENT状态表示客户端已发送SYN报文。

  • ESTABLISHED: 这个容易理解了,表示连接已经建立了。

  • FIN_WAIT_1(重要): 这个状态要好好解释一下,其实FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。而这两种状态的区别 是:FIN_WAIT_1状态实际上是当SOCKET在ESTABLISHED状态时,它想主动关闭连接,向对方发送了FIN报文,此时该SOCKET即 进入到FIN_WAIT_1状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2状态,当然在实际的正常情况下,无论对方何种情况下,都应该马 上回应ACK报文,所以FIN_WAIT_1状态一般是比较难见到的,而FIN_WAIT_2状态还有时常常可以用netstat看到。

  • FIN_WAIT_2(重要): 上面已经详细解释了这种状态,实际上FIN_WAIT_2状态下的SOCKET,表示半连接,也即有一方要求close连接,但另外还告诉对方,我暂时还有点数据需要传送给你,稍后再关闭连接。

  • TIME_WAIT(重要): 表示收到了对方的FIN报文,并发送出了ACK报文,就等2MSL后即可回到CLOSED可用状态了。如果FIN_WAIT_1状态下,收到了对方同时带 FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。

  • CLOSING: 这种状态比较特殊,实际情况中应该是很少见,属于一种比较罕见的例外状态。正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的 ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什 么情况下会出现此种情况呢?其实细想一下,也不难得出结论:那就是如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报 文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接。

  • CLOSE_WAIT: 这种状态的含义其实是表示在等待关闭。怎么理解呢?当对方close一个SOCKET后发送FIN报文给自己,你系统毫无疑问地会回应一个ACK报文给对 方,此时则进入到CLOSE_WAIT状态。接下来呢,实际上你真正需要考虑的事情是看你是否还有数据发送给对方,如果没有的话,那么你也就可以 close这个SOCKET,发送FIN报文给对方,也即关闭连接。所以你在CLOSE_WAIT状态下,需要完成的事情是等待你去关闭连接。

  • LAST_ACK: 这个状态还是比较容易好理解的,它是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,也即可以进入到CLOSED可用状态了。


四、TCP协议的拆包粘包


TCP 是一种面向连接的、可靠的、基于字节流的传输层通信协议。这种基于流的协议是没有明显边界的,TCP 这种底层协议是不会理解上层的业务业务含义的,因此在通信过程中,发送数据流的时候,有可能出现一份完整的数据,被 TCP 拆分为多个数据包进行发送,当然也有可能将多个数据包合并为一个数据包进行发送。从而产生了拆包以及粘包的问题。


1、拆包和粘包


粘包

业务侧理想的是分别发送三个数据包到服务端,服务端根据不同的数据包进行对应的业务处理。但是实际事与愿违,现实很骨感,实际上服务端接收到的是一个数据包,而且是三个数据包揉在一起的。这种现象就是TCP
粘包。



拆包

这种情况下,服务端理想是要接收一个数据包,但是实际收到了拆解后的两个数据包。这种场景就成为拆包。



2、出现拆包粘包的原因


主要归纳为三个:socket缓冲区与滑动窗口、MSS/MTU限制、Nagle算法


a、socket缓冲区与滑动窗口

每个TCP socket在内核中都有一个发送缓冲区(SO_SNDBUF )和一个接收缓冲区(SO_RCVBUF),TCP的全双工的工作模式以及TCP的滑动窗口便是依赖于这两个独立的buffer的填充状态。


SO_SNDBUF

进程发送的数据的时候假设调用了一个send方法,最简单情况(也是一般情况),将数据拷贝进入socket的内核发送缓冲区之中,然后send便会在上层返回。换句话说,send返回之时,数据不一定会发送到对端去(和write写文件有点类似),send仅仅是把应用层buffer的数据拷贝进socket的内核发送buffer中。


SO_RCVBUF

把接受到的数据缓存入内核,应用进程一直没有调用read进行读取的话,此数据会一直缓存在相应socket的接收缓冲区内。再啰嗦一点,不管进程是否读取socket,对端发来的数据都会经由内核接收并且缓存到socket的内核接收缓冲区之中。read所做的工作,就是把内核缓冲区中的数据拷贝到应用层用户的buffer里面。


滑动窗口

TCP连接在三次握手的时候,会将自己的窗口大小(window size)发送给对方,其实就是SO_RCVBUF指定的值。之后在发送数据的时,发送方必须要先确认接收方的窗口没有被填充满,如果没有填满,则可以发送。



b、MSS/MTU限制

MTU (Maxitum Transmission Unit,最大传输单元)是链路层对一次可以发送的最大数据的限制。MSS(Maxitum Segment Size,最大分段大小)是TCP报文中data部分的最大长度,是传输层对一次可以发送的最大数据的限制。

要了解MSS/MTU,首先需要回顾一下TCP/IP五层网络模型模型:

数据在传输过程中,每经过一层,都会加上一些额外的信息:

  • 应用层:只关心发送的数据DATA,将数据写入socket在内核中的缓冲区SO_SNDBUF即返回,操作系统会将SO_SNDBUF中的数据取出来进行发送。

  • 传输层:会在DATA前面加上TCP Header(20字节)

  • 网络层:会在TCP报文的基础上再添加一个IP Header,也就是将自己的网络地址加入到报文中。IPv4中IP Header长度是20字节,IPV6中IP Header长度是40字节。

  • 链路层:加上Datalink Header和CRC。会将SMAC(Source Machine,数据发送方的MAC地址),DMAC(Destination Machine,数据接受方的MAC地址 )和Type域加入。SMAC+DMAC+Type+CRC总长度为18字节。

  • 物理层:进行传输

在回顾这个基本内容之后,再来看MTU和MSS。MTU是以太网传输数据方面的限制,每个以太网帧最大不能超过1518bytes。刨去以太网帧的帧头(DMAC+SMAC+Type域)14Bytes和帧尾(CRC校验)4Bytes,那么剩下承载上层协议的地方也就是Data域最大就只能有1500Bytes这个值 我们就把它称之为MTU

MSS是在MTU的基础上减去网络层的IP Header和传输层的TCP Header的部分,这就是TCP协议一次可以发送的实际应用数据的最大大小。

MSS = MTU(1500) -IP Header(20 or 40)-TCP Header(20)
复制

由于IPV4和IPV6的长度不同,在IPV4中,以太网MSS可以达到1460byte;在IPV6中,以太网MSS可以达到1440byte。

发送方发送数据时,当SO_SNDBUF中的数据量大于MSS时,操作系统会将数据进行拆分,使得每一部分都小于MSS,也形成了拆包,然后每一部分都加上TCP Header,构成多个完整的TCP报文进行发送,当然经过网络层和数据链路层的时候,还会分别加上相应的内容。

注:

  • eth0需要走以太网,所以MTU是1500;

  • lo是本地回环,不需要走以太网,所以不受1500的限制。

c、Nagle算法

TCP/IP协议中,无论发送多少数据,总是要在数据(DATA)前面加上协议头(TCP Header+IP Header),同时,对方接收到数据,也需要发送ACK表示确认。

即使从键盘输入的一个字符,占用一个字节,可能在传输上造成41字节的包,其中包括1字节的有用信息和40字节的首部数据。这种情况转变成了4000%的消耗,这样的情况对于重负载的网络来是无法接受的。称之为"糊涂窗口综合征"。

Nagle算法的规则:

  1. 如果SO_SNDBUF中的数据长度达到MSS,则允许发送;

  2. 如果该SO_SNDBUF中含有FIN,表示请求关闭连接,则先将SO_SNDBUF中的剩余数据发送,再关闭;

  3. 设置了TCP_NODELAY=true选项,则允许发送。TCP_NODELAY是取消TCP的确认延迟机制,相当于禁用了Negale 算法。正常情况下,当Server端收到数据之后,它并不会马上向client端发送ACK,而是会将ACK的发送延迟一段时间(假一般是40ms),它希望在t时间内server端会向client端发送应答数据,这样ACK就能够和应答数据一起发送,就像是应答数据捎带着ACK过去。当然,TCP确认延迟40ms并不是一直不变的,TCP连接的延迟确认时间一般初始化为最小值40ms,随后根据连接的重传超时时间(RTO)、上次收到数据包与本次接收数据包的时间间隔等参数进行不断调整。另外可以通过设置TCP_QUICKACK选项来取消确认延迟。

  4. 未设置TCP_CORK选项时,若所有发出去的小数据包(包长度小于MSS)均被确认,则允许发送;

  5. 上述条件都未满足,但发生了超时(一般为200ms),则立即发送。


3、通信协议

在了解了粘包、拆包产生的原因之后,现在来分析接收方如何对此进行区分。道理很简单,如果存在不完整的数据(拆包),则需要继续等待数据,直至可以构成一条完整的请求或者响应。

通过定义通信协议(protocol),可以解决粘包、拆包问题。协议的作用就定义传输数据的格式这样在接受到的数据的时候:

  • 如果粘包了,就可以根据这个格式来区分不同的包

  • 如果拆包了,就等待数据可以构成一个完整的消息来处理。

a、定长协议

定长协议:顾名思义,就是指定一个报文的必须具有固定的长度。例如,我们规定每3个字节,表示一个有效报文,如果我们分4次总共发送以下9个字节:

  +--|A|BC|DEFG|HI|--+
复制

那么根据协议,我们可以判断出来,这里包含了3个有效的请求报文,如下:

  +--|ABC|DEF|GHI|--+
复制

在定长协议中:

  • 发送方,必须保证发送报文长度是固定的。如果报文字节长度不能满足条件,如规定长度是1024字节,但是实际需要发送的内容只有900个字节,那么不足的部分可以补充0。因此定长协议可能会浪费带宽。

  • 接收方,每读取到固定长度的内容时,则认为读取到了一个完整的报文。

提示:Netty中提供了FixedLengthFrameDecoder,支持把固定的长度的字节数当做一个完整的消息进行解码

b、特殊字符分隔符协议

在包尾部增加回车或者空格符等特殊字符进行分割 。例如,按行解析,遇到字符\n、\r\n的时候,就认为是一个完整的数据包。对于以下二进制字节流:

   +--|ABC\nDEF\r\n|--+
复制

那么根据协议,我们可以判断出来,这里包含了2个有效的请求报文

   +--|ABC|DEF|--+
复制

在特殊字符分隔符协议中:

  • 发送方,需要在发送一个报文时,需要在报文尾部添加特殊分割符号;

  • 接收方,在接收到报文时,需要对特殊分隔符进行检测,直到检测到一个完整的报文时,才能进行处理。

在使用特殊字符分隔符协议的时候,需要注意的是,我们选择的特殊字符,一定不能在消息体中出现,否则可能会出现错误的拆包例如,发送方希望把”12\r\n34”,当成一个完整的报文,如果是按行拆分,那么就会错误的拆分为2个报文。一种解决策略是,发送方对需要发送的内容预先进行base64编码,由于base64编码只包含64个字符:0-9、a-z、A-Z、+、/,我们可以选择这64个字符之外的特殊字符作为分隔符。

提示:netty中提供了DelimiterBasedFrameDecoder根据特殊字符进行解码。事实上,我们熟悉的的缓存服务器redis,也是通过换行符来区分一个完整的报文。

c、变长协议

将消息区分为消息头和消息体,在消息头中,我们使用一个整形数字,例如一个int,来表示消息体的长度。而消息体实际实际要发送的二进制数据字节。以下是一个基本格式:

  header body+-+| Length|Content |+-
复制

在变长协议中:

  • 发送方,发送数据之前,需要先获取需要发送内容的二进制字节大小,然后在需要发送的内容前面添加一个整数,表示消息体二进制字节的长度。

  • 接收方,在解析时,先读取内容长度Length,其值为实际消息体内容(Content)占用的字节数,之后必须读取到这么多字节的内容,才认为是一个完整的数据报文。

提示:Netty中提供了LengthFieldPrepender给实际内容Content进行编码添加Length字段,接受方使用LengthFieldBasedFrameDecoder解码。

d、序列化

序列化本质上已经不是为了解决粘包和拆包问题,而是为了在网络开发中可以更加的便捷。在变长协议中,我们看到可以在实际要发送的数据之前加上一个length字段,表示实际要发送的数据的长度。这实际上给我们了一个很好的思路,我们完全可以将一个对象转换成二进制字节,来进行通信,例如使用一个Request对象表示请求,使用一个Response对象表示响应。

序列化框架有很多种,我们在选择时,主要考虑序列化/反序列化的速度,序列化占用的体积,多语言支持等。下面列出了业界流行的序列化框架:

框架

支持语言

官网

jdk序列化

Java


hessian

支持多种,不包含go

http://hessian.caucho.com/

fst

Java

https://github.com/RuedigerMoeller/fast-serialization

protobuf

几乎所有主流语言

https://developers.google.cn/protocol-buffers/

protostuff

Java

https://protostuff.github.io/

avro

Java、c\c++、c#、python

http://avro.apache.org

kyro

java

https://github.com/EsotericSoftware/kryo

msgback

几乎所有主流语言

https://msgpack.org/

thrift

几乎所有主流语言

http://thrift.apache.org/

JBoss Marshlling

java

http://jbossmarshalling.jboss.org/

提示:xml、json也属于序列化框架的范畴,上面的表格中并没有列出。

一些网络通信的RPC框架通常会支持多种序列化方式,例如dubbo支持hessian、json、kyro、fst等。在支持多种序列化框架的情况下,在协议中通常需要有一个字段来表示序列化的类型,例如,我们可以将上述变长协议的格式改造为:

+-+|Length|serializer|Content|+-
复制

这里使用1个字节表示Serializer的值,使用不同的值代表不同的框架。

  • 发送方,选择好序列化框架后编码后,需要指定Serializer字段的值。

  • 接收方,在解码时,根据Serializer的值选择对应的框架进行反序列化;

e、压缩

通常,为了节省网络开销,在网络通信时,可以考虑对数据进行压缩。常见的压缩算法有lz4、snappy、gzip等。在选择压缩算法时,我们主要考虑压缩比以及解压缩的效率。

我们可以在网络通信协议中,添加一个compress字段,表示采用的压缩算法:

+-|Length|serializer|compress|Content|+-
复制

通常,我们没有必要使用一个字节,来表示采用的压缩算法,1个字节可以标识256种可能情况,而常用压缩算法也就那么几种,因此通常只需要使用2~3个bit来表示采用的压缩算法即可。

另外,由于数据量比较小的时候,压缩比并不会太高,没有必要对所有发送的数据都进行压缩,只有再超过一定大小的情况下,才考虑进行压缩。如rocketmq,producer在发送消息时,默认消息大小超过4k,才会进行压缩。因此,compress字段,应该有一个值,表示没有使用任何压缩算法,例如使用0。

f、查错校验码

一些通信协议传输的数据中,还包含了查错校验码。典型的算法如CRC32、Adler32等。java对这两种校验方式都提供了支持,java.util.zip.Adler32、java.util.zip.CRC32。

+-|Length|serializer|compress|Content|CRC32|+-
复制

这里并不对CRC32、Adler32进行详细说明,主要是考虑,为什么需要进行校验?

有人说是因为考虑到安全,这个理由似乎并不充分,因为我们已经有了TLS层的加密,CRC32、Adler32的作用不应该是为了考虑安全。

一位同事的观点,我非常赞同:二进制数据在传输的过程中,可能因为电磁干扰,导致一个高电平变成低电平,或者低电平变成高电平。这种情况下,数据相当于受到了污染,此时通过CRC32等校验值,则可以验证数据的正确性。

另外,通常校验机制在通信协议中,是可选的配置的,并不需要强制开启,其虽然可以保证数据的正确,但是计算校验值也会带来一些额外的性能损失。如Mysql主从同步,虽然高版本默认开启CRC32校验,但是也可以通过配置禁用。


五、总结


本来想多聊聊几种协议,甚至把访问一个网站的全流程讲一下,但是太多了,ARP、IP等协议就可以写一篇,TCP协议主要在于好多时候我们需要去调整一些TCP的参数,以确保连接上限及效率等。另外参考文章已经找不到了,之前自己记录的文档,就不写参考了

文章转载自炎炎录,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论