WebBench中关于Socket.c的学习优化尝试

原生WebBench版本是Radim Kolar于1997年构建的一个在Linux环境下使用的简单高效的网站测压工具。它使用socket.c完成套接字连接(GitHub: WebBench

socket.c的优势在于:简洁易懂,在当时的环境下也十分高效。但是很显然,时过境迁,原生socket.c在现如今则会显得有些力不从心。

首先,socket.c只支持IPv4,而现在我们则不得不考虑IPv6的兼容性,例如DNS解析函数gethostbyname函数,仅支持IPv4、线程不安全且仅支持主机名,无法解析服务名等。其次,socket.c没有设置超时机制,仅仅使用阻塞模式完成连接过程。此外还有错误处理日志、端口号验证等可以改进的方面。

但是,我们必须理解,这已经是快三十年前的代码了,我们以现在的标准去评判如此久远的代码是不合适的。因此,我仅仅从学习、利用、更新角度出发,构建了一个更加符合如今标准的modernsocket.c并辅以详尽的注释。

(再次声明,本文仅从学习、借鉴角度出发,不具备任何学术价值和评判意味)

#include <sys/socket.h>
#include <netdb.h>     // 包含 getaddrinfo, freeaddrinfo, gai_strerror
#include <arpa/inet.h> // 包含 inet_pton, htons 等
#include <unistd.h>    // 包含 close
#include <fcntl.h>     // 包含 fcntl
#include <stdio.h>     // 包含 fprintf, snprintf
#include <string.h>    // 包含 memset
#include <errno.h>     // 包含 errno
#include <sys/epoll.h> // 包含 epoll

/**
 * 创建并连接到服务器的现代化实现
 *
 * @param host 主机名或IP地址(IPv4/IPv6)
 * @param port 端口号(主机字节序)
 * @param timeout_ms 连接超时时间(毫秒),0表示使用系统默认
 *
 * @return 成功返回套接字描述符,失败返回-1并设置errno
 */
int ModernSocket(const char *host, int port, int timeout_ms)
{
    // 端口号合法性验证
    if (port <= 0 || port > 65535) {
        errno = EINVAL;
        return -1;
    }

    struct addrinfo hints;          // 用于指定地址查询条件的结构体
    struct addrinfo *result = NULL; // 存储getaddrinfo返回的地址链表
    struct addrinfo *rp = NULL;     // 用于遍历地址链表的指针
    int sockfd = -1;                // 套接字描述符,初始化为无效值
    int ret;                        // 存储系统调用返回值
    char port_str[16];              // 存储端口号的字符串形式

    /* 初始化hints结构体:指定我们需要的地址类型 */
    memset(&hints, 0, sizeof(hints));
    hints.ai_family = AF_UNSPEC;     // 支持IPv4和IPv6
    hints.ai_socktype = SOCK_STREAM; // TCP套接字
    hints.ai_protocol = IPPROTO_TCP; // TCP协议

    /* 将数字端口号转换为字符串形式(getaddrinfo要求字符串参数) */
    snprintf(port_str, sizeof(port_str), "%d", port);

    /* 步骤1:获取符合hint结构要求的目标主机的地址信息(DNS解析或IP转换) */
    ret = getaddrinfo(host, port_str, &hints, &result);
    if (ret != 0)
    {
        fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(ret));
        return -1;
    }

    /* 步骤2:遍历所有返回的地址,尝试连接 */
    for (rp = result; rp != NULL; rp = rp->ai_next)
    {
        /* 2.1 针对目标地址创建套接字 */
        sockfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
        if (sockfd == -1)
        {
            continue; // 尝试下一个地址
        }

        /* 2.2 如果设置了超时,配置非阻塞模式 */
        if (timeout_ms > 0)
        {
            /* 通过F_GETFL操作符获取套接字的当前状态标志。*/
            int flags = fcntl(sockfd, F_GETFL, 0);
            if (flags == -1)
            {
                close(sockfd);
                continue;
            }
            // 如果获取了sockfd的状态,则通过F_SETFL操作符向其添加一个非阻塞状态
            if (fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) == -1)
            {
                close(sockfd);
                continue;
            }
        }

        /* 2.3 尝试连接 */
        ret = connect(sockfd, rp->ai_addr, rp->ai_addrlen);
        if (ret == 0)
        {
            /* 立即连接成功(罕见) */
            if (timeout_ms > 0)
            {
                // 连接成功则恢复阻塞模式
                int flags = fcntl(sockfd, F_GETFL, 0);
                fcntl(sockfd, F_SETFL, flags & ~O_NONBLOCK);
            }
            break;
        }

        /* 2.4 处理非阻塞连接(EINPROGRESS状态) */
        if (timeout_ms > 0 && errno == EINPROGRESS)
        {
            int epoll_fd = epoll_create1(0);    // 创建epoll实例
            if(epoll_fd == -1){
                close(sockfd);
                continue;
            }

            struct epoll_event event;
            event.events = EPOLLOUT;            // 监控可写(连接)事件
            event.data.fd = sockfd;             // 指定文件描述符

            /* 添加事件描述符至监控 */
            if(epoll_ctl(epoll_fd,EPOLL_CTL_ADD,sockfd,&event) == -1){
                close(epoll_fd);
                close(sockfd);
                continue;
            }

            struct epoll_event events[1];       // 用于接受就绪事件
            /* 等待epoll_fd中的事件发生,如果发生了,最多返回1个就绪事件到events数组,或者超时退出 */
            int nfds = epoll_wait(epoll_fd,events,1,timeout_ms);
            /* 无论成功与否,都先移除监控并关闭epoll实例 */
            epoll_ctl(epoll_fd,EPOLL_CTL_DEL,sockfd,NULL);
            close(epoll_fd);
            
            if (nfds > 0)
            {
                /** 
                 * 我们检测的事件是EPOLLOUT,它既可以表示连接建立,
                 * 也可以表示套接字写缓冲区有空间、连接尝试失败
                 * 因此我们需要检测连接是否真正成功
                 **/
                int error = 0;
                socklen_t len = sizeof(error);
                /* 检查非阻塞套接字的连接状态 */
                /*
                int getsockopt(
                int sockfd,           // 套接字描述符
                int level,            // 选项级别(SOL_SOCKET 表示套接字层)
                int optname,          // 选项名(SO_ERROR 表示获取错误状态)
                void *optval,         // 存储结果的缓冲区(此处是 error 的地址)
                socklen_t *optlen     // 缓冲区长度(需初始化为 sizeof(int))
                );
                */
                if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) == 0 && error == 0)
                {
                    /* 连接成功 */
                    if (timeout_ms > 0)
                    {
                        // 连接成功则恢复阻塞模式
                        int flags = fcntl(sockfd, F_GETFL, 0);
                        fcntl(sockfd, F_SETFL, flags & ~O_NONBLOCK);
                    }
                    break;
                }
            }
            /* 超时或连接失败会继续执行到下面的close,如果成功了就会返回*/
        }

        /* 超时或连接失败会继续执行到下面的close */
        close(sockfd);
        sockfd = -1;
    }

    /* 步骤3:释放地址信息内存 */
    if (result != NULL)
    {
        freeaddrinfo(result);
    }

    /* 步骤4:检查是否所有地址尝试都失败 */
    if (rp == NULL)
    {
        // 理论上不可能,防御性编程
        if (sockfd != -1)
        {
            close(sockfd);
        }
        return -1;
    }

    // 返回成功连接的套接字
    return sockfd;
}

功能对比:(理论,未经验证)

特性ModernSocket (现代实现)Socket (传统实现)
IPv4/IPv6 支持✅ 支持 (AF_UNSPEC)❌ 仅 IPv4 (AF_INET)
DNS 解析✅ 使用 getaddrinfo✅ 使用 gethostbyname
超时控制✅ 支持 epoll 超时❌ 无超时机制
非阻塞连接✅ 支持 O_NONBLOCK❌ 仅阻塞模式
错误处理✅ 详细错误日志 (gai_strerror)❌ 仅返回 -1
端口号验证✅ 验证合法性❌ 未验证
代码复杂度⚠️ 较高(支持更多功能)✅ 简单直接
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值