一、理解CLOSE_WAIT状态
当客户端和服务器在进行TCP通信时,若客户端调用close函数关闭对应的文件描述符,此时客户端底层操作系统就会向服务器发起FIN请求,服务器收到该请求后会对其进行ACK响应。
但若当服务器收到客户端的FIN请求后,服务器端不调用close函数关闭对应的文件描述符,那么服务器就不会给客户端发送FIN请求,相当于只完成了四次挥手中的前两次挥手,此时客户端和服务器的连接状态分别会变为FIN_WAIT_2和CLOSE_WAIT
可以编写一个简单的TCP服务器端来模拟出该现象,采用一些网络工具来充当客户端向服务器发起连接请求
服务器的初始化需要进行套接字的创建、绑定以及监听,然后主线程就可以通过调用accept函数从底层获取建立好的连接了。获取到连接后主线程创建新线程为该连接提供服务,而新线程只需执行一个死循环逻辑即可
#include <iostream> #include <cstring> #include <unistd.h> #include <sys/socket.h> #include <sys/types.h> #include <arpa/inet.h> #include <netinet/in.h> #include <pthread.h> const int port = 8082; const int num = 5; void* Routine(void* arg) { pthread_detach(pthread_self()); int fd = *(int*)arg; delete (int*)arg; while (1){ std::cout << "socket " << fd << " is serving the client" << std::endl; sleep(1); } return nullptr; } int main() { //创建监听套接字 int listen_sock = socket(AF_INET, SOCK_STREAM, 0); if (listen_sock < 0) { std::cerr << "socket error" << std::endl; return 1; } //绑定 struct sockaddr_in local; memset(&local, 0, sizeof(local)); local.sin_port = htons(port); local.sin_family = AF_INET; local.sin_addr.s_addr = INADDR_ANY; if (bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){ std::cerr << "bind error" << std::endl; return 2; } //监听 if (listen(listen_sock, num) < 0){ std::cerr << "listen error" << std::endl; return 3; } //启动服务器 struct sockaddr_in peer; memset(&peer, 0, sizeof(peer)); socklen_t len = sizeof(peer); for (;;){ int sock = accept(listen_sock, (struct sockaddr*)&peer, &len); if (sock < 0){ std::cerr << "accept error" << std::endl; continue; } std::cout << "get a new link: " << sock << std::endl; int* p = new int(sock); pthread_t tid; pthread_create(&tid, nullptr, Routine, (void*)p); } return 0; }
代码编写完成后编译并运行服务器,并用telnet工具连接服务器,最后运行下面监控脚本
[cl@VM-0-15-centos DisconnectStatus]$ while :; do sudo netstat -ntp|head -2&&sudo netstat -ntp | grep 8082; sleep 1; echo "##################"; done
令telnet退出,即客户端向服务器发起了连接断开请求,但此时服务器端并没有调用close函数关闭对应的文件描述符,所以当telnet退出后,客户端维护的连接的状态会变为FIN_WAIT_2,而服务器维护的连接的状态会变为CLOSE_WAIT
因此若不及时关闭不用的文件描述符,除了会造成文件描述符泄漏,可能导致连接资源没有完全释放,即内存泄漏问题
二、理解TIME_WAIT状态
当客户端和服务器在进行TCP通信时,客户端调用close函数关闭对应的文件描述符,若服务器收到后也调用close函数进行了关闭,那么此时双方将正常完成四次挥手。但主动发起四次挥手的一方在四次挥手后,不会立即进入CLOSED状态,而是进入短暂的TIME_WAIT状态等待若干时间,最终才会进入CLOSED状态
继续刚才的实验,由于telnet退出后服务器端没有调用close关闭对应的文件描述符,因此客户端维护的客户端维护连接的状态停留在了FIN_WAIT_2状态,而服务器维护连接的状态停留在了CLOSE_WAIT状态。
要让客户端和服务器继续完成后两次挥手,就需要服务器端调用close函数关闭对应的文件描述符。虽然服务器代码中没有调用close函数,但因为文件描述符的生命周期是随进程的,当进程退出的时候,该进程所对应的文件描述符都会自动关闭。因此只需要在telnet退出后让服务器进程退出就行了,此时服务器进程所对应的文件描述符会自动关闭,此时服务器底层TCP就会向客户端发送FIN请求,完成剩下的两次挥手。
四次挥手后客户端维护的连接就会进入到TIME_WAIT状态,而服务器维护的连接则会进入到CLOSED状态
解决TIME_WAIT状态引起的bind失败的方法
主动发起四次挥手的一方在四次挥手后,会进入TIME_WAIT状态。若在有客户端连接服务器的情况下服务器进程退出了,就相当于服务器主动发起了四次挥手,此时服务器维护的连接在四次挥手后就会进入TIME_WAIT状态
在该连接处于TIME_WAIT期间,若服务器想重新启动,则会出现绑定失败问题
在TIME_WAIT期间,连接并没有被完全释放,即服务器绑定的端口号仍然被占用,此时服务器若想继续绑定该端口号,则只能等待TIME_WAIT结束。但当服务器崩溃后最重要的是让服务器立马重新启动,若想要让服务器崩溃后在TIME_WAIT期间也能立刻重新启动,需要让服务器在调用socket函数创建套接字后,继续调用setsockopt函数设置端口复用,这也是编写服务器代码时的推荐做法
setsockopt函数
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
sockfd:需要设置的套接字对应的文件描述符
level:被设置选项的层次。比如在套接字层设置选项对应就是SOL_SOCKET
optname:需要设置的选项。该选项的可取值与设置的level参数有关
optval:指向存放选项待设置的新值的指针
optlen:待设置的新值的长度
返回值:设置成功返回0,设置失败返回-1,同时错误码会被设置
设置监听套接字,将监听套接字在套接字层设置端口复用选项SO_REUSEADDR,该选项设置为非零值表示开启端口复用
int opt = 1; setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
此时当服务器崩溃后即可立马重新启动,而不用等待TIME_WAIT结束
实验
先启动服务器,再启动客户端连接服务器,此时出现两个ESTABLISHED连接
此时向服务器发送信号使得服务器退出,客户端检测到服务器关闭也关闭了文件描述符,由于服务器主动发起四次挥手,其状态变为TIME_WAIT
由于代码中调用了setsockopt函数,设置监听套接字端口复用,此时可用重新启动服务器。接下来重新启动服务器和客户端,此时出现了三个连接,其中8082端口绑定了两个连接(端口复用)
从上述实验中可以看出,即便通信双方对应的进程都退出,但服务器端依然存在一个处于TIME_WAIT状态的连接,这也说明了进程管理和连接管理是两个相对独立的单元。连接由TCP自行管理,连接不一定会随进程的退出而关闭