共计 3282 个字符,预计需要花费 9 分钟才能阅读完成。
什么是三次握手?
所谓三次握手其实指的是三次信息交换过程,三次信息交换完毕后我们就可以认为一个“连接”建立好了,那么什么是一个“连接”呢?
一个连接唯一确定了发送方和接收方,除此之外一个连接还确定了双方“说话的方式”,三次握手规定:双方在说话前都要加一个数字,该数字用来记录这是彼此的第几句话了,比如:
A:1, 今天天气不错啊
1,对啊 :B
2,明天天气也不错 :B
A: 2, 不过好像明天有雨
A: 3, 而且明天还要加班
A: 4, 不想上班 :(
3, 我明天休假啦 :B
6, 下班啦 :B
4, 今天可以早点撤 :B
5, 开心 :B
从这里我们可以看出双方说话前都报上这是自己说的第几句话,当然在真实情况下只有无聊到极点的人才会在微信里用这种方式聊天。但是微信所依赖的网络通信协议其实就用这种看上去略显神经质的方式在彼此通信,有的同学可能已经看出来了,这种方式的好处在于 可靠性。
从上面的聊天中我们可看到,B 最后说的几句话可能因为网络问题出现了乱序,但 A 依然知道该怎么阅读 B 发过来的信息,原因就在于每一句都带有编号, A 可以依照编号的顺序而不是接收数据的顺序进行阅读,这就是每句话加编号的作用。
真实的网络协议双方的第一句话都不是从 1 开始的,三次握手的目的就在于协商双方最开始的数字是几,比如 A 和 B 说:” 我的信息序号从 X 开始 ”,B 对 A 说:” 我的信息序号从 Y 开始 ”,此后双方在 X 和 Y 的基础上每说一句就加 1,以此来确保通信的可靠性。
那么什么网络协议需要依赖三次握手交换说话序号来确保可靠性呢,答案就是网络通信协议中的 TCP,只有 TCP 协议在通信前才需要进行三次握手彼此交换起始序号。
那么 UDP 协议在双方通信前需要三次握手吗?当然是不需要的,UDP 协议不负责通信的可靠性,依赖 UDP 的通信双方无需三次握手就可以直接发送数据。
本文的关注重点在 TCP 协议,以下内容都是关于 TCP 协议的,这一点要注意。
为通信加上序号的重要性
TCP 规定接收方需要对接收到的数据进行回复确认,也就是发送 ACK,发送方接收到 ACK 后就知道接收方确实已经收到信息了,如果在一段时间后还没有接收到 ACK 信息,那么发送方就要重传消息,这就是 TCP 协议中所谓的 超时重传机制 ,那么这里有一个问题, 发送怎么知道该重传哪个消息?
不要忘了使用 TCP 进行通信的双方每句话都带有 序号 ,接收方回复的 ACK 中同样带有 序号,比如接收方发送的 ACK 消息为:
ACK 15
这句话的意思其实是在说:
序号 14 之前的消息我都已收到,可以发送序号 15 之后的数据了
当发送方接收到该 ACK 后就知道序号 15 之前的所有信息接收到都已收到,这样通过在 ACK 中携带上序号接收方可以准确的知道哪些数据没有发送成功。
这就是为通信加上序号的重要性,一句话,就是为了确保 TCP 协议的可靠性。
可以说序号是 TCP 实现可靠性的基石。
三次握手的过程
现在我们已经知道了序号在 TCP 协议中的重要性,三次握手本质上就是交换彼此说话的起始序号,没有该序号 TCP 的可靠性无从谈起。三次握手其实是类似如下过程:
A: 我说话的序号是从 X 开始的
收到(不要忘了 TCP 协议中需要对每句话进行确认) :B
我说话的序号是从 Y 开始的 :B
A: 收到(不要忘了 TCP 协议中需要对每句话进行确认)
即:
但是,我们可以看到 B 说的两句话起始完全可以合并成一句,因此:
A: 我说话的序号是从 X 开始的
收到,我说话的序号是从 Y 开始的 :B
A: 收到
即:
这就是三次握手的由来,现在你应该明白了吧。
当然教科书上不是这样写的,教科书上是这样写的:
总之是以你看不懂的方式来说就对了 :),开玩笑哈,本文接下来的部分也以上图为例来讲解,前两张图的目的是为了让大家更好的理解三次握手。
总之,交换双方说话的起始序号就是三次握手的目的,该序号极其重要,是 TCP 协议实现可靠性的基础。
三次握手之后 TCP 双方就可以交换数据了。
Socket API
在讲解 socket API 前我们需要理解 TCP 协议的双方分为 主动打开 和被动打开,从三次握手的角度讲,主动发起握手的一方属于主动打开;被动接受握手的一方属于被动打开。
很显然,客户端属于主动打开,服务器端属于被动打开。
接下来我们就可以看 socket API 了。
在 socket 编程中有几个 API 非常重要,但很多资料对其解释差强人意。
实际上这些 API 分为两类,一类客户端和服务器端都可以调用;另一类 API 独属于客户端或者服务器端:
客户端和服务器端都可以调用的 API:
socket(), bind(), send/write(),write()/recv(),close()
独属于客户端和服务器端的 API:
客户端:connect()
服务器端:listen(),accept()
在这里我们比较感兴趣的是第二类 API,为什么 connect 函数只能被客户端调用、listen 与 accept 函数只能被服务器端调用?
要想理解这个问题我们必须清楚的知道这些 API 与三次握手之间的联系。
三次握手与 Socket API
我们再来看一下客户端和服务器这两个概念,实际上当双方三次握手后正常通信时无需区分服务器端和客户端,服务器端可以向客户端发送数据,客户端也可以向服务器端发送数据,在这个阶段客户端也好服务器也罢没什么区别,唯一能区分服务器还是客户端其实是通过三次握手这个过程来实现的。
怎么区分呢?很简单,主动发起连接的一方是客户端,被动打开的一方是服务器端,而独属于客户端或者服务器端的几个 API 与三次握手密切相关。
实际上 connect、listen 以及 accept 函数与三次握手的关系如下:
从图中我们可以看到,三次握手其实是客户端通过 connect 函数发起的,客户端调用 connect 函数不会立即返回,只有当三次握手成功完成后 connect 函数才会返回。
对于服务器 server 来说,调用 listen 仅仅是服务器告诉操作系统已经准备好了被动打开,也就是被动接受握手,当服务器端还没有执行 listen 函数时,客户端调用 connect 函数是不会成功返回的,原因很简单,connect 函数的功能实际上是发起三次握手,但此时服务器端还没有准备好,因此三次握手不会成功,connect 函数也不会成功返回。
只有当服务器端调用 listen 函数后,服务器端才会做好准备来进行三次握手,注意这和调没调用 accept 函数没有任何关系。只要服务器端调用了 listen 函数,即使没有调用 accept 三次握手也可以成功。
三次握手后服务器端和客户端成功建立起链接 (准确讲是成功交换了彼此说话的起始序号),服务器和相应客户端的连接信息会被放到 操作系统的等待队列中 ,等等,为什么要放入队列中呢?因为 一个服务器可以和多个客户端建立连接,三次握手成功后需要维护这些客户端的连接信息,因此这些信息通常是操作系统用队列来维护的。
那么队列中的这些连接数据什么时候会被取出来呢?这就是 accept 函数的作用了,服务器端调用 accept 函数后会从队列中取出一个已经成功三次握手的连接数据,此后双方就可以进行正常通信了。
从这里我们也可以看出 accept 函数不会影响三次握手,但该函数能否很快返回是和三次握手有关的,当服务器端调用 listen 准备进行三次握手后假设还没有任何客户端同服务器端进行通信,这时服务器端调用 accept 函数是不会返回的,原因很简单,因为此时队列中还没有任何成功建立的连接,该情形就是上图所示,当第一个客户端同服务器端成功三次握手后队列中才会有连接信息,此时 accept 函数从队列取出该数据后才会返回。
基于以上分析,connect、listen 以及 accept 同三次握手有密切关系。
connect 函数用于发起三次握手因此只能被客户端使用。
listen 用于准备接受握手,accept 函数用于取出成功进行三次握手的连接信息,因此这两个函数只能被服务器端使用。
总结
本文中我们讲解了什么是三次握手以及三次握手与 socket API 之间的联系,注意只有 TCP 协议才需要三次握手,希望本文的讲解能加深同学们对 TCP 协议的理解。
如果你喜欢这篇文章,欢迎关注微信公共账号:码农的荒岛求生,获取更多内容。
计算机内功决定程序员职业生涯高度