星期二, 十二月 30, 2008

Python Note: struct for binary protocol

roczhou

30 Dec 2008 ChangeLog:

  • 30 Dec 2008, roczhou, 创建文档

最近在做一个项目的时候,使用了基于文本的 JSON 协议,总的来说,效率还可以。不过后来在做压力测试时,因为协议使用 UDP,因此会有大报文分片的情况,所以服务端只能基于 IP 分配任务,但因为最初没有找到虚拟大量客户端的有效方法(后来通过虚拟 IP 加 iptables 实现),故当时出现的一个情况就是大量请求只压在了一个任务线程(Task Thread)上,服务端不能完全压满。

因此最初提出了一个将协议头改成二进制的方法,这样前端接收线程可以在收到报文后进行一个比较快速的解析,并将内容分配给正确的任务线程。但此时客 户端使用的是 Python 实现,而服务器端使用的是 C/C++ 实现,为了实现这种二进制协议,需要使用 Python 的 struct 模块来进行转换。例子如下:

Python 端:

  #!/usr/bin/env python
# -*- encoding: utf-8 -*-

__author__ = "roczhou"
__date__ = "11 Dec 2008"
__version__ = "0.3"

import time
import struct

headobj = {
"V" : 1,
"C" : 0x01,
"S" : 10000,
"R" : 0,
# "T" : int(time.time()),
"T" : 1111111111,
"c" : 1,
"i" : 0,
"L" : 32,
}

values = []
for k in ["V", "C", "S", "R", "T", "c", "i", "L"]:
values.append(headobj[k])
print "SIZE", struct.calcsize("!2BHIQ2IH")
open("/tmp/head.bin", 'wb').write(struct.pack("!2BHIQ2IH", *values))

C 端:

  #include 
#include
#include

struct _head {
uint8_t version;
uint8_t command;
uint16_t sequence;
uint32_t reserved;
uint64_t timestamp;
uint32_t count;
uint32_t index;
uint16_t length;
} typedef head;

int main(int argc, char *argv[]) {
char buffer[1024];
FILE *fp = fopen("/tmp/head.bin", "rb");
if(fp == NULL) {
printf("File /tmp/head.bin does not exists\n");
return 1;
}
// size_t len = fread(buffer, sizeof(head), 1024, fp);
size_t len = fread(buffer, 1, 1024, fp);
head *hd = (head *)buffer;
printf("version: %d\n", hd->version);
printf("command: %d\n", hd->command);
printf("sequence: %d\n", ntohs(hd->sequence));
printf("reserved: %d\n", ntohl(hd->reserved));
printf("timestamp: %d\n", ntohl(hd->timestamp));
printf("count: %d\n", ntohl(hd->count));
printf("index: %d\n", ntohl(hd->index));
printf("length: %d\n", ntohs(hd->length));
printf("LEN: %d\n", len)
return 0;
};

对于二进制协议,第一是要保证每个字段(元素)的长度固定不变,第二是要保证各个字段(元素)的顺序固定不必。

关于 Python struct 的用法,可以参考官方文档,最重要的是保证格式串所表示的各个字段(元素)的字节长一致,列表如下:

  B, [unsigned] char, 1 bytes
H, [unsigned] short, 2 bytes
I, [unsigned] int, 4 bytes
Q, [unsigned] long long, 8 bytes

l/L(long) 和 i/L(int) 的字节长度一样?

  >>> import struct
>>> struct.calcsize("!i")
4
>>> struct.calcsize("!l")
4

struct.calcsize(format) 会计算这个格式表示的字长,这个字节长必须和 C/C++ 的 sizeof(struct) 所计算出的长度一样。在上面的 C 代码中,最后也打印出了这个长度 LEN。

使用二进制协议另一个问题是字节序。不同的操作系统平台使用的字节序可能不一样,例如 Linux 和 Solaris。使用网络序一般不会有什么问题,所以在 Python 端的 format 使用了 ! 表示使用网络序,而 C 端使用 ntohs/ntohl 表示从网络序转换成整型和长整形,否则在 Linux 和 Solaris 下得到的结果会不同。

星期五, 十二月 26, 2008

自动化文档管理方案

