C/C++:TCP服务压测客户端connect报错(Cannot assign requested address)

C/C++:TCP服务压测客户端connect报错(Cannot assign requested address)

最近我们对自己的服务器进行了一次压测,在测试中出现客户端在调用connect时报错:

Cannot assign requested address(errno=99)

原因在于:
内核无法为客户端分配临时端口资源,所有连接都处于不可复用的状态。
一、什么是“连接”?

在TCP/IP协议中,一个TCP连接是一个五元组:

客户端IP、客户端PORT、协议、服务端IP、服务端PORT

这五个元素决定一个唯一连接。只要有一个元素不一样,就是不同的连接。

二、什么时候连接处于“不可复用状态”?

TCP在断开连接时将进行四次握手。谁先主动断开连接,谁就将进入TIME_WAIT状态。

MSL= maximum segment lifetime 最长分节生命周期,是任何IP数据包能够在因特网中存活的最长时间。

TIME_WAIT状态将持续2MSL,不同实现有不同大小,不过一般来说,一个MSL是30秒,两个就是60秒。

一条连接处于TIME_WAIT状态时,不可复用。

三、TCP为何要有TIME_WAIT状态?

1.可靠地实现TCP全双工连接的终止:

仔细思考下四次握手的最后一个ACK分节的发送。

主动关闭连接者发出最后一个ACK确认分节,但是主动关闭连接者怎么知道被动关闭连接者最后是否收到了自己发的ACK呢?

如果主动关闭连接者发现自己发的ACK在网络中阻塞或者丢失,应当及时重新发送ACK到被动关闭连接者。

当主动关闭连接者发送ACK后,连接进入TIME_WAIT状态,在主动关闭连接一方看来,有两种情况:

1)ACK正确到达了被动关闭连接一方,超过2MSL时间后,认定被动关闭连接一方收到最后的ACK分节;

2)ACK由于某些原因没有按时到达被动关闭连接一方,此时,能保证ACK出问题最长的极限是一个MSL时间,而被动关闭连接一方给出重发FIN分节,也应该是在一个MSL周期保证能到达。

2.保证所有老的重复分节在网络中消逝:

假设有以下一个连接:

192.168.144.43:50000 TCP 192.168.144.44:12500

从192.168.144.43:50000给192.168.144.44:12500发的一个分节在某个路由器阻塞(路由循环?)停滞,一段时间后192.168.144.43:50000检测出分节“丢失”并重发了这个被阻塞的分节,并且192.168.144.44:12500收到这个重发的分节且这条连接很快被释放关闭。

假设这条连接立刻能被复用,正常建立连接==》

之前被阻塞的分节慢悠悠的到达了192.168.144.44:12500,且恰好确认序号一致(这种情况完全可能!),于是乎,新的连接被旧有的连接的废弃数据“污染”啦!

如何保证新连接不会被旧的连接的某个“被丢失”的分节所污染呢?==》

好吧,等个2MSL,保证所有旧的连接的分节,不论是正常的不正常的,都已经在网络中消逝了,这个旧连接才可被复用。

这就是TIME_WAIT的意义。这是一个很有价值的机制,不要试图规避,要搞清楚它,利用它。

代码模拟复现错误:

我们的服务端IP是192.168.144.44,服务端PORT是12500。

客户端发起TCP请求建立连接,读取服务端返回的连接信息并主动关闭连接。

客户端接受TCP请求建立连接,发送连接相关信息给客户端并等待客户端关闭连接,服务端close,完成四次握手。

一个Shell脚本串行调用客户端程序,发起大量(短)连接,直到客户端执行异常。

Code:

服务端:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define BACKLOG 16

