总述
????????文章以【网络编程】TCP socket 单机通信实验?为基础。对 TCP 连接中的四种 WAIT 状态进行分析实验。从服务端开始监听到建立连接、收发数据,到最后连接完全关闭,TCP 一共会经历 11 种状态,总体状态变化图如下(假设由 client 主动关闭连接,server 被动关闭),其中有四种 "WAIT" 状态 —— FIN_WAIT_1、FIN_WAIT_2、TIME_WAIT、CLOSE_WAIT?—— 容易混淆,而且有很多 “诡异” 的问题也于此有关:
?图片来源:自己拍的《TCP/IP 详解 卷一:协议》 第 441 页
FIN_WAIT_1 和?FIN_WAIT_2
? ? ? ? client?应用层调用 close 接口,主动发送 FIN 包(第一次挥手),进入 FIN_WAIT_1 状态。待 FIN 包发送到 server,server 返回的?ACK 到达后(第二次挥手),client 进入 FIN_WAIT_2 状态。正常情况下,FIN_WAIT_1 是一个非常短暂的状态。FIN_WAIT_2 的时间长度则取决于 server 什么时候进行第三次挥手。
CLOSE_WAIT 和?LAST_ACK
? ? ? ? server 在收到 FIN(第一次挥手)后马上答复 ACK(第二次挥手),自身的状态也变化为?CLOSE_WAIT。等到 server 的应用层主动调用 close 接口,发送 FIN 包给 client(第三次挥手),server 结束 CLOSE_WAIT 状态,转换为?LAST_ACK 状态。待 server 发送的 FIN 包到达 client,client 返回 ACK,server 的状态变为 CLOSED,连接结束。和 FIN_WAIT_1 一样,LAST_ACK 的存在时间很短。CLOSE_WAIT 持续时间的长短则取决于 server 的应用层:
????????CLOSE_WAIT 可能几乎不存在。在【网络编程】TCP socket 单机通信实验?中因为 client 和 sever 几乎同时发完数据、同时调用 close 接口,第二次挥手和第三次挥手合并在一个数据包里发送(这里是 server 先调用 close):
?? ? ? ? CLOSE_WAIT 也可能相对长时间地存在。如果完成前两次挥手后,server 迟迟不调用 close(可能是数据还真的没有发完,也可能是 server 应用层代码的 bug,关闭连接不够及时),就会一直保持在 CLOSE_WAIT 状态。修改 server 的代码,让 server 持续读取数据,直到 recv 返回 0,说明 client 已经调用了 close,此时让 server 线程休眠 30s:
????????30s 内用 netstat 查看连接状态,server 会一直处于 CLOSE_WAIT 状态,client 则处于 FIN_WAIT_2 状态(因为没有收到 server 的 FIN 包):?
关于 close 和 shutdown
????????在 CLOSE_WAIT 阶段,因为从 server 到 client 这一方向的连接还没有关闭,如果 server 还没有发送完数据,是可以给 client 继续发送数据的。但实际上,如果只是简单地在 client 调用 close 后增加 recv 的代码:
? ? ? ? ?同时,server 在 recv 返回 0 后增加发送数据的代码(SERVER_SEND_BYE 为 宏定义,值是 "Goodbye Client~"):
????????client 的应用层不仅接收不到最后的 good bye,client 在收到数据包后还会给?server 返回 RST 包直接断掉连接:
? ? ? ? 原因是 close 会把套接字标记为已关闭,关闭之后就不能再被应用层使用,也就是说不能再作为 recv 或 send 的第一个参数。为了能在 CLOSE_WAIT 阶段进行数据的收发,需要把 client 调用 close 改为调用 shutdown,在发送完数据后先关闭连接写的一半,接收完最后的数据再关闭连接读的一半:
TIME_WAIT?
? ? ? ? client 收到 server 的 FIN 后答复 ACK,并进入 TIME_WAIT 状态(这个 ACK 发到 server,server 就进入 CLOSED 状态)。TIME_WAIT 的时间长度是 2MSL。MSL(Maximum Segment Lifetime)是任何 IP 数据报能够在因特网中存活的最长时间,如果一个报文段发出后,经过 MSL 还没有来得及到达终点,就会被丢弃。关于为什么要设置 TIME_WAIT,从《UNIX 网络编程 卷1:套接字联网 API》里找到了两点:
- 可靠地实现 TCP 全双工连接的终止:假设第四次挥手的报文段(ACK)没有到达 server,那么 server 将重发 FIN(重新进行第三次挥手),收到重发的 FIN 后,主动关闭连接的 client 的也需要重发 ACK;如果没有 TIME_WAIT 的等待时间,重发的 FIN 达到后,client 将回以 RST,导致连接异常终止。
- 允许老的重复分节在网络中消逝:我们关闭一个连接,过一段时间后在相同的 IP 地址和端口之间建立另一个连接;后一个连接称为前一个连接的化身,因为它们的地址和端口号都相同;TCP 必须防止来自某个连接的老的重复分在在该连接已终止后再出现,从而被误解成属于同一连接的某个新的化身;为了做到这一点,TCP 将不给处于?TIME_WAIT 状态的连接发起新的化身;既然?TIME_WAIT 状态的持续时间是 MSL 的 2 倍,这就足以让某个方向上的分组最多存活 MSL 秒即被丢弃,另一个方向的应答最多存活 MSL 秒也被丢弃。通过实施这个规则,我们就能保证每成功建立一个 TCP 连接时,来自该连接先前化身的老的重复分组都已在网络中消逝了。
? ? ? ? 在【网络编程】TCP socket 单机通信实验 里,如果在短时间内连续启动服务端,可能出现端口绑定失败的情况,印证了第 2 点的说法:
? ? ? ? MSL 在实现中的常用值是 30 秒、1 分钟或者 2 分钟,?也就是一旦进入 TIME_WAIT 状态,就会有至少 1 分钟的时间,端口无法使用。另一方面,在 TIME_WAIT 状态,连接句柄是不会被释放的,只有转到 CLOSED 状态后才会。文件描述符的数量有限(一般是 1024 个),如果短时间内,有大量的处于?TIME_WAIT 状态的连接,会导致文件描述符耗尽,涉及到文件描述符的操作都会受到影响。在这一点上,CLOSE_WAIT 也是一样的,大量 CLOSE_WAIT 状态的连接同样会导致文件描述符耗尽。所以,最好让这两种状态尽快结束或者干脆不要进入。规避 TIME_WAIT 带来的影响的方式主要有两种:
让客户端先关闭连接
? ? ? ? 只有主动关闭连接的一方,才会进入 TIME_WAIT 状态。如果 client 先调用 close 接口,server 后调用,server 就不会进入 TIME_WAIT 状态。?按照上面?CLOSE_WAIT 和?LAST_ACK 部分对代码的修改 —— 直到 recv 返回 0 才调用 close 接口,就可以实现让客户端先(主动)关闭连接,避免 server 进入 TIME_WAIT 状态。?
设置端口可复用
? ? ? ? 如果屏蔽掉?CLOSE_WAIT 和?LAST_ACK 部分对代码的修改,允许 server 主动关闭连接进入 TIME_WAIT 状态,则可以设置 socket 的 SO_REUSEADDR(端口重用) 属性,在 TIME_WAIT 时允许端口复用:
? ? ? ? ?设置选项后,先运行一次程序,然后用 netstat 查看,server 处于?TIME_WAIT 状态:
????????紧接着再次运行程序,server 绑定端口成功。第二次运行结束后,用 netstat 查看,有两个服务端的连接处于 TIME_WAIT 状态(在 TIME_WAIT 状态结束前用同一个端口又完成了一次消息通信):
? ? ? ? 针对 TIME_WAIT,让客户端先关闭连接是比设置端口重用更好的一种方式,避免 server 进入 TIME_WAIT 状态可以让资源尽快释放。设置端口可重用也还有其他更大的用处。
完整代码
头文件
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <pthread.h> // 用到了 pthread 库,编译要加选项 -lpthread
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
宏定义
#define LOCAL_IP_ADDR "127.0.0.1"
#define SERVER_LISTEN_PORT 5197
#define NET_MSG_BUF_LEN 128
#define CLINET_SEND_MSG "Hello Server~"
#define SERVER_SEND_MSG "Hello Client~"
#define SERVER_SEND_BYE "Goodbye Client~"
服务端线程入口函数
void* socketServer(void* param){
int iRes = 0;
int iLsnFd, iConnFd;
int iReuse = 0;
int iNetMsgLen = 0;
socklen_t iSockAddrLen = 0;
char szNetMsg[NET_MSG_BUF_LEN] = {0};
struct sockaddr_in stLsnAddr;
struct sockaddr_in stCliAddr;
// 1 参指定协议族,AF_INET 对应 IPv4
// 2 参指定套接字类型,SOCK_STREAM 对应 面向连接的流式套接字
// 3 参指定协议类型,0 对应 TCP 协议
iLsnFd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == iLsnFd) {
printf("Server failed to create socket, err[%s]\n",
strerror(errno));
return NULL;
}
// 设置端口复用需要在 bind 端口之前进行,而且所有使用同一端口的套接字都要设置可复用选项
// 1 参传入 socket 句柄(监听句柄)
// 2 参传入 socket 选项所在的协议层,SOL_SOCKET 表示在套接字级别上设置选项
// 3 参传入选项名,设置端口可复用用选项名 SO_REUSEPORT
// 4 参传入保存选项值的内存地址,设置端口可复用,选项值传 1
// 5 参传入保存选项值的内存空间大小
iReuse = 1;
iRes = setsockopt(iLsnFd, SOL_SOCKET, SO_REUSEPORT, &iReuse, sizeof (iReuse));
if (-1 == iRes) {
printf("Server failed set reuse attr, err[%s]\n", strerror(errno));
close(iLsnFd);
return NULL;
}
// 填写监听地址,设置 s_addr = INADDR_ANY 表示监听所有网卡上对应的端口
stLsnAddr.sin_family = AF_INET;
stLsnAddr.sin_port = htons(SERVER_LISTEN_PORT);
stLsnAddr.sin_addr.s_addr = INADDR_ANY;
// 1 参传入 socket 句柄,2 参传入监听地址,3 参传入监听地址结构体的大小
iRes = bind(iLsnFd, (struct sockaddr*)&stLsnAddr, sizeof(stLsnAddr));
if (-1 == iRes) {
printf("Server failed to bind port[%u], err[%s]\n",
SERVER_LISTEN_PORT, strerror(errno));
close(iLsnFd);
return NULL;
} else {
printf("Server succeeded to bind port[%u], start listen.\n",
SERVER_LISTEN_PORT);
}
// 1 参传入监听句柄,2 参设置已完成连接队列(已完成三次握手,未 accept 的连接)的长度
iRes = listen(iLsnFd, 16);
if (-1 == iRes) {
printf("Server failed to listen port[%u], err[%s]\n",
SERVER_LISTEN_PORT, strerror(errno));
close(iLsnFd);
return NULL;
}
iSockAddrLen = sizeof(stCliAddr);
// 1 参传入监听句柄,2 传入地址结构体指针接收客户端地址,3 参传入地址结构体大小
iConnFd = accept(iLsnFd, (struct sockaddr*)&stCliAddr, &iSockAddrLen);
if (-1 == iConnFd) {
printf("Server failed to accept connect request, err[%s]\n",
strerror(errno));
close(iLsnFd);
return NULL;
} else {
printf("Server accept connect request from[%s:%u]\n",
inet_ntoa(stCliAddr.sin_addr), ntohs(stCliAddr.sin_port));
}
// 1 参传已连接套接字描述符,2 参传缓冲区指针,3 参传缓冲区大小,4 参指定行为,默认为 0
iNetMsgLen = recv(iConnFd, szNetMsg, sizeof(szNetMsg), 0);
if (iNetMsgLen < 0) {
printf("Server failed to read from network, err[%s]\n", strerror(errno));
close(iConnFd);
close(iLsnFd);
return NULL;
} else {
printf("Server recv msg[%s]\n", szNetMsg);
}
// 1 参传已连接套接字的描述符,2 参传指向消息数据的指针
// 3 参传消息长度,4 参指定行为,默认为 0
iNetMsgLen = send(iConnFd, SERVER_SEND_MSG, strlen(SERVER_SEND_MSG), 0);
if (iNetMsgLen < 0) {
printf("Server failed to reply client, err[%s]\n", strerror(errno));
close(iConnFd);
close(iLsnFd);
return NULL;
}
while (1)
{
iNetMsgLen = recv(iConnFd, szNetMsg, sizeof(szNetMsg), 0);
if (iNetMsgLen < 0) {
printf("Server failed to read from network, err[%s]\n", strerror(errno));
break;
} else if (iNetMsgLen == 0) {
printf("Server recv return zero, client already closed connection\n");
iNetMsgLen = send(iConnFd, SERVER_SEND_BYE, strlen(SERVER_SEND_BYE), 0);
if (iNetMsgLen < 0) {
printf("Server failed to say bye, err[%s]\n", strerror(errno));
}
break;
}
}
close(iConnFd);
close(iLsnFd);
return NULL;
}
客户端线程入口函数
void* socketClient(void* param){
int iRes = 0;
int iConnFd;
int iNetMsgLen = 0;
char szNetMsg[NET_MSG_BUF_LEN] = {0};
struct sockaddr_in stServAddr;
iConnFd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == iConnFd) {
printf("Client failed to create socket, err[%s]\n", strerror(errno));
return NULL;
}
// 填充目标地址结构体,指定协议族、目标端口、目标主机 IP 地址
stServAddr.sin_family = AF_INET;
stServAddr.sin_port = htons(SERVER_LISTEN_PORT);
stServAddr.sin_addr.s_addr = inet_addr(LOCAL_IP_ADDR);
// 1 参传套接字句柄,2 参传准备连接的目标地址结构体指针,3 参传地址结构体大小
while (1)
{
iRes = connect(iConnFd, (struct sockaddr *)&stServAddr, sizeof(stServAddr));
if (0 != iRes) {
printf("Client failed to connect to[%s:%u], err[%s]\n",
LOCAL_IP_ADDR, SERVER_LISTEN_PORT, strerror(errno));
sleep(2);
continue;
}
else {
printf("Client succeeded to connect to[%s:%u]\n",
LOCAL_IP_ADDR, SERVER_LISTEN_PORT);
break;
}
}
iNetMsgLen = send(iConnFd, CLINET_SEND_MSG, strlen(CLINET_SEND_MSG), 0);
if (iNetMsgLen < 0) {
printf("Client failed to send msg to server, err[%s]\n", strerror(errno));
close(iConnFd);
return NULL;
}
iNetMsgLen = recv(iConnFd, szNetMsg, sizeof(szNetMsg), 0);
if (iNetMsgLen < 0) {
printf("Client failed to read from network, err[%s]\n", strerror(errno));
close(iConnFd);
return NULL;
}
else {
printf("Client recv reply[%s]\n", szNetMsg);
}
#if 0
// close 后套接字无法再被使用
close(iConnFd);
#else
// 1 参传入 socket 句柄,2 参传入 socket 连接的断开方式:
// SHUT_WR 关闭连接的写这一半,SHUT_RD 关闭连接的读这一半,SHUT_RDWR 把读和写都关掉
shutdown(iConnFd, SHUT_WR);
#endif
while (1)
{
iNetMsgLen = recv(iConnFd, szNetMsg, sizeof(szNetMsg), 0);
if (iNetMsgLen < 0) {
printf("Client failed to read from network after close, err[%s]\n",
strerror(errno));
break;
} else if (iNetMsgLen == 0) {
printf("Client recv return zero, server closed connection too\n");
break;
} else {
printf("Client recv msg[%s] after close socket\n", szNetMsg);
}
}
#if 1
// 关闭连接读的一半
shutdown(iConnFd, SHUT_RD);
#endif
return NULL;
}
主函数
int main(){
// 线程 ID,实质是 unsigned long 类型整数
pthread_t thdServer = 1;
pthread_t thdClient = 2;
// 1 参传线程 ID,2 参传线程属性,3 参指定线程入口函数,4 参指定传给入口函数的参数
pthread_create(&thdServer, NULL, socketServer, NULL);
pthread_create(&thdClient, NULL, socketClient, NULL);
// 1 参传入线程 ID,2 参用于接收线程入口函数的返回值,不需要返回值则置 NULL
pthread_join(thdServer, NULL);
pthread_join(thdClient, NULL);
return 0;
}
|