kk Blog —— 通用基础


date [-d @int|str] [+%s|"+%F %T"]
netstat -ltunp

Linux nf_conntrack连接跟踪的实现

http://bbs.chinaunix.net/thread-4082396-1-1.html

连接跟踪,顾名思义,就是识别一个连接上双方向的数据包,同时记录状态。下面看一下它的数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct nf_conn {
	/* Usage count in here is 1 for hash table/destruct timer, 1 per skb, plus 1 for any connection(s) we are `master' for */
	struct  nf_conntrack  ct_general;     /* 连接跟踪的引用计数 */
	spinlock_t  lock;

	/* Connection tracking(链接跟踪)用来跟踪、记录每个链接的信息(目前仅支持IP协议的连接跟踪)。
	    每个链接由“tuple”来唯一标识,这里的“tuple”对不同的协议会有不同的含义,例如对tcp,udp
		 来说就是五元组: (源IP,源端口,目的IP, 目的端口,协议号),对ICMP协议来说是: (源IP, 目
	    的IP, id, type, code), 其中id,type与code都是icmp协议的信息。链接跟踪是防火墙实现状态检
	    测的基础,很多功能都需要借助链接跟踪才能实现,例如NAT、快速转发、等等。*/
	struct  nf_conntrack_tuple_hash  tuplehash[IP_CT_DIR_MAX];

	unsigned long  status;               /* 可以设置由enum ip_conntrack_status中描述的状态 */

	struct  nf_conn  *master;         /* 如果该连接是某个连接的子连接,则master指向它的主连接 */
	/* Timer function; drops refcnt when it goes off. */
	struct  timer_list  timeout;

	union nf_conntrack_proto proto;       /* 用于保存不同协议的私有数据 */
	/* Extensions */
	struct nf_ct_ext *ext;            /* 用于扩展结构 */
};

这个结构非常简单,其中最主要的就是tuplehash(跟踪连接双方向数据)和status(记录连接状态),这也连接跟踪最主要的功能。

在status中可以设置的标志,由下面的enum ip_conntrack_status描述,它们可以共存。这些标志设置后就不会再被清除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
enum ip_conntrack_status {
	IPS_EXPECTED_BIT = 0,     /* 表示该连接是个子连接 */
	IPS_SEEN_REPLY_BIT = 1,       /* 表示该连接上双方向上都有数据包了 */
	IPS_ASSURED_BIT = 2,      /* TCP:在三次握手建立完连接后即设定该标志。UDP:如果在该连接上的两个方向都有数据包通过,
								则再有数据包在该连接上通过时,就设定该标志。ICMP:不设置该标志 */
	IPS_CONFIRMED_BIT = 3,        /* 表示该连接已被添加到net->ct.hash表中 */
	IPS_SRC_NAT_BIT = 4,      /*在POSTROUTING处,当替换reply tuple完成时, 设置该标记 */
	IPS_DST_NAT_BIT = 5,      /* 在PREROUTING处,当替换reply tuple完成时, 设置该标记 */
	/* Both together. */
	IPS_NAT_MASK = (IPS_DST_NAT | IPS_SRC_NAT),
	/* Connection needs TCP sequence adjusted. */
	IPS_SEQ_ADJUST_BIT = 6,
	IPS_SRC_NAT_DONE_BIT = 7, /* 在POSTROUTING处,已被SNAT处理,并被加入到bysource链中,设置该标记 */
	IPS_DST_NAT_DONE_BIT = 8, /* 在PREROUTING处,已被DNAT处理,并被加入到bysource链中,设置该标记 */
	/* Both together */
	IPS_NAT_DONE_MASK = (IPS_DST_NAT_DONE | IPS_SRC_NAT_DONE),
	IPS_DYING_BIT = 9,        /* 表示该连接正在被释放,内核通过该标志保证正在被释放的ct不会被其它地方再次引用。有了这个标志,当某个连接要被删除时,即使它还在net->ct.hash中,也不会再次被引用。*/
	IPS_FIXED_TIMEOUT_BIT = 10,   /* 固定连接超时时间,这将不根据状态修改连接超时时间。通过函数nf_ct_refresh_acct()修改超时时间时检查该标志。 */
	IPS_TEMPLATE_BIT = 11,        /* 由CT target进行设置(这个target只能用在raw表中,用于为数据包构建指定ct,并打上该标志),用于表明这个ct是由CT target创建的 */
};

连接跟踪对该连接上的每个数据包表现为以下几种状态之一,由enum ip_conntrack_info表示,被设置在skb->nfctinfo中。

1
2
3
4
5
6
7
8
9
enum ip_conntrack_info {
	IP_CT_ESTABLISHED(0),  /* 表示这个数据包对应的连接在两个方向都有数据包通过,并且这是ORIGINAL初始方向数据包(无论是TCP、UDP、ICMP数据包,只要在该连接的两个方向上已有数据包通过,就会将该连接设置为IP_CT_ESTABLISHED状态。不会根据协议中的标志位进行判断,例如TCP的SYN等)。但它表示不了这是第几个数据包,也说明不了这个CT是否是子连接。*/
	IP_CT_RELATED(1),     /* 表示这个数据包对应的连接还没有REPLY方向数据包,当前数据包是ORIGINAL方向数据包。并且这个连接关联一个已有的连接,是该已有连接的子连接,(即status标志中已经设置了IPS_EXPECTED标志,该标志在init_conntrack()函数中设置)。但无法判断是第几个数据包(不一定是第一个)*/
	IP_CT_NEW(2),         /* 表示这个数据包对应的连接还没有REPLY方向数据包,当前数据包是ORIGINAL方向数据包,该连接不是子连接。但无法判断是第几个数据包(不一定是第一个)*/
	IP_CT_IS_REPLY(3),        /* 这个状态一般不单独使用,通常以下面两种方式使用 */
	IP_CT_ESTABLISHED + IP_CT_IS_REPLY(3),    /* 表示这个数据包对应的连接在两个方向都有数据包通过,并且这是REPLY应答方向数据包。但它表示不了这是第几个数据包,也说明不了这个CT是否是子连接。*/
	IP_CT_RELATED + IP_CT_IS_REPLY(4),        /* 这个状态仅在nf_conntrack_attach()函数中设置,用于本机返回REJECT,例如返回一个ICMP目的不可达报文, 或返回一个reset报文。它表示不了这是第几个数据包。*/
	IP_CT_NUMBER = IP_CT_IS_REPLY * 2 - 1(5)  /* 可表示状态的总数 */
};

