TCP 是一个很复杂的课题,这篇文章只是我在学习过程中的一些浅层的想法与思考的记录。先放一张 TCP 的状态迁移图,本篇博客记录一下对于这个状态迁移图中的 建立连接 、 断开连接 的情况,下面的内容针对图中有标记的部分进行介绍。
1 建立连接
建立连接对应图中的红色和橙色的箭头,其中 红色实线 是一般情况下客户端的状态迁移路线,红色虚线 是一般情况下服务器的状态迁移路线,而 橙色实线 说明了另一种特殊的状态迁移路线:P2P 连接
一般情况建立连接(红色实线/虚线)
下图是正常连接建立的状态图,也就是 TCP 的三次握手建立连接,左侧是客户端,对应大图中红色实线的状态迁移,右侧是服务器端,对应大图中红色虚线的状态迁移
关注以下几个点:
-
- listen()
-
listen()
函数是用于准备接受传入连接的系统调用。当在一个套接字上调用此函数时,它将该套接字标记为“被动套接字”,这意味着套接字将用于接受传入的连接请求,而不是主动发起连接。 -
tcb(传输控制块,Transmission Control Block)是一个数据结构,用于在TCP协议内部跟踪TCP连接的状态和控制信息。当调用
listen()
函数后,tcb->status = TCP_STATUS_LISTEN
,该状态被设置为TCP_STATUS_LISTEN
,表示套接字正在监听模式,准备接受传入的连接请求。 -
tcb->syn_queue
:是一个用于存放已经收到的SYN(同步)包,但还未完成整个TCP三次握手过程的连接请求的队列。当一个客户端尝试连接到服务器时,它首先发送一个带有SYN标志的TCP包。服务器接收到这个包后,会将该连接放入SYN队列,并发送SYN-ACK响应。 -
tcb->accept_queue
:是一个用于存放已经完成三次握手并准备被应用程序“接受”的连接队列。当TCP三次握手成功完成后,连接从SYN队列移动到接受队列。应用程序随后可以通过调用accept()
函数从此队列中获取连接。
-
- 三次握手与api的关系
- SYN:客户端发送一个SYN包(通过
connect()
函数)来发起一个连接。 - SYN-ACK:服务端接收到SYN包,然后返回一个SYN-ACK包(在
accept()
之前的内部处理)。 - ACK:客户端接收到SYN-ACK包后,发送一个ACK包(这通常是
connect()
函数的一部分)。 - 一旦三次握手完成,TCP连接就建立了,此时服务端的
accept()
函数返回,客户端的connect()
函数完成,双方可以开始数据传输。
-
- 第三次握手数据包匹配半连接队列中的节点
- 接收ACK包:服务器接收到来自客户端的ACK包。
- 查找匹配的连接请求:
- 服务器使用ACK包中的信息(源和目标IP地址和端口号)来识别和匹配半连接队列中相应的连接请求。
- 检查ACK包中的确认号是否与半连接队列中某个条目的序列号加1匹配(因为ACK的确认号应该是服务器SYN-ACK包中的序列号加1)。
-
- listen(fd, backlog) 中 backlog 参数
- SYN/半连接队列。早期为了防止syn泛洪设置的,但是对于半连接队列的控制可以在防火墙完成,因此这种定义逐渐被弃用。
- SYN+ACCEPT 队列总长度。
- ACCEPT 队列。已经完成三次握手,等待应用程序接受的连接数。
P2P 建立连接的过程(橙色实线)
TCP 连接的建立并不一定需要使用 listen()
函数,原因就在于有橙色的这条状态迁移路线,两端对等的情况下,相互调用 connect() 函数,也就是都向对方发送 syn 包,然后收到对方的 syn 后向对方回复 ack、syn 包,收到之后再向对方发送 ack 包的过程,如下图所示
描述这个过程的部分代码如下
int main(int argc, char *argv[]) {
if (argc < 3) {
printf("wrong arg number\n");
exit(EXIT_FAILURE);
}
char *ip = argv[1];
int port = atoi(argv[2]);
int sockfd = bind_port("0.0.0.0", 28888);
while (1) {
if (connect_remote(sockfd, ip, port) < 0) {
usleep(1);
continue;
} else {
printf("connected\n\n");
break;
}
}
……
bind_port()
:返回一个fd,该套接字绑定了本地的ip和端口connect_remote()
:返回 connect() 函数的结果,-1为connect失败,0为成功。在while循环里尝试连接参数ip、port的远程主机,连接上了就退出循环,连接不上就一直尝试连接
2 断开连接
断开连接对应大图中下半段绿色的部分,绿色虚线 描述了一般情况下服务器断开连接的状态迁移过程,绿色实线 描述了客户端一般情况下断开连接的状态迁移过程,方框中表达的是:由于数据包到达先后造成的 1 和 2 ,双方同时断开连接的情况 3)。所以分类为实线客户端和虚线服务器不是很准确,暂时这样描述。下面主要讲解一下方框中的 3 种情况
*1* 一般的正常情况
*2* 先收到 fin,再收到 ack
- A发送FIN:A完成数据发送,发送FIN包到B,表明A已完成发送数据。A进入
FIN_WAIT_1
状态。 - B收到FIN,发送ACK和FIN:B几乎同时发送ACK响应A的FIN,并发送自己的FIN。由于网络原因,B的FIN比ACK先到达A。
- A先收到B的FIN:A首先收到B的FIN,作为响应发送ACK。此时,A没有更多的数据要发送,也已经知道B已经完成了数据发送,所以A跳过
FIN_WAIT_2
状态,直接进入TIME_WAIT
状态。 - A收到B的ACK:随后,A收到对自己原始FIN的ACK响应。由于A已经处于
TIME_WAIT
状态,这个ACK不会导致状态改变。
*3* 没收到ack,先收到fin,直接发送ack
没收到ack,先收到fin,直接发送ack。收到fin变为closing状态。这是双方同时调用close()的结果
- 双方各自发送FIN:每个端点发送FIN报文以关闭其发送方向的连接。这意味着每个端点都不再发送数据,但仍然可以接收数据。
- 接收对方的FIN:每个端点接收到对方的FIN报文。
- 发送ACK响应对方的FIN:每个端点在接收到对方的FIN后,发送ACK作为响应。
在这种特殊情况下,因为双方都在等待对方对自己发送的FIN的确认,所以他们可能会进入 CLOSING
的状态。当一个端点发送了 FIN 并收到了对方的 FIN(但还没有收到对方对其FIN的确认),它就会进入 CLOSING
状态。
写在最后
这里只是讨论了 TCP 状态迁移图中建立连接和断开连接的几种情况,主要关注的是一些特殊的情况,比如 ack 和 fin 没有按照正常的顺序到达、以及建立连接的特殊情况:P2P 方式
这个状态迁移图还有一些重点比如: TIME_WAIT
状态、服务器出现大量 TIME_WAIT
状态的原因、滑动窗口、拥塞控制、可靠传输机制等,后续再整理学习