1.服务器环境以及配置
物理机/虚拟机/云/容器 | 物理机 |
处理器: | HUAWEI Kunpeng 920 |
具体操作系统版本 | Kylin-Server-10-SP1-Release-Build20-20210518- aarch64 |
内核版本 | kernel-4.19.90-23.8.v2101.ky10.aarch64 |
2.问题现象描述
对TCP长连接有疑问
1、如何从命令查看及确认哪些是长连接?
2、netstat命令查看结果中timers字段中第一列四种状态的具体含义
- keepalive #表示是keepalive的时间计时(希望可对照图 1-1具体是出现在哪个环节,从哪里开始计时?)
- on #表示是重发(retransmission)的时间计时(希望可对照图 1-1说明出现在哪个环节?)
- off #表示没有时间计时(希望可对照图 1-1说明出现在哪个环节?)
- timewait #表示等待(timewait)时间计时(希望可对照图 1-1说明出现在哪个环节?)
3、netstat命令查看结果中timers字段中第二列三个数据的具体含义状态的具体含义
例如:keepalive (576.47/1/1)
3.问题分析
3.1.长连接与短连接
长连接(long connnection),指在一个连接上可以连续发送多个数据包,在连接保持期间,如果没有数据包发送,需要双方发链路检测包。
短连接(short connnection),是相对于长连接而言的概念,指的是在数据传送过程中,只在需要发送数据时才去建立一个连接,数据发送完成后则断开此连接,即每次连接只完成一项业务的发送。
区分长连接和短连接主要看以下几点:
- 连接持续时间:长连接通常持续较长时间,而短连接持续时间较短。通常情况下,长连接可以持续数分钟、数小时甚至更长时间,而短连接通常只持续几秒钟或几分钟。
- 连接的目的:长连接通常用于需要保持持久连接的应用程序,例如在线聊天、实时通信、视频流传输等。短连接通常用于一次性数据传输,如HTTP/1.0请求和响应。
- 连接的频率:长连接通常会在一段时间内持续传输数据,而短连接通常是一次性的数据传输。如果您看到多次数据传输在同一个连接上进行,那么可能是长连接。如果每个数据传输都伴随着新的连接建立和关闭,那么可能是短连接。
- 连接状态:使用网络监控工具如netstat或ss,可以查看当前的TCP连接状态。长连接通常处于ESTABLISHED状态,而短连接可能会更快地进入CLOSED状态。
- 应用程序协议:有些应用程序协议明确规定了连接的类型。例如,HTTP/1.0通常使用短连接,而HTTP/1.1支持持久连接。
3.2.如何从命令查看及确认哪些是长连接
3.2.1 使用tcpdump或者wireshark抓包查看
以HTTP1.1为例的长连接。
HTTP 1.1默认采用Keep-Alive连接,允许一个HTTP连接上的多个请求/响应交换,而不需要为每个请求重新建立一个TCP连接。短连接是指每次请求/响应完成后就关闭TCP连接,下次请求需要重新建立TCP连接。相比之下,Keep-Alive连接在一定时间内可以重复使用,提高了效率和性能。当然,如果需要,客户端和服务器都可以通过相应的配置或请求头来使用短连接。但是,随着Web应用的复杂度增加,使用Keep-Alive连接成为了一种更常见的做法。
从抓包可见HTTP1.1的Keep-Alive设置的timeout为60s。从654号包,开始计时,如果超时时间不更新的话,就会在60s之后断开连接,在654和2794之间没有timeout的更新,所以连接断开了。
3.2.2 使用netstat和ss查看
netstat -tonp,查看tcp状态和timer。
可以查看当前的TCP连接状态。长连接通常处于ESTABLISHED状态,而短连接可能会更快地进入CLOSED状态。长连接一般会使用心跳机制进行探测。
3.3 netstat命令查看结果中timers字段中第一列四种状态的具体含义
3.3.1 keepalive
keepalive:keepalive计时器通常在ESTABLISHED状态下使用。当TCP连接处于ESTABLISHED状态时,系统可以启动keepalive计时器来定期发送keepalive探测包,以检测连接的健康状态。
3.3.2 on
on:重发计时器。
建立连接时:当TCP连接刚刚建立时,RTO的计时器通常不会立即启动,因为还没有数据包被发送。连接建立后,RTO会在数据包被发送后开始计时。
数据包发送:RTO与每个已发送但未确认的数据包相关联。每当发送方发送一个数据包,RTO就会启动并开始计时,等待接收方的确认。
等待确认:RTO在等待接收方确认数据包的过程中运行。如果在RTO超时之前未收到确认,发送方将触发数据包的重传。
传输结束:RTO也可以在TCP连接关闭时出现。如果有未确认的数据包仍然存在于连接中,RTO会继续运行,以确保这些数据包得到确认或重传。
3.3.3 timewait
timewait:timewait计时器在TIME_WAIT状态下使用。在连接关闭后,连接的一方会进入TIME_WAIT状态,timewait计时器用于等待一段时间,以确保在此期间不会出现重复的连接请求。
3.3.4 off
off:没有时间计时,例如ESTABLISHED没有特殊的计时器操作,应用程序设置关闭了keepalive。如果State列为CLOSE_WAIT状态是,Timer列多为off (0.00/0/0),因为CLOSE_WAIT的是属于被动关闭那一方,这个是没有超时(timeout)设置的,所以也就不用计时了。CLOSE_WAIT除非你杀进程,CLOSE_WAIT是不会自动消失的。一个CLOSE_WAIT会维持至少2个小时的时间。当然不消失意味着占着资源呢,这里就是占着FD。
例如:应用服务端设置关闭KeepAlive
1、准备两台虚拟机
server 192.168.55.52 clinet 192.168.55.12 |
2、准备服务端程序
服务端监听1234端口。
ssh root@192.168.55.52 vim tcp_server.c |
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/tcp.h> #include <arpa/inet.h> #include <sys/epoll.h> #include <unistd.h>
#define MAX_EVENTS 10 #define BUFFER_SIZE 1024
int main(int argc, char *argv[]) { if (argc != 2) { printf("请提供要监控的端口号作为参数\n"); return 1; }
int port = atoi(argv[1]);
// 创建套接字 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { perror("无法创建套接字"); return 1; } int keepAlive=0;//设置关闭keepAlive if (setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, &keepAlive, sizeof(keepAlive)) < 0) { perror("设置失败"); return 1; }
// 绑定地址和端口 struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(port); addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { perror("无法绑定地址和端口"); return 1; }
// 监听连接 if (listen(sockfd, 10) < 0) { perror("无法监听连接"); return 1; }
printf("正在监听端口 %d ...\n", port);
// 创建 epoll 实例 int epollfd = epoll_create1(0); if (epollfd < 0) { perror("无法创建 epoll 实例"); return 1; }
// 添加 sockfd 到 epoll 实例中进行监听 struct epoll_event event; event.events = EPOLLIN; event.data.fd = sockfd; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &event) < 0) { perror("无法添加 sockfd 到 epoll 实例"); return 1; }
// 准备接收连接的事件 struct epoll_event events[MAX_EVENTS];
while (1) { // 等待事件发生 int num_events = epoll_wait(epollfd, events, MAX_EVENTS, -1); if (num_events < 0) { perror("epoll_wait 出错"); return 1; }
int i=0; // 处理所有事件 for (i = 0; i < num_events; i++) { if (events[i].data.fd == sockfd) { // 有新连接请求 struct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); int client_sockfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_len); if (client_sockfd < 0) { perror("无法接受连接"); return 1; }
// 将新连接的套接字添加到 epoll 实例中进行监听 event.events = EPOLLIN; event.data.fd = client_sockfd; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, client_sockfd, &event) < 0) { perror("无法添加新连接的套接字到 epoll 实例"); return 1; }
// 获取客户端信息 char client_ip[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &(client_addr.sin_addr), client_ip, INET_ADDRSTRLEN); int client_port = ntohs(client_addr.sin_port); printf("已连接客户端:%s:%d\n", client_ip, client_port); } else { // 有数据可读 int client_sockfd = events[i].data.fd; char buffer[BUFFER_SIZE]; ssize_t recv_len = recv(client_sockfd, buffer, BUFFER_SIZE - 1, 0); if (recv_len > 0) { buffer[recv_len] = '\0'; printf("接收到的字符串:%s\n", buffer); } else if (recv_len == 0) { // 客户 // 从 epoll 实例中移除套接字 if (epoll_ctl(epollfd, EPOLL_CTL_DEL, client_sockfd, NULL) < 0) { perror("无法从 epoll 实例中移除套接字"); return 1; }
// 关闭套接字 close(client_sockfd); } else { perror("接收数据出错"); return 1; } } } }
// 关闭套接字和 epoll 实例 close(sockfd); close(epollfd);
return 0; } |
# 编译 gcc tcp_server.c -o server
./server 1234 |
3、准备客户端程序
客户端绑定54321端口
ssh root@192.168.55.12 vim tcp_client.c |
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #define PORT 54321
int main(int argc, char *argv[]) { int sock; // socket struct sockaddr_in server_addr; // server address char *server_ip; // server IP address int server_port; // server port
// Check command line arguments if (argc != 3) { printf("Usage: %s <server_ip> <server_port>\n", argv[0]); return 1; }
// Get server IP address and port server_ip = argv[1]; server_port = atoi(argv[2]);
// Create socket sock = socket(AF_INET, SOCK_STREAM, 0); if (sock < 0) { perror("socket failed"); return 1; }
// Set address and port of server memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = inet_addr(server_ip); server_addr.sin_port = htons(server_port);
// Bind client socket to port 54321 struct sockaddr_in client_addr = { .sin_family = AF_INET, .sin_addr.s_addr = INADDR_ANY, .sin_port = htons(PORT) };
if (bind(sock, (struct sockaddr *)&client_addr, sizeof(client_addr)) < 0) { perror("bind failed"); return 2; }
// Connect to server if (connect(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { perror("connect failed"); return 1; }
// Print success message printf("Connected to server %s:%d\n", server_ip, server_port); send(sock,"hello",6,0); while(1); // Close socket close(sock);
return 0; } |
gcc tcp_client.c -o client |
netstat -W -neoap查看服务端,后面的off表示关闭了timer,若服务端不关闭keepalive,使用netstat 查看到的timer应是keepalive。
3.4 netstat命令查看结果中timers字段第二列三个数据的具体含义状态的具体含义
第二列,(576.47/0/0) -> (a/b/c)
3.3.1 当第一列为keepalive计时器
a | 定时器计时值的倒计时,单位秒;刚建立连接时为最大值。 如果系统没有特殊设置或者应用程序没有设置keepalive,起始值是/proc/sys/net/ipv4/tcp_keepalive_time默认值7200秒,倒计时直到值变为0。 |
b | 没有用 |
c | 已发送的keepalive探测器的数量(已经发送的探测(probe)包的次数)。默认为0;最大值和tcp_keepalive_probes有关系,/proc/sys/net/ipv4/tcp_keepalive_probes代表总共发送探测(probe)包的个数(默认为9个)。 当a值倒计时为0,而连接中间没有数据传输,这个值将会累加。 而/proc/sys/net/ipv4/tcp_keepalive_intvl表示在发送一个探测(probe)包后,如果多少秒内没有收到回复,则再发送一个探测(probe)包。这也代表了之前发送的探测(probe)包超时失效(默认为75秒)。当所有的探测(probe)包都发送完毕后,如果仍然没有收到回应,服务器会主动关闭该连接(长连接)。 因此,通常情况下,如果第二列的c为0,a的范围应在7200到0之间,其中7200是/proc/sys/net/ipv4/tcp_keepalive_time的值,例如keepalive (576.47/0/0) ;如果c不为0,但不可能大于/proc/sys/net/ipv4/tcp_keepalive_probes的值,那么a的范围应在75到0之间,其中75是tcp_keepalive_intvl的值,例如keepalive (73.06/0/2)。 |
3.3.2 当第一列为on计时器
a | 重发(retransmission)倒计时,单位秒 |
b | 已经产生的重发(retransmission)次数 |
c | 没有使用 |
3.3.3 当第一列为timewait计时器
a | TIMEWAIT状态倒计时值,单位秒,起始值为60(两倍MSL的时间值)。当时间倒计时为0,连接就会由TIMEWAIT状态就会变为CLOSE状态 |
b | 没有使用 |
c | 没有使用 |