TCP三握四挥知识拓展
TCP与UDP、三握四挥的拓展
TCP 基本认识

1. TCP 头格式
参考:https://blog.csdn.net/zhouzhenhe2008/article/details/71075969
https://www.cnblogs.com/azraelly/archive/2012/12/25/2832393.html

序列号:在初始建立连接时计算机随机分配一个值作为初始值,之后每发送一次数据就加上此次发送数据的字节数大小。用来解决包乱序问题。
确认号:表示下一次希望接收的数据的序号。发送方通过确认号来确定此序号之前的所有数据都被接收了,用来确认传输过程不丢包。
控制位:(URG、ACK、PSH、SYN、RST、FIN)
URG:为1表示有紧急数据需要发送,结合紧急指针一起使用;
ACK:初始建立连接时为0,之后整个数据传输过程都为1,表示确认号有效;
PSH:为1表示本次传送的数据不为空,真正的有数据需要传输;
SYN:为1表示请求建立连接;
RST:为1表示tcp连接时出现了问题,需要重新建立连接;
FIN:为1表示通信过程结束,请求释放连接。
窗口大小:指的是接收窗口的大小,用来进行流量控制,解决拥塞问题。
校验和:对整个TCP报文段进行检验,包括头和数据部分,并由目标端进行验证。(这里和IP数据报不一样,IP数据报只检验首部);
紧急指针:有紧急数据需要传送,是一个偏移量,和序号字段的值相加表示紧急数据最后一个字节的需要。
选项:长度不确定,最大为40字节(和IP数据报的可选部分类似)。
2. 为什么需要TCP协议?工作在哪一层?
因为网络层是一种不可靠的传输(传输的数据没有序号进行标注),不能够保证网络包按序、完整的交付;那么就需要上层(传输层)来保证传输的可靠性。
TCP是一个工作在传输层的基于连接的可靠数据传输服务,它能够保证传输的数据包的完整有序。
3. 什么是TCP?
TCP 是工作在传输层的一种面向连接、可靠的、基于字节流的通信协议。
- 面向连接:只能是一对一的连接,不能像UDP 那样,可以一个主机向多个主机发送数据。
- 可靠的:无论传输链路如何变化,都能保证数据完整的、有序的到达接收端;
- 基于字节流:消息是没有边界的,无论需要传输的消息有多大都可以传输,不像UDP只能传输上层的报文(不能修改或者合并);而且是有序的,即使后面的字节先到达也不能扔给应用层去处理,必须等到所有数据有序抵达后才能交给上层。
4. 什么是TCP连接?
连接的定义:连接是用于保证可靠性和流量控制所维护的某些状态信息,这些信息的组合,包括 socket、序列号和窗口大小称为连接。
所以建立一个 TCP连接需要客户端和服务器端达成三个信息的共识:
(1)socket:由ip地址和端口号组成;
(2)序列号:保证传输有序;
(3)窗口大小:用来做流量控制;
5. 如何唯一确定一个TCP连接?
通过 TCP 四元组来唯一确定一个连接,四元组包括:
- 源地址
- 目的地址
- 源端口
- 目的端口
源地址和目的地址在 IP数据报的首部中指明,作用是通过IP协议发送报文给对方主机;
源端口和目的端口在 TCP报文的首部中指明,作用是告诉TCP协议应该把报文发送给哪个进程。
6. 有一个IP的服务器监听了一个端口,它的 TCP的最大连接数是多少?
服务器通常在某个固定的端口等待监听客户端请求。
客户端IP 和端口是可变的,理论计算公式如下:
最大TCP连接数=客户端的IP数 × 客户端的端口数
对 IPv4,客户端的 IP 数最多为 2 的 32 次方,客户端的端口数最多为 2 的 16 次方,也就是服务端单机最大 TCP 连接数,约为 2 的 48 次方。
当然服务端最大并发TCP连接数远不能达到理论上限。
- 首先主要是文件描述符限制,Socket都是文件,所以首先要通过 ulimit 配置文件描述符的数目;
- 另一个是内存限制,每个TCP都要占用一定的内存,操作系统内存是有限的。
7. UDP和TCP的区别,分别的运用场景?
介绍一下UDP:
UDP不提供复杂的控制机制,利用IP 提供无连接的通信服务;
UDP协议非常简单,头部只有8个字节。