int main()
{
    // socket
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd < 0)
    {
        printf("create socket error=%d(%s)!!!\n", errno, strerror(errno));
        exit(1);
    }

    // bind
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET; // IPv4
    server_addr.sin_port = htons(12500); // Port
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // IP
    if (bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
    {
        printf("socket bind error=%d(%s)!!!\n", errno, strerror(errno));
        exit(1);
    }

    // listen
    if (listen(listen_fd, BACKLOG) < 0)
    {
        printf("socket listen error=%d(%s)!!!\n", errno, strerror(errno));
        exit(1);
    }
    printf("server init ok, start to accept new connect...\n");

    int connIdx = 0;
    while (1)
    {
        // accept
        int client_fd = accept(listen_fd, NULL, NULL);
        if (client_fd < 0)
        {
            printf("socket accept error=%d(%s)!!!\n", errno, strerror(errno));
            exit(1);
        }
        printf("accept one new connect(%d)!!!\n", connIdx);

        static char msg[1024] = "";
        memset(msg, 0, sizeof(msg));
        snprintf(msg, sizeof(msg)-1, "connIdx=%d\n", connIdx);
        if (write(client_fd, msg, strlen(msg)) != strlen(msg))
        {
            printf("send msg to client error!!!\n");
            exit(1);
        }

        static char readBuf[1024] = "";
        memset(readBuf, 0, sizeof(readBuf));
        if (read(client_fd, readBuf, sizeof(readBuf)-1) != 0)
        {
            printf("read error!!! server close connection!!!\n");
            exit(1);
        }
        printf("server read return 0, client-FIN\n");
        close(client_fd);
        connIdx++;
    }

    // never
    close(listen_fd);

    return 0;
}