以上就是连接跟踪里最重要的数据结构了,用于跟踪连接、记录状态、并对该连接的每个数据包设置一种状态。

除了上面的主要数据结构外,还有一些辅助数据结构,用于处理不同协议的私有信息、处理子连接、对conntrack进行扩展等。

三层协议(IPv4/IPv6)

利用nf_conntrack_proto.c文件中的

1
2
3
nf_conntrack_l3proto_register(struct nf_conntrack_l3proto *proto)
nf_conntrack_l3proto_unregister(struct nf_conntrack_l3proto *proto)

在nf_ct_l3protos[]数组中注册自己的三层协议处理函数。

四层协议(TCP/UDP)

利用nf_conntrack_proto.c文件中的

1
2
3
nf_conntrack_l4proto_register(struct nf_conntrack_l4proto *l4proto)
nf_conntrack_l4proto_unregister(struct nf_conntrack_l4proto *l4proto)

在nf_ct_protos[]数组中注册自己的四层协议处理函数。

处理一个连接的子连接协议

利用nf_conntrack_helper.c文件中的

1
nf_conntrack_helper_register(struct nf_conntrack_helper *me)

来注册nf_conntrack_helper结构,

和nf_conntrack_expect.c文件中的

1
nf_ct_expect_related_report(struct nf_conntrack_expect *expect, u32 pid, int report)

来注册nf_conntrack_expect结构。

扩展连接跟踪结构(nf_conn)

利用nf_conntrack_extend.c文件中的

1
2
3
nf_ct_extend_register(struct nf_ct_ext_type *type)
nf_ct_extend_unregister(struct nf_ct_ext_type *type)

进行扩展,并修改连接跟踪相应代码来利用这部分扩展功能。

了解了上面的数据结构,我们下面来看一下nf_conntrack的执行流程以及如何利用这些数据结构的。首先来看一下nf_conntrack模块加载时的初始化流程。

nf_conntrack的初始化

就是初始化上面提到的那些数据结构,它在内核启动时调用nf_conntrack_standalone_init()函数进行初始化的。初始化完成后,构建出如下图所示的结构图,只是不包含下图中与连接有关的信息(nf_conn和nf_conntrack_expect结构)。

上图中有三个HASH桶,ct_hash、expect_hash、helper_hash这三个HASH桶大小在初始化时就已确定,后面不能再更改。其中ct_hash、expect_hash可在加载nf_conntrack.ko模块时通过参数hashsize和expect_hashsize进行设定,而helper_hash不能通过参数修改,它的默认值是page/sizeof(helper_hash)。

下面再来看一个当创建子连接时,各个数据结构之间的关系。

nf_conn和nf_conntrack_expect都有最大个数限制。nf_conn通过全局变量nf_conntrack_max限制,可通过 /proc/sys/net/netfilter/nf_conntrack_max 文件在运行时修改。nf_conntrack_expect通过全局变量nf_ct_expect_max限制,可通过 /proc/sys/net/netfilter/nf_conntrack_expect_max 文件在运行时修改。nf_conntrack_helper没有最大数限制,因为这个是通过注册不同协议的模块添加的,大小取决于动态协议跟踪模块的多少,一般不会很大。

上面两幅数据结构图中,大部分都已介绍过,下面介绍一下netns_ct数据结构,该结构主要用于linux的网络命名空间,表示nf_conntrack在不同的命名空间中都有一套独立的数据信息(这是另一个话题,这里就不再深入讨论了)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct netns_ct {
	atomic_t          count;              /* 当前连接表中连接的个数 */
	unsigned int      expect_count;           /* nf_conntrack_helper创建的期待子连接nf_conntrack_expect项的个数 */
	unsigned int      htable_size;            /* 存储连接(nf_conn)的HASH桶的大小 */
	struct kmem_cache *nf_conntrack_cachep;       /* 指向用于分配nf_conn结构而建立的高速缓存(slab)对象 */
	struct hlist_nulls_head   *hash;              /* 指向存储连接(nf_conn)的HASH桶 */
	struct hlist_head     *expect_hash;           /* 指向存储期待子连接nf_conntrack_expect项的HASH桶 */
	struct hlist_nulls_head   unconfirmed;            /* 对于一个链接的第一个包,在init_conntrack()函数中会将该包original方向的tuple结构挂入该链,这是因为在此时还不确定该链接会不会被后续的规则过滤掉,如果被过滤掉就没有必要挂入正式的链接跟踪表。在ipv4_confirm()函数中,会将unconfirmed链中的tuple拆掉,然后再将original方向和reply方向的tuple挂入到正式的链接跟踪表中,即init_net.ct.hash中,这是因为到达ipv4_confirm()函数时,应经在钩子NF_IP_POST_ROUTING处了,已经通过了前面的filter表。 通过cat  /proc/net/nf_conntrack显示连接,是不会显示该链中的连接的。但总的连接个数(net->ct.count)包含该链中的连接。当注销l3proto、l4proto、helper、nat等资源或在应用层删除所有连接(conntrack -F)时,除了释放confirmed连接(在net->ct.hash中的连接)的资源,还要释放unconfirmed连接(即在该链中的连接)的资源。*/
	struct hlist_nulls_head   dying;              /* 释放连接时,通告DESTROY事件失败的ct被放入该链中,并设置定时器,等待下次通告。 通过cat  /proc/net/nf_conntrack显示连接,是不会显示该链中的连接的。但总的连接个数(net->ct.count)包含该链中的连接。当注销连接跟踪模块时,同时要清除正再等待被释放的连接(即该链中的连接)*/
	struct ip_conntrack_stat  __percpu *stat;         /* 连接跟踪过程中的一些状态统计,每个CPU一项,目的是为了减少锁 */
	int           sysctl_events;          /* 是否开启连接事件通告功能 */
	unsigned int      sysctl_events_retry_timeout;    /* 通告失败后,重试通告的间隔时间,单位是秒 */
	int           sysctl_acct;            /* 是否开启每个连接数据包统计功能 */
	int           sysctl_checksum;
	unsigned int      sysctl_log_invalid;      /* Log invalid packets */
#ifdef CONFIG_SYSCTL
	struct ctl_table_header   *sysctl_header;
	struct ctl_table_header   *acct_sysctl_header;
	struct ctl_table_header   *event_sysctl_header;
#endif
	int           hash_vmalloc;           /* 存储连接(nf_conn)的HASH桶是否是使用vmalloc()进行分配的 */
	int           expect_vmalloc;         /* 存储期待子连接nf_conntrack_expect项的HASH桶是否是使用vmalloc()进行分配的 */
	char          *slabname;          /* 用于分配nf_conn结构而建立的高速缓存(slab)对象的名字 */
};