(1)源端口号、目的端口号:告诉UDP协议应该将报文发送给哪个进程;
(2)包长度:该字段保存了UDP首部的长度跟数据的长度之和;
(3)校验和:校验和是为了提供可靠的UDP首部和数据而设计。
UDP和TCP的区别:
- 连接
- TCP是面向连接的传输层协议,传输数据前要先建立连接;
- UDP是不需要连接,即刻传输数据;
- 服务对象
- UDP是一对一或者一对多或者多对多进行传输;
- TCP只能是一对一传输;
- 可靠性
- TCP是可靠交付数据的,数据可以无差错、不丢失、不失序、不重复、按需到达;
- UDP是尽最大努力交付,不保证可靠交付数据。
- 拥塞控制、流量控制
- TCP 通过拥塞控制和流量控制来保证数据传输的安全性;
- UDP没有,即使网络非常阻塞,也不会影响UDP传输速率;
- 首部开销
- TCP首部固定20个字节,另外选项字段不固定;
- UDP首部总共8个字节,固定不变开销小;
- 传输方式
- TCP是基于字节流的传输,数据传输单位是报文段,在网络层封装分组进行传输。
- UDP传输单位是用户数据报,在网络层封装为数据包进行传送。
- 分片不同
- TCP的数据大小如果大于MSS(最大报文段长度),就会在传输层进行分片,目标主机收到后会重新组装完数据,接着再传给传输层。如果中途丢失了分片,只需要重传这个分片即可。
- UDP的数据大小如果大于MTU,就会在网络层进行分片,目标主机收到后在IP层组装数据,再传输给传输层。如果中途丢失了分片,就需要重传所有数据包,效率低。
TCP和UDP的应用场景:
TCP是面向连接的,常用于保证可靠性交付的数据传递,包括:
- FTP文件传输;
- HTTP/HTTPS;
UDP面向无连接,随时发送数据:
- 包总量较少的传输,比如SNMP、DNS;
- 视频、音频传输;
- 广播通信。
SNMP 详解:
http://c.biancheng.net/view/6467.html
8.为什么UDP首部没有首部长度字段,而TCP有
因为 TCP 首部有可变长的[选项]字段;而UDP头部长度是固定不会变化的。
9.为什么 UDP首部有包长度字段,而TCP首部没有呢
先了解下TCP是如何计算负载数据的长度的:
TCP的数据长度 = IP总长度 - IP首部长度 - TCP首部长度
其中变量都是已知的可以直接求出TCP的数据长度;
同理UDP也可以通过这种方式计算,但是UDP的首部组成为源端口(16位)、目的端口(16位)、包长度(16位)、校验和(16位),如果去掉了包长度字段那么剩下的48位(6字节)不能满足网络硬件设备的设计需要。
网络硬件设备为了处理的方便,首部长度需要设计为4字节的整数倍,去掉包长度字段就不能达到这个要求。
TCP 连接建立
★ 1. TCP三次握手的过程和状态变迁
TCP是面向连接的通信协议,所以在使用TCP前必须先建立连接,而连接的建立是通过三次握手来进行的。

(1)初始状态时,客户端和服务端都处于 CLOSED
状态。先是服务端主动监听某个端口,处于 LISTEN
状态。

(2)客户端随机初始化一个序号client_isn
作为TCP首部的 [序号] 字段,同时把 SYN 设置为1表示正在请求建立连接。然后将该 SYN 报文发送给服务端,表示请求建立连接,该报文中不含有应用层数据,之后客户端处于 SYS_SENT
状态。

(3)服务器在接收到客户端的 SYN
报文请求后,首先服务端也开始随机初始化自己的序号server_isn
,将此序号填写到TCP首部的序列号字段中;然后把 client_isn+1
作为应答号也填写到TCP首部的确认应答号中;最后把 ACK
和SYN
都设置为1。最后将报文发送给客户端,该报文也不含有应用层数据,之后服务器处于 SYS_RCVD
状态。