基本思路

  1. 使用简单的 t2t 标记进行文档编写
  2. 使用 subversion 对这些文档进行版本控制
  3. 使用 GNU Make 实现自动化管理
  4. 文档编写之后使用 txt2tags 进行文档转换为其它格式(通过调用相应 Makefile target 实现)
  5. 使用 mutt/msmtp 自动发送转换后文本到某个邮件列表(相应 make target)
  6. 自动同步到在线文档系统?
  7. 自动作图?(目前可用 dia)
  8. 上传图片?
  9. 对项目,自动生成站点层级页面?

目前已实现前面五点,后续功能方案研究中...

我使用的系统环境为

  $ uname
CYGWIN_NT-5.1

所以任何 Linux/UNIX 系统都是合适的。

略去部分

  • txt2tags 比较简单,参考官方文档大概 <20min>
  • subversion 使用广泛,在此也不赘述

mutt/msmtp

mutt/msmtp 在 Cygwin 似乎不太稳定,但基本可以使用:

  mutt-1.4.2.2-2
msmtp-1.4.13-1

mutt 是 MUA,它需要一个 MTA 来为它发送邮件,默认情况下它会使用 sendmail 或 postfix 的 sendmail 命令,但安装和配置一个 sendmail/postfix 太麻烦了,对于这种小应用不合适,所以使用 msmtp,它是一个轻量级的 MTA。

因为只需要在命令行调用 mutt,所以不需要进行太复杂的设置,编写 mutt 和 msmtp 相应的配置文件如下:

  sh$ cat ~/.mutt/private.muttrc
# SMTP
set sendmail="/usr/bin/msmtp -f someone@gmail.com"

sh$ cat ~/.msmtprc
account private
host smtp.gmail.com
port 587
protocol smtp
auth on
from someone@gmail.com
user someone@gmail.com
password "********"
tls on
tls_starttls on
tls_certcheck off

~/.mutt/private.muttrc 指明了使用 msmtp 及其参数,-f 即 .msmtprc 中的 from 内容,用这个来标识要使用哪个账号来发送邮件,因为我们可能要使用多个账号发不同的邮件,比如对工作的内容要使用另一个账号,这也是为什么没有使用标准的 ~/.muttrc 或 ~/.mutt/muttrc 作为 mutt 配置的原因,下面会将到如何使用其他账号。

可以先尝试一下是否发送会成功:

  sh$ echo "testing mutt..." | mutt -s "Mutt" -F ~/.mutt/private.muttrc $mail_address

到另一个邮箱 $mail_address 看看是否确实收到了邮件。

将工作时使用的邮箱加入 ~/.msmtprc 后如下:

  $ cat ~/.msmtprc
account private
host smtp.gmail.com
port 587
protocol smtp
auth on
from someone@gmail.com
user someone@gmail.com
password "********"
tls on
tls_starttls on
tls_certcheck off

account default
host ssl.alibaba-inc.com
port 465
protocol smtp
auth on
from someone@company.com
user someone
password "********"
tls on
tls_starttls off
tls_certcheck off
# tls_force_sslv3 on

注意公司的邮箱使用 ironport,参数 tls_starttls off 与 gmail 邮箱的不同,为了使用这个设置,需要另一个 mutt 配置文件:

  sh$ cat ~/.mutt/work.muttrc
# Header
my_hdr From: someone@company.com
# SMTP
set sendmail="/usr/bin/msmtp -f someone@company.com"

因为公司账号没有域名后缀,所以发出去的邮件 header 部分将没有域名部分,故在 ~/.mutt/work.muttrc 中增加 my_hdr 让 mutt 帮补上。

可再用前述方法试验一下是否能够正确发送邮件。

Makefile

邮件客户端配置成功后,就可以在文档目录下编写一个 Makefile。此时正确的目录结构很重要,方便我们进行管理:

  docs/