从nf_conntrack的框架来看,它可用于跟踪任何三层和四协议的连接,但目前在三层协议只实现了IPv4和IPv6的连接跟踪,下面我们以IPv4为例,介绍一下该协议是如何利用nf_conntrack框架和netfilter实现连接跟踪的。有关netfilter框架,可参考我的另一个帖子

linux-2.6.35.6内核netfilter框架

首先介绍一下IPv4协议连接跟踪模块的初始化。

Ipv4连接跟踪模块注册了自己的3层协议,和IPv4相关的三个4层协议TCP、UDP、ICMP。注册后的结构图如下图所示:

在netfilter框架中利用

1
nf_register_hook(struct nf_hook_ops *reg)、nf_unregister_hook(struct nf_hook_ops *reg)

函数注册自己的钩子项,调用nf_conntrack_in()函数来建立相应连接。

这样数据包就会经过ipv4注册的钩子项,并调用nf_conntrack_in()函数建立连接表项,连接表项中的tuple由ipv4注册的3/4层协议处理函数构建。

ipv4_conntrack_in() 挂载在NF_IP_PRE_ROUTING点上。该函数主要功能是创建链接,即创建struct nf_conn结构,同时填充struct nf_conn中的一些必要的信息,例如链接状态、引用计数、helper结构等。

ipv4_confirm() 挂载在NF_IP_POST_ROUTING和NF_IP_LOCAL_IN点上。该函数主要功能是确认一个链接。对于一个新链接,在ipv4_conntrack_in()函数中只是创建了struct nf_conn结构,但并没有将该结构挂载到链接跟踪的Hash表中,因为此时还不能确定该链接是否会被NF_IP_FORWARD点上的钩子函数过滤掉,所以将挂载到Hash表的工作放到了ipv4_confirm()函数中。同时,子链接的helper功能也是在该函数中实现的。

ipv4_conntrack_local() 挂载在NF_IP_LOCAL_OUT点上。该函数功能与ipv4_conntrack_in()函数基本相同,但其用来处理本机主动向外发起的链接。

nf_conntrack_ipv4_compat_init() –> register_pernet_subsys() –> ip_conntrack_net_init() 创建/proc文件ip_conntrack和ip_conntrack_expect

如上面所示,IPv4连接跟踪模块已初始化完成,下面我们来看一下它创建连接的流程图。上图中连接的建立主要由三个函数来完成,即ipv4_conntrack_in(),ipv4_confirm()与ipv4_conntrack_local()。其中ipv4_conntrack_in()与ipv4_conntrack_local()都是通过调用函数nf_conntrack_in()来实现的,所以下面我们主要关注nf_conntrack_in()与ipv4_confirm()这两个函数。nf_conntrack_in()函数主要完成创建链接、添加链接的扩展结构(例如helper, acct结构)、设置链接状态等。ipv4_confirm()函数主要负责确认链接(即将链接挂入到正式的链接表中)、执行helper函数、启动链接超时定时器等。另外还有一个定时器函数death_by_timeout(), 该函数负责链接到期时删除该链接。

nf_conntrack_in()函数流程图

ipv4_confirm()函数流程图

death_by_timeout()函数流程图

上图中有一点需要说明,由于skb会引用nf_conn,同时会增加它的引用计数,所以当skb被释放时,也要释放nf_conn的引用计数,并且在nf_conn引用计数为0时,要释放全部资源。

当数据包经过nf_conntrack_in()和ipv4_confirm()函数处理流程后,就会建立起3楼第二幅结构图所示的连接nf_conn。同时这两个函数已经包含了子连接的处理流程,即流程图中help和exp的处理。子连接建立后的结构图如3楼第三幅结构图,主链接与子连接通过helper和expect关联起来。

连接跟踪到此就介绍完了,下面介绍IPv4基于nf_conntrack框架适合实现NAT转换的。先介绍IPv4-NAT初始化的资源,然后处理流程。

IPv4-NAT连接跟踪相关部分通过函数nf_nat_init()初始化

调用nf_ct_extend_register() 注册一个连接跟踪的扩展功能。

调用register_pernet_subsys() –> nf_nat_net_init() 创建net->ipv4.nat_bysource的HASH表,大小等于net->ct.htable_size。

初始化nf_nat_protos[]数组,为TCP、UDP、ICMP协议指定专用处理结构,其它协议都指向默认处理结构。

为nf_conntrack_untracked连接设置IPS_NAT_DONE_MASK标志。

将NAT模块的全局变量l3proto指向IPV4协议的nf_conntrack_l3proto结构。

设置全局指针nf_nat_seq_adjust_hook指向nf_nat_seq_adjust()函数。

设置全局指针nfnetlink_parse_nat_setup_hook指向nfnetlink_parse_nat_setup()函数。

设置全局指针nf_ct_nat_offset指向nf_nat_get_offset()函数。

IPv4-NAT功能的iptables部分通过函数nf_nat_standalone_init()初始化

调用nf_nat_rule_init() –> nf_nat_rule_net_init()在iptables中注册一个NAT表(通过ipt_register_table()函数,参考另一个帖子iptables)