(4)客户端在接收到服务器的报文后,还要向服务端回应最后一个应答报文。首先该应答报文的TCP
首部的ACK
字段会设置为1,然后确认应答号字段填入server_isn+1
,最后把报文发送给服务端,这次报文可以携带应用层数据自己进入ESTABLISED
状态。
(5)服务器接收到客户端的响应报文后也进入ESTABLISED
状态。
关键点:上面三次握手中,前两次握手都不会携带应用层的数据,只有第三次可以携带应用层数据。
2. 如何在Linux 系统中查看 TCP 状态?
可以通过 netstat -napt
的命令查看
从左到右依次是:TCP协议、源地址+端口、目标地址+端口、连接状态、Web服务的进程PID和进程名称
★ 3. 为什么是三次握手?不是两次?四次?
回答 “因为只有三次握手才能保证双方都具备接受和发送能力” 你就GG了。
原因有三个方面:
- 三次握手可以防止历史连接的初始化(主要原因)
- 三次握手才可以同步双方的初始化序列号
- 三次握手才可以避免浪费资源
原因一:避免历史连接
简单来说,三次握手的首要目的就是为了防止旧的重复连接的初始化造成的混乱。

假设客户端发送一个网络连接到服务器请求建立连接,但是由于网络拥堵的情况,可能长时间未接收到服务端的响应,因此客户端会重复多次发送SYN 报文请求。
- 如果此时一个 [旧 SYN 报文]比[新 SYN 报文]早一点到达了服务端;(假设旧报文的序号为90,新报文的序号为100);
- 那么此时服务端就会返回一个
SYN+ACK
报文交给客户端;(此时确认号为91) - 客户端接收到响应报文后,发现预期的确认号和当前接受的确认号不匹配,由此可以判断出这是一个历史连接,那么客户端应该返回服务端一个
RST
报文来终止此连接,请求重新建立连接。
如果是两次握手连接,就不能判断当前连接是否是历史连接,三次握手则可以在客户端(发送方)准备发送第三次报文时,根据需要发送的报文判断当前连接是否是历史连接:
- 如果是历史连接,则第三次握手发送的是
RST
报文,以此终止历史连接; - 如果不是历史连接,则第三次发送的报文是
ACK
报文,通信双方就会成功建立连接。
原因二:同步双方初始序列号
TCP通信的双方,都必须维护一个 [序列号],序列号是可靠传输的一个关键因素,它的作用为:
- 接收方可以去除重复的数据;
- 接收方可以根据数据的序列号按序接收;
- 可以标识发送出去的数据包中,哪些是已经被对方收到的。
可见,序列号在 TCP 连接中占据着非常重要的作用,所以当客户端发送携带 [初始序列号] 的 SYN
报文的时候,需要服务端返回一个ACK
应答报文,表示服务端成功接收到了客户端的 SYN
报文;同时服务端需要发送一个携带 [初始序列号] 的SYN
报文给客户端的时候,依然也要得到客户端的响应,这样一来一回的过程才能够保证初始序列号能够被可靠的同步。
四次握手其实也能够保证双方序列号的同步,但是由于第二步和第三步可以合并为一步,所以就成了三次握手。
而两次握手只保证一方的初始序列号能够被对方接收,没办法保证双方的序列号都能够被确认接收。
原因三:避免资源的浪费
如果只有 [两次握手],当客户端的SYN
请求在网络中阻塞。
对于客户端而言:客户端指定时间内没有接收到 ACK
报文,就会重新发送SYN
报文;
对于服务端而言,由于没有第三次握手,所以它不能判断客户端是否成功接收到了自己发送的ACK
报文,所以每当有一个SYN
报文到达,它都会主动去建立一个连接。
结果就是初始客户端发送的多次 SYN
请求,服务端都必须一一响应。这样会造成服务器在接收请求后建立多个冗余的无效连接,造成不必要的资源浪费。

小结:
不使用两次握手或四次握手的原因:
- 两次握手:无法防止历史连接的创建,会造成双方资源的浪费,无法确保双方序列号的同步;
- 四次握手:四次握手虽然不会出现问题,但是多建立了一次没有必要的连接,浪费了资源。
4. 为什么客户端和服务端的初始序列号 ISN 是不相同的?
因为如果一个已经失效的连接被重用了,但是该旧连接的历史报文还残留在网络中。如果序列号相同,就无法分辨出当前报文是新报文还是历史报文,如果历史报文被新的连接接受了,则会产生数据混乱。
所以,每次建立连接前重新初始化当前的序列号主要就是为了通信双方能够根据序号将不属于本连接的报文段丢弃,防止网络数据混乱;
另一方面为了防止黑客伪造相同序列号的 TCP报文被对方接收。
5. 初始序列号ISN是如何随机产生的?
起始的 ISN
是基于时钟的,每4毫秒 +1,转一圈需要 4.55个小时;
后来提出了一个较好的初始化序列号ISN
随机生成算法。
ISN = M+F(localhost,localPort,Remotehost,RemotePort)
- M是上述的计时器,每 4毫秒 +1;
- F是一个 Hash 算法,根据源 IP地址、源端口号、目的 IP地址、目的端口号生成一个随机数值。要保证Hash算法不能被外界轻易算出,可以使用 MD5加密。
6. 既然 IP层会分片,为什么TCP还需要MSS?
首先介绍下 MTU
、MSS
的概念以及它们之间的关系:

