1、提高篇答疑:如何理解TCP四次挥手?
1.1、如何理解 TCP 四次挥手?
TCP 建立一个连接需 3 次握手,而终止一个连接则需要四次挥手。四次挥手的整个过程是这样的:
首先,一方应用程序调用 close,我们称该方为主动关闭方,该端的 TCP 发送一个 FIN 包,表示需要关闭连接。
之后主动关闭方进入 FIN_WAIT_1 状态。
接着,接收到这个 FIN 包的对端执行被动关闭。
这个 FIN 由 TCP 协议栈处理,我们知道,TCP 协议栈为 FIN 包插入一个文件结束符 EOF 到接收缓冲区中,应用程序可以通过 read 调用来感知这个 FIN 包。
一定要注意,这个 EOF 会被放在已排队等候的其他已接收的数据之后,这就意味着接收端应用程序需要处理这种异常情况,因为 EOF 表示在该连接上再无额外数据到达。此时,被动关闭方进入 CLOSE_WAIT 状态。
接下来,被动关闭方将读到这个 EOF,于是,应用程序也调用 close 关闭它的套接字,这导致它的 TCP 也发送一个 FIN 包。
这样,被动关闭方将进入 LAST_ACK 状态。
最终,主动关闭方接收到对方的 FIN 包,并确认这个 FIN 包。
主动关闭方进入 TIME_WAIT 状态,而接收到 ACK 的被动关闭方则进入 CLOSED 状态。
经过 2MSL 时间之后,主动关闭方也进入 CLOSED 状态。
每个方向都需要一个 FIN 和一个 ACK,因此通常被称为四次挥手。
当然,这中间使用 shutdown,执行一端到另一端的半关闭也是可以的。
1.2、最大分组 MSL 是 TCP 分组在网络中存活的最长时间吗?
MSL 是任何 IP 数据报能够在因特网中存活的最长时间。
其实它的实现不是靠计时器来完成的,在每个数据报里都包含有一个被称为 TTL(time to live)的 8 位字段,它的最大值为 255。
TTL 可译为“生存时间”,这个生存时间由源主机设置初始值,它表示的是一个 IP 数据报可以经过的最大跳跃数,每经过一个路由器,就相当于经过了一跳,它的值就减 1,当此值减为 0 时,则所在的路由器会将其丢弃,同时发送 ICMP 报文通知源主机。
RFC793 中规定 MSL 的时间为 2 分钟,Linux 实际设置为 30 秒。
问题:MSL和TTL的关系?
TTL:
Time To Live
指定了IP包允许跳转(允许通过的最大网段数量)的路由器数量,最大值为255
,推荐值为64;- TTL在IP数据包中表示。
IP数据包每经过一个路由器其值就会-1,一旦TTL=0
路由器就会将该IP数据包丢弃,并向IP包的发送者发送 ICMP time exceeded
消息;
TTL主要就是为了防止IP数据包在网络上出现无限的循环跳转,一旦出现循环跳转的话就会比较浪费网络资源
MSL:
Maximum Segment Lifetime
最大报文段生存时间,MSL要大于等于TTL;
- 一旦IP包只要送到对方的机器上,那么此包就一定不会超过
MSL
; - 如果MSL小于TTL那么IP包送到了但是TCP包超时了那么就会比较浪费资源,并且因为TCP包是包含在IP包上的,里面的时间按逻辑上就应该大于等于TTL的
- 所以只要经过了MSL时间,那么TCP包就肯定已经被传输线路上的路由器给丢弃了
1.3、关于 listen 函数中参数 backlog 的释义问题
从 Linux 2.2 开始,backlog 的参数内核有了新的语义,它现在定义的是已完成连接队列的最大长度,表示的是已建立的连接(established connection),正在等待被接收(accept 调用返回)
listen 函数:
int listen (int socketfd, int backlog)
第一个参数 socketdf 为套接字描述符;
第二个参数 backlog,在 Linux 中表示已完成 (ESTABLISHED) 且未 accept 的队列大小,这个参数的大小决定了可以接收的并发数目。这个参数越大,并发数目理论上也会越大。但是参数过大也会占用过多的系统资源,一些系统,比如 Linux 并不允许对这个参数进行改变。
1.4、UDP 连接和断开套接字的过程是怎样的?
UDP 连接套接字不是发起连接请求的过程,而是记录目的地址和端口到套接字的映射关系。
断开套接字则相反,将删除原来记录的映射关系。
UDP调用connect,只是将目的地址和端口到socket的映射关系记录了下来,这样内核收到ICMP报文后就能转发给对应的UDP应用
1.5、在 UDP 中不进行 connect,为什么客户端会收到信息?
问题:
有人说,如果按照我在文章中的说法,UDP 只有 connect 才建立 socket 和 IP 地址的映射,那么如果不进行 connect,收到信息后内核又如何把数据交给对应的 socket?
这是两个不同的 API 场景。
**第一个场景:**就是讨论的 connect 场景,在这个场景里,我们讨论的是 ICMP 报文和 socket 之间的定位。
- 我们知道,ICMP 报文发送的是一个不可达的信息,不可达的信息是通过目的地址和端口来区分的,如果没有 connect 操作,目的地址和端口就没有办法和 socket 套接字进行对应,所以,即使收到了 ICMP 报文,内核也没有办法通知到对应的应用程序,告诉它连接地址不可达。
为什么在不 connect 的情况下,我们的客户端又可以收到服务器回显的信息了?
**第二个场景:**报文发送的场景
- 注意服务器端程序,先通过 recvfrom 函数调用获取了客户端的地址和端口信息,这当然是可以的,因为 UDP 报文里面包含了这部分信息。
- 然后我们看到服务器端又通过调用 sendto 函数,把客户端的地址和端口信息告诉了内核协议栈,可以肯定的是,之后发送的 UDP 报文就带上了客户端的地址和端口信息,通过客户端的地址和端口信息,可以找到对应的套接字和应用程序,完成数据的收发。
//服务器端程序,先通过recvfrom函数调用获取了客户端的地址和端口信息
int n = recvfrom(socket_fd, message, MAXLINE, 0, (struct sockaddr *) &client_addr, &client_len);
message[n] = 0;
printf("received %d bytes: %s\n", n, message);
char send_line[MAXLINE];
sprintf(send_line, "Hi, %s", message);
//服务器端程序调用send函数,把客户端的地址和端口信息告诉了内核
sendto(socket_fd, send_line, strlen(send_line), 0, (struct sockaddr *) &client_addr, client_len);
总结:
从代码中可以看到:
- 这里的 connect 的作用是记录客户端目的地址和端口–套接字的关系;
- 而之所以能正确收到从服务器端发送的报文,那是因为系统已经记录了客户端源地址和端口–套接字的映射关系。
1.6、我们是否可以对一个 UDP 套接字进行多次 connect 的操作?
- TCP 套接字,connect 只能调用一次。
- UDP 套接字来说,进行多次 connect 操作是被允许的
UDP套接字多次connect的作用:
- 可以重新指定新的 IP 地址和端口号;
- 可以断开一个已连接的套接字;为了断开一个已连接的 UDP 套接字,第二次调用 connect 时,调用方需要把套接字地址结构的地址族成员设置为 AF_UNSPEC。
1.7、第 11 讲中程序和时序图的解惑
在 11 讲中,我们讲了关闭连接的几种方式,有同学对这一篇文章中的程序和时序图存在疑惑,并提出了下面几个问题:
问题:
1、当一方主动 close 之后,另一方发送数据的时候收到 RST。主动方缓冲区会把这个数据丢弃吗?这样的话,应用层应该读不到了吧?
2、代码中 SIGPIPE 的作用不是忽略吗?为什么服务器端会退出?
默认的 SIGPIPE 忽略行为就是退出程序,什么也不做,当然,实际程序还是要做一些清理工作的。
3、shutdown关闭一段的情况;
- 如果主动关闭的一方调用 shutdown 关闭写端,没有关闭读这一端,主动关闭的一方可以读到对端的数据;
- 注意这个时候主动关闭连接的一方是在使用 read 方法进行读操作,而不是 write 写操作,不会有 RST 的发生,更不会有 SIGPIPE 的发生。