调用 nf_nat_rule_init() 注册SNAT target和DNAT target(通过xt_register_target()函数)

调用nf_register_hooks() 挂载NAT的HOOK函数,橙色部分为NAT挂载的HOOK函数(参考另一个帖子netfilter)

根据上面介绍,可以看到IPv4-NAT的主要是通过nf_nat_fn()钩子函数处理的,下面我就来看看nf_nat_fn()函数的处理流程。

针对上图中的nf_nat_setup_info()函数进一步描述

下面对NAT转换算法中重要部分做一些文字说明

每个ct在第一个包就会做好snat与dnat, nat的信息全放在reply tuple中,orig tuple不会被改变。一旦第一个包建立好nat信息后,后续再也不会修改tuple内容了。

orig tuple中的地址信息与reply tuple中的地址信息就是原始数据包的信息。例如对A->B数据包同时做snat与dnat,PREROUTING处B被dnat到D,POSTROUTING处A被snat到C。则ct的内容是:  A->B | D->C,  A->B说明了orig方向上数据包刚到达墙时的地址内容,D->C说明reply方向上数据包刚到达墙时的地址内容。

在代码中有很多!dir操作,原理是: 当为了反向的数据包做事情的时候就取反向tuple的数据,这样才能保证NAT后的tuple信息被正确使用。

bysource链中链接了所有CT(做过NAT和未做过NAT),通过ct->nat->bysource,HASH值的计算使用的是CT的orig tuple。其作用是,当为一个新连接做SNAT,需要得到地址映射时,首先对该链进行查找,查找此源IP、协议和端口号是否已经做过了映射。如果做过的话,就需要在SNAT转换时,映射为相同的源IP和端口号。为什么要这么做呢?因为对于UDP来说,有些协议可能会用相同端口和同一主机不同的端口(或不同的主机)进行通信。此时,由于目的地不同,原来已有的映射不可使用,需要一个新的连接。但为了保证通信的的正确性,此时,就要映射为相同的源IP和端口号。其实就是为NAT的打洞服务的。所以bysource就是以源IP、协议和端口号为hash值的一个表,这样在做snat时保证相同的ip+port影射到相同的ip+port。

IP_NAT_RANGE_PROTO_RANDOM指的是做nat时,当计算端口时,如果没有此random标志,则会先使用原始得tuple中的端口试一下看是否可用,如果可用就使用该原始端口作为nat后的端口, 即尽量保证转换后的端口与转换前的端口保持一致。如果不可用,再根据nat的端口算法计算出一个端口。 如果有此标记,则直接根据端口算法计算出端口。

第一个包之后,ct的两个方向的tuple内容就固定了,所有的nat操作都必须在第一个包就完成。所以会有daddr = &ct->tuplehash[!dir].tuple.dst.u3;这样的操作。

IPS_SRC_NAT与IPS_DST_NAT,如果被设置,表示经过了NAT,并且ct中的tuple被做过SNAT或DNAT。

数据包永远都是在PREROUTING链做目的地址和目的端口转换,在POSTROUTING链做原地址和原端口转换。是否要做NAT转换则要根数据包方向(dir)和NAT标志(IPS_SRC_NAT或IPS_DST_NAT)来判断。

在PREROUTING链上—>数据包是original方向、并且连接上设置IPS_DST_NAT标志,或数据包是reply方向、并且连接上设置IPS_SRC_NAT标志,则做DNAT转换。

在POSTROUTING链上—>数据包是original方向、并且连接上设置IPS_SRC_NAT标志,或数据包是reply方向、并且连接上设置IPS_DST_NAT标志,则做SNAT转换。

IPS_DST_NAT_DONE_BIT与IPS_SRC_NAT_DONE_BIT,表示该ct进入过NAT模块,已经进行了源或者目的NAT判断,但并不表示ct中的tuple被修改过。

源目的nat都是在第一个包就判断完成的,假设先添加了snat策略,第一个包通过,这时又添加了dnat策略, 第二个包到来时是不会匹配dnat策略的 。

对于一个ct,nf_nat_setup_info函数最多只能进入2次,第一次DNAT,第二次SNAT。在nf_nat_follow_master函数中,第一次SNAT,第二次DNAT。

下面介绍有子连接的NAT实现。有两个关键点:1.主链接能正确的构建出NAT后的expect来识别子连接。2.能够修改主链接数据通道的信息为NAT后的信息。这两点都在动态协议的help中完成,下面我们来看一下它的流程图:

下面针对有无子连接的NAT做一下对比

无子连接的NAT

一个ct用于跟踪一个连接的双方向数据,ct->orig_tuple用于跟踪初始方向数据,ct->reply_tuple用于跟踪应答方向数据。当根据初始方向数据构建ct->orig_tuple时,同时要构建出ct->reply_tuple,用于识别同一连接上应答方向数据。

如果初始方向的数据在通过防火墙后被做了NAT转换,为识别出NAT数据的应答数据包,则对ct->reply_tuple也要做NAT转换。同时ct上做好相应NAT标记。

因此,上面的信息在初始方向第一个数据包通过后,就要求全部建立好,并且不再改变。

一个连接上不同方向的数据,都有相对应的tuple(orig_tuple和reply_tuple),所以该连接后续数据都将被识别出来。如果ct上有NAT标记,则根据要去往方向(即另一个方向)的tuple对数据做NAT转换。所以会有ct->tuplehash[!dir].tuple这样的操作。

有子连接的NAT

子连接是由主连接构建的expect项识别出来的。

help用于构建expect项,它期待哪个方向的连接,则用那个方向的tuple和数据包中数据通道信息构建expect项。例如期待和当前数据包相反方向的连接,则用相反方向的tuple中的信息(ct->tuplehash[!dir].tuple)。调用help时,NAT转换都已完成(tuple中都包含有正确的识别各自方向的信息),所以这时所使用的信息都是正确和所期望的信息。

如果子连接还可能有子连接,则构建expect项时,初始化一个helper结构,并赋值给expect->helper指针。