MTU
:最大传输单元,一个网络数据包的最大长度,以太网中一般是 46~1500个字节;MSS
:最大报文段长度,除去 IP头部、TCP头部之后剩余的 TCP数据部分的最大长度。
如果将 TCP的整个报文(头部+数据)交给 IP层进行分片,会出现什么异常呢?
由于传输层不会分片,因此在IP层会等到数据长度达到 MTU
时进行分片处理,将报文分成若干片,每一片都小于 MTU
。把一份 IP数据报进行分片后,由目标主机的 IP层来进行重新组装,然后再交给上一层 TCP传输层。
这看起来没有问题,那如果传送的分片中有一个丢失了呢?
IP层不是可靠的传输,因此一旦数据包分片丢失,整个IP报文的所有分片都必须重新传输;而传输层的TCP协议是面向连接的,当报文段分片发送失败时,能够进行超时重传。
只在IP层分片:
当接收方发现 TCP报文(数据+头部)的某一片段丢失后,不会立刻响应ACK
给对方,发送方的TCP在超时后,就会重发整个TCP报文。因此可以得知由 IP层进行分片传输是十分没有效率的。
在传输层分片:
如果双方协商好了 MSS
值,且当 TCP层发现数据超过 MSS
时,就会先进行分片,那么由它形成的IP数据包的长度就不会大于 MTU
。这样即使发生了数据丢失,重新传输的也只是一个分片,大大增加了重传的效率。
7. 什么是SYN攻击,如何避免SYN攻击?
什么是SYN攻击:
TCP的连接的建立需要三次握手,假设攻击者伪造不同的 SYN
报文,服务端每接收到一个SYN
报文,就会进入到 SYN_RCVD
状态。但是服务端发送出去的 ACK+SYN
报文又没有响应,久而久之客户端发来的 SYN
报文就会占满接收队列(队列中存储的都是等待建立的连接)。
避免SYN攻击方式(一):
其中一种解决方式是通过修改 Linux 内核参数,控制队列大小和当队列满时应做什么处理。
当网卡接收数据包的速度大于内核处理的速度时,会有一个队列保存这些数据包。控制该队列的最大值如下参数:
net.core.netdev_max_backlog
控制服务器的
SYN_RCVD
的状态数:net.ipv4.tcp_max_syn_backlog
超出处理能力时,发送
RST
报文丢弃连接:net.ipv4.tcp_abort_on_overflow
避免SYN攻击方式(二):
先看下 Linux内核的 SYN
(未完成连接建立)队列与 Accept
(已完成连接的队列)的工作过程:

正常流程:
- 客户端发送
SYN
报文给服务端,服务端接收后会将其加入到 [SYN队列]中; - 服务端接着发送
SYN+ACK
给客户端,等待客户端回应ACK
报文; - 服务端接收到客户端的
ACK
报文后,从 [SYN队列] 中移除对应的SYN报文放入到 [Accept 队列] - 应用通过调用
accept()
socket 接口,从 [Accept队列] 取出连接。

应用程序过慢:如果应用程序过慢,会导致 [Accept队列] 被占满。

受到攻击:如果不断受到 SYN 攻击,就会导致 [SYN队列] 被占满;
tcp_syncookies的方式可以应对SYN攻击:
net.ipv4.tcp_syncookies = 1

