文章目录
- Ⅰ. 三次握手
- Ⅱ. 建立连接后的通信
- Ⅲ. 四次挥手

Ⅰ. 三次握手
1、首先双方都是处于未通信的状态,也就是关闭状态 CLOSE
。
2、因为服务端是为了服务客户端的,所以它会提前调用 listen()
函数进行对客户端请求的监听。
3、接着客户端就想访问服务端,所以通过 connect()
函数主动发起请求报文,此时报文中将 SYN
字段设为 1
,表示请求建立连接,并且选择一个初始化序号 seq = x
,(这是因为 TCP
规定 SYN
报文段不能携带数据,但要消耗掉一个序号)此时客户端就进入了 SYN_SEND
状态,等待服务端的回应。
4、服务端收到了客户端发来的 SYN
报文之后,会向客户端发送给 SYN + ACK
都置一的报文段表示同意建立连接,同时携带确认号为 ack = x+1
,并且也为自己选择一个初始序号 seq = y
。最后调用 accept()
函数进行 阻塞,直到操作系统收到了来自客户端的确认应答,然后进入 SYN_RECV
状态!其实服务端收到的这条请求,会将这条请求放到一个 半连接队列 中等待处理。(这个后面会讲)
5、接着如果没有意外的话,客户端就收到了同意连接的应答,进入 ESTABLISHED
状态。同时为了让服务端知道自己知道了应答,就向服务端发送最后一个 ACK
报文,表示确认应答,同时携带 seq = x+1
和 ack = y+1
。(这是因为 TCP
规定 ACK
报文段可以携带数据,但如果不携带数据的话则不消耗序号)
除此之外,客户端应用层一直阻塞的 connect()
函数也返回了,通过其返回值可以判断是否建立连接成功!
6、最后服务端也收到了来自客户端的确认应答,便进入了 ESTABLISHED
状态,此时服务端应用层一直阻塞的 accept()
函数也 返回了一个新分配的文件描述符,是一个用于与客户端进行通信套接字!
实际上需要强调的一点是这个新分配的文件描述符是从一个叫做 完全连接队列 中也就是 accpet
队列 中取出来的,其是需要一定的排队时间的!这也是 accept()
函数的第二个参数 backlog
的含义,也就是完全连接队列的最大长度,后面我们还会详细讲!
Ⅱ. 建立连接后的通信
一般来说网络编程中,我们可以使用两套对应的接口来通信,都是可以的,分别是
send()
、recv()
和write()
、read()
函数,它们的区别我们之前在讲套接字接口的时候就提到过,一般网络编程中会使用send()
、recv()
,而write()
、read()
函数多用于文件操作中(虽然网络编程本质也是文件操作=-=),但是它们其实都是通用的,所以要是看到别人用上面两套接口进行网络编程的话,都是可以的! 而我们这里讲的主要是前一套!
在通信过程中,无论是客户端还是服务端,它们都是处于 ESTABLISHED
状态的,这是不变的!
如果某一端想发送数据了,那么直接调用 send()
函数进行发送,因为我们前面实现过 tcp
服务器,所以我们知道一般服务器都是多线程、线程池版本,实现读写分离的,所以一般收发是分开的(本质是因为有两个缓冲区),很体现 全双工 的通信特点,所以此时服务端肯定是有线程或者进程在调用着 recv()
函数阻塞着读取客户端发来的信息的!如果收到了消息,那么 recv()
就会停止阻塞,然后返回对应的字节数!
如果说对方是断开连接请求的话,recv()
就会返回一个 0
表示要断开连接啦,此时就会进入四次挥手的过程,这个下面会讲!
除此之外需要提醒的是,一般来说服务端是不会主动向发送信息,除非是回应业务处理,最能体现这种情况的就是 http
协议啦,而相反会经常主动给客户端发送数据的大多都是需要实时处理的业务,比如说游戏画面,就像我们学过的 websocket
协议,就是一个样例!
Ⅲ. 四次挥手
1、首先客户端应用层调用了 close()
函数之后,会向服务端发送 FIN
报文段,其中因为上面可能通信发送了其它数据,所以这里我们用 seq = u
表示当前的报文序号,并且进入 FIN_WAIT1
状态等待服务端回应。
2、此时服务端因为一直调用着 recv()
函数接收数据,此时突然 recv()
函数返回 0
,表示客户端要断开连接了,所以服务端就进入了 CLOSE_WAIT
状态,并向客户端发送 ACK
报文段表示确认断开连接,并且携带 seq = v
和 ack = u+1
报文序号,这时的 TCP
连接处于半关闭的状态!
之后客户端收到服务端的确认关闭之后,会进入 FIN_WAIT2
状态;而服务端之所以进入 CLOSE_WAIT
状态,是因为可能此时服务端还有之前没处理完的数据,在这个状态之内会 将这些剩余的数据都发送给客户端,而客户端也依然会接收,相当于客户端之前调用的 close()
只关闭了写端,而这个状态可能会持续一段时间直到数据发完!
3、当服务端发送完剩余数据之后,服务端的操作系统就会通知对应的进程,告诉其该调用 close()
函数了,所以该服务端进程就调用了 close()
函数进行关闭套接字(因为 TCP
连接时全双工通信,所以双方都得发起断开连接请求),此时会向客户端发送 FIN
报文段表示断开连接。
假设此时服务端的序号为 w
(因为前面传输剩余数据之后可能序号发送了变化),则服务端会在报文中携带 seq = w
和 ack = u+1
(重复发一次),然后服务端就进入了 LAST_ACK
状态等待客户端的确认应答。
4、客户端收到了服务端的连接释放报文段之后,必须对此发出确认。所以会向服务端发送 ACK
报文,顺便携带自己的序号 seq = u+1
和确认号 ack = w+1
,然后就进入了 TIME_WAIT
状态!
注意,此时进入 TIME_WAIT
状态之后是不会立刻释放连接的,还需要经过 时间等待计时器(TIME-WAIT timer
)设置的时间 2MSL
之后才会进入 CLOSED
状态,而 MSL
时间叫做最长报文段寿命(Maximum Segment Lifetime
)。
一般这个时间是可以自行设定的,根据 RFC793
规定建议设为 2
分钟,所以来回总共就是 4
分钟 的等待时间,不过一般会根据具体的情况使用更小的 MSL
值,在 linux
中默认 2MSL
是 60
秒。
至于为什么要设置这个
2MSL
值,是因为有可能客户端发出去的ACK
报文丢失了,在这段时间内服务端收不到应答,就可以进行超时重传,重传上面的FIN + ACK
报文段,一般来说,重传之后这个时间等待计数器是会重置的!如果没有2MSL
的话,服务端就不知道是否要断开连接,则一直等待着客户端的应答,浪费了资源! 另外,当 服务端重传第三次挥手报文的次数达到
2
时, 达到了最大重传次数,于是再等待一段时间(时间为上一次超时时间的2
倍),如果还是没能收到客户端的第四次挥手(ACK
报文),那么服务端就会断开连接。 而 客户端 在收到第三次挥手后,就会进入
TIME_WAIT
状态,开启时长为2MSL
的定时器,如果途中再次收到第三次挥手(FIN
报文)后,就会重置定时器,当等待2MSL
时长后,客户端就会断开连接。如下图所示:
5、而当服务端收到了确认应答之后,就会直接进入 CLOSED
状态,释放连接!