如果该连接已被做了NAT转换,则对数据包中数据通道信息也要做NAT转换

Linux编程之UDP SOCKET

https://www.cnblogs.com/skyfsm/p/6287787.html

一、基本的udp socket编程

1. UDP编程框架

要使用UDP协议进行程序开发,我们必须首先得理解什么是什么是UDP?这里简单概括一下。

UDP(user datagram protocol)的中文叫用户数据报协议,属于传输层。UDP是面向非连接的协议,它不与对方建立连接,而是直接把我要发的数据报发给对方。所以UDP适用于一次传输数据量很少、对可靠性要求不高的或对实时性要求高的应用场景。正因为UDP无需建立类如三次握手的连接,而使得通信效率很高。

UDP的应用非常广泛,比如一些知名的应用层协议(SNMP、DNS)都是基于UDP的,想一想,如果SNMP使用的是TCP的话,每次查询请求都得进行三次握手,这个花费的时间估计是使用者不能忍受的,因为这会产生明显的卡顿。所以UDP就是SNMP的一个很好的选择了,要是查询过程发生丢包错包也没关系的,我们再发起一个查询就好了,因为丢包的情况不多,这样总比每次查询都卡顿一下更容易让人接受吧。

UDP通信的流程比较简单,因此要搭建这么一个常用的UDP通信框架也是比较简单的。以下是UDP的框架图。

由以上框图可以看出,客户端要发起一次请求,仅仅需要两个步骤(socket和sendto),而服务器端也仅仅需要三个步骤即可接收到来自客户端的消息(socket、bind、recvfrom)。

2. UDP程序设计常用函数

1
2
3
#include <sys/types.h>          
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

参数domain:用于设置网络通信的域,socket根据这个参数选择信息协议的族

1
2
3
4
5
6
7
8
9
10
11
12
Name                                     Purpose                         
AF_UNIX, AF_LOCAL              Local communication              
AF_INET                        IPv4 Internet protocols          //用于IPV4
AF_INET6                       IPv6 Internet protocols          //用于IPV6
AF_IPX                         IPX - Novell protocols
AF_NETLINK                     Kernel user interface device     
AF_X25                         ITU-T X.25 / ISO-8208 protocol   
AF_AX25                        Amateur radio AX.25 protocol
AF_ATMPVC                      Access to raw ATM PVCs
AF_APPLETALK                   AppleTalk                        
AF_PACKET                      Low level packet interface       
AF_ALG                         Interface to kernel crypto API

对于该参数我们仅需熟记AF_INET和AF_INET6即可  

小插曲:PF_XXX和AF_XXX

我们在看Linux网络编程相关代码时会发现PF_XXX和AF_XXX会混着用,他们俩有什么区别呢?以下内容摘自《UNP》。

AF_ 前缀表示地址族(Address Family),而PF_前缀表示协议族(Protocol Family)。历史上曾有这样的想法:单个协议族可以支持多个地址族,PF_的值可以用来创建套接字,而AF_值用于套接字的地址结构。但实际上,支持多个地址族的协议族从来就没实现过,而头文件 <sys/socket.h> 中为一给定的协议定义的PF_值总是与此协议的AF_值相同。 所以我在实际编程时还是偏向于使用AF_XXX。   参数type(只列出最重要的三个):

1
2
3
SOCK_STREAM         Provides sequenced, reliable, two-way, connection-based byte streams.   //用于TCP
SOCK_DGRAM          Supports datagrams (connectionless, unreliable messages ). //用于UDP
SOCK_RAW            Provides raw network protocol access.  //RAW类型,用于提供原始网络访问

参数protocol:置0即可

返回值:

成功:非负的文件描述符

失败:-1  

1
2
3
4
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
		const struct sockaddr *dest_addr, socklen_t addrlen);

第一个参数sockfd:正在监听端口的套接口文件描述符,通过socket获得
第二个参数buf:发送缓冲区,往往是使用者定义的数组,该数组装有要发送的数据
第三个参数len:发送缓冲区的大小,单位是字节
第四个参数flags:填0即可
第五个参数dest_addr:指向接收数据的主机地址信息的结构体,也就是该参数指定数据要发送到哪个主机哪个进程
第六个参数addrlen:表示第五个参数所指向内容的长度

返回值:

成功:返回发送成功的数据长度

失败: -1

1
2
3
4
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
		struct sockaddr *src_addr, socklen_t *addrlen);

第一个参数sockfd:正在监听端口的套接口文件描述符,通过socket获得
第二个参数buf:接收缓冲区,往往是使用者定义的数组,该数组装有接收到的数据
第三个参数len:接收缓冲区的大小,单位是字节
第四个参数flags:填0即可
第五个参数src_addr:指向发送数据的主机地址信息的结构体,也就是我们可以从该参数获取到数据是谁发出的
第六个参数addrlen:表示第五个参数所指向内容的长度

返回值:

成功:返回接收成功的数据长度

失败: -1  

1
2
3
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);

第一个参数sockfd:正在监听端口的套接口文件描述符,通过socket获得
第二个参数my_addr:需要绑定的IP和端口
第三个参数addrlen:my_addr的结构体的大小

返回值:

成功:0

失败:-1  

1
2
#include <unistd.h>
int close(int fd);

close函数比较简单,只要填入socket产生的fd即可。

3. 搭建UDP通信框架

server:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>

#define SERVER_PORT 8888
#define BUFF_LEN 1024

void handle_udp_msg(int fd)
{
	char buf[BUFF_LEN];  //接收缓冲区,1024字节
	socklen_t len;
	int count;
	struct sockaddr_in clent_addr;  //clent_addr用于记录发送方的地址信息
	while (1) {
		memset(buf, 0, BUFF_LEN);
		len = sizeof(clent_addr);
		count = recvfrom(fd, buf, BUFF_LEN, 0, (struct sockaddr*)&clent_addr, &len);  //recvfrom是拥塞函数,没有数据就一直拥塞
		if (count == -1) {
			printf("recieve data fail!\n");
			return;
		}
		printf("client:%s\n",buf);  //打印client发过来的信息
		memset(buf, 0, BUFF_LEN);
		sprintf(buf, "I have recieved %d bytes data!\n", count);  //回复client
		printf("server:%s\n",buf);  //打印自己发送的信息给
		sendto(fd, buf, BUFF_LEN, 0, (struct sockaddr*)&clent_addr, len);  //发送信息给client,注意使用了clent_addr结构体指针

	}
}


