1. Address模块概述
在网络编程中,地址处理是最基础也是最重要的环节之一。Address模块作为C++高性能服务器框架的核心组件,封装了Linux系统下IPv4和IPv6地址的相关操作,提供了一套类型安全、易于使用的API接口。这个模块的设计目标是简化网络地址操作,同时保持高性能和跨平台兼容性。
我在实际开发中发现,原生socket API中的地址处理存在几个痛点:类型不安全(使用裸指针sockaddr*)、手动内存管理繁琐、IPv4/IPv6兼容性代码重复。Address模块通过面向对象的方式解决了这些问题,将地址相关的操作封装成一系列类,让开发者能更专注于业务逻辑的实现。
提示:Address模块使用智能指针管理内存,避免了原生socket API中常见的内存泄漏问题,这在长期运行的服务器程序中尤为重要。
2. 核心类设计解析
2.1 Address基类设计
Address是所有网络地址类的基类,采用抽象工厂模式设计。它的核心职责是:
- 提供统一的地址类型接口
- 封装sockaddr的公共操作
- 实现地址解析和查询功能
cpp复制class Address {
public:
typedef std::shared_ptr<Address> ptr;
// 获取地址族类型(AF_INET, AF_INET6等)
virtual int getFamily() const = 0;
// 返回sockaddr指针,用于socket API
virtual const sockaddr* getAddr() const = 0;
virtual sockaddr* getAddr() = 0;
// 获取地址长度
virtual socklen_t getAddrLen() const = 0;
// 地址转字符串
virtual std::ostream& insert(std::ostream& os) const = 0;
// 域名解析
static bool Lookup(std::vector<Address::ptr>& result,
const std::string& host,
int family = AF_UNSPEC,
int type = 0,
int protocol = 0);
// 获取网卡地址信息
static bool GetInterfaceAddresses(
std::multimap<std::string,
std::pair<Address::ptr, uint32_t>>& result,
int family = AF_INET);
};
这个设计有几个值得注意的细节:
- 使用纯虚函数确保子类必须实现关键方法
- 所有返回的地址对象都用shared_ptr管理生命周期
- 提供了流式输出接口(insert)而不是直接返回字符串,避免了不必要的内存分配
2.2 IPAddress抽象类
IPAddress继承自Address,增加了IP地址特有的操作:
cpp复制class IPAddress : public Address {
public:
typedef std::shared_ptr<IPAddress> ptr;
// 创建IP地址(自动识别IPv4/IPv6)
static IPAddress::ptr Create(const char* address, uint16_t port = 0);
// 计算广播地址
virtual IPAddress::ptr broadcastAddress(uint32_t prefix_len) = 0;
// 计算网络地址
virtual IPAddress::ptr networkAddress(uint32_t prefix_len) = 0;
// 计算子网掩码
virtual IPAddress::ptr subnetMask(uint32_t prefix_len) = 0;
// 端口操作
virtual uint16_t getPort() const = 0;
virtual void setPort(uint16_t v) = 0;
};
这个抽象层确保了无论是IPv4还是IPv6地址,都能以统一的方式处理网络计算相关的操作。我在实际项目中经常需要计算广播地址或子网掩码,这个设计让代码更加简洁。
3. IPv4地址实现细节
3.1 IPv4Address类结构
IPv4Address是IPAddress的具体实现,封装了sockaddr_in结构:
cpp复制class IPv4Address : public IPAddress {
public:
typedef std::shared_ptr<IPv4Address> ptr;
// 通过点分十进制字符串创建
static IPv4Address::ptr Create(const char* address, uint16_t port);
// 构造函数
IPv4Address(uint32_t address = INADDR_ANY, uint16_t port = 0);
IPv4Address(const sockaddr_in& address);
// 实现基类接口
const sockaddr* getAddr() const override;
sockaddr* getAddr() override;
socklen_t getAddrLen() const override;
std::ostream& insert(std::ostream& os) const override;
// IP地址特定操作
IPAddress::ptr broadcastAddress(uint32_t prefix_len) override;
IPAddress::ptr networkAddress(uint32_t prefix_len) override;
IPAddress::ptr subnetMask(uint32_t prefix_len) override;
uint16_t getPort() const override;
void setPort(uint16_t v) override;
private:
sockaddr_in m_addr;
};
3.2 关键方法实现
广播地址计算是一个典型例子,展示了如何通过位运算实现网络计算:
cpp复制IPAddress::ptr IPv4Address::broadcastAddress(uint32_t prefix_len) {
if(prefix_len > 32) {
return nullptr;
}
sockaddr_in baddr(m_addr);
baddr.sin_addr.s_addr |= htonl((1 << (32 - prefix_len)) - 1);
return IPv4Address::ptr(new IPv4Address(baddr));
}
这段代码的算法逻辑是:
- 将IP地址与子网掩码取反后的值进行或运算
- (1 << (32 - prefix_len)) - 1 计算出主机位的掩码
- htonl确保字节序正确
注意:所有涉及网络字节序的操作都必须使用htonl/ntohl等函数转换,这是网络编程中常见的错误点。
4. IPv6地址实现
4.1 IPv6Address类设计
IPv6Address的实现与IPv4类似,但处理128位地址需要更复杂的操作:
cpp复制class IPv6Address : public IPAddress {
public:
typedef std::shared_ptr<IPv6Address> ptr;
static IPv6Address::ptr Create(const char* address, uint16_t port = 0);
IPv6Address();
IPv6Address(const sockaddr_in6& address);
IPv6Address(const uint8_t address[16], uint16_t port = 0);
// 实现基类接口...
private:
sockaddr_in6 m_addr;
};
4.2 IPv6特殊处理
IPv6的地址计算比IPv4复杂,以网络地址计算为例:
cpp复制IPAddress::ptr IPv6Address::networkAddress(uint32_t prefix_len) {
sockaddr_in6 addr(m_addr);
if(prefix_len > 128) {
return nullptr;
}
int bytes = prefix_len / 8;
int bits = prefix_len % 8;
// 处理完整字节
for(int i = bytes; i < 16; ++i) {
addr.sin6_addr.s6_addr[i] = 0;
}
// 处理不完整字节
if(bits != 0) {
uint8_t mask = ((1 << bits) - 1) << (8 - bits);
addr.sin6_addr.s6_addr[bytes] &= mask;
}
return IPv6Address::ptr(new IPv6Address(addr));
}
这段代码展示了IPv6地址处理的几个关键点:
- IPv6地址是128位,存储在16字节的数组中
- 网络计算需要分别处理完整字节和部分字节
- 位运算需要考虑字节序问题
5. 地址解析与查询
5.1 域名解析实现
Address::Lookup方法封装了getaddrinfo系统调用,支持多种查询方式:
cpp复制bool Address::Lookup(std::vector<Address::ptr>& result,
const std::string& host,
int family, int type, int protocol) {
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 = NULL;
// 处理IPv6地址格式[::1]:80
if(!host.empty() && host[0] == '[') {
const char* endipv6 = (const char*)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地址格式和域名
if(node.empty()) {
service = (const char*)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;
}
}
}
if(node.empty()) {
node = host;
}
int error = getaddrinfo(node.c_str(), service, &hints, &results);
if(error) {
LOG_ERROR(g_logger) << "Address::Lookup getaddress(" << host << ", "
<< family << ", " << type << ") err=" << error << " errstr="
<< gai_strerror(error);
return false;
}
next = results;
while(next) {
result.push_back(Create(next->ai_addr, (socklen_t)next->ai_addrlen));
next = next->ai_next;
}
freeaddrinfo(results);
return !result.empty();
}
这段代码有几个值得注意的实现细节:
- 同时支持IPv4/IPv6地址格式解析
- 正确处理服务端口分离
- 使用RAII风格确保getaddrinfo结果被释放
- 支持多种地址族和套接字类型的组合查询
5.2 网卡地址查询
GetInterfaceAddresses方法封装了getifaddrs系统调用,可以获取本机所有网卡信息:
cpp复制bool Address::GetInterfaceAddresses(
std::multimap<std::string,
std::pair<Address::ptr, uint32_t>>& result,
int family) {
struct ifaddrs *results, *next;
if(getifaddrs(&results) != 0) {
LOG_ERROR(g_logger) << "Address::GetInterfaceAddresses getifaddrs "
<< " err=" << errno << " errstr=" << strerror(errno);
return false;
}
try {
for(next = results; next; next = next->ifa_next) {
Address::ptr addr;
uint32_t prefix_len = ~0u;
if(family != AF_UNSPEC && family != next->ifa_addr->sa_family) {
continue;
}
switch(next->ifa_addr->sa_family) {
case AF_INET:
{
addr = Create(next->ifa_addr, sizeof(sockaddr_in));
uint32_t netmask = ((sockaddr_in*)next->ifa_netmask)->sin_addr.s_addr;
prefix_len = CountBytes(netmask);
}
break;
case AF_INET6:
{
addr = Create(next->ifa_addr, sizeof(sockaddr_in6));
in6_addr& netmask = ((sockaddr_in6*)next->ifa_netmask)->sin6_addr;
prefix_len = 0;
for(int i = 0; i < 16; ++i) {
prefix_len += CountBytes(netmask.s6_addr[i]);
}
}
break;
default:
break;
}
if(addr) {
result.insert(std::make_pair(next->ifa_name,
std::make_pair(addr, prefix_len)));
}
}
} catch (...) {
freeifaddrs(results);
return false;
}
freeifaddrs(results);
return !result.empty();
}
这个方法在实际应用中非常有用,比如:
- 服务发现时自动绑定可用网卡
- 多网卡环境下选择最优网络路径
- 网络监控工具获取接口状态
6. 性能优化技巧
在实现Address模块时,我们采用了多种性能优化手段:
6.1 对象池技术
频繁创建和销毁Address对象会影响性能,我们使用对象池管理常用地址对象:
cpp复制class AddressPool {
public:
static Address::ptr Get(const sockaddr* addr, socklen_t addrlen) {
static AddressPool pool;
uint64_t key = Hash(addr, addrlen);
MutexType::Lock lock(pool.m_mutex);
auto it = pool.m_pool.find(key);
if(it != pool.m_pool.end()) {
return it->second;
}
Address::ptr ptr = Address::Create(addr, addrlen);
if(ptr) {
pool.m_pool[key] = ptr;
}
return ptr;
}
private:
static uint64_t Hash(const sockaddr* addr, socklen_t addrlen) {
// 简化的哈希计算
if(addr->sa_family == AF_INET) {
auto ipv4 = reinterpret_cast<const sockaddr_in*>(addr);
return (uint64_t(ipv4->sin_addr.s_addr) << 16) | ipv4->sin_port;
} else if(addr->sa_family == AF_INET6) {
auto ipv6 = reinterpret_cast<const sockaddr_in6*>(addr);
const uint8_t* p = ipv6->sin6_addr.s6_addr;
return ((uint64_t(p[0]) << 56) | (uint64_t(p[1]) << 48) |
(uint64_t(p[2]) << 40) | (uint64_t(p[3]) << 32) |
(uint64_t(p[4]) << 24) | (uint64_t(p[5]) << 16) |
(uint64_t(p[6]) << 8) | p[7]) ^ ipv6->sin6_port;
}
return 0;
}
MutexType m_mutex;
std::unordered_map<uint64_t, Address::ptr> m_pool;
};
这个对象池可以显著减少相同地址的重复构造开销,特别是在处理大量短连接时效果明显。
6.2 延迟解析策略
域名解析是相对耗时的操作,我们实现了延迟解析机制:
cpp复制class LazyAddress : public Address {
public:
LazyAddress(const std::string& host, uint16_t port = 0)
: m_host(host), m_port(port), m_resolved(false) {}
const sockaddr* getAddr() const override {
if(!m_resolved) {
const_cast<LazyAddress*>(this)->resolve();
}
return m_addr->getAddr();
}
// 其他方法实现...
private:
void resolve() {
std::vector<Address::ptr> addrs;
if(Address::Lookup(addrs, m_host)) {
m_addr = addrs[0];
if(m_port != 0) {
m_addr->setPort(m_port);
}
m_resolved = true;
}
}
std::string m_host;
uint16_t m_port;
mutable Address::ptr m_addr;
mutable bool m_resolved;
};
这种设计特别适合配置阶段加载的地址,实际使用时才进行解析,可以加快服务启动速度。
7. 实际应用案例
7.1 服务器监听配置
Address模块可以简化服务器监听配置:
cpp复制// 传统方式
sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
inet_pton(AF_INET, "0.0.0.0", &addr.sin_addr);
int fd = socket(AF_INET, SOCK_STREAM, 0);
bind(fd, (sockaddr*)&addr, sizeof(addr));
// 使用Address模块
auto addr = IPv4Address::Create("0.0.0.0", 8080);
int fd = socket(addr->getFamily(), SOCK_STREAM, 0);
bind(fd, addr->getAddr(), addr->getAddrLen());
虽然代码量减少不多,但消除了手动内存管理和字节序转换的潜在错误。
7.2 客户端连接管理
在实现连接池时,Address模块的优势更加明显:
cpp复制class ConnectionPool {
public:
void addServer(const std::string& host, uint16_t port) {
auto addr = Address::LookupAny(host, port);
if(addr) {
m_servers.push_back(addr);
}
}
Connection::ptr getConnection() {
for(auto& addr : m_servers) {
int fd = socket(addr->getFamily(), SOCK_STREAM, 0);
if(connect(fd, addr->getAddr(), addr->getAddrLen()) == 0) {
return std::make_shared<Connection>(fd);
}
close(fd);
}
return nullptr;
}
private:
std::vector<Address::ptr> m_servers;
};
这个实现可以自动处理IPv4和IPv6地址,无需为不同地址族编写特殊代码。
8. 测试与验证
8.1 单元测试要点
完善的测试是保证Address模块可靠性的关键,主要测试点包括:
-
基本功能测试
- IPv4地址创建与解析
- IPv6地址创建与解析
- 端口设置与获取
-
网络计算测试
- 广播地址计算正确性
- 网络地址计算正确性
- 子网掩码计算正确性
-
域名解析测试
- IPv4域名解析
- IPv6域名解析
- 带端口号的域名解析
-
网卡查询测试
- 获取所有网卡信息
- 过滤特定地址族
8.2 性能测试
我们使用Google Benchmark对关键操作进行了性能测试:
code复制Benchmark Time CPU Iterations
---------------------------------------------------------
IPv4Create 28.3 ns 28.3 ns 24888890
IPv6Create 42.7 ns 42.7 ns 16417910
IPv4BroadcastCalc 56.1 ns 56.1 ns 12400000
IPv6NetworkCalc 132 ns 132 ns 5300000
DNSLookupLocal 12500 ns 12500 ns 56000
InterfaceAddressQuery 45800 ns 45800 ns 15200
测试结果显示:
- 基本地址创建操作在100ns以内完成
- 网络计算操作IPv6比IPv4慢约2倍
- 系统调用相关的操作(DNS查询、网卡查询)耗时明显较高
9. 跨平台兼容性
虽然Address模块主要针对Linux平台设计,但通过条件编译也支持其他平台:
cpp复制#ifdef __linux__
// Linux特有实现
#elif defined(_WIN32)
// Windows特有实现
#elif defined(__APPLE__)
// macOS特有实现
#endif
主要差异点包括:
- Windows使用WSAStartup初始化网络库
- macOS的getifaddrs返回结构略有不同
- 各平台字节序可能不同(虽然现在基本都是小端)
10. 最佳实践与经验分享
在实际项目中使用Address模块时,我总结了以下几点经验:
-
智能指针管理生命周期
总是使用Address::ptr而不是裸指针,避免内存泄漏 -
优先使用工厂方法
使用Create/Lookup等方法创建地址对象,而不是直接构造具体类 -
合理缓存解析结果
对频繁使用的地址(如配置中的服务器地址)应该缓存解析结果 -
注意线程安全
地址对象本身是线程安全的(只读),但对象池等共享资源需要加锁 -
错误处理要全面
特别是域名解析和网卡查询可能失败,必须有适当的错误处理 -
性能敏感场景避免频繁解析
在高性能网络编程中,应该将域名提前解析为IP地址 -
日志记录关键操作
对地址解析、网络计算等操作记录详细日志,方便调试
Address模块作为网络编程的基础设施,其稳定性和性能直接影响整个服务器框架的质量。经过多个项目的实践验证,这个设计在保证易用性的同时,也提供了足够的灵活性和性能。