4

什么是三次握手?

所谓三次握手其实指的是三次信息交换过程,三次信息交换完毕后我们就可以认为一个“连接”建立好了,那么什么是一个“连接”呢?

一个连接唯一确定了发送方和接收方,除此之外一个连接还确定了双方“说话的方式”,三次握手规定:双方在说话前都要加一个数字,该数字用来记录这是彼此的第几句话了,比如:

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协议的理解。

如果你喜欢这篇文章,欢迎关注微信公共账号:码农的荒岛求生,获取更多内容。

clipboard.png

计算机内功决定程序员职业生涯高度

码农的荒岛求生
191 声望167 粉丝