LongLong's Blog

分享IT技术,分享生活感悟,热爱摄影,热爱航天。

Zip文件格式解析

Zip是一种非常常见的压缩格式,其可以将多个文件打包并压缩为一个zip文件,同时Android所使用的apk包就是一个zip格式的压缩文件。这里我们尝试在不解压缩zip文件的情况下对zip包中的一部分内容进行修改。

1. Zip文件格式

Zip文件是将多个文件按照顺序进行排列,每个文件包括文件头和文件内容,其结构如下图所示

其中每个文件头部分包括签名(固定值0x04034b50),解包需要的Zip版本,通用标志位,压缩方法,文件最后修改时间,文件最后修改日期,CRC-32校验值,压缩后的文件大小,压缩前的文件大小,文件名长度,扩展域长度,文件名,扩展域。其中获取一些文件头部分的代码如下所示

<?php
$file = fopen($argv[1], 'r');

//signature
fseek($file, 0);
$data = fread($file, 4);

//Compressed size
fseek($file, 18);
$data = fread($file, 4);
$size1 = unpack('i', $data);

//Uncompressed size
fseek($file, 22);
$data = fread($file, 4);
$size2 = unpack('i', $data);

//File last modification time
fseek($file, 10);
$data = fread($file, 2);

//File last modification date
fseek($file, 12);
$data = fread($file, 2);

//File name length
fseek($file, 26);
$data = fread($file, 2);
$len1 = unpack('s', $data);

//File name
fseek($file, 30);
$data = fread($file, $len1[1]);

2. MS-DOS时间日期解析

Zip文件中文件头对应的头部分中的最后修改时间和日期采用了MS-DOS时间日期格式进行保存,其使用了4个字节的各个位段保存了年月日和时分秒这6个数值。

其中前两个字节保存时间,解析为一个16位整数,其中0~4位为秒数除以2,5~10位为分钟,11~15位为小时

后两个字节保存日期,解析为一个16位整数,其中0~4位为日,5~8位为月,9~15位年份减去1980

其解析的代码如下

<?php
//...
//File last modification time
fseek($file, 10);
$data = fread($file, 2);
$v = unpack('s', $data);
$h = ($v[1] & (0b11111 << 11)) >> 11;
$i = ($v[1] & (0b111111 << 5)) >> 5;
$s = ($v[1] & (0b11111)) * 2;

//File last modification date
fseek($file, 12);
$data = fread($file, 2);
$v = unpack('S', $data);
$y = (($v[1] & (0b1111111) << 9) >> 9) + 1980;
$m = ($v[1] & (0b1111) << 5) >> 5;
$d = ($v[1] & (0b11111));

其编码的代码如下

<?php
$h = date('G');
$i = date('i');
$s = date('s');
$v1 = pack('s', ($h << 11) + ($i << 5) + $s);

$y = date('Y') - 1980;
$m = date('n');
$d = date('j');
$v2 = pack('s', ($y << 9) + ($m << 5) + $d);

$data = $v1 . $v2;

3. 使用实例

在Nginx向用户发送Zip文件时,使用Lua的body filter修改Zip文件中第一个文件的最后修改时间为当前时间,其中需要使用到Lua的struct模块

    location ~ \.zip$ {
        #记录当前chunked号
        set $c "0";
        header_filter_by_lua_block { 
            ngx.header.content_length = nil;
        }
        body_filter_by_lua_block {
            --替换第一个文件的最后修改时间
            if ngx.var.c == "0" then
                --加载struct模块
                local struct = require("struct");
                --获取当前时间
                local t = os.date("*t");
                --生成MS-DOS格式的时间日期
                local v2 = struct.pack("I2", (t["year"] - 1980) * 2^9 + t["month"] * 2^5 + t["day"]);
                local v1 = struct.pack("I2", t["hour"] * 2^11 + t["min"] * 2^5 + math.floor(t["sec"] / 2));
                ngx.arg[1] = string.sub(ngx.arg[1], 1, 10) .. v1 .. v2 .. string.sub(ngx.arg[1], 15, -1);
            end
            ngx.var.c = ngx.var.c + 1;
        }
    }