/*
	server:
		socket-->bind-->recvfrom-->sendto-->close
*/

int main(int argc, char* argv[])
{
	int server_fd, ret;
	struct sockaddr_in ser_addr; 

	server_fd = socket(AF_INET, SOCK_DGRAM, 0); //AF_INET:IPV4;SOCK_DGRAM:UDP
	if (server_fd < 0) {
		printf("create socket fail!\n");
		return -1;
	}

	memset(&ser_addr, 0, sizeof(ser_addr));
	ser_addr.sin_family = AF_INET;
	ser_addr.sin_addr.s_addr = htonl(INADDR_ANY); //IP地址,需要进行网络序转换,INADDR_ANY:本地地址
	ser_addr.sin_port = htons(SERVER_PORT);  //端口号,需要网络序转换

	ret = bind(server_fd, (struct sockaddr*)&ser_addr, sizeof(ser_addr));
	if (ret < 0) {
		printf("socket bind fail!\n");
		return -1;
	}

	handle_udp_msg(server_fd);   //处理接收到的数据

	close(server_fd);
	return 0;
}

client:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>

#define SERVER_PORT 8888
#define BUFF_LEN 512
#define SERVER_IP "172.0.5.182"


void udp_msg_sender(int fd, struct sockaddr* dst)
{

	socklen_t len;
	struct sockaddr_in src;
	while (1) {
		char buf[BUFF_LEN] = "TEST UDP MSG!\n";
		len = sizeof(*dst);
		printf("client:%s\n",buf);  //打印自己发送的信息
		sendto(fd, buf, BUFF_LEN, 0, dst, len);
		memset(buf, 0, BUFF_LEN);
		recvfrom(fd, buf, BUFF_LEN, 0, (struct sockaddr*)&src, &len);  //接收来自server的信息
		printf("server:%s\n",buf);
		sleep(1);  //一秒发送一次消息
	}
}

/*
	client:
		socket-->sendto-->revcfrom-->close
*/

int main(int argc, char* argv[])
{
	int client_fd;
	struct sockaddr_in ser_addr;

	client_fd = socket(AF_INET, SOCK_DGRAM, 0);
	if (client_fd < 0) {
		printf("create socket fail!\n");
		return -1;
	}

	memset(&ser_addr, 0, sizeof(ser_addr));
	ser_addr.sin_family = AF_INET;
	//ser_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
	ser_addr.sin_addr.s_addr = htonl(INADDR_ANY);  //注意网络序转换
	ser_addr.sin_port = htons(SERVER_PORT);  //注意网络序转换

	udp_msg_sender(client_fd, (struct sockaddr*)&ser_addr);

	close(client_fd);

	return 0;
}

以上的框架用于一台主机不同端口的UDP通信,现象如下:

我们先建立server端,等待服务;然后我们建立client端请求服务。

server端:

  client端:   自己主机跟自己通信不是很爽,我们想跟其他主机通信怎么搞?很简单,上面client的代码的第49行的注释打开,并注释掉下面那行,在宏定义里填入自己想通信的serverip就可以了。现象如下:

server端:   client端:   这样我们就实现了主机172.0.5.183和172.0.5.182之间的网络通信。   UDP通用框架搭建完成,我们可以利用该框架跟指定主机进行通信了。   如果想学习UDP的基础知识,以上的知识就足够了;如果想继续深入学习一下UDP SOCKET一些高级知识(奇技淫巧),可以花点时间往下看。  

二、高级udp socket编程

1. udp的connect函数

什么?UDP也有conenct?connect不是用于TCP编程的吗?

是的,UDP网络编程中的确有connect函数,但它仅仅用于表示确定了另一方的地址,并没有其他含义。

有了以上认识后,我们可以知道UDP套接字有以下区分:

1)未连接的UDP套接字

2)已连接的UDP套接字

对于未连接的套接字,也就是我们常用的的UDP套接字,我们使用的是sendto/recvfrom进行信息的收发,目标主机的IP和端口是在调用sendto/recvfrom时确定的;

在一个未连接的UDP套接字上给两个数据报调用sendto函数内核将执行以下六个步骤:

1)连接套接字
2)输出第一个数据报
3)断开套接字连接
4)连接套接字
5)输出第二个数据报
6)断开套接字连接

对于已连接的UDP套接字,必须先经过connect来向目标服务器进行指定,然后调用read/write进行信息的收发,目标主机的IP和端口是在connect时确定的,也就是说,一旦conenct成功,我们就只能对该主机进行收发信息了。

已连接的UDP套接字给两个数据报调用write函数内核将执行以下三个步骤:

1)连接套接字
2)输出第一个数据报
3)输出第二个数据报

由此可以知道,当应用进程知道给同一个目的地址的端口号发送多个数据报时,显示套接字效率更高。

下面给出带connect函数的UDP通信框架

具体框架代码不再给出了,因为跟上面不带connect的代码大同小异,仅仅多出一个connect函数处理而已,下面给出处理conenct()的基本步骤。

1
2
3
4
5
6
7
8
9
10
void udp_handler(int s, struct sockaddr* to)
{
	char buf[1024] = "TEST UDP !";
	int n = 0;
	connect(s, to, sizeof(*to);

	n = write(s, buf, 1024);

	read(s, buf, n);
}

 

2. udp报文丢失问题

因为UDP自身的特点,决定了UDP会相对于TCP存在一些难以解决的问题。第一个就是UDP报文缺失问题。

在UDP服务器客户端的例子中,如果客户端发送的数据丢失,服务器会一直等待,直到客户端的合法数据过来。如果服务器的响应在中间被路由丢弃,则客户端会一直阻塞,直到服务器数据过来。

防止这样的永久阻塞的一般方法是给客户的recvfrom调用设置一个超时,大概有这么两种方法:

1)使用信号SIGALRM为recvfrom设置超时。首先我们为SIGALARM建立一个信号处理函数,并在每次调用前通过alarm设置一个5秒的超时。如果recvfrom被我们的信号处理函数中断了,那就超时重发信息;若正常读到数据了,就关闭报警时钟并继续进行下去。

