楔子 随着工作经验的积累,你会越来越意识到 TCP/IP 协议的重要性。比如时刻在使用的 HTTP 协议其实只负责包装数据,并不负责数据传输,真正负责传输的是 TCP/IP 协议;再比如 Web 框架,本质上就是一个 Socket,而 Socket 又是对 TCP/IP 协议的一个封装,可以让我们更方便地使用 TCP/IP 协议,而不用关注背后的原理。 所以如果想成为一个高手的话,那么 TCP/IP 协议是必须要了解的。特别是大厂,相比框架,面试官更喜欢问底层的 TCP/IP 协议。那么接下来我们就来看看 TCP/IP 中的 TCP,以及三次握手和四次挥手到底是怎么一回事。 啥是 TCP TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。 这里面有三个关键词,分别解释一下。
那么 TCP 传输的数据格式长什么样子呢? 以上就是 TCP 的报文格式,相比 HTTP 报文(Header + Body),TCP 报文就显得复杂许多。里面有几个字段很重要: 1)序列号:在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,序列号就累加一次。序列号的目的是用来解决网络包乱序问题。 2)确认应答号:指下一次期望收到的数据的序列号,发送端收到这个确认应答后,可以认为在这个序号以前的数据都已经被正常接收,用来解决不丢包的问题。 3)控制位,有以下几个可选值。
这些字段在后续介绍三次握手的时候,就会彻底明白是做什么用的。 为什么需要 TCP 协议?TCP 工作在哪一层? IP 层是不可靠的,它不保证网络包的交付、不保证网络包的按序交付、也不保证网络包中数据的完整性。 如果需要保障网络数据包的可靠性,那么就需要由上层(传输层)的 TCP 协议来负责。因为 TCP 是一个工作在传输层并且可靠的数据传输服务,它能确保接收端接收的网络包是「无损坏、无间隔、非冗余和按序的」。 什么是 TCP 连接? 简单来说,用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合(包括 socket、序列号和窗口大小)称为连接。 所以我们可以知道,建立一个 TCP 连接需要客户端与服务端达成上述三个信息的共识:
如何唯一确定一个 TCP 连接? TCP 四元组可以唯一确定一个连接,四元组包括:源地址、源端口、目的地址、目的端口。其中源地址和目的地址的字段(32 位)是在 IP 头部中,作用是通过 IP 协议发送报文给对方主机;源端口和目的端口的字段(16 位)是在 TCP 头部中,作用是告诉 TCP 协议应该把报文发给哪个进程。 UDP 和 TCP 有什么区别呢? UDP 不提供复杂的控制机制,利用 IP 提供面向「无连接」的通信服务,UDP 协议非常简单,头部只有 8 个字节( 64 位),其报文格式如下:
至于两者的区别有如下几点: 1)连接:TCP 是面向连接的传输层协议,传输数据前要先建立连接;UDP 不需要连接,便可传输数据。 2)服务对象:TCP 是一对一的两点服务,即一个连接只有两个端点;UDP 支持一对一、一对多、多对多的交互通信。 3)可靠性:TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按需到达;UDP 是尽最大努力交付,不保证可靠交付数据; 4)拥塞控制、流量控制:TCP 有拥塞控制和流量控制机制,保证数据传输的安全性;UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。 5)首部开销:TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是 20 个字节,如果使用了「选项」字段则会变得更长;UDP 首部只有 8 个字节,并且是固定不变的,开销较小。 6)应用场景:由于 TCP 是面向连接,能保证数据的可靠性交付,因此经常用于 FTP 文件传输、HTTP / HTTPS;由于 UDP 面向无连接,它可以随时发送数据,再加上 UDP 本身的处理既简单又高效,因此经常用于包总量较少的通信(如 DNS、SNMP )、视频和音频等多媒体通信、广播通信。 再来思考一个问题,我们知道报文由首部(或者说头部)+ 数据组成,但如果对比 TCP 和 UDP 报文,会发现 TCP 头部里面包含了一个首部长度字段,而 UDP 却没有,这是为什么呢?其实很简单,原因是 TCP 有可变长的「选项」字段,所以 TCP 报文的头部长度不固定,因此需要额外记录;但 UDP 的头部长度则是不会变化的,因此无需多一个字段去记录。 还有一个问题,为什么 TCP 里面没有包长度这个字段,而 UDP 却有呢?要回答这个问题,需要先说说 TCP 如何计算负载数据的长度: 数据从应用层开始自上而下传输的时候,每一层都会加上该层的头部信息,比如 TCP 报文在经过 IP 层传输的时候,会加上一个 IP 头部,得到 IP 报文,就是一个不断封装的过程。 所以 IP报文总长 - IP头部长度,得到的就是 TCP 报文长度(TCP头部+TCP数据)。如果再减去 TCP 头部长度,就是 TCP 数据长度,显然它又是上一层的报文长度(头部+数据)。 由于 IP 报文长度 和 IP 首部长度,在 IP 首部是已知的,而 TCP 首部长度,在 TCP 首部也是已知的,所以就可以求得 TCP 数据的长度。既然可以求得,那完全没必要用一个字段,单独保存它。 这时可能有人就奇怪了:UDP 也是基于 IP 层的,并且其首部是固定的,那 UDP 的数据长度也可以通过这个公式计算呀,为何还要有「包长度」呢? 其实 UDP 包长度确实是冗余的,但为了网络设备硬件设计和处理方便,首部长度需要是 4 字节的整数倍。如果去掉包长度字段,那 UDP 首部长度就不是 4 字节的整数倍了,所以补充了包长度字段。 TCP 连接是如何建立的 TCP 是面向连接的协议,所以使用 TCP 前必须先建立连接,而建立连接是通过三次握手进行的。整个过程的示意图如下:
一开始,客户端和服务端都处于 CLOSED 状态,然后服务端主动监听某个端口,处于 LISTEN 状态。接下来当客户端和服务端建立连接时,就开始三次握手了,我们来解释一下每个步骤都干了什么事情。 1)客户端会随机初始化一个序列号(client_isn),并将其置于 TCP 首部的序列号字段中,同时把 SYN 标志位设置成 1,表示这是一个 SYN 报文。然后将这个 SYN 报文发送给服务端,表示要和服务端建立连接,发送之后客户端处于 SYN_SENT 状态。注意:该报文不包含应用层数据。 2)服务端收到客户端的 SYN 报文之后,也会随机初始化自己的序列号(server_isn),并填入 TCP 首部的序列号字段中。其次把 TCP 首部的确认应答号设置为 client_isn + 1,把 SYN 和 ACK 标志位设置为 1。最后把报文发给客户端,该报文也不包含应用层数据,发送之后服务端处于 SYN_RCVD 状态。 3)客户端收到服务端报文后,还要向服务端回应一个应答报文,首先将应答报文的 TCP 首部的 ACK 标志位设置为 1,其次将确认应答号设置为 server_isn + 1,最后把报文发送给服务端,之后客户端处于 ESTABLISHED 状态。 服务端收到客户端的应答报文后,也进入 ESTABLISHED 状态。 从上面的过程可以发现:第三次握手可以携带数据,但前两次握手是不可以携带数据的,这也是面试常问的题。一旦完成三次握手,双方都处于 ESTABLISHED 状态,至此连接就已建立完成,客户端和服务端就可以相互发送数据了。 关于这里面一些细节,一会儿再详细聊,比如这里的应答号和确认应答号究竟有什么用。 如何在 Linux 系统中查看 TCP 状态 TCP 的连接状态,在 Linux 可以通过 netstat -napt 命令查看。
为什么握手需要三次,而不是两次、四次 相信大多数人的回答是:因为三次握手才能保证双方具有接收和发送的能力,这种回答是没有问题的,但比较片面,没有说出主要的原因。 在前面我们知道了什么是 TCP 连接:用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合(包括 socket、序列号、窗口大小)称为连接。所以重点是为什么要三次握手才可以初始化 socket、序列号、窗口大小,并建立 TCP 连接。 主要有以下三个原因:
下面逐一解释。 原因一:避免历史连接 我们来看看 RFC 793 指出的 TCP 连接使用三次握手的首要原因:The principle reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion。简单来说,三次握手的首要原因是为了防止旧的重复连接初始化造成混乱。 由于网络环境是错综复杂的,往往并不是如我们期望的一样,先发送的数据包,就先到达目标主机。反而它很骚,可能会由于网络拥堵等乱七八糟的原因,会使得旧的数据包,先到达目标主机,那么这种情况下 TCP 三次握手是如何避免的呢? 客户端连续发送多次 SYN 建立连接的报文,在网络拥堵等情况下:
而如果是两次握手,就不能判断当前连接是否是历史连接,三次握手则可以在客户端(发送方)准备发送第三次报文时,有足够的上下文来判断当前连接是否是历史连接:
所以, TCP 使用三次握手建立连接的最主要原因是「防止历史连接初始化」。 原因二:同步双方初始序列号 TCP 协议的通信双方, 都必须维护一个「序列号」, 序列号是可靠传输的一个关键因素,它的作用:
可见,序列号在 TCP 连接中占据着非常重要的作用,所以当客户端发送携带「序列号」的 SYN 报文的时候,需要服务端回一个 ACK 应答报文(其中的确认应答号必须等于 SYN 报文中的序列号 + 1),表示客户端的 SYN 报文已被服务端成功接收。同理当服务端发送「序列号」给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号被可靠的同步。
咦,这貌似变成了四步啊,不是三次握手吗?没错,只是第二步和第三步合在一起了,服务端在收到 SYN 报文后发送的是 SYN + ACK 报文。 四次握手其实也能够可靠地同步双方的序列号,但由于「第二步和第三步可以优化成一步」,所以就成了「三次握手」。而两次握手只保证了一方的序列号能被另一方成功接收,没办法保证双方的序列号都能被彼此确认接收。 原因三:避免资源浪费 如果只有「两次握手」,那么当客户端的 SYN 请求连接在网络中阻塞,一直没有接收到来自服务端的 ACK 报文时,就会重新发送 SYN。而由于没有第三次握手,服务器不清楚客户端是否收到了自己发送的建立连接的 ACK 确认信号,所以只能每收到一个 SYN 就主动建立一个连接。 因此,如果客户端的 SYN 阻塞了,重复发送多次 SYN 报文,那么服务器在收到请求后就会「建立多个冗余的无效链接,造成不必要的资源浪费」。 我们举个例子,如果是两次握手,看看会出现什么情况,由于比较简单,就不画图了。
所以两次握手会在消息滞留情况下,造成服务器重复接受无用的连接请求(SYN 报文),而重复分配资源。 总结,TCP 建立连接时,通过三次握手「能防止历史连接的建立,能减少双方不必要的资源开销,能帮助双方同步初始化序列号」。序列号能够保证数据包不重复、不丢弃和按序传输。而不使用「两次握手」和「四次握手」的原因:
序列号 ISN 是如何随机产生的? 第一次产生的 ISN 也被称为初始序列号,它是基于时钟的,每 4 毫秒 + 1,转一圈要 4.55 个小时。RFC1948 中提出了一个较好的初始化序列号随机生成算法: 另外客户端和服务端的初始序列号 ISN 是不相同的,因为网络中的报文「会延迟、会复制重发、也有可能丢失」,这样会造成的不同连接之间产生互相影响。所以为了避免互相影响,客户端和服务端的初始序列号是随机且不同的。 既然 IP 层会分片,为什么 TCP 层还需要 MSS 呢? 我们先来认识下 MTU 和 MSS:
当我们想发送一些 TCP 数据时,它要和 TCP 头部组合起来,形成 TCP 报文。然后 TCP 报文再进入网络层,和 IP 头部组合起来,形成 IP 报文,因为每一层都会加上自己的头部信息。而网络层每次发送的最大数据量是 1500 字节(被称为 MTU),MTU 减去 IP 和 TCP 的头部长度之后,就是 TCP 数据的最大长度(被称为 MSS)。 假设现在要传输的 TCP 数据的长度大于 MSS,会有什么结果呢?显然此时网络层要传输的 IP 报文会大于 MTU,那么 IP 层就要进行分片,把数据分成若干片,保证每一个分片都小于 MTU。将一个个分片传过去之后,再由目标主机的网络层来进行重新组装,并交给上一层的 TCP 传输层。 这看起来井然有序,但其实存在隐患,如果一个 IP 分片丢失,整个 IP 报文的所有分片都得重传。因为 IP 层本身没有超时重传机制,当 IP 分片出现丢失时,那么接收方拿到的 TCP 报文也是不完整的,因此不会响应 ACK 给对方。而发送方就要一直等待,直到 TCP 超时,而超时的后果就是重发整个 TCP 报文。 因此不难发现,由 IP 层进行分片传输,是非常没有效率的。所以为了达到最佳的传输效能,TCP 协议在建立连接的时候通常要协商双方的 MSS 值,当 TCP 层发现数据超过 MSS 时,则先进行分片,使得由它形成的 IP 包的长度不会大于 MTU ,自然也就不用 IP 分片了。 并且 TCP 是有超时重传机制的,如果一个 TCP 分片丢失,重发时也是以 MSS 为单位,而不用重传所有的分片,大大增加了重传的效率。 什么是 SYN 攻击?如何避免 SYN 攻击? 我们都知道 TCP 建立连接需要三次握手,假设攻击者短时间伪造不同 IP 地址的 SYN 报文,而服务端每接收到一个 SYN 报文,就会回复一个 ACK + SYN 报文,并进入SYN_RCVD 状态。 但如果服务端发送出去的 ACK + SYN 报文,无法得到未知 IP 主机的 ACK 应答,久而久之就会占满服务端的 SYN 接收队列(未连接队列),使得服务器不能为正常用户服务。 那如何避免呢?可以通过如下两种方式。 避免 SYN 攻击方式第一种: 其中一种解决方式是通过修改 Linux 内核参数,控制队列大小和当队列满时应做什么处理。
避免 SYN 攻击方式第二种: 我们先来看下 Linux 内核的 SYN(未完成连接建立)队列与 Accpet(已完成连接建立)队列是如何工作的? 正常流程:
但如果不断受到 SYN 攻击,就会导致「SYN 队列」被占满。 当收到客户端的 ACK 报文之后会将连接从 「SYN 队列」移除并放入到 「Accept 队列」,但 SYN 攻击的特点就是在发送完 SYN 报文之后故意不发 ACK 报文,因此最终 SYN 队列会被塞满,Accept 队列会为空。
那么如何解决这一点呢?答案是启动 cookie。 通过设置 net.ipv4.tcp_syncookies = 1 实现。
tcp_syncookies 参数的取值有三种,值为 0 时表示关闭该功能,2 表示无条件开启功能,1 则表示仅当 SYN 半连接队列放不下时,再启用它。 另外 tcp_syncookie 仅用于应对 SYN 泛洪攻击(攻击者恶意构造大量的 SYN 报文发送给服务器,造成 SYN 半连接队列溢出,导致正常客户端的连接无法建立),因为这种方式建立的连接,许多 TCP 特性都无法使用。所以应当把 tcp_syncookies 设置为 1,仅在 SYN 队列满的时候启用。 以上就是 TCP 三次握手相关的内容。 TCP 连接是如何断开的 天下没有不散的宴席,对于 TCP 连接也是这样, TCP 断开连接是通过「四次挥手」的方式实现的。而双方都可以主动断开连接,断开连接后主机中的「资源」将被释放。 我们来看一下 TCP 四次挥手过程以及状态变迁。
可以看到,每个方向都需要「一个 FIN」和「一个 ACK」,因此通常被称为「四次挥手」。注意:主动关闭连接的,才有 TIME_WAIT 状态。 为什么挥手需要四次? 再来回顾下四次挥手双方发 FIN 包的过程,就能理解为什么需要四次了?
从上述过程可以得出,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK 和 FIN 一般都会分开发送,从而比三次握手导致多了一次。 所以如果面试官问你:为什么握手要三次,挥手要四次,你就可以这么回答:「因为通过三次握手建立连接时尚未涉及数据的传输,因此服务端的 ACK 和 SYN 是一起发送的;而四次挥手断开连接时,服务端可能还有数据没发送完,因此需要先回复 ACK 表示同意断开连接,等到数据传输完毕时,再发送 FIN 真正断开连接,这两步是分开的。所以握手要三次,挥手要四次」。 为什么需要 TIME_WAIT 状态? 主动发起关闭连接的一方,才会有 TIME_WAIT 状态,而需要 TIME_WAIT 状态,主要基于两个原因:
我们分别解释一下。 原因一:防止旧连接的数据包 假设 TIME-WAIT 没有等待时间或时间过短,被延迟的数据包抵达后会发生什么呢? 如上图黄色框框显示的那样,服务端在关闭连接之前发送的 SEQ = 301 报文,被网络延迟了,这时有相同端口的 TCP 连接被复用后,被延迟的 SEQ = 301 抵达了客户端,那么客户端是有可能正常接收这个过期的报文,这就会产生数据错乱等严重的问题。 所以 TCP 就设计出了这么一个机制,经过 2MSL(关于这个时间是怎么来的,一会说),足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。 原因二:保证连接正确关闭 其实在四次挥手示意图中应该就能发现端倪,我们说服务端在传输完数据之后会发送 FIN 表示正式关闭连接,然后处于 LAST_ACK 状态,等待客户端的最后一次确认。如果客户端再发送一次 ACK 给服务端,那么服务端收到之后就会进入 CLOSED 状态,但问题是这最后一次 ACK 报文如果在网络中丢失了该怎么办? 如果没有 TIME_WAIT,那么客户端把 ACK 报文发送之后就进入 CLOSED 了,但 ACK 报文并没有到达服务端,这时服务端就会一直处于 LAST_ACK 状态。那么如果后续客户端再发起新的建立连接的 SYN 报文后,服务端就不会再返回 SYN + ACK 了,而是直接发送 RST 报文表示终止连接的建立。 因此客户端在发送完 ACK 之后不能直接 CLOSED,而是要等一段时间,如果服务端在发「FIN 关闭连接报文」之后的规定时间内没有收到来自客户端的 ACK 报文,那么服务端就知道这个 ACK 报文在网络中丢失了,此时会重新给客户端发送 FIN 报文。 所以客户端要等待(此时处于 TIME_WAIT 状态),因为它不知道自己最后发送的 ACK 报文是否成功抵达服务端,它只知道服务端收不到 ACK 报文时,会再度给自己发送 FIN 报文,因此只能默默等待 2MSL(发送 ACK 加上当服务端收不到时返回 FIN,整个过程最多 2MSL)。 如果再次收到服务端的 FIN,那么它要再次发送 ACK,但如果等了 2MSL 后,服务端没有再次发送 FIN,那么它就知道自己上一次发送的 ACK 被服务端成功接收了,此时也会进入 CLOSED 状态。至此,四次挥手完成,客户端和服务端之间连接断开。 为什么 TIME_WAIT 等待的时间是 2MSL? MSL 是 Maximum Segment Lifetime,即报文最大生存时间,它是报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 基于是 IP 协议的,而 IP 头中有一个 TTL 字段,是 IP 数据报可以经过的最大路由数,每经过一个处理它的路由器,此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。
TIME_WAIT 等待 2 倍的 MSL,比较合理的解释是:网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以「一来一回需要等待 2 倍的时间」。比如主动断开连接方最后要发一个 ACK 报文给被动关闭方,最多需要 1MSL,但如果被动关闭方在 1MSL 内没有收到,就会触发超时并向主动断开连接方重发 FIN 报文,这又是 1MSL,总共正好 2 个 MSL。 因此 TIME_WAIT(2MSL)是从客户端接收到 FIN 后发送 ACK 开始计时的,如果在 TIME_WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时。 那么问题来了,这个 2MSL 到底是多长时间呢?在 Linux 系统里 2MSL 默认是 60 秒,那么一个 MSL 就是 30 秒,所以 Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒。 TIME_WAIT 是一个宏,位于 include/net/tcp.h 中,如果要修改 TIME_WAIT 的时间长度,只能修改 Linux 内核代码里 TCP_TIMEWAIT_LEN 的值,并重新编译 Linux 内核。 TIME_WAIT 过多有什么危害? 如果服务器有处于 TIME_WAIT 状态的 TCP,则说明是由服务器方主动发起的断开请求。而过多的 TIME_WAIT 状态的主要危害有两种:
第二个危害会造成严重后果,要知道端口资源是有限的,一般可以开启的端口为 32768~61000,也可以通过参数 net.ipv4.ip_local_port_range 指定。 总之,如果服务端 TIME_WAIT 状态过多,占满了所有端口资源,则会导致无法创建新连接。 如何优化 TIME_WAIT? 这里给出优化 TIME_WAIT 的几个方式,都是有利有弊:
下面分别解释一下。 第一种方式: 下面这个 Linux 内核参数开启后,则可以让处于 TIME_WAIT 的 socket 为新的连接所用。 net.ipv4.tcp_tw_reuse = 1 这个时间戳的字段是在 TCP 头部的「选项」里,用于记录 TCP 发送方的当前时间戳和从另一端接收到的最新时间戳。由于引入了时间戳,我们在前面提到的 2MSL 问题就不复存在了,因为重复的数据包会因为时间戳过期被自然丢弃。
第二种方式: net.ipv4.tcp_max_tw_buckets 的值默认为 18000,当系统中处于 TIME_WAIT 的连接一旦超过这个值时,系统就会将所有的 TIME_WAIT 连接状态重置,我们可以改变这个值。但这个方法过于暴力,而且治标不治本,带来的问题远比解决的问题多,不推荐使用。 第三种方式: 我们可以通过设置 socket 选项,来设置调用 close 关闭连接行为。
如果 l_onoff 为非 0, 且 l_linger 值为 0,那么调用 close 后,会立该发送一个 RST 标志给另一端,该 TCP 连接将跳过四次挥手,也就跳过了 TIME_WAIT 状态,直接关闭。虽然这为跨越 TIME_WAIT 状态提供了一个可能,不过是一个非常危险的行为,不值得提倡。 struct linger 结构体非常简单,只有 l_onoff 和 l_linger 两个成员,其定义在include/linux/socket.h 中。 如果已经建立了连接,但是客户端突然出现故障了怎么办? TCP 有一个「保活机制」,这个机制的原理是这样的:定义一个时间段,在这个时间段内,如果连接没有任何活动,TCP 保活机制会开始作用。每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。 在 Linux 内核可以有对应的参数可以设置保活时间、保活探测的次数、保活探测的时间间隔,以下都为默认值: # 表示保活时间是 7200 秒(2 小时) 也就是说在 Linux 系统中,最少需要经过 2 小时 11 分 15 秒才可以发现一个「死亡」连接。 显然这个时间是有点长的,我们也可以根据实际的需求,对以上的保活相关的参数进行设置。如果开启了 TCP 保活,需要考虑以下几种情况: 1)对端程序是正常工作的。当 TCP 保活的探测报文发送给另一端,另一端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。 2)对端程序崩溃并重启。当 TCP 保活的探测报文发送给另一端后,另一端是可以响应的,但由于没有该连接的有效信息,会产生一个 RST 报文,这样很快就会发现 TCP 连接已经被重置。 3)对端程序崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给另一端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡。 socket 编程 socket 是什么我们已经说过了,下面来看看如何使用 socket 进行编程。
我们使用 Python 来演示一下这个过程,首先是服务端:
接下来编写客户端: import socket 启动服务端和客户端进行测试: 还是比较简单的,当然我们这里的服务端每次只能服务一个客户端,如果想服务多个客户端的话,那么需要为已连接套接字单独开一个线程和客户端进行通信,然后主线程继续执行 accept 等待下一个客户端。 listen 时候的参数 backlog 的意义? 根据上面的 socket 流程图,我们可以得知 Linux 内核会维护两个队列:
在早期 Linux 内核 backlog 是 SYN 队列大小,也就是未完成的队列大小。但从 Linux 内核 2.2 之后,backlog 变成 Accept 队列,也就是已完成连接建立的队列长度,所以现在通常认为 backlog 是 Accept 队列。 accept 发送在三次握手的哪一步? 回顾一下三次握手的规则:
从上面的描述过程,我们可以得知客户端 connect 成功返回是在第二次握手之后,服务端 accept 成功返回是在第三次握手成功之后。 客户端调用 close 了,连接是断开的流程是什么? 过程分为如下:
小结 以上就是 TCP 的三次握手和四次挥手,在面试中是非常容易被问的点,我们要明白这背后的整个过程。此外三次握手和四次挥手还存在可以优化的地方,我们以后再说。 本文转载自公众号《小林coding》 |
|
来自: 新用户0118F7lQ > 《微信文章》