如此就能够动态的修改Zip包中的一些内容而不需要每次都重新打包,能够较大程度提高Web服务器的效率。

4. 补充

以上的配置中取消了Content-Length头,会导致不能够支持断点续传,为了能够继续支持断点续传需要对请求中的Range头进行简要的分析,并做出对应的处理,同时替换文件尾部的一段随机注释字符串,处理后的配置如下

#将加载Lua模块放在Nginx初始化阶段
init_by_lua_block {
   struct = require("struct");
}

server {
    #....
    location ~ \.zip$ {
        set $c "0";
        body_filter_by_lua_block {
            --替换第一个文件的最后修改时间
            if ngx.var.c == "0" then
                --设置Range的默认起止位置
                local s1 = 0;
                local s2 = ngx.header.content_length;
                if ngx.var.http_range then
                    --分析Range头
                    local m = ngx.re.match(ngx.var.http_range, "bytes=(\\d+)-(\\d+)?");
                    --如果匹配成功则更新起止位置
                    if m then
                        s1 = tonumber(m[1]);
                        if m[2] then
                            s2 = tonumber(m[2]);
                        end
                    end
                end
                --需要更新的时间值在当前的范围内时,则进行修改
                if s1 < 10 and s2 > 15 then
                    local t = os.date("*t");
                    local v2 = struct.pack("I2", (t["year"] - 1980) * 2^9 + t["month"] * 2^5 + t["day"]);
                    local v1 = struct.pack("I2", t["hour"] * 2^11 + t["min"] * 2^5 + math.floor(t["sec"] / 2));
                    --调整修改的位置
                    ngx.arg[1] = string.sub(ngx.arg[1], 1, 10 - s1) .. v1 .. v2 .. string.sub(ngx.arg[1], 15 - s1, -1);
                end
            end
            --替换文件尾部的一段随机注释字符串(需要源文件尾部有一段32字节长度的注释字符)
            if ngx.arg[2] == true then
                if ngx.header.content_range then
                    local m = ngx.re.match(ngx.header.content_range, "bytes (\\d+)-(\\d+)/(\\d+)");
                    if m then
                        local m2 = tonumber(m[2]);
                        local m1 = m2 - string.len(ngx.arg[1]) + 1;
                        local m3 = tonumber(m[3]);
                        if m2 >= m3 - 32 then
                            local s = ngx.md5(math.random());
                            if m1 >= m3 - 32 then
                                ngx.arg[1] = string.sub(s, 1, m2 - m1 + 1);
                            else
                                ngx.arg[1] = string.sub(ngx.arg[1], 1, -1 - (33 + m2 - m3)) .. string.sub(s, 1, m2 - m3);
                            end
                        end
                    end
                else
                    ngx.arg[1] = string.sub(ngx.arg[1], 1, -33) .. ngx.md5(math.random());
                end
            end
            ngx.var.c = ngx.var.c + 1;
        }
    }
}

实现Zabbix的高可用

Zabbix作为一个分布式的监控系统,相比Nagios其较大的优点在于设置和数据保存在数据库中,便于维护和分析,同时自身或是通过grafana还可以实现监控项目曲线图的绘制。可以完成以前使用Cacti+Nagios来实现的系统监控任务,相关的监控主机配置只需要保存一份即可,带来了很大的便利。

Zabbix和Nagios的监控方式较为类似,其包括Zabbix-server和Zabbix-agent,对应Nagios中的Nagios和Nrpe,但Zabbix可以支持被监控的主机主动向Zabbix-server提交需要监控项目的相关数据,如此可以很大程度减少Zabbix-server所在的服务器在数据采集时的工作量,以之前的经验在Cacti+Nagios采集数据的时间点,监控服务器的负载会非常高,同时大量读写RRD文件也会成为系统之后的瓶颈,同时监控一两百台服务器就已经力不从心了。

1. Zabbix的高可用问题

对于一个监控系统都存在的一个问题就是如何来监控自己,如果自身发生故障会导致全部监控实效,并且也无法发送报警消息。这就需要实现Zabbix系统的高可用,至少在监控系统出现故障时能够及时发送报警消息。一个比较简单的想法就是通过其他的脚本来监控Zabbix的状态,但这并不是一个很好的做法,相当于又做了一套新的监控系统,而且同样存在如何自监控的问题。