当 [SYN队列] 满的时候,新到来的SYN报文不再进入队列中,而是根据SYN报文计算出一个cookie
值,当作 [SYN+ACK 中] 报文中的序列号返回给客户端;
当客户端返回一个 ACK
包时,根据包头信息计算 cookie,与返回的确认号(初始序列号+1)进行比对,如果相同,则是一个正常连接,然后分配资源建立连接。
TCP 连接断开
★ 1. TCP四次握手的过程和状态变迁
第一次挥手:客户端主动请求关闭连接,会发送一个 FIN标志位为1的报文给服务端;之后客户端就会进入到
FIN_WAIT_1
状态;第二次挥手:服务端接收到报文后,先向客户端响应一个 ACK应答报文,接着进入到
CLOSED_WAIT
状态;客户端在接收到此 ACK应答报文后,会进入到
FIN_WAIT_2
状态;但是此时可能服务器还有数据未完全发送,接着服务端就会不断地发送数据给客户端,客户端中途就不会给予回应。
第三次挥手:等到服务端将所有数据发送完毕,服务端会发送一个关闭请求报文
FIN
给客户端,之后服务器就会进入到LAST_ACK
状态;第四次挥手:客户端接收到服务端 FIN 报文,回应一个 ACK报文,然后进入
TIME_WAIT
状态。但是客户端的应答报文服务端是否真的收到了呢?如果此次应答发送失败,那么客户端肯定不能结束。
同时对于服务端来说,它发送给客户端的结束请求在规定时间内(重传计时器)没有收到回复也会重新发送请求。
那么A就设置了一个等待计时器(等待时间 2MSL,比对方的超时计时器还要长),如果在计时器结束之前没有收到第三次握手的重复数据包,说明B成功接收了此次回复,那么整个过程就结束了。
★ 2. 为什么需要四次挥手?
关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能够接收数据。
服务端接收到客户端的 FIN 报文时,先回应一个 ACK应答报文,但是此时服务端可能还有数据未传送完毕,因此需要等待服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。
因此服务端的 ACK报文和 FIN报文会分开发送,就比三次握手多了一次。
3. 为什么 TIME_WAIT 等待的时间是 2MSL?
MSL:Maximum Segment Lifetime,报文的最大生存时间。它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP报文是基于 IP协议的,而IP首部中有一个TTL
字段,是IP数据报可以经过的最大路由数,每经过一个处理它的路由器该值就减1,当此值减少为0时数据报就会被丢弃。
MSL和TTL的区别:MSL的单位是时间,而TTL是经过路由的跳数。所以 MSL应该要大于等于 TTL消耗为0的时间。
为什么等于2倍的MSL?
因为发送方在发送数据包给接收方后,需要等待接收方返回一个响应,这样一来一回就刚好是2MSL。
从客户端最后一次发送给服务端ACK应答报文开始计时,该报文最长会经过MSL到达到达服务端,如果超过这个时间服务端没有接收到,服务端就会重新发送FIN报文给客户端,这个FIN报文也会最长经过MSL到达客户端。总共加起来就是2MSL,因此在2MSL时间内能够确保服务端在未接收到应答的情况下重新发送一个FIN报文并且到达客户端。
MSL的默认值: Linux系统默认 MSL=30s
,TIME_WAIT
=60s,可以通过修改TCP_TIMEWAIT_LEN
来修改 TIME_WAIT
的时间长度,并且需要重新编译 Linux 内核。
4. 为什么需要 TIME_WAIT 状态?
有两个原因: ① 为了防止旧数据包被收到; ② 为了保证服务端能够正常关闭。
①防止旧数据包被收到:
假设没有 TIME_WAIT 字段或者时间很短,此时在关闭连接前服务端发送了一个报文,但是由于网络阻塞的问题,客户端还没来得及接收整个连接就关闭了。
当具有相同端口的TCP连接被重用后,延迟的报文到达了客户端,客户端是有可能接收到这个过期的报文的,这就可能造成数据错乱的问题。
但是如果关闭前有 2MSL的等待时间,就能够保证在这个时间内客户端和服务端的过期数据包都能够被丢弃,之后重启TCP连接时的数据包一定都是新产生的。
②保证连接的正常关闭:
假设TIME_WAIT
字段过短或者没有,那么服务端在发送最后一次ACK应答报文后不管客户端是否成功接收都会直接进入到 CLOSED状态,假设服务端没能够成功接收这个应答报文,它会一直处在 LAST_ACK 状态。
下次客户端再发送连接请求时,服务端会发送 RST
报文来终止此连接。
5. TIME_WAIT 过多会有什么危害?
总共有两个危害:①对于内存资源的占用(服务端);②对端口资源的占用(客户端);
首先我们知道多次TCP连接会占用客户端的多个端口,同时会占用服务端的一个监听端口。
对于客户端而言:
端口资源有限,总共就65536个(0~65535),被占满就会导致无法创建新的连接。
对于服务端而言:
服务端虽然就开启一个监听端口,但是会把连接的处理交给内部线程去处理,所以监听端口是可以继续监听的。但是如果TIME_WAIT过多,会导致线程池也处理不了这么多一直不断的连接。结果就会导致系统资源被占满,处理不过来新的连接。
6. 如何优化 TIME_WAIT(有时间看)?
这里给出优化 TIME-WAIT
的几个方式,都是有利有弊:
打开 net.ipv4.tcp_tw_reuse
和 net.ipv4.tcp_timestamps
选项;net.ipv4.tcp_max_tw_buckets
程序中使用 SO_LINGER ,应用强制使用 RST 关闭。
方式一:net.ipv4.tcp_tw_reuse
和 tcp_timestamps
如下的 Linux 内核参数开启后,则可以复用处于 TIME_WAIT 的 socket 为新的连接所用。
有一点需要注意的是,tcp_tw_reuse
功能只能用客户端(连接发起方),因为开启了该功能,在调用 connect() 函数时,内核会随机找一个 time_wait 状态超过 1 秒的连接给新的连接复用。
net.ipv4.tcp_tw_reuse = 1
使用这个选项,还有一个前提,需要打开对 TCP 时间戳的支持,即
net.ipv4.tcp_timestamps=1(默认即为 1)
这个时间戳的字段是在 TCP 头部的「选项」里,用于记录 TCP 发送方的当前时间戳和从对端接收到的最新时间戳。
由于引入了时间戳,我们在前面提到的 2MSL 问题就不复存在了,因为重复的数据包会因为时间戳过期被自然丢弃。
方式二:net.ipv4.tcp_max_tw_buckets
这个值默认为 18000,当系统中处于 TIME_WAIT 的连接一旦超过这个值时,系统就会将后面的 TIME_WAIT 连接状态重置。
这个方法过于暴力,而且治标不治本,带来的问题远比解决的问题多,不推荐使用。
方式三:程序中使用 SO_LINGER
我们可以通过设置 socket 选项,来设置调用 close 关闭连接行为。
1 | struct linger so_linger; |
如果l_onoff为非 0, 且l_linger值为 0,那么调用close后,会立该发送一个RST标志给对端,该 TCP 连接将跳过四次挥手,也就跳过了TIME_WAIT状态,直接关闭。
但这为跨越TIME_WAIT状态提供了一个可能,不过是一个非常危险的行为,不值得提倡。
7. 如果已经建立了连接,但是客户端突然出现了故障怎么办?
- 如何解决?
TCP有一个保活机制,这个机制的原理如下:
定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP保活机制会开始作用。每隔一个时间间隔就发送一次探测报文,该探测报文所包含的数据量非常少,如果连续几个探测报文都没有得到响应,就可以认为TCP连接已经失效,系统内核会将这个错误通知给上层的应用程序。
- 如何配置?
在 Linux 内核中有对应的参数可以进行配置,参数如下:
1 | net.ipv4.tcp_keepalive_time = 7200 //保活时间,默认2小时,也就是2小时没有任何连接相关的活动,就会启动保活机制 |
也就是说在 Linux系统中,最少需要经过 2小时11分15秒才可以发现一个死亡连接 7200+75*9。
- 开启TCP保活后存在的几种情景:
(1)端程序是正常工作的,那么发送探测报文后能够正常响应,此时保活时间会被重置,等待下一个TCP保活时间的到来;
(2)端程序崩溃后重启,当探测报文发送给对方,对方能够响应但是由于没有连接的有效信息,会返回一个 RST报文,很快就能发现TCP连接被重置;
(3)端程序崩溃,或端程序由于其它原因导致报文不可达。当接连几个探测报文到达对端后没有响应,TCP会报告该TCP连接已经死亡。
Socket 编程
1. 什么是Socket?
Socket是位于传输层和应用层之间的一个软件抽象层,是对下层TCP/IP协议的封装,可以理解为对上层应用提供的一个接口,但是它并不是一种协议。
更简单的理解为: TCP/IP只是一个协议栈,但是就像操作系统的运行机制一样必须实现,同时还要对外提供操作接口(Socket),Socket是发动机提供网络通信的能力,HTTP看成轿车提供了封装或者显示数据的具体形式。
2. Socket的原理
一次socket建立、数据传送、关闭的过程:
首先,TCP服务器端会初始化一个Socket,然后绑定端口bind(),对端口进行监听listen(xxx),监听过程是阻塞的,等待客户端连接。
此时如果有个客户端初始化了一个Socket,然后连接服务器connect(),连接成功后客户端和服务端就成功建立连接。
客户端发送数据,服务器接收请求并处理最后返回数据给客户端。
关闭连接,一次交互结束。
详细方法操作:
3. 针对TCP如何 Socket编程?

