当客户端设置 keep-alive 时,Apache 主动关闭 tcp 连接

Apache actively close tcp connections when keep-alive is set by the client

我正在尝试为我的课程项目进行 Apache 性能基准测试。但是我遇到了一个奇怪的问题。当我使用单个客户端与 Apache 服务器建立多个 TCP 连接(例如 100 个)并使用 Connection: keep-alive header 发送 HTTP 1.1 请求时,我假设可以重用 TCP 连接。但是Apache服务器会主动终止TCP连接,即使包含Connection:Keep-AliveKeep-Alive:xxx HTTP 响应 header.

这是我的客户端代码(我混淆了IP地址):

#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <time.h>
#include <errno.h>
#include <sys/time.h>

#define DEBUG

#define MAX_SOCKETS_NUM 100
#define BUF_LEN 4096

int main() {
    int i;
    struct sockaddr_in server_addr, peer_addr;
    int res, len = sizeof(peer_addr);
    char *req = "GET /20KB HTTP/1.1\r\n"
                "Host: x.x.x.192\r\n"
                "Connection: keep-alive\r\n"
                "Upgrade-Insecure-Requests: 1\r\n"
                "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36\r\n"
                "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\r\n"
                "Accept-Encoding: gzip, deflate\r\n"
                "Accept-Language: zh-CN,zh;q=0.9\r\n"
                "\r\n";

    char buf[BUF_LEN + 1];
    int sockets[MAX_SOCKETS_NUM];
    int estb_num = 0;
    int estb_map[MAX_SOCKETS_NUM];
    struct timeval goal, now, interval;

    // create sockets
    for (i = 0; i < MAX_SOCKETS_NUM; i++) {
        if ((sockets[i] = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0)) < 0) {
            printf("Socket %d error!\n", i);
            return -1;
        }
    }

    // initialize server_addr
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(80);
    if (inet_pton(AF_INET, "x.x.x.192", &server_addr.sin_addr) <= 0) {
        printf("Invalid address/Address not supported\n");
        return -1;
    }

    // connect to the victim server
    for (i = 0; i < MAX_SOCKETS_NUM; i++) {
        if ((res = connect(sockets[i], (struct sockaddr *)&server_addr, sizeof(server_addr))) == 0) {
#ifdef DEBUG
            printf("Socket %d connected immediately.\n", i);
#endif
            estb_map[i] = 1;
            estb_num++;
        }
        else if (res == -1) {
            if (errno != EINPROGRESS) {
                printf("Error occured when connect() is called on socket %d.\n", i);
                return -1;
            }

#ifdef DEBUG
            printf("Socket %d sends SYN packet but the ACK is not received.\n", i);
#endif
        }
    }

    while (1) {
        if (estb_num == MAX_SOCKETS_NUM)
            break;

        for (i = 0; i < MAX_SOCKETS_NUM; i++) {
            while (1) {
                res = getpeername(sockets[i], (struct sockaddr *)&peer_addr, &len);
                if (res == 0) {
                    estb_num++;
#ifdef DEBUG
                    printf("Socket %d connects successfully.\n", i);
#endif
                    break;
                }
            }
        }
    }
    
    interval = (struct timeval) {
        .tv_sec = 1,
        .tv_usec = 0
    };
    len = strlen(req);
    while (1) {
        gettimeofday(&now, NULL);
        timeradd(&now, &interval, &goal);
#ifdef DEBUG
        printf("%ld.%ld\n", goal.tv_sec, goal.tv_usec);
#endif

        // send requests
        for (i = 0; i < MAX_SOCKETS_NUM; i++) {
            res = send(sockets[i], req, len, 0);
              
            if (res == -1) {
                printf("socket:%ld errno:%ld\n", i, errno);
            }
        }
        
        while (1) {
            for (i = 0; i < MAX_SOCKETS_NUM; i++) {
                res = recv(sockets[i], buf, BUF_LEN, 0);
                if (res == 0) {
                    struct sockaddr_in dbg_addr;
                    int dgb_addr_len = sizeof(struct sockaddr_in);
                    getsockname(sockets[i], &dbg_addr, &dgb_addr_len);

                    printf("socket:%d port:%d\n", i, ntohs(dbg_addr.sin_port));
                    goto end;
                }
                else if (res == -1) {
                    if (errno != EAGAIN)
                        printf("Error occurs when recv is called.\n");
                }
                else {
                    // do nothing because we don't need the response
                }
            }

            gettimeofday(&now, NULL);
            if (timercmp(&now, &goal, >) != 0) {
#ifdef DEBUG
                printf("%d.%d\n", now.tv_sec, now.tv_usec);
#endif
                break;
            }
        }
    }
end:
    return 0;
}

客户端(IP地址x.x.x.198)的工作流程是:

  1. 使用 non-blocking socket.[= 与服务器(IP 地址 x.x.x.192)建立 100 个 tcp 连接16=]

  2. 在 100 个 tcp 连接上发送相同的请求

  3. 在这 100 个套接字上重复调用 non-blocking recv 1 秒

  4. 转到 2.

一次执行该程序会生成以下输出:

Socket 0 sends SYN packet but the ACK is not received.
(some lines are omitted)
Socket 99 sends SYN packet but the ACK is not received.
Socket 0 connects successfully.
(some lines are omitted)
Socket 99 connects successfully.
1639450192.343129
1639450192.343155
1639450193.343163
socket:63 port:56804

输出表示在第二轮请求中(第一轮结束,因为打印了两个时间戳,而第二轮只打印了一个),recv函数在第63个socket上与本地端口number 56804 returns 0,表示Apache服务器主动终止tcp连接。并且我用tcpdump把所有的包都dump到了客户端,下图是本地56804端口连接的包轨迹:

数据包跟踪显示相同的结果,服务器主动向客户端发送tcp FIN数据包以终止TCP连接。但是我们可以看到响应中包含 Connection: Keep-Alive and Keep-Alive: timeout=10m, max=1999 header 这意味着 Apache 服务器可以正确处理 keep-alive。

服务器运行 Ubuntu 20.04.3 和 Apache 2.4.41。

我很困惑为什么会这样,为什么Apache会关闭keep-alive个连接?如果你能帮助我,我将不胜感激,谢谢!

来自this document

A host MAY keep an idle connection open for longer than the time that it indicates, but it SHOULD attempt to retain a connection for at least as long as indicated.

有key的大写字母,在文件中是这样写的:

您的案例符合“应该”部分。例如。 keep-alive 是一个建议 - 但如果服务器需要这些资源(或配置为打开的连接数少于客户端数量),则可以随意关闭它们。您的客户将需要处理这种状态。

如果所描述的行为取决于您打开的并行套接字的数量(除了超时),您很可能 运行 进入服务器的资源限制 - 显式配置或隐式配置, 来自默认值。

想象一下,如果只需要几个保持活动请求来饱和服务器提供的并发连接数,那么 DDOS 攻击会有多容易。

另请注意,超时是基于服务器(从发送最后一个数据包开始)和客户端(从接收最后一个数据包开始)对时间的不同理解