【技术实操】银河高级服务器操作系统实例分享,TCP长连接与短连接详细说明

发布于:2024-05-22 ⋅ 阅读:(178) ⋅ 点赞:(0)

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字段中第一列四种状态的具体含义

  1. keepalive #表示是keepalive的时间计时(希望可对照图 1-1具体是出现在哪个环节,从哪里开始计时?)
  2. on #表示是重发(retransmission)的时间计时(希望可对照图 1-1说明出现在哪个环节?)
  3. off #表示没有时间计时(希望可对照图 1-1说明出现在哪个环节?)
  4. 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

没有使用


网站公告

今日签到

点亮在社区的每一天
去签到