2. Zabbix的系统组成

首先分析一下Zabbix的系统组成,其包括负责处理监控数据的zabbix-server进程,另外还包括一个用来做Zabbix相关配置的Web前端服务和一个用来保存配置和监控数据的MySQL数据库,在被监控的主机上还存在一个用来执行监控逻辑和发送监控信息的zabbix-agentd进程。

对于一个监控项目,监控的流程为zabbix-server向zabbix-agentd发送要获取的监控数据,如果为被动监控,则zabbix-server通过调用zabbix-agentd中提供的相关监控函数获取到需要的监控项目数据并保存到数据库,如果为主动监控,则zabbix-server通知zabbix-agentd需要的数据项目和提交的时间间隔,然后由zabbix-agentd按照要求主动提交监控数据。

3. Zabbix的高可用实现

其中需要做到高可用的几个过程分别为,zabbix-server读取数据库,zabbix-agentd向zabbix-server提交数据。这里使用的方法是让zabbix-server通过本机安装的Haproxy来连接数据库,同时zabbix-agentd则通过本机的Haproxy来向zabbix-server提交数据。整个系统的结构设计入图所示

使用两台服务器来安装Zabbix监控服务,如图中左右两个虚线方框中所示,其中包括配置使用的Web前端,Zabbix-server,Haproxy,MySQL,Haproxy中配置MySQL的后端分别指向两台服务器上的MySQL,两个MySQL服务通过主从复制保持数据的一致性,Haproxy中设置Zabbix-server的后端分别指向两台服务器上的Zabbix-server,另外需要监控的主机上安装Haproxy设置Zabbix-server后端为上述的两个Zabbix-server,其Zabbix-agent通过这个Haproxy来向Zabbix-server提交数据。

如此任何一个Zabbix-server服务器出现问题,都可以通过Haproxy切换到另外一个Zabbix-server服务器上,实现监控服务的高可用。同时如此配置后建议监控项目都使用主动模式,因为如果配置为Zabbix-server通过zabbix-agentd获取,则两个Zabbix-server会采集两次数据,写入时可能会产生一些冲突。

4. 相关配置

Haproxy的配置,在Haproxy的配置文件haproxy.cfg中增加以下配置

#两个MySQL后端,这里设置其中的从库为backup,防止两个库同时写入导致数据的冲突
listen mysql
	bind 127.0.0.1:33061
	mode tcp
	server z1 192.168.1.101:3306
	server z2 192.168.1.102:3306 backup

#Zabbix-server后端
listen zabbix
	bind 127.0.0.1:10061
	mode tcp
	server z1 192.168.1.101:10051
	server z2 192.168.1.102:10051 backup

Zabbix-agent的配置,在配置中将连接的Zabbix-server指向本机的Haproxy

#修改ServerActive
ServerActive=127.0.0.1:10061

另外为了方便Zabbix-agent配置的管理,建议将Hostname参数单独写到一个文件中(这个参数是Zabbix配置中配置的对应该主机的名字),如/etc/zabbix/zabbix_agentd.d/hostname.conf,这样除了这一个文件以外,全部需要监控的主机的Zabbix-agent配置都是相同的。

最后将Web前端的配置也进行修改如下,即使连接的过程都通过Haproxy来实现

<?php
// Zabbix GUI configuration file.
global $DB;

$DB['TYPE']     = 'MYSQL';
$DB['SERVER']   = '127.0.0.1';
$DB['PORT']     = '33061';
$DB['DATABASE'] = 'zabbix';
$DB['USER']     = 'zabbix';
$DB['PASSWORD'] = 'zabbix';

// Schema name. Used for IBM DB2 and PostgreSQL.
$DB['SCHEMA'] = '';

$ZBX_SERVER      = '127.0.0.1';
$ZBX_SERVER_PORT = '10061';
$ZBX_SERVER_NAME = '';

$IMAGE_FORMAT_DEFAULT = IMAGE_FORMAT_PNG;