1. Address模块设计背景与核心价值
在网络编程中,地址处理是最基础却最容易出错的环节之一。传统C风格的sockaddr结构体使用起来相当不便——需要手动处理字节序转换、地址族判断、内存布局等底层细节。我在开发高并发服务器的过程中,经常遇到地址解析失败、类型转换错误、内存越界等问题。Address模块正是为了解决这些痛点而设计的。
这个模块的核心价值在于:
- 统一封装IPv4/IPv6地址操作,避免直接操作sockaddr
- 提供类型安全的地址创建和转换接口
- 内置DNS解析和本地网卡查询能力
- 支持流式输出和格式化操作
举个例子,传统方式创建一个IPv4地址需要这样写:
cpp复制struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
inet_pton(AF_INET, "192.168.1.1", &addr.sin_addr);
而使用Address模块只需要:
cpp复制auto addr = IPv4Address::Create("192.168.1.1", 8080);
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心类结构与设计原理
2.1 Address基类设计
Address作为所有地址类型的基类,采用抽象工厂模式设计。关键设计点包括:
- 使用智能指针管理生命周期(std::shared_ptr)
- 提供统一的sockaddr转换接口
- 支持流式输出操作符重载
- 内置RTTI机制识别具体地址类型
cpp复制class Address {
public:
using ptr = std::shared_ptr<Address>;
// 获取sockaddr指针(const版本)
virtual const sockaddr* getAddr() const = 0;
// 获取地址长度
virtual socklen_t getAddrLen() const = 0;
// 流式输出
virtual std::ostream& insert(std::ostream& os) const = 0;
// 地址族识别
int getFamily() const;
// 创建具体地址实例的工厂方法
static Address::ptr Create(const sockaddr* addr, socklen_t addrlen);
};
注意:所有派生类必须实现getAddr()和getAddrLen()这两个纯虚函数,这是模块能够透明处理不同地址类型的关键。
2.2 IPAddress抽象类
IPAddress在Address基础上增加了IP协议相关的抽象接口:
cpp复制class IPAddress : public Address {
public:
using ptr = std::shared_ptr<IPAddress>;
// 获取/设置端口号
virtual uint16_t getPort() const = 0;
virtual void setPort(uint16_t v) = 0;
// 子网相关操作
virtual IPAddress::ptr subnetMask(uint32_t prefix_len) = 0;
virtual IPAddress::ptr broadcastAddress(uint32_t prefix_len) = 0;
virtual IPAddress::ptr networkAddress(uint32_t prefix_len) = 0;
};
这里使用了"Template Method"设计模式——IPAddress定义算法骨架,具体实现延迟到子类。比如计算广播地址的算法逻辑是固定的,但IPv4和IPv6的具体计算方式不同。
3. 具体地址实现详解
3.1 IPv4Address实现要点
IPv4Address内部封装了sockaddr_in结构体,关键实现包括:
- 地址构造:
cpp复制IPv4Address::ptr IPv4Address::Create(const char* address, uint16_t port) {
IPv4Address::ptr rt(new IPv4Address);
rt->m_addr.sin_port = htons(port);
int result = inet_pton(AF_INET, address, &rt->m_addr.sin_addr);
if(result <= 0) {
LOG_ERROR(g_logger) << "IPv4Address::Create(" << address << ", "
<< port << ") rt=" << result << " errno=" << errno
<< " errstr=" << strerror(errno);
return nullptr;
}
return rt;
}
- 子网掩码计算:
cpp复制IPAddress::ptr IPv4Address::subnetMask(uint32_t prefix_len) {
if(prefix_len > 32) {
return nullptr;
}
sockaddr_in subnet;
memset(&subnet, 0, sizeof(subnet));
subnet.sin_family = AF_INET;
subnet.sin_addr.s_addr = htonl(~((1 << (32 - prefix_len)) - 1));
return IPv4Address::ptr(new IPv4Address(subnet));
}
技巧:计算子网掩码时,32位前缀长度对应32个1的二进制数,可以通过位运算快速生成。
3.2 IPv6Address特殊处理
IPv6Address相比IPv4有几个特殊处理点:
-
地址字符串解析需要支持以下几种格式:
- 完整格式:2001:0db8:85a3:0000:0000:8a2e:0370:7334
- 压缩格式:2001:db8:85a3::8a2e:370:7334
- 带端口格式:[2001:db8::1]:8080
-
二进制地址存储使用uint8_t数组而非in6_addr结构体,便于处理:
cpp复制IPv6Address::IPv6Address(const uint8_t address[16], uint16_t port) {
memset(&m_addr, 0, sizeof(m_addr));
m_addr.sin6_family = AF_INET6;
m_addr.sin6_port = htons(port);
memcpy(&m_addr.sin6_addr.s6_addr, address, 16);
}
- 子网计算需要考虑128位地址空间:
cpp复制IPAddress::ptr IPv6Address::subnetMask(uint32_t prefix_len) {
sockaddr_in6 subnet;
memset(&subnet, 0, sizeof(subnet));
subnet.sin6_family = AF_INET6;
for(uint32_t i = 0; i < 16; ++i) {
if(prefix_len >= 8) {
subnet.sin6_addr.s6_addr[i] = 0xff;
prefix_len -= 8;
} else if(prefix_len > 0) {
subnet.sin6_addr.s6_addr[i] = ~((1 << (8 - prefix_len)) - 1);
prefix_len = 0;
} else {
subnet.sin6_addr.s6_addr[i] = 0x00;
}
}
return IPv6Address::ptr(new IPv6Address(subnet));
}
4. 地址解析与查询实战
4.1 DNS解析实现剖析
Address::Lookup()方法是模块中最复杂的函数之一,它封装了getaddrinfo系统调用:
cpp复制bool Address::Lookup(std::vector<Address::ptr>& result,
const std::string& host,
int family, int type, int protocol) {
// 初始化hints结构
addrinfo hints, *results, *next;
memset(&hints, 0, sizeof(hints));
hints.ai_flags = 0;
hints.ai_family = family;
hints.ai_socktype = type;
hints.ai_protocol = protocol;
// 分离主机名和服务名(端口)
std::string node;
const char* service = nullptr;
// 处理IPv6地址格式[2001::1]:8080
if(!host.empty() && host[0] == '[') {
auto endipv6 = memchr(host.c_str() + 1, ']', host.size() - 1);
if(endipv6) {
if(*(endipv6 + 1) == ':') {
service = endipv6 + 2;
}
node = host.substr(1, endipv6 - host.c_str() - 1);
}
}
// 处理普通IPv4地址格式192.168.1.1:8080
if(node.empty()) {
service = memchr(host.c_str(), ':', host.size());
if(service) {
if(!memchr(service + 1, ':', host.c_str() + host.size() - service - 1)) {
node = host.substr(0, service - host.c_str());
++service;
}
}
}
// 调用getaddrinfo
int error = getaddrinfo(node.empty() ? host.c_str() : node.c_str(),
service, &hints, &results);
// 错误处理和结果转换...
}
注意事项:getaddrinfo返回的链表需要手动释放,这里使用RAII技法通过unique_ptr管理:
cpp复制std::unique_ptr<addrinfo, void(*)(addrinfo*)>
results_ptr(results, freeaddrinfo);
4.2 网卡地址查询技巧
GetInterfaceAddresses()函数通过ioctl系统调用获取本地网卡信息:
cpp复制bool GetInterfaceAddresses(std::multimap<std::string,
std::pair<Address::ptr, uint32_t>>& result,
int family) {
struct ifconf ifc;
int sock = socket(family, SOCK_DGRAM, 0);
// 第一次调用获取缓冲区大小
if(ioctl(sock, SIOCGIFCONF, &ifc) < 0) {
close(sock);
return false;
}
// 分配足够大的缓冲区
std::vector<char> buffer(ifc.ifc_len);
ifc.ifc_buf = buffer.data();
// 第二次调用获取实际数据
if(ioctl(sock, SIOCGIFCONF, &ifc) < 0) {
close(sock);
return false;
}
// 解析每个网卡信息
for(char* ptr = buffer.data(); ptr < buffer.data() + ifc.ifc_len; ) {
struct ifreq* ifr = (struct ifreq*)ptr;
ptr += sizeof(struct ifreq);
// 获取网卡地址
Address::ptr addr;
uint32_t prefix_len = 0;
// 处理逻辑...
}
close(sock);
return true;
}
关键点说明:
- ioctl需要调用两次——第一次获取所需缓冲区大小,第二次获取实际数据
- 不同系统下ifreq结构体大小可能不同,需要正确处理指针偏移
- 通过SIOCGIFNETMASK获取子网掩码
5. 性能优化与使用建议
5.1 地址解析缓存策略
DNS解析是比较耗时的操作,建议在应用层实现缓存机制。一个简单的实现方案:
cpp复制class AddressCache {
public:
static Address::ptr Lookup(const std::string& host) {
static std::unordered_map<std::string, Address::ptr> cache;
static std::mutex mutex;
{
std::shared_lock<std::shared_mutex> lock(mutex);
auto it = cache.find(host);
if(it != cache.end()) {
return it->second;
}
}
std::vector<Address::ptr> addrs;
if(!Address::Lookup(addrs, host)) {
return nullptr;
}
{
std::unique_lock<std::shared_mutex> lock(mutex);
cache[host] = addrs[0];
return addrs[0];
}
}
};
5.2 线程安全注意事项
Address类本身是值对象,设计为线程安全。但需要注意:
- getAddr()返回的sockaddr指针指向内部数据,多线程访问需要加锁
- 派生类中的成员变量(如sockaddr_in)需要保证原子访问
- 全局DNS解析函数使用线程安全的getaddrinfo
5.3 常见问题排查
-
地址解析失败:
- 检查host是否包含非法字符
- 确认网络连接正常
- 尝试指定family参数(AF_INET/AF_INET6)
-
端口绑定失败:
- 使用getsockopt检查SO_REUSEADDR选项
- 确认没有其他进程占用端口
- 检查防火墙设置
-
IPv6兼容性问题:
- 确保系统支持IPv6(检查/proc/sys/net/ipv6)
- 测试时优先使用标准IPv6格式
- 注意scope-id在链路本地地址中的使用
6. 扩展应用场景
6.1 与协程调度器集成
在网络框架中,Address模块通常与IO协程调度器配合使用:
cpp复制void TcpServer::start() {
auto addr = Address::LookupAnyIPAddress(m_address);
m_listenSock = Socket::CreateTCP(addr->getFamily());
m_listenSock->bind(addr);
m_listenSock->listen();
while(!isStop()) {
auto client = m_listenSock->accept();
IOManager::GetThis()->schedule(
std::bind(&TcpServer::handleClient, shared_from_this(), client));
}
}
6.2 负载均衡中的应用
通过GetInterfaceAddresses可以获取所有网卡信息,用于实现负载均衡:
cpp复制std::vector<Address::ptr> getAvailableAddresses() {
std::multimap<std::string, std::pair<Address::ptr, uint32_t>> results;
Address::GetInterfaceAddresses(results);
std::vector<Address::ptr> available;
for(auto& it : results) {
if(it.first != "lo") { // 排除回环接口
available.push_back(it.second.first);
}
}
return available;
}
6.3 网络诊断工具开发
基于该模块可以快速开发网络诊断工具:
cpp复制void diagnose(const std::string& host) {
std::vector<Address::ptr> addrs;
if(!Address::Lookup(addrs, host)) {
std::cout << "DNS lookup failed for " << host << std::endl;
return;
}
for(auto& addr : addrs) {
std::cout << "Found address: " << *addr << std::endl;
auto ipaddr = std::dynamic_pointer_cast<IPAddress>(addr);
if(ipaddr) {
std::cout << " Port: " << ipaddr->getPort() << std::endl;
std::cout << " Subnet: " << *ipaddr->subnetMask(24) << std::endl;
}
}
}
在实际项目中使用这个模块后,网络相关的代码量减少了约40%,而且再没有出现过地址处理相关的bug。特别是在处理IPv6和DNS解析时,封装后的接口让代码可读性和可维护性大幅提升。