- 服务端和客户端初始化
socket
,得到文件描述符。 - 服务端调用
bind()
,绑定指定的ip地址和端口号。服务端需要绑定指定的地址和端口号,而客户端不需要指定,由系统自动分配一个端口号和自身的ip地址进行组合。这就是为什么服务端在listen()之前需要调用bind(),而客户端不需要,它会在connect时自动创建一个。 - 服务端调用
listen()
,进行监听。 - 服务端调用
accept()
,等待客户端连接。 - 客户端调用
connect()
,向服务器地址和端口发送连接请求。 - 服务端
accept()
返回用于数据传输的socket()
文件描述符。 - 客户端通过
write()
写入数据;服务端调用read()
读取数据。 - 客户端断开连接时,会调用
close()
,那么服务端read()
就会读取到EOF
,等待处理完数据后,服务端调用close()
,表示关闭连接。
需要注意的是:服务端调用accept()
时,连接成功会返回一个已经完成连接的socket,后续用来客户端传输数据。
所以监听socket 和 数据传输socket 其实是两个。
4. listen 时候参数 backlog的意义?
Linux内核中会维护两个队列:
- 未完成连接的队列:(SYN队列),接收一个 SYN建立连接请求,处于 SYN_RCVD状态;
- 已完成连接的队列:(Accept队列),以完成3次握手过程,处于 ESTABLISHED状态;
int listen(int socketfd,int backlog)
- 参数一
socketfd
为文件描述符; - 参数二
backlog
为accept队列;
5. Accept和Connect发生在三次握手的哪一步?