index.t2t, 用来生成结构化文档
*.t2t, 生成单独文档到 html/ 和 text/ 下
html/*.html, 转换后的 html 文件
text/*.txt, 转换后的 txt文件
_mail/work, 使用 work 邮箱时的时间戳文件
_mail/private, 使用私有邮箱时的时间戳文件
_release, 发布文档列表,只有在这个列表中的文件发生变更后才会发送邮件和进行在线同步
Makefile -> ../Makefile, 可制成符号链接到父目录 Makefile,这样可以用同一个 Makefile 管理大量分类文档

Makefile 内容如下:

  SA_MAIL := sa@list.company.com

html: *.t2t
for f in $?; do fn=`echo $$f | awk -F. '{print $$1}'` && txt2tags -t html -o html/$$fn.html $$f; done

txt: *.t2t
for f in $?; do fn=`echo $$f | awk -F. '{print $$1}'` && txt2tags -t txt -o text/$$fn.txt $$f; done

_mail/work: *.t2t
for f in $?; do \
grep $$f _release >/dev/null && \
( \
echo "mail $$f ..."; \
fn=`echo $$f | awk -F. '{print $$1}'`; \
title=`sed -n '1p' $$f`; \
cat text/$$fn.txt | mutt -F ~/.mutt/work.muttrc -s "$$title" -a html/$$fn.html $(SA_MAIL); \
) || \
echo "skip $$f ..."; \
done
touch _mail/work

当运行make htmlmake txt这两个 target 时就会分别在 html/ 或 text/ 下生成转换文档,运行make _mail/work后会用工作邮箱发送邮件到邮件列表 SA_MAIL,邮件内容为生成 txt 内容,附件为生成 html 文件。

星期二, 十二月 09, 2008

利用 IFS 变量在 Bash 中进行行处理

有一个文件
$ cat services.txt
锘?name script(basename) args("") mask(1/2) script_type(1/2[d]) dss
#step = "300"
#rra = "RRA:MIN:0.5:1:2016 RRA:MIN:0.5:6:8640 RRA:MIN:0.5:36:2920 RRA:MIN:0.5:288:1825 RRA:AVERAGE:0.5:1:2016 RRA:AVERAGE:0.5:6:8640 RRA:AVERAGE:0.5:36:2920 RRA:AVERAGE:0.5:288:1825 RRA:MAX:0.5:1:2016 RRA:MAX:0.5:6:8640 RRA:MAX:0.5:36:2920 RRA:MAX:0.5:288:1825"

disk adapter "-t alarm /home/testenv/script/check_disk -l -w 10% -c 5% -e" 1 2
inode adapter "-t alarm /home/testenv/script/check_disk -l -W 15% -K 10% -e" 1 2
load adapter "-t alarm /home/testenv/script/check_load -w 4,5,6 -c 6,7,8" 1 2
#load adapter "-t state /home/testenv/script/check_load -w 4,5,6 -c 6,7,8" 2 2
crond adapter "-t alarm /home/testenv/script/check_procs -w 1:1 -c 1:3 -C crond" 1 2
portmap adapter "-t alarm /home/testenv/script/check_procs -w 1:1 -c 1:3 -C portmap" 1 2
syslog adapter "-t alarm /home/testenv/script/check_procs -w 1:1 -c 1:3 -C syslogd" 1 2
snmpd adapter "-t alarm /home/testenv/script/check_procs -w 1:1 -c 1:3 -C snmpd" 1 2
sshd adapter "-t alarm /home/testenv/script/check_procs -w 1:1 -c 1:3 -C sshd" 1 2
#cpu
#mem
#disksp
#diskio
#iftraffic
#ifpackage

现在我需要按行处理,将每一行的结果进行重组并作为参数去调用另一个脚本。所以首先我需要得到每一行的结果,按照以前的做法,我通常是使用 while 循环来做,因为使用 for 得到的结果不对:
$ for line in $(grep -Pv '^$|^#' services.txt);do echo $line;done
锘?name
script(basename)
args("")
mask(1/2)
script_type(1/2[d])
dss
disk
adapter
"-t
alarm
/home/testenv/script/check_disk
-l
-w
10%
-c
5%
-e"
1
2
inode
adapter
"-t
alarm
/home/testenv/script/check_disk
-l
-W
15%
-K
10%
-e"
1
2
load
adapter
"-t
alarm
/home/testenv/script/check_load
-w
4,5,6
-c
6,7,8"
1
2
crond
adapter
"-t
alarm
/home/testenv/script/check_procs
-w
1:1
-c
1:3
-C
crond"
1
2
portmap
adapter
"-t
alarm
/home/testenv/script/check_procs
-w
1:1
-c
1:3
-C
portmap"
1
2
syslog
adapter
"-t
alarm
/home/testenv/script/check_procs
-w
1:1
-c
1:3
-C
syslogd"
1
2
snmpd
adapter
"-t
alarm
/home/testenv/script/check_procs
-w
1:1
-c
1:3
-C
snmpd"
1
2
sshd
adapter
"-t
alarm
/home/testenv/script/check_procs
-w
1:1
-c
1:3
-C
sshd"
1
2

得到的不是每一行的输出!

而使用 while 结果如下:
$ grep -Pv '^$|^#' services.txt | while read line; do echo $line; done
锘? ame script(base ame) args("") mask(1/2) script_type(1/2[d]) dss
disk adapter "-t alarm /home/teste v/script/check_disk -l -w 10% -c 5% -e" 1 2
i ode adapter "-t alarm /home/teste v/script/check_disk -l -W 15% -K 10% -e" 1 2
load adapter "-t alarm /home/teste v/script/check_load -w 4,5,6 -c 6,7,8" 1 2
cro d adapter "-t alarm /home/teste v/script/check_procs -w 1:1 -c 1:3 -C cro d" 1 2
portmap adapter "-t alarm /home/teste v/script/check_procs -w 1:1 -c 1:3 -C portmap" 1 2
syslog adapter "-t alarm /home/teste v/script/check_procs -w 1:1 -c 1:3 -C syslogd" 1 2
s mpd adapter "-t alarm /home/teste v/script/check_procs -w 1:1 -c 1:3 -C s mpd" 1 2
sshd adapter "-t alarm /home/teste v/script/check_procs -w 1:1 -c 1:3 -C sshd" 1 2


但是使用 while 循环会启动一个子 Shell,如果我要在循环后再进行其他操作则会有问题,因为此时 $line 已经不可用了,于是可以用 IFS 来做 for 循环,因为 for 循环不会在子 Shell 中进行:
$ IFS=$'\n' && for line in $(grep -Pv '^$|^#' services.txt);do echo $line;done
锘?name script(basename) args("") mask(1/2) script_type(1/2[d]) dss
disk adapter "-t alarm /home/testenv/script/check_disk -l -w 10% -c 5% -e" 1 2
inode adapter "-t alarm /home/testenv/script/check_disk -l -W 15% -K 10% -e" 1 2
load adapter "-t alarm /home/testenv/script/check_load -w 4,5,6 -c 6,7,8" 1 2
crond adapter "-t alarm /home/testenv/script/check_procs -w 1:1 -c 1:3 -C crond" 1 2
portmap adapter "-t alarm /home/testenv/script/check_procs -w 1:1 -c 1:3 -C portmap" 1 2
syslog adapter "-t alarm /home/testenv/script/check_procs -w 1:1 -c 1:3 -C syslogd" 1 2
snmpd adapter "-t alarm /home/testenv/script/check_procs -w 1:1 -c 1:3 -C snmpd" 1 2
sshd adapter "-t alarm /home/testenv/script/check_procs -w 1:1 -c 1:3 -C sshd" 1 2


注意这个地方必须使用单引号,使用双引号的结果不对:
$ IFS=$"\n" && for line in $(grep -Pv '^$|^#' services.txt);do echo $line;done
锘?
ame script(base
ame) args("") mask(1/2) script_type(1/2[d]) dss
disk adapter "-t alarm /home/teste
v/script/check_disk -l -w 10% -c 5% -e" 1 2
i
ode adapter "-t alarm /home/teste
v/script/check_disk -l -W 15% -K 10% -e" 1 2
load adapter "-t alarm /home/teste
v/script/check_load -w 4,5,6 -c 6,7,8" 1 2
cro
d adapter "-t alarm /home/teste
v/script/check_procs -w 1:1 -c 1:3 -C cro
d" 1 2
portmap adapter "-t alarm /home/teste
v/script/check_procs -w 1:1 -c 1:3 -C portmap" 1 2
syslog adapter "-t alarm /home/teste
v/script/check_procs -w 1:1 -c 1:3 -C syslogd" 1 2
s
mpd adapter "-t alarm /home/teste
v/script/check_procs -w 1:1 -c 1:3 -C s
mpd" 1 2
sshd adapter "-t alarm /home/teste
v/script/check_procs -w 1:1 -c 1:3 -C sshd" 1 2