客户端:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main()
{
    int client_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (client_fd < 0)
    {
        printf("create socket error=%d(%s)!!!\n", errno, strerror(errno));
        exit(1);
    }

    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(12500);
    if (inet_pton(AF_INET, "192.168.44.144", &server_addr.sin_addr) <= 0)
    {
        printf("inet_pton error!!!\n");
        exit(1);
    }

    if (connect(client_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
    {
        printf("socket connect error=%d(%s)!!!\n", errno, strerror(errno));
        exit(1);
    }
    printf("connect to server ok!\n");

    char msg[1024];
    int rbytes = read(client_fd, msg, sizeof(msg)-1);
    if (rbytes <= 0)
    {
        printf("read error=%d(%s)!!!\n", errno, strerror(errno));
        exit(1);
    }
    msg[rbytes] = 0; // null terminate
    printf("%s", msg);

    close(client_fd);

    return 0;
}

压测脚本:

#/bin/bash

PATH=$PATH:$HOME/server:$HOME/client
export PATH

client
while test "x$?" = "x0"
do
    client
done

netstat -an | grep TIME_WAIT | wc -l

编译 & 运行 服务端:

[jiang@localhost server]$ gcc -o server server.c 
[jiang@localhost server]$ ./server 
server init ok, start to accept new connect...

编译 & 运行 脚本:(脚本中循环调用客户端发起短连接)

[jiang@localhost ~]$ ./simu.sh
……
connect to server ok!
connIdx=28231
connect to server ok!
connIdx=28232
socket connect error=99(Cannot assign requested address)!!!
28233
[jiang@localhost ~]$ netstat -an | grep TIME_WAIT
tcp        0      0 192.168.44.144:52632        192.168.44.144:12500        TIME_WAIT   
tcp        0      0 192.168.44.144:52711        192.168.44.144:12500        TIME_WAIT   
tcp        0      0 192.168.44.144:52315        192.168.44.144:12500        TIME_WAIT   
tcp        0      0 192.168.44.144:52532        192.168.44.144:12500        TIME_WAIT   
tcp        0      0 192.168.44.144:52261        192.168.44.144:12500        TIME_WAIT   
tcp        0      0 192.168.44.144:52623        192.168.44.144:12500        TIME_WAIT
……

我们可以观察到:

服务端bind端口12500,客户端并未bind,但是客户端也是一个socket对象,而一个socket对象是IP+PORT。

客户端的socket的端口哪里来的呢?

是在connect调用中,由内核分配一个临时的PORT给客户端

(当然,客户端也是可以通过调用bind,给自己一个确定端口!不过一般不这么做。)

我们想一下过程:

客户端发起对某个服务端的连接请求,内核分配一个临时端口给客户端socket,然后发SYN包到服务端…

在五元组中,服务端IP、PORT是确定的,客户端IP也是确定的,协议也是确定的,只有客户端的临时PORT是内核分配的。

当客户端发起大量TCP连接,内核也会分配大量临时端口给这个客户端,并记录每次的连接(客户端断开连接进入TIME_WAIT)。

于是乎,当发起速度和连接断开速度足够快时(比最早的连接的2MSL提前),就会出现,大量连接虽然被四次握手正常释放,但是还处于TIME_WAIT状态。

端口是一个16bit的数值,最大也就是65535,当达到一定上限时,内核无法分配一个可用的临时端口给客户端,即无法建立客户端socket,无法分配一个协议地址(TCP协议地址=IP+PORT)给客户端,自然connect调用报错。

emmm…有点复杂,简单举个栗子。

假设端口是一个2bit的数值,只有0、1、2、3四个值。

当客户端发起请求时,内核首先为客户端分配端口0,组成连接:

192.168.144.X:0 TCP 192.168.144:44:12500

然后客户端断开连接,此连接进入TIME_WAIT状态。

然后客户端再次发起三个请求,内核依次分配1、2、3,组成连接:

192.168.144.X:1 TCP 192.168.144:44:12500

==》连接释放,进入TIME_WAIT

192.168.144.X:2 TCP 192.168.144:44:12500

==》连接释放,进入TIME_WAIT

192.168.144.X:3 TCP 192.168.144:44:12500

==》连接释放,进入TIME_WAIT

注:

第二次请求时为啥内核不能再次分配端口0呢?因为此时那个连接还处于TIME_WAIT状态,如果分配了端口0并连接成功,就不能完全保证TCP的安全可靠传输!一定要等到2MSL后脱离TIME_WAIT状态,然后才可分配端口0,再次复用此连接。

好,到目前为止,4条连接已经都进入TIME_WAIT状态,然后我尝试再次执行客户端connect,还能由内核分配临时端口吗?

不能了!内核没有别的临时端口,让这个临时端口和其他四个元素值组成不重复的连接。自然connect调用失败。

到此为止,我们终于搞清楚,为啥connect时出现“Cannot assign requested address”错误啦!

仔细思考,假设服务端(同一主机多网络接口)的IP有两个,除了192.168.144.44,还有一个192.168.144.45。我们在存在大量TIME_WAIT连接的情况下,能否与144.145建立连接呢?

答案是可以的。

内核只是不能分配可用的临时端口给客户端,让其组成一个不重复的连接。

如果服务端的IP变了,即使存在:

192.168.144.X:12345 TCP 192.168.144.44:12500 【TIME_WAIT】连接,

也可以新建:

192.168.144.X:12345 TCP 192.168.144.45:12500

为啥?因为内核只保证五元组唯一就可以呀!端口不能被使用,仅在与特定连接关联才成立。

假设客户端访问100个IP:PORT服务端,本地socket完全可能有100个相同的PORT被临时分配到100个不同的连接中。

#include "ffrtmp_read.h" #include "ADTSHeader.h" #include "comm_interface.h" #include <sys/socket.h> #include <netdb.h> #include <arpa/inet.h> #include <sys/types.h> #include <netinet/in.h> #include <unistd.h> CFFRtmpRead::CFFRtmpRead() { } CFFRtmpRead::~CFFRtmpRead() { } int CFFRtmpRead::interrupt_callback(void* ctx) { CFFRtmpRead* pThis = static_cast<CFFRtmpRead*>(ctx); // 处理超时时间为0的情况(无限等待) if (pThis->m_time_out_ms <= 0) { return 0; // 永不超时 } const int64_t timeout_us = static_cast<int64_t>(pThis->m_time_out_ms) * 1000; // 转为微秒 const int64_t elapsed = av_gettime() - pThis->m_open_start_time; // 超时返回1(中断),否则返回0(继续) return (elapsed >= timeout_us) ? 1 : 0; } int CFFRtmpRead::Start(const char *url, int time_out_ms) { // 添加必要的头文件后,使用现代网络API printf("\n=== Network Diagnostics (Modern API) ===\n"); struct addrinfo hints = {0}, *res = nullptr; hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; int dns_ret = getaddrinfo("sgiothubms.tracksolidpro.com", "1936", &hints, &res); if (dns_ret != 0) { printf("DNS resolution failed! Error: %s\n", gai_strerror(dns_ret)); } else { char ipstr[INET6_ADDRSTRLEN]; void *addr; const char *ipver; // 获取第一个IP地址 if (res->ai_family == AF_INET) { // IPv4 struct sockaddr_in *ipv4 = (struct sockaddr_in *)res->ai_addr; addr = &(ipv4->sin_addr); ipver = "IPv4"; } else { // IPv6 struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)res->ai_addr; addr = &(ipv6->sin6_addr); ipver = "IPv6"; } inet_ntop(res->ai_family, addr, ipstr, sizeof(ipstr)); printf("Resolved %s: %s\n", ipver, ipstr); freeaddrinfo(res); } printf("==========================\n\n"); AVDictionary *options = NULL; int ret = 0; int sockfd; struct sockaddr_in serv_addr; // 1. 添加详细的协议识别日志 printf("Opening URL: %s\n", url); printf("Protocol: %s\n", avio_find_protocol_name(url)); // 1. 移除无效的原始socket试代码 printf("Skipping raw socket test for production\n"); m_IfmtCtx = avformat_alloc_context(); if (m_IfmtCtx == nullptr) { printf("[%s: %s: %d]\n", __FILE__, __FUNCTION__, __LINE__); goto fail; } m_time_out_ms = time_out_ms; // 保存超时时间 // 设置超时中断回调 m_IfmtCtx->interrupt_callback.callback = &CFFRtmpRead::interrupt_callback; m_IfmtCtx->interrupt_callback.opaque = this; // 传递当前对象指针 m_open_start_time = av_gettime(); // 记录开始时间 if (time_out_ms > 0) { char tmp_str[256] = {0}; snprintf(tmp_str, sizeof(tmp_str), "%d", time_out_ms * 1000); // 微秒 // 核心超时选项 av_dict_set(&options, "timeout", tmp_str, 0); av_dict_set(&options, "rw_timeout", tmp_str, 0); // 2. 修复RTMP关键参数 av_dict_set(&options, "rtmp_live", "any", 0); // 支持所有直播模式 av_dict_set(&options, "rtmp_buffer", "1000", 0); // 增加缓冲区 // 5. 添加TCP连接参数 av_dict_set(&options, "rtsp_transport", "tcp", 0); av_dict_set(&options, "tcp_nodelay", "1", 0); } // 6. 强制使用TCP协议 av_dict_set(&options, "rtmp_proto", "tcp", 0); // 7. 设置用户代理(部分服务器需要) av_dict_set(&options, "user_agent", "FFmpeg/LibRtmp", 0); // 4. 添加调试级别 av_log_set_level(AV_LOG_DEBUG); m_IfmtCtx->probesize = 100 * 1024; m_IfmtCtx->max_analyze_duration = AV_TIME_BASE; // if (avformat_open_input(&m_IfmtCtx, url, NULL, &options) != 0) // { // perror("avformat_open_input"); // goto fail; // } // 6. 带详细错误处理的avformat_open_input printf("Calling avformat_open_input...\n"); avformat_network_init(); printf("Testing raw socket connection...\n"); sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { perror("socket creation failed"); } else { memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(1936); // 使用之前解析的IP或直接使用域名 if (inet_pton(AF_INET, "解析的IP", &serv_addr.sin_addr) <= 0) { printf("Invalid address\n"); } else { if (connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr))) { perror("connect failed"); } else { printf("Raw socket connected successfully!\n"); } } close(sockfd); } ret = avformat_open_input(&m_IfmtCtx, url, NULL, &options); if (ret != 0) { char errbuf[256]; av_strerror(ret, errbuf, sizeof(errbuf)); // 获取更多系统级错误信息 const char* sys_err = strerror(errno); printf("avformat_open_input failed (%d): %s\n", ret, errbuf); printf("System error: %d - %s\n", errno, sys_err); // 检查是否超时触发 if (ret == AVERROR_EXIT) { printf("Operation aborted by interrupt callback\n"); } goto fail; } if (options != nullptr) { av_dict_free(&options); } if (avformat_find_stream_info(m_IfmtCtx, NULL) < 0) { perror("avformat_find_stream_info"); fprintf(stdout, "Couldn't find stream information \n"); goto fail; } if (m_IfmtCtx->nb_streams == 0) { printf("nb_streams is 0,exit \n"); goto fail; } m_VideoStreamIndex = av_find_best_stream(m_IfmtCtx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0); if (m_VideoStreamIndex < 0) { printf("Didn't find a video stream\n"); } else { // m_VideoType = m_IfmtCtx->streams[m_VideoStreamIndex]->codecpar->codec_id; } printf("m_VideoStreamIndex = %d\n", m_VideoStreamIndex); m_AudioStreamIndex = av_find_best_stream(m_IfmtCtx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0); if (m_AudioStreamIndex < 0) { printf("Didn't find a audio stream \n"); } else { m_AudioType = m_IfmtCtx->streams[m_AudioStreamIndex]->codecpar->codec_id; } printf("m_AudioStreamIndex = %d\n", m_AudioStreamIndex); m_AvPacket = av_packet_alloc(); if (m_AvPacket == nullptr) { printf("[%s: %s: %d]\n", __FILE__, __FUNCTION__, __LINE__); goto fail; } av_dump_format(m_IfmtCtx, 0, url, 0); return 0; fail: printf("[%s: %s: %d] open %s fail\n", __FILE__, __FUNCTION__, __LINE__, url); Stop(); return -1; } void CFFRtmpRead::Stop() { if (m_IfmtCtx != nullptr) { avformat_close_input(&m_IfmtCtx); avformat_free_context(m_IfmtCtx); m_IfmtCtx = nullptr; } if (m_AvPacket != nullptr) { av_free(m_AvPacket); m_AvPacket = nullptr; } m_VideoStreamIndex = -1; m_AudioStreamIndex = -1; m_VideoType = AV_CODEC_ID_NONE; m_AudioType = AV_CODEC_ID_NONE; m_Buffer.Drain(); } uint8_t *CFFRtmpRead::GetStream(FrameInfo_t &FrameInfo) { m_Buffer.Drain(); int ret = av_read_frame(m_IfmtCtx, m_AvPacket); if (ret >= 0) { if (m_AvPacket->stream_index == m_VideoStreamIndex) { AVRational time_base = m_IfmtCtx->streams[m_VideoStreamIndex]->time_base; AVRational time_base_q = {1, AV_TIME_BASE}; FrameInfo.frame_pts = av_rescale_q(m_AvPacket->dts, time_base, time_base_q); FrameInfo.frame_mode = m_VideoType == AV_CODEC_ID_H265 ? 1 : 0; FrameInfo.frame_type = m_AvPacket->flags ? 1 : 0; m_Buffer.Add(m_AvPacket->data, m_AvPacket->size); FrameInfo.frame_len = m_Buffer.Length(); av_packet_unref(m_AvPacket); } else if (m_AvPacket->stream_index == m_AudioStreamIndex) { ADTS_Header header(m_IfmtCtx->streams[m_AudioStreamIndex]->codecpar->sample_rate, m_IfmtCtx->streams[m_AudioStreamIndex]->codecpar->ch_layout.nb_channels, m_AvPacket->size + 7); m_Buffer.Add(&header, sizeof(ADTS_Header)); m_Buffer.Add(m_AvPacket->data, m_AvPacket->size); FrameInfo.frame_len = m_Buffer.Length(); FrameInfo.frame_type = 3; FrameInfo.frame_mode = m_AudioType == AV_CODEC_ID_AAC ? 5 : 3; AVRational time_base = m_IfmtCtx->streams[m_AudioStreamIndex]->time_base; AVRational time_base_q = {1, AV_TIME_BASE}; FrameInfo.frame_pts = av_rescale_q(m_AvPacket->dts, time_base, time_base_q); av_packet_unref(m_AvPacket); } if (m_Buffer.Length() > 0) { return m_Buffer.Buffer(); } } return nullptr; } 更改后代码如上,试依旧失败,报错信息如下 === Network Diagnostics (Modern API) === Resolved IPv4: 124.243.184.237 ========================== Opening URL: rtmp://sgiothubms.tracksolidpro.com:1936/16/865478070000569 Protocol: rtmp Skipping raw socket test for production Calling avformat_open_input... Testing raw socket connection... Invalid address [NULL @ 0xb28005a0] Opening 'rtmp://sgiothubms.tracksolidpro.com:1936/16/865478070000569' for reading [rtmp @ 0xb2800a00] No default whitelist set [tcp @ 0xb2800b30] No default whitelist set [rtmp @ 0xb2800a00] Cannot open connection tcp://sgiothubms.tracksolidpro.com:1936?listen&listen_timeout=2000000000&tcp_nodelay=1 avformat_open_input failed (-99): Cannot assign requested address System error: 99 - Cannot assign requested address [ffrtmp_read.cpp: Start: 222] open rtmp://sgiothubms.tracksolidpro.com:1936/16/865478070000569 fail
06-13
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值