- 客户端的协议栈向服务器端发送了 SYN 包,并告诉服务器端当前发送序列号 client_isn,客户端进入 SYN_SENT 状态;
- 服务器端的协议栈收到这个包之后,和客户端进行 ACK 应答,应答的值为 client_isn+1,表示对 SYN 包 client_isn 的确认,同时服务器也发送一个 SYN 包,告诉客户端当前我的发送序列号为 server_isn,服务器端进入 SYN_RCVD 状态;
- 客户端协议栈收到 ACK 之后,使得应用程序从 connect 调用返回,表示客户端到服务器端的单向连接建立成功,客户端的状态为 ESTABLISHED,同时客户端协议栈也会对服务器端的 SYN 包进行应答,应答数据为 server_isn+1;
- 应答包到达服务器端后,服务器端协议栈使得 accept 阻塞调用返回,这个时候服务器端到客户端的单向连接也建立成功,服务器端也进入 ESTABLISHED 状态。
因此 客户端的Connect函数是在第二次握手成功返回的,服务端的 accept函数是在三次握手成功之后。
6. Close发生在四次挥手的哪一步?

- 客户端调用 close,表明客户端没有数据需要发送了,则此时会向服务端发送 FIN 报文,进入 FIN_WAIT_1 状态;
- 服务端接收到了 FIN 报文,TCP 协议栈会为 FIN 包插入一个文件结束符 EOF 到接收缓冲区中,应用程序可以通过 read 调用来感知这个 FIN 包。这个 EOF 会被放在已排队等候的其他已接收的数据之后,这就意味着服务端需要处理这种异常情况,因为 EOF 表示在该连接上再无额外数据到达。此时,服务端进入 CLOSE_WAIT 状态;
- 接着,当处理完数据后,自然就会读到 EOF,于是也调用 close 关闭它的套接字,这会使得客户端会发出一个 FIN 包,之后处于 LAST_ACK 状态;
- 客户端接收到服务端的 FIN 包,并发送 ACK 确认包给服务端,此时客户端将进入 TIME_WAIT 状态;
服务端收到 ACK 确认包后,就进入了最后的 CLOSE 状态;
客户端经过 2MSL 时间之后,也进入 CLOSE 状态;