HTTP协议中,有很多机制都和Time有关,这里先撇开缓存控制及过期机制,就拿Date开刀。
Date的格式大致如下:
Tue, 21 Jul 2015 21:19:41 GMT
好,现在问题就来了,如何得到这个字符串?先别急,本文将会对Linux下Time的格式化以及转换做一下解读。
0x00 一切的起点——time_t
从某个纪元开始time_t这个概念就存在了,这里不谈什么历史故事,感兴趣的可以自行wiki。获取time_t值可以通过如下的函数获得:
time_t time(time_t *t);
我们可以如此使用:
time_t t;
time(&t);
当然也可以这么使用:
time_t t = time(NULL);
至于使用何种方式,就得看你是否需要指定值的存储位置了,这里并不涉及什么线程安全的问题,大家大可放心。
tip: linux下线程安全的函数大多会采用_r结尾的方式,比如sterror_r。
其实,单纯一个值并没有什么作用,我们只是将time_t的值作为衡量时间的基准,当涉及时间比较等计算问题时,它的作用就会凸显。
0x01 格式化的基础——tm
tm是什么?简单来说tm是time_t和格式化的时间字符串之间的一种中间表示,有了这个中间表示,我们才能按需定制时间字符串的格式。那么首先来看下tm结构体的声明:
struct tm {
int tm_sec; /* Seconds (0-60) */
int tm_min; /* Minutes (0-59) */
int tm_hour; /* Hours (0-23) */
int tm_mday; /* Day of the month (1-31) */
int tm_mon; /* Month (0-11) */
int tm_year; /* Year - 1900 */
int tm_wday; /* Day of the week (0-6, Sunday = 0) */
int tm_yday; /* Day in the year (0-365, 1 Jan = 0) */
int tm_isdst; /* Daylight saving time */
};
想必现在大家应该明白为何tm是中间表示了吧,它将time_t从一个单纯的数值转换成了包含各类详细信息的结构,这些信息包含时分秒、日月年、星期等。
那么,应该如何得到tm结构?且看如下函数声明:
struct tm *localtime_r(const time_t *timep, struct tm *result);
struct tm *gmtime_r(const time_t *timep, struct tm *result);
如上给出的是线程安全版本的函数,分别获得本地时间和GM时间,两者的差距在于timezone的不同,在处理local和gm时间的转换之前,不需要关注这个问题。
既然time_t能够转换到tm,想必应该也有个方法能够给转回去。确实有这么个函数,声明如下:
time_t mktime(struct tm *tm);
0x02 时间的格式化——strftime
本节就是关于如何得到格式化时间的内容了,这里我们考虑如何将tm格式化成Date中所使用的格式,采用的如下函数:
size_t strftime(char *s, size_t max, const char *format, const struct tm *tm);
函数参数中,s表示的是用来存放字符串的位置的指针,max为最大长度,它和format有关,format为格式化的规则字符串。函数的返回值代表字符串的实际长度,注意不包括‘0’,因为平常使用的时候,基本都能够估计好具体的长度,因此这个返回值似乎也只是用来判断错误了。
让我们通过一个例子来学习使用时间的格式化——获取Date中的gm时间字符串:
time_t current_time = time(NULL);
struct tm current_tm;
gmtime_r(¤t_time, ¤t_tm);
// 此格式为通用的Date标准格式之一
char* format = "%a, %d %b %Y %H:%M:%S GMT";
// 注意为‘\0’预留一个坑
char time_str[30];
strftime(time_str, 30, format, ¤t_tm);
// 判断是否出错,此处略
这里的format内容可以参照manual,内容比较多,但是应该不难理解,在此不再复述。
0x03 时间字符串的解析——strptime
前文既然谈到了过期机制,那么必然涉及到时间字符串的解析,毕竟直接拿类似Date中的格式来处理是很不现实的,我们必须将其转换到time_t。好在,大家并不需要自己编写解析函数,已经有现成的了,如下为strptime函数的声明:
char *strptime(const char *s, const char *format, struct tm *tm);
和strftime差不多,只不过少了个最大长度,它就是strftime的逆过程,但是返回值需要注意一下,当解析出错的时候,返回的是NULL。
现在我们来将上一个例子中的字符串给转到原始的time_t:
struct tm res_tm;
char* format = "%a, %d %b %Y %H:%M:%S GMT";
strptime(time_str, format, &res_tm);
// 判断是否出错,此处略
time_t res_time = mktime(&res_tm);
通过上述步骤,我们就简单地将字符串转成了time_t,很方便,不是吗?
0x04 local和gm的转换
最后一部分,来点干货,谈谈local和gm之间的转换,这部分需求的解决方案可不好找呐。
造成local和gm时间不一致的原始是因为时区,因此,起初我想总有函数是处理时区offset的吧,但是很遗憾,没找到。
然后,我在tm的结构中发现了tm_gmtoff,然后尝试通过设置这个值来转换,但是这并没有什么卵用。。。
走投无路了,干脆自己来处理偏移吧,思想是同一个time_t能够分别得到gm和local,且tm能够转换成time_t,那么我们就能够精确地得到这个偏移。
如下为取得这个偏移的代码:
time_t local = time_t(NULL);
struct tm gmtm;
gmtime(&local, &gmtm);
time_t gmt = mktime(&gmtm);
const time_t OFFSET = gmt - local;