2)使用select为recvfrom设置超时

设置select函数的第五个参数即可。

 

3. udp报文乱序问题

所谓乱序就是发送数据的顺序和接收数据的顺序不一致,例如发送数据的顺序为A、B、C,但是接收到的数据顺序却为:A、C、B。产生这个问题的原因在于,每个数据报走的路由并不一样,有的路由顺畅,有的却拥塞,这导致每个数据报到达目的地的顺序就不一样了。UDP协议并不保证数据报的按序接收。

解决这个问题的方法就是发送端在发送数据时加入数据报序号,这样接收端接收到报文后可以先检查数据报的序号,并将它们按序排队,形成有序的数据报。

4. udp流量控制问题

总所周知,TCP有滑动窗口进行流量控制和拥塞控制,反观UDP因为其特点无法做到。UDP接收数据时直接将数据放进缓冲区内,如果用户没有及时将缓冲区的内容复制出来放好的话,后面的到来的数据会接着往缓冲区放,当缓冲区满时,后来的到的数据就会覆盖先来的数据而造成数据丢失(因为内核使用的UDP缓冲区是环形缓冲区)。因此,一旦发送方在某个时间点爆发性发送消息,接收方将因为来不及接收而发生信息丢失。

解决方法一般采用增大UDP缓冲区,使得接收方的接收能力大于发送方的发送能力。

1
2
int n = 220 * 1024; //220kB
setsocketopt(sockfd, SOL_SOCKET, SO_RCVBUF, &n, sizeof(n));

这样我们就把接收方的接收队列扩大了,从而尽量避免丢失数据的发生。

nignx log_format 日志时间格式

https://bnxb.com/nginx/27544.html

Nginx的默认访问日志的时间格式是:[08/Mar/2018:10:30:58 +0800],由日志参数中的$time_local变量表示。

改成常用格式:2018-06-08 10:11:23

有两种方法,一种是修改源码,然后编译,一种是外挂lua来实现

一、lua 方法

不修改 nginx 源代码的,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
http {
	log_format  main  '$fmt_localtime $request_time $server_addr $server_port $remote_addr $remote_port $status $body_bytes_sent $request_method $server_protocol'
			  ' "$host" "$uri" "$query_string" "$http_referer" "$http_user_agent" "$http_x_forwarded_for" - $remote_user';

	access_log  /var/log/nginx/access.log  main;

	map $host $fmt_localtime {
		default '';
	}
	log_by_lua_block {
		ngx.var.fmt_localtime = ngx.localtime();
	}
	...
}

代码的解释如下:

首先我们自定义一个nginx 变量 $fmt_localtime , 因为在http context不能够使用 set $variable。

所以我们采用map的方式如下

1
2
3
map $host $fmt_localtime {
	default '';
}

2) 然后我们用 log_by_lua_block 设置 ngx.fmt_localtime 的时间

3) 设置日志格式 log_format使用 $fmt_localtime 作为时间参数

二、修改nginx源代码

需要修改的文件

src/core/nginx_times.c

src/http/modules/ngx_http_log_module.c

首先修改ngx_http_log_module.c文件:

1
ngx_string("time_iso8601"), sizeof("1970-09-28T12:00:00+06:00") - 1,

更改后

1
ngx_string("time_iso8601"), sizeof("1970-09-28 12:00:00") - 1,

然后修改nginx_times.c文件:

1
[sizeof("1970-09-28T12:00:00+06:00")];

更改后

1
[sizeof("1970-09-28 12:00:00")];
1
ngx_cached_http_log_iso8601.len = sizeof("1970-09-28T12:00:00+06:00") - 1;

更改为

1
ngx_cached_http_log_iso8601.len = sizeof("1970-09-28 12:00:00") - 1;
1
2
3
4
5
6
(void) ngx_sprintf(p3, "%4d-%02d-%02dT%02d:%02d:%02d%c%02d:%02d",
		tm.ngx_tm_year, tm.ngx_tm_mon,
		tm.ngx_tm_mday, tm.ngx_tm_hour,
		tm.ngx_tm_min, tm.ngx_tm_sec,
		tp->gmtoff < 0 ? '-' : '+',
		ngx_abs(tp->gmtoff / 60), ngx_abs(tp->gmtoff % 60));

更改为

1
2
3
4
(void) ngx_sprintf(p3, "%4d-%02d-%02d %02d:%02d:%02d",
		tm.ngx_tm_year, tm.ngx_tm_mon,
		tm.ngx_tm_mday, tm.ngx_tm_hour,
		tm.ngx_tm_min, tm.ngx_tm_sec);

最后重新编译,并使用新的时间变量

将 nginx 配置文件中的 $time_local 改为 $time_iso8601 即可。

用html+css+js实现一个无限级树形控件

http://t.zoukankan.com/rxbook-p-10975673.html

gg.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
<!DOCTYPE html>
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
	<title>树形菜单示例</title>

	<style type="text/css">
	ul>li {
		list-style: none;
	}

	/* 可展开*/
	.switch-open {
		margin-left:-12px;
		border:6px solid transparent;
		display:inline-block;
		height:0px;
		border-top-color: black;
	}

	/* 展开完毕*/
	.switch-close {
		margin-left:-12px;
		border:6px solid transparent;
		display:inline-block;
		height:0px;
		border-left-color: black;
		margin-bottom: 2px;

	}
	/* 改变CheckBox样式*/
	input[type='checkbox'] {
		height: 20px;

		-webkit-appearance:none;
		-moz-appearance: none;
		border: 1px solid #c9c9c9;
		border-radius: 3px;
		outline: none;
		color:white;
		text-align: center;
	}
	input[type='checkbox']:before {
		content: '√ ';
		color:transparent;
	}
	input[type=checkbox]:checked {
		background-color: #30add6;
	}
	input[type=checkbox]:checked:before {
		content: '√';
		color:white;
		font-weight: bold;
	}

	</style>
