原生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 |
端口号验证 | ✅ 验证合法性 | ❌ 未验证 |
代码复杂度 | ⚠️ 较高(支持更多功能) | ✅ 简单直接 |