今天来聊聊Python常见面试题中面试频率特别高的一个题目:TCP 协议中的三次握手与四次挥手。
涉及到的知识点有:
1、TCP、UDP 协议的区别
2、TCP 头部结构
3、三次握手与四次挥手过程详解
4、什么是 TIME_WATI 状态
内容稍微有点多,大家耐心些看完!
一、TCP、UDP 协议的区别
在介绍这两者的区别之前,我们要需要了解一个概念:TCP/IP 协议族。定义如下:
目前 Internet(因特网)使用的主流协议族是 TCP/IP 协议族,它是一个分层、多协议的通信体系 《Linux高性能编程》
提取关键词:分层、多协议和通信。也就是说,它有多个层次,每个层次有不同的协议,这些层次之间通过协议相互协作,最终达到网络通信的目的。
说到分层,应该不会很陌生,TCP/IP 协议族是一个四层协议系统,自底而上分别是:数据链接层、网络层、传输层、应用层。我们这里要说到的 TCP 和 UDP 协议属于传输层。(各层的作用及相关协议这里暂时先不做介绍)
下面我们回到标题:TCP、UDP 协议的区别,总结起来这个问题的答案要点如下:
1、首页它们俩都是传输层的协议,而所谓“传输层”,它是为两台主机提供端到端的通信,即从 A <-> B 。
2、TCP 协议可靠,UDP 协议不可靠。可靠即指数据由 A 发送到 B,是否能确保数据真的有送达到 B。TCP 协议使用 超时重传、数据确认等方式来确保数据包被正确地发送至目的端,而 UDP 协议无法保证数据从发送端正确传送到目的端,如果数据在传输过程中丢失、或者目的端通过数据检验发现数据错误,则 UDP 协议只是简单地通知应用程序发送失败,对于 TCP 协议拥有的超时重传、数据确认等需要应用程序自己来处理这些逻辑。
3、TCP 是面向连接的,UDP 是无连接的。这也比较好理解,因为 TCP 连接才需要“三次握手,四次挥手”。
4、TCP 服务是基于流的,而 UDP 是基于数据报的,基于流的数据没有边界(长度)限制,而基于数据报的服务,每个 UDP 数据报都有一个长度,接收端必须以该长度为最小单位将其所有内容一次性读出。
5、当发送方多次执行写操作时,TCP 模块会先将这些数据放入 TCP 发送缓冲区中,当 TCP 模块真正开始发送数据时,发送缓冲区中这些等待发送的数据可能被封装成一个或多个 TCP 报文段发出,因此,TCP 模块发出的 TCP 报文段的个数与应用程序执行的写操作次数是没有固定数量关系的。同样,当接收端收到一个或多个 TCP 报文段后,TCP 模块将这些数据按照序号(序号说明见下面 的 TCP 头部结构)依次放入 TCP 接收缓冲区中,并通知应用程序读取数据。接收端可选择一次或者分多次将数据从缓冲区中读出(这取决于用户指定的应用程序读缓冲区的大小)。因此,接收端读取数据的次数与发送端发出多少个报文段个数也没有固定的数量关系。总结来说,即**对于 TCP 连接,发送端执行的写操作次数与接收端执行的读操作次数之间没有任何数据关系,这也是基于流服务的特点。**而对于 UDP 服务,发送端每执行一次写操作,就会将其封装成一个 UDP 数据报并发送之,同时接收端必须根据发送的进行读,否则就会丢包。因此,对于 UDP 连接,发送端写的次数据与读的次数是一致的,这也是基于数据报的服务的特点。
6、TCP 的连接是一对一的,所以如果是基于广播或者多播的的应用程序不能使用 TCP,而 UDP 则非常适合广播和多播。
总结一句定义:
TCP 协议(Transmission Control Protocal,传输控制协议)为应用层提供可靠的、面向连接的、基于流的服务。而 UDP 协议(User Datagram Protocal,用户数据报协议)则与 TCP 协议完全相反,它为应用层提供不可靠、无连接和基于数据报的服务。
二、TCP 头部结构
TCP 报文结构分为头部部分和数据部分,为什么需要了解 TCP 头部结构,因为在后面“三次握手与四次挥手”里会用到头部结构里的标志位。简单了解下对理解后面的过程有一定的好处。
下面一一说明每个的作用:
16位源端口号与目的端口号,这个比较好理解,就不过多解释。
32位序号:在建立连接(或者关闭)的过程,这个序号是用来做占位,当 A 发送连接请求到 B,这个时候会带上一个序号(随机值,称为 ISN),而 B 确认连接后,会把这个序号 +1 返回,同时带上自己的充号。当建立连接后,该序号为生成的随机值 ISN 加上该段报文段所携带的数据的第一个字节在整个字节流中的偏移量。比如,某个 TCP 报文段发送的数据是字节流中的第 100 ~ 200 字节,那该序号为 ISN + 100。所以总结起来说明建立连接(或者关闭)时,序号的作用是为了占位,而连接后,是为了标记当前数据流的第一个字节。
4位头部长度:标识 TCP 头部有多少个 32 bit 字,因为是 4位,即 TCP 头部最大能表示 15,即最长是 60 字节。即它是用来记录头部的最大长度。
6位标志位,包括:
URG 标志:表示紧急指针是否有效。
ACK 标志:确认标志。通常称携带 ACK 标志的 TCP 报文段为确认报文段。
PSH 标志:提示接收端应该程序应该立即从 TCP 接收缓冲区中读走数据,为接收后续数据腾出空间(如果不读走,数据就会一直在缓冲区内)。
RST 标志:表示要求对方重新建立连接。通常称携带 RST 标志的 TCP 报文段为复位报文段。
SYN 标志:表示请求建立一个连接。通常称携带 SYN 标志的 TCP 报文段称为同步报文段。
FIN 标志:关闭标志,通常称携带 FIN 标志的 TCP 报文段为结束报文段。
这些标志位说明了当前请求的目的,即要干什么。
16 位窗口大小:表示当前 TCP 接收缓冲区还能容纳多少字节的数据,这样发送方就可以控制发送数据的速度,它是 TCP 流量控制的一个手段。
16 位校验和:验证数据是否损坏,通过 CRC 算法检验。这个校验不仅包括 TCP 头部,也包括数据部分。
16 位紧急指针:正的偏移量,它和序号字段的值相加表示最后一个紧急数据的下一字节的序号。TCP 的紧急指针是发送端向接收端发送紧急数据的方法。
TCP 头部选项:可变长的可选信息,这部分最多包含 40 字节,因为 TCP 头部最长是 60 字节,所以固定部分占 20 字节。这里不做详细介绍,可以参考《Linux高性能编程》3.2.2
三、三次握手与四次挥手过程详解
画图
先来解释三次握手过程:
1、发送端发送连接请求,6位标志为 SYN,同时带上自己的序号(此时由于不传输数据,所以不表示字节的偏移量,只是占位),比如是 223。
2、接收端接到请求,表示同意连接,发送同意响应,带上 SYN + ACK 标志位,同时将确认序号为 224(发送端序号加1),并带上自己的序号(此时同样由于不传输数据,所以不表示字节的偏移量,只是占位),比如是 521。
3、发送端接收到确认信息,再发回给接收端,表示我已接受到你的确认信息,此时标志仍为 ACK,确认序号为 522。
涉及到的问题:为什么是三次握手,而不是四次或者两次?
首先解释为什么不是四次。四次的过程是这样的:
发送方:我要连你了。
接收方:好的。
接收方:我准备好了,你连吧。
发送方:好的。
显然接收方准备好连接并同意连接是可以合并的,这样可以提高连接的效率。
再来,我们解释为什么不是两次。其实也比较好理解,我们知道 TCP 是全双工通信的,同时也是可靠的,连接和关闭都是两边都要执行才算真正的完成,同时还需要确保两端都已经执行了连接或者关闭。如果只有两次,过程是这样的:
发送方:我要连你了。
接收方:好的。
很明显,接收方并不知道也不能保证发送方一定接收到 “好的” 这条信息,一旦接收方真的没有收到这条信息,就会出现接收收“单方面连接”的情况,这个时候发送方就会一直重试发送连接请求,直到真正收到 “好的” 这条信息之后才算连接完成。而对于三次,如果发送方没有等待到你回复确认,它是不会真正处于连接状态的,它会重试确认请求。
接着我们来看看四次挥手过程:
1、发送方发送关闭请求,标志位为:FIN,同时也会带上自己的序号(此时同样由于不传输数据,所以不表示字节的偏移量,只是占位)。
2、接收方接到请求后,回复确认:ACK,同时确认序号为请求序号加1。
3、接收方也决定关闭连接,发送关闭通知,标志位为 FIN,同时还会带上第2步中的确认信息,即 ACK,以及确认序号和自己的序号。
4、发送方回复确认信息:ACK,接收方序号加1。
涉及到的问题:为什么需要四次握手,不是三次?
三次的过程是这样的:
发送方:我不再给你发送数据了。
接收方:好的,我也不给你发了。
发送方:好的,拜拜。
这是因为当接收方收到关闭请求后,它能立马响应的就是确认关闭,它这里确认的是接收方的关闭,即发送方不再发数据给接收方了,但他还是可以接收接收方发给他的数据。而接收方是否需要关闭“发送数据给发送方”这条通道,取决于操作系统。操作系统也有可能 sleep 个几秒再关闭,如果合并成三次,就可能造成接收方不能及时收到确认请求,可能造成超时重试等情况。因此需要四次。
四、什么是 TIME_WAIT 状态
首先,我们来看一段代码(说了这么多理论,终于要看点代码了)。这里举一个 python 简单的使用 socket 进行 tcp 通信的示例:
服务端:
socket_server_test.py
# -*- coding: utf-8 -*-
"""
@Time : 2019/6/26 下午4:58
@Author : Demon
@File : socket_server_test.py
@Desc :
"""
import socket
HOST = '127.0.0.1' # 标准的回环地址 (localhost)
PORT = 9999 # 监听的端口 (非系统级的端口: 大于 1023)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 第三个参数,如果为0,也不可复用
# 第三个如果为1,可以复用
# s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((HOST, PORT))
s.listen()
conn, addr = s.accept()
with conn:
print('Connected by', addr)
while True:
data = conn.recv(1024)
print("data:", data)
if data:
print("close")
s.close()
break
conn.sendall(data)
客户端:
# -*- coding: utf-8 -*-
"""
@Time : 2019/6/26 下午4:55
@Author : yrr
@File : socket_client_test.py
@Desc : 测试 self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
"""
import socket
HOST = '127.0.0.1' # 服务器的主机名或者 IP 地址
PORT = 9999 # 服务器使用的端口
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
s.sendall(b'Hello, world')
data = s.recv(1024)
print('Received', repr(data))
执行效果:
我们会发现,当我们服务端主动关闭时,如果我们再次运行这个程序,会报错误说端口仍然被占用。这就很奇怪了,明明已经关闭了连接,为什么还会占用着端口呢?我们使用 netstat -an|grep 9999 命令查看,发现当前这个连接处于 TIME_WAIT 状态。
我们来说下 TIME_WAIT 状态。即当一方断开连接后,它并没有直接进入 CLOSED 状态,而是转移到 TIME_WAIT 状态,在这个状态,需要等待 2MSL(Maximum Segment Life,报文段最大生存时间)的时间,才能完全关闭。
涉及问题:
1、为什么需要有 TIME_WAIT 状态存在?
简单来说有两点原因如下:
a. 当最后发送方发出确认信息后,仍然不能保证接收方能收到信息,万一没收到,那接收方就会重试,而此时发送方已经真正关闭了,就接受不到请求了。
b. 如果发送方在发出确认信息后就关闭了,在接收方接到确认信息的过程中,发送方是有可能再次发出连接请求的,那这个时候就乱套了。刚连接完,又收到确认关闭的信息。
2、为什么时长是 2MSL 呢?
这个其实也比较好理解,所以我发送确认信息,到达最长时间是 MSL,而你如果没接受到,再重试,时间最长也是 MSL,那我等 2MSL,如果还没收到请求,证明你真的已经正常收到了。
正因为我们有这个 TIME_WAIT 状态,所以通常我们说是客户端先关闭,一般不会让服务器端先关闭。那如何避免出现关闭后端口被占用的情况(即上面代码示例问题怎么解决)呢?很简单,加一行代码即可实现:
# socket.SO_REUSEADDR 表示 close 后端口可复用
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
Python常见面试题中这个三次握手与四次挥手的出题率还是比较高的,包含的知识点也比较多,大家慢慢消化!有哪里不清楚的地方,