文章目录
前言
TCP/IP 的学习使我们避不开的
一直以来记得笔记什么的也都是在云笔记上,或者在书上, 比较杂乱, 现在写一个系列博客也算是一个归纳总结把
以后复习也方便
TCP 连接的建立
我们知道一个 TCP 连接是由一个四元组构成的, 分别是 dest IP
dest port
source IP
source port
更准确地说, 一个 TCP 连接是由一对端点或套接字构成, 其中通信的每一端都由一对 (IP 地址, 端口号 ) 以标识
客户端和服务器之间建立可靠的 TCP 连接的过程是 :
- 客户端发送 SYN 消息
- 服务器回复 SYN + ACK 应答表示收到了
- 客户端以 ACK 响应
这个过程被形象的称为三次握手
三次握手
TCP 是一个面向连接的协议 (相对的 UDP 是无连接的), 我们要先使用 socket 套接字的 API 建立起一条双工的连接, 才能发送数据
而这个建立连接的过程被形象的称为三次握手
什么是三次握手
一个 TCP 连接的建立, 需要发送三个报文段来完成, 于是也被称为三次握手
, 而这三个报文段的主要作用是交换其中包含的初始序列号
一般是客户端主动向服务器发起一个 TCP 连接, 相对应的客户端就是 TCP 连接的主动开启者, 详细过程是:
- 客户端向服务器发送一个
SYN
报文段, 即在 TCP 头部的SYN
位置位的一个 TCP/IP 数据包 (如图1), 里面还包含了它的初始序列号ISN(c)
ISN
随机生成, 不然可能被预测进行 TCP 包的伪造, 随时间而变化, 每一个连接都拥有不同的 ISN, 现代操作系统一般采用半随机的方法来选择初始序列号, Linux 采用一个基于时钟的方案
- 服务器也回复客户端一个
SYN
报文段作为响应, 里面还包含了他自己的初始序列号ISN(s)
, 并且回复一个ACK(c+1)
, 即每多发送一个SYN
序列号就会加一, 这样在出现丢包的情况, 这个包就会被重传 - 为了确认服务器的
SYN
, 客户端将ISN(s)
的数值加一后作为ACK
回复给服务器
我们知道 socket 分为主动 socket 和被动 socket, 调用 connect 的 socket 为主动 socket (客户端), 而调用 listen 的 socket 为被动 socket(服务器)
图一(TCP 头)
简单来说, 就是 (如图二):
- 客户端发送 SYN 报文段
- 服务器使用 SYN + ACK 应答, 表示接收到了这个消息
- 客户端以 ACK 响应
发现下图有误, 四次挥手示意图, 主动关闭方在接收到服务器发挥的 ACK 后, 应处于
FIN-WAIT-2
, 而不是FIN-WAIT-1
,
图二(TCP 连接的建立与终止示意图)
三次握手的本质
三次握手握的是啥? 是通信双方数据原点的序列号
它的的主要目的, 就是交换报文段其中包含的初始序列号
为什么是三次握手
- 为什么一定是交换三次报文段?为什么不是四次握手? 两次握手?
因为 TCP 是建立在一个不可靠数据传输上的可靠连接,我并不能保证我发出的数据对端一定收到了, TCP 连接中数据发送到对端和数据从对端回来的路线不一定是一样的, 而且因为网络或者各种其他意想不到的情况, 并不能保证我发出的数据对方一定能收到, 那么 TCP 为了做到可靠, 就是接收方收到了数据后回复一个 ACK, 当发送方没有收到接收方发来的 ACK, 他就重发数据
所以在服务器回复 SYN
报文段后, 客户端要回一个 ACK
来确认
打个比方, 就像打电话
三次握手就像 :
- A 对 B 说 :" 嗨! 你好啊! 你能听到我么?"
- B 对 A 说 :" 嗨! 你好, 我能听到你, 你能听到我么? "
- A 对 B 说 :" 我能听到你 "
- balabalabala…
两次握手就像 :
- A 对 B 说 :" 嗨 你能听到我么?
- B 对 A 说 :" 嗨, 我能听到你, 你能听到我么? ?? …
… 你能听到我么??? 人呢"
四次握手就像 :
- A 对 B 说 :" 嗨! 你好啊! 你能听到我么?"
- B 对 A 说 :" 嗨! 你好, 我能听到你, 你能听到我么? "
- A 对 B 说 :" 我能听到你 "
- A 对 B 说 :" 啊哈, 那你中午吃了啥啊? 你下午干啥呀? 唉昨天电影好看么?"
- B 对 A 说 :" 我能听到你啊 !"
- A :"…"
四次挥手
当你想要关闭一个 TCP 连接, 连接的任何一方都能发起一个关闭操作, 当然一般是客户端发起关闭操作
该过程是:
- 主动关闭方发送方一个
FIN
报文段(位置如图一), 一般还包含一个ACK
来确认对方最后一次发来的数据 - 被动关闭方回复一个
ACK
表示收到了该FIN
报文段 - 然后, 被动关闭方变成主动关闭者, 给对方发送一个自己的
FIN
, - 对端回复一个
ACK
表示收到了这个FIN
报文段
为什么是四次挥手
这个应该比较容易理解, 因为 TCP 是一个全双工的连接, 为了关闭所有通信通道加上 ACK
的回复, 便是四次了
TCP 状态转换
图三 TCP 状态转换图
TCP 连接的每一端都可以在这些状态中进行转换, 引发转换的条件和转换引发的动作各不相同, 在一个连接中的状态转换更形象的可以对照图一
状态含义
-
CLOSED
表示初始状态。 -
LISTEN
表示服务器端的某个 socket 处于监听状态,可以接受连接。 -
SYN_SENT
在服务端监听后,客户端 socket 执行 connect 连接时,客户端发送SYN
报文,此时客户端就进入SYN_SENT
状态,等待服务端的确认。 -
SYN_RCVD
表示服务端接受到了SYN
报文,在正常情况下,这个状态是服务器端的 socket 在建立 TCP 连接时的三次握手会话过程中的一个中间状态,很短暂,基本上用 netstat 你是很难看到这种状态的,除非你特意写了一个客户端测试程序,故意将三次 TCP 握手过程中最后一个 ACK 报文不予发送。因此这种状态时,当收到客户端的 ACK 报文后,它会进入到ESTABLISHED
状态。 -
ESTABLISHED
表示连接已经建立了。 -
FIN_WAIT_1
这个是已经建立连接之后,其中一方请求终止连接,等待对方的FIN
报文。FIN_WAIT_1
状态是当 socket 在ESTABLISHED
状态时,它想主动关闭连接,向对方发送了FIN
报文,此时该 socket 即进入到FIN_WAIT_1
状态。而当对方回应 ACK 报文后,则进入到FIN_WAIT_2
状态,当然在实际的正常情况下,无论对方何种情况下,都应该马上回应 ACK 报文,所以FIN_WAIT_1
状态一般是比较难见到的,而FIN_WAIT_2
状态还有时常常可以用 netstat 看到。 -
FIN_WAIT_2
实际上FIN_WAIT_2
状态下的 socket,表示半连接,也即有一方要求 close 连接,但另外还告诉对方,我暂时还有点数据需要传送给你,稍后再关闭连接。 -
TIME_WAIT
表示收到了对方的FIN
报文,并发送出了 ACK 报文,就等 2MSL 后即可回到CLOSED
可用状态了。如果FIN_WAIT_1
状态下,收到了对方同时带FIN
标志和 ACK 标志的报文时,可以直接进入到TIME_WAIT
状态,而无须经FIN_WAIT_2
状态。 -
CLOSING
这种状态比较特殊,实际情况中应该是很少见,属于一种比较罕见的例外状态。正常情况下,当你发送FIN
报文后,按理来说是应该先收到(或同时收到)对方的 ACK 报文,再收到对方的FIN
报文。但是CLOSING
状态表示你发送FIN
报文后,并没有收到对方的 ACK 报文,反而却也收到了对方的FIN
报文。什么情况下会出现此种情况呢?其实细想一下,也不难得出结论:那就是如果双方几乎在同时 close 一个 socket 的话,那么就出现了双方同时发送FIN
报文的情况,也即会出现CLOSING
状态,表示双方都正在关闭 socket 连接。 -
CLOSE_WAIT
这种状态的含义其实是表示在等待关闭。怎么理解呢?当对方 close 一个 socket 后发送FIN
报文给自己,你系统毫无疑问地会回应一个 ACK 报文给对方,此时则进入到CLOSE_WAIT
状态。接下来呢,实际上你真正需要考虑的事情是察看你是否还有数据发送给对方,如果没有的话,那么你也就可以 close 这个 socket,发送FIN
报文给对方,也即关闭连接。所以你在CLOSE_WAIT
状态下,需要完成的事情是等待你去关闭连接。 -
LAST_ACK
这个状态还是比较容易好理解的,它是被动关闭一方在发送FIN
报文后,最后等待对方的 ACK 报文。当收到 ACK 报文后,也即可以进入到CLOSED
可用状态了
listen 的 backlog 参数与 TCP 状态
我们知道 listen 函数, 它的的第二个参数是指定能接受连接队列大小
int listen ( int sockfd, int backlog );
而其实, 这个连接队列有两个队列 :
- 一个
未完成连接队列
, 这个队列里面的套接字处于SYN_RCVD
状态, 也就是在收到客户端发来的第一次SYN
请求后, 将这个连接放入该队列 - 一个
已完成连接队列
, 这个队列里的套接字处于ESTABLISHED
状态, 也就是接收到了客户端回复的最后一次ACK
, 于是把这个套接字放入该队列, 处于该队列的套接字等待被进程调用accept
, 就把这个该队列的队列头的套接字返回给进程
关于这两个队列的示意图以及
listen
connect
accept` 在三次握手中的具体细节在 这里 的博客里有
当这两个队列满了, TCP 就会忽略到来的连接请求
TIME_WAIT 参数
我们知道, 当一个连接断开后, 立马再次请求连接该端口, 是连接不上的, 这是因为该连接正处于图二所示的连接中的 TIME_WAIT
状态
之所以有这个状态, 是因为一个报文段在网络中的有一个最大段生存期 (MSL)
, 而TIME_WAIT
状态会等待 2MSL
的时间, 来确保能重新发送最后的 ACK 并保证对端能收到这个最后的 ACK (而重发 ACK 并不是因为 TCP 会重传 ACK, ACK 并不小号序列号, 不会被 TCP 重传 ), 会重新发送 ACK, 是因为对端会重传 FIN
, TCP 总是会重传 FIN
, 直到收到一个最终的 ACK
即, TIME_WAIT
的目的是允许任何一个受制于一条半关闭连接的数据报被丢弃
设置 SO_REUSEADDR
而一个 MSL
为两分钟, 所以你可能会等待 1~4 分钟才能重新想这个端口发起连接, 所以我们一般在服务器会设置 SO_REUSEADDR
选项
SO_REUSEADDR
对于 TCP 来说它允许多了监听套接字绑定到同一个端口, 使用它就是为了在服务端出现 TIME_WAIT
连接时, 能重启这个连接
SO_REUSEPORT
的用法也很简单,只要每个监听套接字在调用 bind()
之前设置好这个选项就可以了:
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
int optval = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval));
bind(sockfd, ...);
重置报文段 RST
如图一所示, TCP 头部有一个 RST
位, 一个将该字段置位的报文段被称为 “重置报文段”, 重置报文段不会被回应----即, 他不会被回复 ACK
重置报文段
会导致一个 TCP 连接的迅速终止 :
- 假如在这个报文段前有数据在排队等待发送, 他们会被立刻抛弃来让 重置报文段 第一时刻发送
- 而发送重置报文段后, 发送方会
立刻
关闭, 而不会等待确认数据是否到达对端,
触发 RST
:
- 访问不存在的端口
- 处理半打开连接
- 异常终止连接
半打开连接
: 在未告知另一端的情况下通信的一端关闭或终止
比如, 通信一方主机崩溃
只要不尝试通过半连接传输数据, 通信另一端不会检测出另一端已经崩溃