目录
[理解 CLOSE_WAIT 状态](#理解 CLOSE_WAIT 状态)
[理解 listen 的第二个参数](#理解 listen 的第二个参数)
理解TIME_WAIT状态
现在做一个测试,首先启动server,然后启动client,然后用Ctrl-C使server终止,这时马上再运行server,类似结果是:

这是因为,虽然server的应用程序终止了,但TCP协议层的连接并没有完全断开,因此不能再次监 听同样的server端口。我们用netstat命令查看一下可以看到如下类似效果:

可以看到,我们是server先退出,则此时client则会进入到TIME_WAIT状态。
这里需要补充的是:根据前面文章学的,并不是服务端才会有CLOSE_WAIT状态,而是四次挥手过程中,CLOSE_WAIT 状态可以出现在任何一方(客户端或服务端),具体取决于哪一方先主动发起关闭连接。
- TCP协议规定,主动关闭连接的一方要处于TIME_ WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态。
- 我们使用Ctrl-C终止了server,所以server是主动关闭连接的一方,在TIME_WAIT期间仍然不能再次监听同样的server端口。
- MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同,例如在Centos7上默认配置的值是60s。
- 其可以通过 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看msl的值。
想一想,为什么是TIME_WAIT的时间是2MSL?
- MSL是TCP报文的最大生存时间,因此TIME_WAIT持续存在2MSL的话。
- 就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启,可能会收到来自上一个进程的迟到的数据,但是这种数据很可能是错误的)。
- 同时也是在理论上保证最后一个报文可靠到达(假设最后一个ACK丢失,那么服务器会再重发一个FIN。这时虽然客户端的进程不在了,但是TCP连接还在,仍然可以重发LAST_ACK)。
理解 CLOSE_WAIT 状态
以之前写过的 TCP 服务器为例,我们稍加修改,服务器的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>
#include <netinet/tcp.h>
const int port = 8081;
const int backlog = 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;
}
// 设置SO_REUSEADDR,避免TIME_WAIT状态影响重启
int opt = 1;
if (setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
std::cerr << "setsockopt error" << std::endl;
}
// 绑定
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;
close(listen_sock);
return 2;
}
// 监听
if (listen(listen_sock, backlog) < 0){
std::cerr << "listen error" << std::endl;
close(listen_sock);
return 3;
}
std::cout << "Server started on port " << port << std::endl;
// 启动服务器
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;
}
// 获取客户端IP和端口
char client_ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &peer.sin_addr, client_ip, sizeof(client_ip));
std::cout << "get a new connection from " << client_ip << ":"
<< ntohs(peer.sin_port) << " on socket " << sock << std::endl;
int* p = new int(sock);
pthread_t tid;
if (pthread_create(&tid, nullptr, Routine, (void*)p) != 0){
std::cerr << "pthread_create error" << std::endl;
close(sock);
delete p;
}
}
close(listen_sock);
return 0;
}
代码编写完毕后运行服务器,并用telnet工具连接我们的服务器的状态,此时通过以下监控脚本就可以看到两条状态为ESTABLISHED的连接,没有问题。
执行脚本
while :; do sudo netstat -ntp|head -2 && sudo netstat -ntp | grep 8081; sleep 1; echo "##################"; done

然后我们关闭客户端程序,观察 TCP 状态。

此时服务器进入了 CLOSE_WAIT 状态,客户端进入了FIN_WAIT_2,结合我们四次挥手的流程图,可以认为四次挥手没有正确完成。
理解 listen 的第二个参数
对于listen函数的原型为:
int listen(int sockfd, int backlog);
其中对于第二个参数的含义是:全连接队列的最大长度。如果有多个客户端同时发来连接请求,此时未被服务器处理的连接就会放入连接队列,该参数代表的就是这个全连接队列的最大长度,一般不要设置太大,设置为5或10即可。
对于此,我们进行实验,将第二个参数修改为2,进行实验。
#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>
#include <netinet/tcp.h>
const int port = 8081;
const int backlog = 2;
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;
}
// 设置SO_REUSEADDR,避免TIME_WAIT状态影响重启
int opt = 1;
if (setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
std::cerr << "setsockopt error" << std::endl;
}
// 绑定
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;
close(listen_sock);
return 2;
}
// 监听
if (listen(listen_sock, backlog) < 0){
std::cerr << "listen error" << std::endl;
close(listen_sock);
return 3;
}
std::cout << "Server started on port " << port << std::endl;
// 启动服务器
for (;;){
//不调用accept获取连接
}
close(listen_sock);
return 0;
}
运行服务器后使用netstat -nltp命令,可以看到该服务器当前正处于监听状态。
此时启动 3 个客户端同时连接服务器,,用netstat查看服务器状态,一切正常。

但当我们启动第四个的时候就会出现问题。

客户端状态正常,但是服务器端出现了 SYN_RECV 状态,而不是 ESTABLISHED 状态。
这是因为,Linux内核协议栈为一个tcp连接管理使用两个队列:
- 半链接队列(用来保存处于SYN_SENT和SYN_RECV状态的请求)
- 全连接队列(accpetd队列)(用来保存处于established状态,但是应用层没有调用accept取走的请求)
而全连接队列的长度会受到 listen 第二个参数的影响。
全连接队列满了的时候,就无法继续让当前连接的状态进入 established 状态了。这个队列的长度通过上述实验可知,是 listen 的第二个参数 + 1。
使用Wireshark工具分析TCP通信流程
Wireshark(前称Ethereal)是一个Windows下的网络抓包工具。
此软件(Wireshark)主要用于网络抓包分析,虽然它本身不能作为客户端直接连接服务器,但我们可以结合其他客户端工具来完整分析TCP通信。
下载 wireshark
https://1.na.dl.wireshark.org/win64/Wireshark-win64-2.6.3.exe