</head>

<body>

<div class="warp">
	<ul id="container">
	</ul>
</div>

<script type="text/javascript">

	//结构
	var json = {
		'0-0': {
			'0-0-0': {},
			'0-0-1': {
				'0-0-1-0': {},
				'0-0-1-1': {},
				'0-0-1-2': {}
			},
			'0-0-2': {}
		},
		'0-1': {
			'0-1-0': {},
			'0-1-1': {}
		},
		'0-2': {}
	};

	//这里生成DOM
	function generate(json, par)
	{
		for (var attr in json) {
			var ele = document.createElement('li');
			if (json[attr].length > 0) {
				ele.innerHTML = ' <input type="checkbox"></input>'+attr;
			} else {
				ele.innerHTML = '<span><span class="switch-open" onclick="toggle(this)"></span><input type="checkbox" onclick="checkChange(this)"></input>'+attr+'</span>';
				var nextpar = document.createElement('ul');
				ele.appendChild(nextpar);
				generate(json[attr], nextpar);
			}
			par.appendChild(ele);
		}
	}

	generate(json, document.getElementById('container'));


	//处理展开和收起
	function toggle(eve)
	{
		var par = eve.parentNode.nextElementSibling;
		if (par.style.display == 'none') {
			par.style.display = 'block';
			eve.className = 'switch-open';
		} else {
			par.style.display = 'none';
			eve.className = 'switch-close';
		}
	}

	//处理全部勾选和全部不选
	function checkChange(eve)
	{
		var oul = eve.parentNode.nextElementSibling;
		if (eve.checked) {
			for (var i = 0; i < oul.querySelectorAll('input').length; i++) {
				oul.querySelectorAll('input')[i].checked = true;
			}
		} else {
			for (var i = 0; i < oul.querySelectorAll('input').length; i++) {
				oul.querySelectorAll('input')[i].checked = false;
			}
		}
	}

</script>

</body>
</html>

两个 table 实现固定列

https://blog.csdn.net/psp0001060/article/details/49705247

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
 <head> 
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 
  <title>固定列头</title> 
  <script src="/rs/js/jquery.min.js" type="text/javascript"></script>
  <style type="text/css">

	.ui-table {
		border-collapse: collapse;
		width:100%;
		white-space: nowrap;
	}
	.ui-table th {
		border: 1px solid #ffffff;
		padding: 1px;
		text-align: center;
		height: 28px;
	}
	.ui-table td {
		border: 1px solid #ffffff;
		background-color: #eeeeee;
		padding: 3px;
		height: 28px;
	}
	div.x_auto_box {
		position: absolute;
		z-index: 1;
	}

	div.lock_box {
		float: left;
		position: sticky;
		left: 0;
		z-index: 5;

	}
	</style> 
 </head> 
 <body> 
  <!-- 左侧DIV --> 
  <div id="list" > 
   <div class="lock_box" style='display: inline-block'> 
    <table class="ui-table"> 
     <thead>
      <tr> 
       <td>No号</td> 
       <td>员工号</td> 
       <td>部门</td> 
      </tr> 
     </thead>
     <tbody>
<?php
	for ($i = 0; $i < 30; $i ++) {
		echo "<tr>";
		echo "<td>$i</td>";
		echo "<td>DHL130023</td>";
		echo "<td>动画联盟</td>";
		echo "</tr>";
	}
?>
     </tbody>
    </table> 
   </div> 

   <!-- 右侧 --> 
   <div class="x_auto_box" id="showDiv" style='display: inline-block'> 
<?php
	for ($i = 0; $i < 30; $i ++) {
		echo '<input type="hidden" name="dto.position" value="1" />';
	}
?>
    <table id="tableRight" class="ui-table"> 
     <thead>
      <tr> 
       <td>性别</td> 
       <td>出生日期</td> 
       <td>民族</td> 
       <td>籍贯</td> 
       <td>婚姻状况</td> 
       <td>学历</td> 
       <td>职位</td> 
       <td>毕业院校</td> 
       <td>专业</td> 
       <td>院校类别</td> 
       <td>毕业日期</td> 
       <td>年龄</td> 
       <td>户籍地址</td> 
       <td>入职时间</td> 
       <td>转正日期</td> 
       <td>本单位工龄</td> 
       <td>合同签订</td> 
       <td>合同年限</td> 
       <td>日语等级</td> 
       <td>社保账号</td> 
       <td>公积金帐号</td> 
       <td>身份证号</td> 
       <td>毕业证书</td> 
       <td>离职时间</td> 
       <td>离职原因</td> 
       <td>户口性质</td> 
       <td>政治面貌</td> 
       <td>参加工作</td> 
       <td>联系电话</td> 
       <td>邮箱</td> 
       <td>家庭住址</td> 
       <td>办公电话</td> 
       <td>备注</td> 
      </tr>
     </thead>
     <tbody>
<?php
$h = <<<here
      <tr οnclick="javascript:window.location.href='/HRMS2/pages/personManageInfo.jsp?userid=SX00001'"> 
       <td>男</td> 
       <td></td> 
       <td></td> 
       <td></td> 
       <td>未婚</td> 
       <td>小学</td> 
       <td>普通员工</td> 
       <td></td> 
       <td></td> 
       <td>专升本</td> 
       <td></td> 
       <td>0</td> 
       <td></td> 
       <td></td> 
       <td></td> 
       <td>0.0</td> 
       <td></td> 
       <td>0</td> 
       <td>四级</td> 
       <td></td> 
       <td></td> 
       <td></td> 
       <td></td> 
       <td></td> 
       <td></td> 
       <td>非农业户口</td> 
       <td>民主党派</td> 
       <td></td> 
       <td></td> 
       <td></td> 
       <td></td> 
       <td></td> 
       <td></td> 
      </tr> 
here;
	for ($i = 0; $i < 30; $i ++) {
		echo $h;
	}
?>
     </tbody>
    </table> 
   </div> 
  </div>  
 </body>
</html>