目录
[一、 TimeZone.h](#一、 TimeZone.h)
[1. TimeZone.h 整体定位](#1. TimeZone.h 整体定位)
[2. 逐模块拆解代码](#2. 逐模块拆解代码)
[3. 核心使用场景示例](#3. 核心使用场景示例)
[4. 设计思想](#4. 设计思想)
[5. 总结](#5. 总结)
[二、 TimeZone.cc](#二、 TimeZone.cc)
[1. 代码整体定位](#1. 代码整体定位)
[2. 核心数据结构解析(Pimpl 模式的 Data 结构体)](#2. 核心数据结构解析(Pimpl 模式的 Data 结构体))
[3. 辅助工具模块(detail 命名空间)](#3. 辅助工具模块(detail 命名空间))
[4. 核心逻辑:时区文件解析(readTimeZoneFile/readDataBlock)](#4. 核心逻辑:时区文件解析(readTimeZoneFile/readDataBlock))
[5. 核心查找逻辑:findLocalTime(处理夏令时)](#5. 核心查找逻辑:findLocalTime(处理夏令时))
[6. 核心接口实现:toLocalTime/fromLocalTime](#6. 核心接口实现:toLocalTime/fromLocalTime)
[7. 常用时区构造:UTC ()/China ()](#7. 常用时区构造:UTC ()/China ())
一、 TimeZone.h
先贴出完整代码,再逐部分解释:
cpp
// 本源代码的使用受 BSD 风格许可证约束
// 该许可证可在 License 文件中查阅。
//
// 作者:陈硕 (chenshuo at chenshuo dot com)
#ifndef MUDUO_BASE_TIMEZONE_H
#define MUDUO_BASE_TIMEZONE_H
#include "muduo/base/copyable.h" // 可拷贝基类(语义标记)
#include "muduo/base/Types.h" // 基础类型定义
#include <memory> // 智能指针(unique_ptr/shared_ptr)
#include <time.h> // 时间相关结构体(tm)
namespace muduo
{
// 未指定时区的本地时间结构体
// 约束:1分钟固定为60秒,不考虑闰秒(简化时间计算)
struct DateTime
{
// 默认构造函数(值初始化为0)
DateTime() {}
// 从 tm 结构体构造 DateTime
explicit DateTime(const struct tm&);
// 显式构造函数:指定年/月/日/时/分/秒
DateTime(int _year, int _month, int _day, int _hour, int _minute, int _second)
: year(_year), month(_month), day(_day), hour(_hour), minute(_minute), second(_second)
{
}
// 转换为 ISO 格式字符串(如:"2011-12-31 12:34:56")
string toIsoString() const;
int year = 0; // 年份范围 [1900, 2500]
int month = 0; // 月份范围 [1, 12](1=1月,12=12月)
int day = 0; // 日期范围 [1, 31](根据月份自动适配)
int hour = 0; // 小时范围 [0, 23](24小时制)
int minute = 0; // 分钟范围 [0, 59]
int second = 0; // 秒数范围 [0, 59](不考虑闰秒)
};
// 时区类(支持 1970~2100 年的时区转换)
class TimeZone : public muduo::copyable
{
public:
// 默认构造函数:创建一个**无效的**时区对象
TimeZone() = default;
// 构造固定偏移的时区(无夏令时)
// @param eastOfUtc 相对于 UTC 的东偏移量(单位:秒,如 GMT+8 为 8*3600=28800)
// @param tzname 时区名称(如 "CST")
TimeZone(int eastOfUtc, const char* tzname);
// 获取 UTC 时区对象(偏移量 0)
static TimeZone UTC();
// 获取中国时区对象(固定 GMT+8,无夏令时 DST)
static TimeZone China();
// 从时区文件加载时区规则(支持夏令时等动态规则)
// @param zonefile 时区文件路径(如 /usr/share/zoneinfo/Asia/Shanghai)
static TimeZone loadZoneFile(const char* zonefile);
// 默认的拷贝构造/赋值/析构函数均适用(无需自定义)
// 注:data_ 是 shared_ptr,拷贝时仅增加引用计数,效率高
// 判断时区对象是否有效
// 注:C++11 中可通过 'explicit operator bool() const' 实现隐式布尔转换
bool valid() const
{
return static_cast<bool>(data_);
}
// 将 UTC 时间戳(从 epoch 开始的秒数)转换为当前时区的本地时间
// @param secondsSinceEpoch UTC 时间戳(秒级)
// @param utcOffset [输出参数] 可选,返回当前时区相对于 UTC 的偏移量(秒)
// @return 转换后的本地时间(DateTime 结构体)
struct DateTime toLocalTime(int64_t secondsSinceEpoch, int* utcOffset = nullptr) const;
// 将当前时区的本地时间转换为 UTC 时间戳(从 epoch 开始的秒数)
// @param dt 本地时间(DateTime 结构体)
// @param postTransition 夏令时切换时,是否使用切换后的规则(默认 false)
// @return 转换后的 UTC 时间戳(秒级)
int64_t fromLocalTime(const struct DateTime&, bool postTransition = false) const;
// 静态方法:将 UTC 时间戳转换为 UTC 时间(等价于 gmtime(3) 功能)
// @param secondsSinceEpoch UTC 时间戳(秒级)
// @return UTC 时间(DateTime 结构体)
static struct DateTime toUtcTime(int64_t secondsSinceEpoch);
// 静态方法:将 UTC 时间转换为 UTC 时间戳(等价于 timegm(3) 功能)
// @param dt UTC 时间(DateTime 结构体)
// @return UTC 时间戳(秒级)
static int64_t fromUtcTime(const struct DateTime&);
// 前向声明:时区核心数据结构(具体实现隐藏在 cpp 文件中,封装细节)
struct Data;
private:
// 私有构造函数:从智能指针创建时区对象(仅内部/友元使用)
explicit TimeZone(std::unique_ptr<Data> data);
// 时区核心数据(共享指针,支持高效拷贝)
std::shared_ptr<Data> data_;
// 测试友元类:允许单元测试访问私有成员
friend class TimeZoneTestPeer;
};
} // namespace muduo
#endif // MUDUO_BASE_TIMEZONE_H
1. TimeZone.h 整体定位
TimeZone 类是 Muduo 时间处理体系的时区层,核心解决「UTC 时间 ↔ 本地时间(不同时区)」的转换问题:
- 弥补了
Timestamp(纯 UTC 微秒时间戳)和Date(纯 UTC 日期)的「时区缺失」问题; - 支持两种时区模式:固定偏移时区 (如中国 GMT+8,无夏令时)和时区文件加载(如欧美含夏令时的复杂时区);
- 限定时间范围 1970~2100 年,覆盖绝大多数业务场景,简化时区规则的实现。
2. 逐模块拆解代码
(1)DateTime 结构体:本地时间容器
cpp
// 无特定时区的本地时间结构体(仅存年月日时分秒,无时区/微秒)
// 注意:分钟固定为60秒,不处理闰秒(符合日常业务场景)
struct DateTime
{
// 空构造:初始值全0(无效时间)
DateTime() {}
// 从系统tm结构体构造(对接系统时间接口)
explicit DateTime(const struct tm&);
// 显式构造:指定年月日时分秒(符合人类习惯)
DateTime(int _year, int _month, int _day, int _hour, int _minute, int _second)
: year(_year), month(_month), day(_day), hour(_hour), minute(_minute), second(_second)
{
}
// 转为ISO标准字符串:"2011-12-31 12:34:56"(日志/展示常用)
string toIsoString() const;
// 成员变量:标注合法范围,避免无效值
int year = 0; // [1900, 2500]
int month = 0; // [1, 12]
int day = 0; // [1, 31]
int hour = 0; // [0, 23]
int minute = 0; // [0, 59]
int second = 0; // [0, 59]
};
核心解释:
-
DateTimevsDate/Timestamp:类型 核心特征 时区属性 Date仅年月日(UTC) 仅 UTC Timestamp微秒级时间戳(UTC) 仅 UTC DateTime年月日时分秒(本地时间) 无(需结合 TimeZone) -
无闰秒设计:注释明确 "a minute is always 60 seconds",因为闰秒在业务场景中极少用到,简化实现且避免复杂度;
-
ISO 字符串格式:
toIsoString()返回YYYY-MM-DD HH:MM:SS,是日志、网络协议中最通用的时间格式。
(2)TimeZone 类的基础设计(Pimpl 模式 + 智能指针)
cpp
class TimeZone : public muduo::copyable
{
public:
// 默认构造:无效时区(data_为空)
TimeZone() = default;
// 构造1:固定偏移时区(如GMT+8 → eastOfUtc=8*3600),tzname为时区名(如"CST")
TimeZone(int eastOfUtc, const char* tzname);
// 预定义常用时区:UTC(偏移0)、中国(GMT+8,无夏令时)
static TimeZone UTC();
static TimeZone China();
// 构造2:加载时区文件(如/etc/localtime),支持含夏令时的复杂时区
static TimeZone loadZoneFile(const char* zonefile);
// 判断时区是否有效:通过data_是否非空(C++11 explicit operator bool的兼容写法)
bool valid() const { return static_cast<bool>(data_); }
// 核心接口:UTC ↔ 本地时间转换
struct DateTime toLocalTime(int64_t secondsSinceEpoch, int* utcOffset = nullptr) const;
int64_t fromLocalTime(const struct DateTime&, bool postTransition = false) const;
// 静态工具:UTC时间 ↔ DateTime转换(无需时区,对接gmtime/timegm)
static struct DateTime toUtcTime(int64_t secondsSinceEpoch);
static int64_t fromUtcTime(const struct DateTime&);
// 前向声明:时区核心数据(Pimpl模式,隐藏实现细节)
struct Data;
private:
// 私有构造:接收Data的unique_ptr,封装实现细节
explicit TimeZone(std::unique_ptr<Data> data);
// 核心成员:共享时区数据(copyable的同时避免深拷贝)
std::shared_ptr<Data> data_;
// 测试友元:方便单元测试访问私有成员
friend class TimeZoneTestPeer;
};
核心设计细节解析:
① Pimpl 模式(Pointer to Implementation)
struct Data前向声明,具体实现放在.cpp文件中:- 降低头文件依赖:头文件无需暴露时区数据的具体结构(如夏令时规则、偏移量表),编译更快;
- 隔离实现变更:修改时区数据结构时,只需重新编译
.cpp,无需重新编译所有包含TimeZone.h的文件。
② std::shared_ptr<Data> 的选择(关键!)
TimeZone继承copyable(可拷贝),但时区数据(如偏移量表、夏令时规则)可能较大,若深拷贝会有性能开销;shared_ptr<Data>实现数据共享 :多个TimeZone对象拷贝时,仅拷贝指针,共享同一份时区数据,兼顾copyable语义和性能;- 对比
unique_ptr:私有构造用unique_ptr(独占数据),成员用shared_ptr(共享数据),分工明确。
③ 两种时区构造方式(覆盖不同场景)
| 构造方式 | 适用场景 | 特点 |
|---|---|---|
| 固定偏移(eastOfUtc) | 简单时区(如中国 GMT+8、UTC),无夏令时 | 实现简单,效率高 |
| 加载时区文件 | 复杂时区(如欧美,含夏令时切换) | 兼容完整时区规则,稍复杂 |
(3)核心接口:时区转换
| 接口 | 作用 | 关键参数 / 说明 |
|---|---|---|
toLocalTime |
UTC 秒数 → 本地时间(DateTime) | utcOffset:可选输出当前 UTC 偏移量(秒),比如中国时区返回 28800(8*3600) |
fromLocalTime |
本地时间(DateTime)→ UTC 秒数 | postTransition:处理夏令时切换时的歧义时间(比如凌晨 2 点回拨 1 小时,同一个本地时间对应两个 UTC 时间,该参数指定取切换后的值) |
toUtcTime |
UTC 秒数 → UTC 时间(DateTime) | 等价于 gmtime,无需时区,纯 UTC 转换 |
fromUtcTime |
UTC 时间(DateTime)→ UTC 秒数 | 等价于 timegm(反向 gmtime),Linux 下需手动实现(无标准库) |
3. 核心使用场景示例
cpp
#include "muduo/base/TimeZone.h"
#include "muduo/base/Timestamp.h"
#include <iostream>
int main() {
// 1. 获取中国时区(GMT+8,无夏令时)
muduo::TimeZone cst = muduo::TimeZone::China();
if (!cst.valid()) {
std::cerr << "无效时区" << std::endl;
return 1;
}
// 2. 获取当前UTC秒数(从Timestamp转换)
muduo::Timestamp now = muduo::Timestamp::now();
int64_t utc_seconds = now.secondsSinceEpoch();
// 3. UTC → 中国本地时间
muduo::DateTime local = cst.toLocalTime(utc_seconds);
std::cout << "中国本地时间:" << local.toIsoString() << std::endl;
// 4. 中国本地时间 → UTC秒数
int64_t utc_seconds2 = cst.fromLocalTime(local);
std::cout << "转回UTC秒数:" << utc_seconds2 << "(和原数是否相等:" << (utc_seconds == utc_seconds2) << ")" << std::endl;
// 5. UTC时间 ↔ DateTime(无需时区)
muduo::DateTime utc_dt = muduo::TimeZone::toUtcTime(utc_seconds);
std::cout << "UTC时间:" << utc_dt.toIsoString() << std::endl;
return 0;
}
输出示例(假设 UTC 时间为 2025-01-20 12:00:00):
cpp
中国本地时间:2025-01-20 20:00:00
转回UTC秒数:1737345600(和原数是否相等:1)
UTC时间:2025-01-20 12:00:00
4. 设计思想
- 分层设计:将「时间存储(Timestamp/Date)」和「时区转换(TimeZone)」解耦,Timestamp/Date 专注 UTC 时间,TimeZone 专注时区转换,符合单一职责原则;
- 性能与易用性平衡 :
shared_ptr实现数据共享,固定时区快速构造,时区文件兼容复杂场景; - Pimpl 模式:隔离实现细节,降低编译依赖,提升代码可维护性;
- 业务友好:忽略闰秒、限定时间范围 1970~2100,聚焦主流业务场景,避免过度设计。
5. 总结
DateTime是「本地时间容器」,封装年月日时分秒,无时区信息,用于和TimeZone配合完成时区转换;TimeZone采用 Pimpl 模式 + shared_ptr,实现「可拷贝、低拷贝开销、隐藏实现细节」,支持固定偏移和时区文件两种构造方式;- 核心功能是「UTC 秒数 ↔ 本地时间(DateTime)」的双向转换,补全了 Muduo 时间处理体系的时区能力,适配不同地区的时间展示 / 计算需求。
二、 TimeZone.cc
先贴出完整代码,再逐部分解释:
cpp
// 本源代码的使用受 BSD 风格许可证约束
// 该许可证可在 License 文件中查阅。
//
// 作者:陈硕 (chenshuo at chenshuo dot com)
#include "muduo/base/TimeZone.h"
#include "muduo/base/noncopyable.h" // 不可拷贝基类
#include "muduo/base/Date.h" // 日期处理类
#include <algorithm> // 提供 upper_bound 等算法
#include <memory> // 智能指针
#include <stdexcept> // 异常类(logic_error)
#include <string> // 字符串
#include <vector> // 动态数组
#include <assert.h> // 断言
//#define _BSD_SOURCE // 启用 BSD 扩展函数(如 be64toh)
#include <endian.h> // 字节序转换函数(be64toh/be32toh)
#include <stdint.h> // 固定宽度整数类型
#include <stdio.h> // 文件操作(fopen/fread/fseek 等)
using namespace muduo;
// TimeZone 类的核心数据结构(封装时区规则,对外隐藏实现)
struct TimeZone::Data
{
// 时区切换点结构体(如夏令时开始/结束的时间点)
struct Transition
{
int64_t utctime; // 切换点的 UTC 时间(从 epoch 开始的秒数)
int64_t localtime; // 切换点的本地时间(偏移后的 epoch 秒数)
int localtimeIdx; // 对应 LocalTime 数组的索引
Transition(int64_t t, int64_t l, int localIdx)
: utctime(t), localtime(l), localtimeIdx(localIdx)
{ }
};
// 本地时间规则结构体(描述某一时区规则的属性)
struct LocalTime
{
int32_t utcOffset; // 相对于 UTC 的东偏移量(秒,如 GMT+8 为 28800)
bool isDst; // 是否为夏令时(DST)模式
int desigIdx; // 时区缩写在 abbreviation 字符串中的索引
LocalTime(int32_t offset, bool dst, int idx)
: utcOffset(offset), isDst(dst), desigIdx(idx)
{ }
};
// 添加一条本地时间规则到数组
void addLocalTime(int32_t utcOffset, bool isDst, int desigIdx)
{
localtimes.push_back(LocalTime(utcOffset, isDst, desigIdx));
}
// 添加一个时区切换点
// @param utcTime 切换点的 UTC 时间(秒);localtimeIdx 对应 LocalTime 索引
void addTransition(int64_t utcTime, int localtimeIdx)
{
LocalTime lt = localtimes.at(localtimeIdx); // 取出对应的本地时间规则
// 切换点的本地时间 = UTC 时间 + 偏移量
transitions.push_back(Transition(utcTime, utcTime + lt.utcOffset, localtimeIdx));
}
// 根据 UTC 时间查找对应的本地时间规则
const LocalTime* findLocalTime(int64_t utcTime) const;
// 根据本地时间查找对应的本地时间规则(处理夏令时切换的歧义)
const LocalTime* findLocalTime(const struct DateTime& local, bool postTransition) const;
// 比较器:按 UTC 时间升序排序 Transition
struct CompareUtcTime
{
bool operator()(const Transition& lhs, const Transition& rhs) const
{
return lhs.utctime < rhs.utctime;
}
};
// 比较器:按本地时间升序排序 Transition
struct CompareLocalTime
{
bool operator()(const Transition& lhs, const Transition& rhs) const
{
return lhs.localtime < rhs.localtime;
}
};
std::vector<Transition> transitions; // 所有时区切换点(按 UTC 时间排序)
std::vector<LocalTime> localtimes; // 所有本地时间规则
string abbreviation; // 时区缩写字符串(如 "CST/CDT")
string tzstring; // 时区完整描述字符串
};
namespace muduo
{
// 常量:一天的秒数(24*60*60 = 86400)
const int kSecondsPerDay = 24*60*60;
namespace detail
{
// 文件操作封装类(不可拷贝,简化二进制文件读取)
class File : noncopyable
{
public:
// 构造函数:打开指定二进制文件
File(const char* file)
: fp_(::fopen(file, "rb")) // "rb" 以二进制模式打开
{
}
// 析构函数:关闭文件(自动释放资源)
~File()
{
if (fp_)
{
::fclose(fp_);
}
}
// 判断文件是否成功打开
bool valid() const { return fp_; }
// 读取指定字节数的二进制数据(失败抛异常)
string readBytes(int n)
{
char buf[n];
ssize_t nr = ::fread(buf, 1, n, fp_); // 每次读 1 字节,共读 n 次
if (nr != n)
throw std::logic_error("数据不足"); // 读取字节数不符,抛异常
return string(buf, n);
}
// 读取文件剩余所有数据(直到 EOF)
string readToEnd()
{
char buf[4096]; // 栈缓冲区,减少堆分配
string result;
ssize_t nr = 0;
while ( (nr = ::fread(buf, 1, sizeof buf, fp_)) > 0)
{
result.append(buf, nr); // 追加读取到的数据
}
return result;
}
// 读取 64 位整数(网络字节序转主机字节序)
int64_t readInt64()
{
int64_t x = 0;
ssize_t nr = ::fread(&x, 1, sizeof(int64_t), fp_);
if (nr != sizeof(int64_t))
throw std::logic_error("无效的 int64_t 数据");
return be64toh(x); // 大端(网络序)转主机序
}
// 读取 32 位整数(网络字节序转主机字节序)
int32_t readInt32()
{
int32_t x = 0;
ssize_t nr = ::fread(&x, 1, sizeof(int32_t), fp_);
if (nr != sizeof(int32_t))
throw std::logic_error("无效的 int32_t 数据");
return be32toh(x); // 大端转主机序
}
// 读取 8 位无符号整数
uint8_t readUInt8()
{
uint8_t x = 0;
ssize_t nr = ::fread(&x, 1, sizeof(uint8_t), fp_);
if (nr != sizeof(uint8_t))
throw std::logic_error("无效的 uint8_t 数据");
return x;
}
// 跳过指定字节数(移动文件指针)
off_t skip(ssize_t bytes)
{
return ::fseek(fp_, bytes, SEEK_CUR); // 从当前位置偏移 bytes 字节
}
private:
FILE* fp_; // 文件指针(NULL 表示打开失败)
};
// 读取 TZif 格式的时区数据块(RFC 8536 标准:https://www.rfc-editor.org/rfc/rfc8536.html)
// @param f 已打开的文件对象;data 存储解析结果;v1 是否为 TZif v1 版本(v1 用 32 位时间,v2+ 用 64 位)
bool readDataBlock(File& f, struct TimeZone::Data* data, bool v1)
{
const int time_size = v1 ? sizeof(int32_t) : sizeof(int64_t); // 时间字段的字节数
const int32_t isutccnt = f.readInt32(); // UTC 标志数量
const int32_t isstdcnt = f.readInt32(); // 标准时间标志数量
const int32_t leapcnt = f.readInt32(); // 闰秒记录数量
const int32_t timecnt = f.readInt32(); // 切换点时间数量
const int32_t typecnt = f.readInt32(); // 本地时间规则数量
const int32_t charcnt = f.readInt32(); // 时区缩写字符串长度
// 简化处理:不支持闰秒、UTC/标准时间标志数量需与规则数一致
if (leapcnt != 0)
return false;
if (isutccnt != 0 && isutccnt != typecnt)
return false;
if (isstdcnt != 0 && isstdcnt != typecnt)
return false;
// 读取所有切换点时间(v1 读 32 位,v2+ 读 64 位)
std::vector<int64_t> trans;
trans.reserve(timecnt); // 预分配空间,提升效率
for (int i = 0; i < timecnt; ++i)
{
if (v1)
{
trans.push_back(f.readInt32());
}
else
{
trans.push_back(f.readInt64());
}
}
// 读取每个切换点对应的本地时间规则索引
std::vector<int> localtimes;
localtimes.reserve(timecnt);
for (int i = 0; i < timecnt; ++i)
{
uint8_t local = f.readUInt8();
localtimes.push_back(local);
}
// 读取所有本地时间规则
data->localtimes.reserve(typecnt);
for (int i = 0; i < typecnt; ++i)
{
int32_t gmtoff = f.readInt32(); // UTC 偏移量(秒)
uint8_t isdst = f.readUInt8(); // 是否夏令时
uint8_t abbrind = f.readUInt8(); // 时区缩写索引
data->addLocalTime(gmtoff, isdst, abbrind);
}
// 构建所有时区切换点
for (int i = 0; i < timecnt; ++i)
{
int localIdx = localtimes[i];
data->addTransition(trans[i], localIdx);
}
// 读取时区缩写字符串
data->abbreviation = f.readBytes(charcnt);
// 跳过闰秒数据、UTC/标准时间标志
f.skip(leapcnt * (time_size + 4));
f.skip(isstdcnt);
f.skip(isutccnt);
if (!v1)
{
// FIXME: 读取到下一个换行符(TZif v2+ 包含注释行)
data->tzstring = f.readToEnd();
}
return true;
}
// 读取 TZif 格式的时区文件(如 /usr/share/zoneinfo/Asia/Shanghai)
// @param zonefile 时区文件路径;data 存储解析结果
bool readTimeZoneFile(const char* zonefile, struct TimeZone::Data* data)
{
File f(zonefile); // 打开时区文件
if (f.valid())
{
try
{
// 读取文件头(4 字节 "TZif")
string head = f.readBytes(4);
if (head != "TZif")
throw std::logic_error("无效的文件头");
string version = f.readBytes(1); // 版本号(空=v1,2=v2,3=v3)
f.readBytes(15); // 跳过剩余 15 字节保留字段
// 读取 v1 版本的计数器(isgmtcnt/isstdcnt/leapcnt/timecnt/typecnt/charcnt)
const int32_t isgmtcnt = f.readInt32();
const int32_t isstdcnt = f.readInt32();
const int32_t leapcnt = f.readInt32();
const int32_t timecnt = f.readInt32();
const int32_t typecnt = f.readInt32();
const int32_t charcnt = f.readInt32();
if (version == "2") // TZif v2 版本(包含 64 位时间)
{
// 跳过 v1 版本的旧数据块
size_t skip = sizeof(int32_t) * timecnt + timecnt + 6 * typecnt +
charcnt + 8 * leapcnt + isstdcnt + isgmtcnt;
f.skip(skip);
// 读取 v2 版本的文件头
head = f.readBytes(4);
if (head != "TZif")
throw std::logic_error("无效的 v2 文件头");
f.skip(16); // 跳过版本号+保留字段
return readDataBlock(f, data, false); // 解析 64 位时间的 v2 数据块
}
else // TZif v1 版本(32 位时间)
{
// TODO: 用真实的 v1 文件测试
f.skip(-4 * 6); // 回退到计数器位置(重新读取)
return readDataBlock(f, data, true); // 解析 32 位时间的 v1 数据块
}
}
catch (std::logic_error& e) // 捕获解析异常,打印错误信息
{
fprintf(stderr, "%s\n", e.what());
}
}
return false;
}
// 辅助函数:将一天内的秒数拆分为时/分/秒,填充到 DateTime 结构体
inline void fillHMS(unsigned seconds, struct DateTime* dt)
{
dt->second = seconds % 60; // 秒(0-59)
unsigned minutes = seconds / 60; // 总分钟数
dt->minute = minutes % 60; // 分(0-59)
dt->hour = minutes / 60; // 时(0-23)
}
// 将 epoch 秒数转换为 DateTime 结构体(UTC 时间)
// @param t 从 epoch 开始的秒数
DateTime BreakTime(int64_t t)
{
struct DateTime dt;
int seconds = static_cast<int>(t % kSecondsPerDay); // 当天的秒数
int days = static_cast<int>(t / kSecondsPerDay); // 从 epoch 开始的天数
// C++11 中负数取模结果向零取整,需修正(如 -1 秒 → 86399 秒,天数-1)
if (seconds < 0)
{
seconds += kSecondsPerDay;
--days;
}
// 拆分时/分/秒
detail::fillHMS(seconds, &dt);
// 计算年月日(基于儒略日)
Date date(days + Date::kJulianDayOf1970_01_01);
Date::YearMonthDay ymd = date.yearMonthDay();
dt.year = ymd.year;
dt.month = ymd.month;
dt.day = ymd.day;
return dt;
}
} // namespace detail
} // namespace muduo
// 根据 UTC 时间查找对应的本地时间规则
const TimeZone::Data::LocalTime* TimeZone::Data::findLocalTime(int64_t utcTime) const
{
const LocalTime* local = NULL;
// 示例数据(中国曾短暂使用的夏令时切换点):
// 行 UTC 时间 是否夏令时 偏移量 本地时间(中国)
// 1 1989-09-16 17:00:00Z 0 8.0 1989-09-17 01:00:00
// 2 1990-04-14 18:00:00Z 1 9.0 1990-04-15 03:00:00
// 3 1990-09-15 17:00:00Z 0 8.0 1990-09-16 01:00:00
// 4 1991-04-13 18:00:00Z 1 9.0 1991-04-14 03:00:00
// 5 1991-09-14 17:00:00Z 0 8.0 1991-09-15 01:00:00
// 输入 '1990-06-01 00:00:00Z',std::upper_bound 返回第 3 行,
// 因此输入时间属于第 2 行的规则,偏移量 9 小时,本地时间为 1990-06-01 09:00:00
// 无切换点 或 UTC 时间早于第一个切换点:使用第一条规则(非夏令时)
if (transitions.empty() || utcTime < transitions.front().utctime)
{
// FIXME: 应使用第一条非夏令时规则
local = &localtimes.front();
}
else
{
// 创建哨兵对象,用于二分查找
Transition sentry(utcTime, 0, 0);
// 二分查找第一个大于 utcTime 的切换点
std::vector<Transition>::const_iterator transI =
std::upper_bound(transitions.begin(), transitions.end(), sentry, CompareUtcTime());
assert(transI != transitions.begin()); // 确保至少有一个切换点
if (transI != transitions.end())
{
--transI; // 回退到小于等于 utcTime 的最后一个切换点
local = &localtimes[transI->localtimeIdx]; // 取对应的本地时间规则
}
else
{
// UTC 时间晚于所有切换点:使用最后一条规则(FIXME: 可读取 TZ 环境变量)
local = &localtimes[transitions.back().localtimeIdx];
}
}
return local;
}
// 根据本地时间查找对应的本地时间规则(处理夏令时切换的歧义)
// @param lt 本地时间;postTransition 夏令时切换时是否使用切换后的规则
const TimeZone::Data::LocalTime* TimeZone::Data::findLocalTime(
const struct DateTime& lt, bool postTransition) const
{
// 将本地时间转换为 epoch 秒数(先假设为 UTC 时间)
const int64_t localtime = fromUtcTime(lt);
// 无切换点 或 本地时间早于第一个切换点:使用第一条规则
if (transitions.empty() || localtime < transitions.front().localtime)
{
// FIXME: 应使用第一条非夏令时规则
return &localtimes.front();
}
// 创建哨兵对象,按本地时间二分查找
Transition sentry(0, localtime, 0);
std::vector<Transition>::const_iterator transI =
std::upper_bound(transitions.begin(), transitions.end(), sentry, CompareLocalTime());
assert(transI != transitions.begin());
if (transI == transitions.end())
{
// 本地时间晚于所有切换点:使用最后一条规则(FIXME: 可读取 TZ 环境变量)
return &localtimes[transitions.back().localtimeIdx];
}
// 获取当前切换点的前一个切换点
Transition prior_trans = *(transI - 1);
// 计算前一个切换点结束前的最后一秒的本地时间
int64_t prior_second = transI->utctime - 1 + localtimes[prior_trans.localtimeIdx].utcOffset;
// 示例数据(含夏令时切换的时间跳变/重复):
// 行 UTC 时间 是否夏令时 偏移量 本地时间(中国) 前一切换点最后一秒的本地时间
// 1 1989-09-16 17:00:00Z 0 8.0 1989-09-17 01:00:00
// 2 1990-04-14 18:00:00Z 1 9.0 1990-04-15 03:00:00 1990-04-15 01:59:59
// 3 1990-09-15 17:00:00Z 0 8.0 1990-09-16 01:00:00 1990-09-16 01:59:59
// 4 1991-04-13 18:00:00Z 1 9.0 1991-04-14 03:00:00 1991-04-14 01:59:59
// 5 1991-09-14 17:00:00Z 0 8.0 1991-09-15 01:00:00
// 场景1:时间跳变(Skip)------ 夏令时开始,本地时间跳过 1 小时
// 输入 1991-04-14 02:30:00,找到第 4 行,
// 前一切换点最后一秒是 1991-04-14 01:59:59 < 输入时间 → 属于跳变区间
if (prior_second < localtime)
{
// 处于时间跳变区间(该本地时间不存在)
// printf("SKIP: prev %ld local %ld start %ld\n", prior_second, localtime, transI->localtime);
if (postTransition)
{
return &localtimes[transI->localtimeIdx]; // 使用切换后的规则
}
else
{
return &localtimes[prior_trans.localtimeIdx]; // 使用切换前的规则
}
}
// 场景2:时间重复(Repeat)------ 夏令时结束,本地时间重复 1 小时
// 输入 1990-09-16 01:30:00,找到第 4 行,回退到第 3 行
--transI;
if (transI != transitions.begin())
{
prior_trans = *(transI - 1);
prior_second = transI->utctime - 1 + localtimes[prior_trans.localtimeIdx].utcOffset;
}
if (localtime <= prior_second)
{
// 处于时间重复区间(该本地时间对应两个 UTC 时间)
// printf("REPEAT: prev %ld local %ld start %ld\n", prior_second, localtime, transI->localtime);
if (postTransition)
{
return &localtimes[transI->localtimeIdx]; // 使用切换后的规则
}
else
{
return &localtimes[prior_trans.localtimeIdx]; // 使用切换前的规则
}
}
// 场景3:无歧义 ------ 本地时间唯一对应一个 UTC 时间
return &localtimes[transI->localtimeIdx];
}
// 静态方法:获取 UTC 时区对象(偏移量 0,无时区切换)
TimeZone TimeZone::UTC()
{
return TimeZone(0, "UTC");
}
// 静态方法:从时区文件加载时区对象
TimeZone TimeZone::loadZoneFile(const char* zonefile)
{
std::unique_ptr<Data> data(new Data); // 创建空的 Data 对象
// 解析时区文件,失败则释放 Data
if (!detail::readTimeZoneFile(zonefile, data.get()))
{
data.reset();
}
return TimeZone(std::move(data)); // 转移所有权,创建 TimeZone 对象
}
// 私有构造函数:从 Data 智能指针创建 TimeZone
TimeZone::TimeZone(std::unique_ptr<Data> data)
: data_(std::move(data)) // unique_ptr 转移到 shared_ptr
{
}
// 构造固定偏移的时区对象(无夏令时)
// @param eastOfUtc 相对于 UTC 的东偏移量(秒);name 时区名称(如 "CST")
TimeZone::TimeZone(int eastOfUtc, const char* name)
: data_(new TimeZone::Data) // 创建 Data 对象
{
data_->addLocalTime(eastOfUtc, false, 0); // 添加固定规则(非夏令时)
data_->abbreviation = name; // 设置时区名称
}
// 将 UTC 时间戳转换为当前时区的本地时间
// @param seconds UTC 时间戳(秒);utcOffset [输出] 可选,返回时区偏移量(秒)
struct DateTime TimeZone::toLocalTime(int64_t seconds, int* utcOffset) const
{
struct DateTime localTime;
assert(data_ != NULL); // 确保时区对象有效
// 查找对应的本地时间规则
const Data::LocalTime* local = data_->findLocalTime(seconds);
if (local)
{
// UTC 时间 + 偏移量 = 本地时间,拆分为 DateTime
localTime = detail::BreakTime(seconds + local->utcOffset);
if (utcOffset)
{
*utcOffset = local->utcOffset; // 输出偏移量
}
}
return localTime;
}
// 将当前时区的本地时间转换为 UTC 时间戳
// @param localtime 本地时间;postTransition 处理夏令时歧义的标志
int64_t TimeZone::fromLocalTime(const struct DateTime& localtime, bool postTransition) const
{
assert(data_ != NULL); // 确保时区对象有效
// 查找对应的本地时间规则
const Data::LocalTime* local = data_->findLocalTime(localtime, postTransition);
// 将本地时间转换为 epoch 秒数(先假设为 UTC 时间)
const int64_t localSeconds = fromUtcTime(localtime);
if (local)
{
// 本地时间 - 偏移量 = UTC 时间
return localSeconds - local->utcOffset;
}
// 降级处理:视为 UTC 时间
return localSeconds;
}
// 静态方法:将 UTC 时间戳转换为 UTC 时间的 DateTime 结构体
DateTime TimeZone::toUtcTime(int64_t secondsSinceEpoch)
{
return detail::BreakTime(secondsSinceEpoch);
}
// 静态方法:将 UTC 时间的 DateTime 结构体转换为 UTC 时间戳
int64_t TimeZone::fromUtcTime(const DateTime& dt)
{
// 转换年月日为儒略日
Date date(dt.year, dt.month, dt.day);
// 计算当天的秒数
int secondsInDay = dt.hour * 3600 + dt.minute * 60 + dt.second;
// 总秒数 = (儒略日 - 1970-01-01 儒略日) * 一天秒数 + 当天秒数
int64_t days = date.julianDayNumber() - Date::kJulianDayOf1970_01_01;
return days * kSecondsPerDay + secondsInDay;
}
// 从 tm 结构体构造 DateTime(修正 tm 结构体的年/月偏移)
DateTime::DateTime(const struct tm& t)
: year(t.tm_year + 1900), month(t.tm_mon + 1), day(t.tm_mday),
hour(t.tm_hour), minute(t.tm_min), second(t.tm_sec)
{
}
// 转换为 ISO 格式字符串(如:"2011-12-31 12:34:56")
string DateTime::toIsoString() const
{
char buf[64];
snprintf(buf, sizeof buf, "%04d-%02d-%02d %02d:%02d:%02d",
year, month, day, hour, minute, second);
return buf;
}
1. 代码整体定位
这份代码是 TimeZone 类的底层实现,核心解决两个问题:
- 解析标准 TZif 格式的时区文件(如
/etc/localtime),提取时区规则(偏移量、夏令时切换点); - 基于这些规则完成「UTC 秒数 ↔ 本地时间(DateTime)」的双向转换,尤其处理夏令时切换时的「跳过时间」「重复时间」等边缘场景;所有实现都遵循 RFC 8536 标准,兼顾兼容性 (支持 TZif v1/v2 版本)和正确性(处理夏令时歧义)。
2. 核心数据结构解析(Pimpl 模式的 Data 结构体)
TimeZone::Data 是时区规则的核心载体,所有时区转换逻辑都基于它展开:
cpp
struct TimeZone::Data
{
// 时区切换点:比如夏令时开始/结束的UTC时间点
struct Transition
{
int64_t utctime; // 切换发生的UTC秒数(Epoch以来)
int64_t localtime; // 切换点对应的本地时间(UTC+偏移量)
int localtimeIdx; // 对应LocalTime的索引
};
// 本地时间规则:某一时间段内的时区属性
struct LocalTime
{
int32_t utcOffset; // UTC东偏移量(秒):比如GMT+8=28800秒
bool isDst; // 是否夏令时(Daylight Saving Time)
int desigIdx; // 时区缩写索引(如CST/CDT)
};
// 辅助方法:添加本地时间规则/切换点
void addLocalTime(int32_t utcOffset, bool isDst, int desigIdx);
void addTransition(int64_t utcTime, int localtimeIdx);
// 核心查找方法:根据UTC/本地时间找对应的LocalTime规则
const LocalTime* findLocalTime(int64_t utcTime) const;
const LocalTime* findLocalTime(const struct DateTime& local, bool postTransition) const;
// 比较器:用于排序/查找Transition
struct CompareUtcTime { /* 按utcTime排序 */ };
struct CompareLocalTime { /* 按localtime排序 */ };
std::vector<Transition> transitions; // 所有时区切换点(按UTC时间排序)
std::vector<LocalTime> localtimes; // 所有本地时间规则
string abbreviation; // 时区缩写(如CST)
string tzstring; // 时区完整字符串(如"GMT+8")
};
核心解释:
Transition:记录「什么时候切换时区规则」------ 比如某年某月某日 UTC 18:00 开始夏令时(偏移从 8h 变 9h);LocalTime:记录「切换后的时区规则」------ 比如偏移量、是否夏令时、时区名;- 两者结合:
transitions是切换点列表,localtimes是规则列表,通过localtimeIdx关联,就能覆盖所有时间段的时区规则。
3. 辅助工具模块(detail 命名空间)
(1)File 类:封装二进制文件读写(线程安全 + 大端转小端)
cpp
class File : noncopyable
{
public:
File(const char* file) : fp_(::fopen(file, "rb")) {}
~File() { if (fp_) ::fclose(fp_); }
// 核心读写方法:处理大端字节序(TZif文件用大端存储)
int64_t readInt64() { int64_t x; ::fread(&x, 1, 8, fp_); return be64toh(x); }
int32_t readInt32() { int32_t x; ::fread(&x, 1, 4, fp_); return be32toh(x); }
// ... 其他读写方法
private:
FILE* fp_;
};
关键细节:
be64toh/be32toh:将 TZif 文件中的大端字节序转换为本地字节序(x86 是小端),保证跨平台兼容;- 二进制读取("rb" 模式):TZif 是二进制格式,必须用二进制模式打开,避免文本模式的换行符转换。
(2)时间拆分工具:fillHMS/BreakTime
cpp
// 秒数(0~86399)拆分为时/分/秒
inline void fillHMS(unsigned seconds, struct DateTime* dt)
{
dt->second = seconds % 60;
unsigned minutes = seconds / 60;
dt->minute = minutes % 60;
dt->hour = minutes / 60;
}
// Epoch秒数 → DateTime(UTC)
DateTime BreakTime(int64_t t)
{
DateTime dt;
int seconds = static_cast<int>(t % kSecondsPerDay); // 当天秒数
int days = static_cast<int>(t / kSecondsPerDay); // 天数
if (seconds < 0) { seconds += kSecondsPerDay; --days; } // 处理负数
fillHMS(seconds, &dt);
// 天数转年月日(复用Date类的儒略日逻辑)
Date date(days + Date::kJulianDayOf1970_01_01);
Date::YearMonthDay ymd = date.yearMonthDay();
dt.year = ymd.year; dt.month = ymd.month; dt.day = ymd.day;
return dt;
}
核心作用:
- 把「Epoch 秒数」这种机器友好的格式,转换为人类可读的「年月日时分秒」(DateTime),是时区转换的基础工具。
4. 核心逻辑:时区文件解析(readTimeZoneFile/readDataBlock)
TZif 是操作系统标准的时区文件格式(如 /etc/localtime),readTimeZoneFile 解析该文件并填充 Data 结构体:
cpp
bool readTimeZoneFile(const char* zonefile, struct TimeZone::Data* data)
{
File f(zonefile);
if (f.valid())
{
try
{
string head = f.readBytes(4);
if (head != "TZif") throw std::logic_error("bad head"); // 验证文件头
string version = f.readBytes(1); // v1/v2版本
// ... 跳过固定长度的头部
if (version == "2") { /* 跳过v1数据,读取v2数据 */ }
else { /* 读取v1数据 */ }
return readDataBlock(f, data, version!="2"); // 解析核心数据块
}
catch (std::logic_error& e) { fprintf(stderr, "%s\n", e.what()); }
}
return false;
}
readDataBlock 核心步骤:
- 读取文件中的计数器(切换点数量、规则数量等);
- 读取所有切换点的 UTC 时间(
trans数组); - 读取每个切换点对应的规则索引(
localtimes数组); - 读取所有本地时间规则(偏移量、是否夏令时、时区名索引);
- 填充
Data的transitions和localtimes数组; - 读取时区缩写和描述字符串。
5. 核心查找逻辑:findLocalTime(处理夏令时)
这是时区转换的核心算法,分两个重载:
(1)UTC 时间 → 本地时间规则(findLocalTime (int64_t utcTime))
cpp
const TimeZone::Data::LocalTime* TimeZone::Data::findLocalTime(int64_t utcTime) const
{
if (transitions.empty() || utcTime < transitions.front().utctime)
{
return &localtimes.front(); // 无切换点/早于第一个切换点:用默认规则
}
else
{
// 用upper_bound找第一个大于utcTime的切换点,取前一个就是当前规则
Transition sentry(utcTime, 0, 0);
auto transI = std::upper_bound(transitions.begin(), transitions.end(), sentry, CompareUtcTime());
--transI;
return &localtimes[transI->localtimeIdx];
}
}
例子:
- 切换点列表:[1989-09-16 17:00:00Z(GMT+8), 1990-04-14 18:00:00Z(GMT+9)];
- 输入 UTC 时间 1990-06-01 00:00:00Z → upper_bound 找到 1990-09-15 17:00:00Z,取前一个(1990-04-14 18:00:00Z)→ 用 GMT+9 规则。
(2)本地时间 → UTC 时间规则(处理夏令时歧义)
夏令时切换时会出现「重复时间」(比如凌晨 2 点回拨 1 小时,2:30 出现两次)或「跳过时间」(比如凌晨 2 点拨快 1 小时,2:30 不存在),这个重载通过 postTransition 参数处理:
cpp
const TimeZone::Data::LocalTime* TimeZone::Data::findLocalTime(
const struct DateTime& lt, bool postTransition) const
{
// 1. 本地时间转Epoch秒数(假设UTC)
const int64_t localtime = fromUtcTime(lt);
// 2. 找本地时间对应的切换点
Transition sentry(0, localtime, 0);
auto transI = std::upper_bound(transitions.begin(), transitions.end(), sentry, CompareLocalTime());
// 3. 判断是「跳过时间」还是「重复时间」,根据postTransition返回对应规则
// ... 核心逻辑:略
}
关键参数:
postTransition=true:取切换后的规则(比如重复时间取第二次出现的);postTransition=false:取切换前的规则(默认)。
6. 核心接口实现:toLocalTime/fromLocalTime
cpp
// UTC秒数 → 本地时间(DateTime)
struct DateTime TimeZone::toLocalTime(int64_t seconds, int* utcOffset) const
{
const Data::LocalTime* local = data_->findLocalTime(seconds);
DateTime localTime = detail::BreakTime(seconds + local->utcOffset); // UTC+偏移=本地时间
if (utcOffset) *utcOffset = local->utcOffset;
return localTime;
}
// 本地时间 → UTC秒数
int64_t TimeZone::fromLocalTime(const struct DateTime& localtime, bool postTransition) const
{
const Data::LocalTime* local = data_->findLocalTime(localtime, postTransition);
const int64_t localSeconds = fromUtcTime(localtime);
return localSeconds - local->utcOffset; // 本地时间-偏移=UTC时间
}
核心逻辑:
toLocalTime:UTC 秒数 + 偏移量 = 本地秒数 → 拆分为 DateTime;fromLocalTime:本地秒数 - 偏移量 = UTC 秒数(关键是找到正确的偏移量)。
7. 常用时区构造:UTC ()/China ()
cpp
TimeZone TimeZone::UTC() { return TimeZone(0, "UTC"); }
TimeZone::TimeZone(int eastOfUtc, const char* name)
: data_(new TimeZone::Data)
{
data_->addLocalTime(eastOfUtc, false, 0); // 固定偏移,无夏令时
data_->abbreviation = name;
}
中国时区 :TimeZone::China() 本质是 TimeZone(8*3600, "CST")(GMT+8,无夏令时),无需解析时区文件,直接用固定偏移,效率更高。
总结
- 数据结构设计 :用
Transition(切换点)+LocalTime(规则)封装时区规则,兼顾简单时区(固定偏移)和复杂时区(夏令时); - 文件解析:遵循 RFC 8536 解析 TZif 格式,处理大端字节序,兼容 v1/v2 版本;
- 核心算法 :用
upper_bound高效查找切换点,处理夏令时的「跳过 / 重复时间」歧义; - 性能优化:固定偏移时区(如中国)无需解析文件,直接用内存规则,效率拉满;
- 复用设计 :复用
Date类的儒略日逻辑完成「秒数→年月日」转换,避免重复造轮子。