centos6.6安装ruby2.4

centos6上安装高版本ruby

生产环境为centos6.6

$ cat /etc/redhat-release 
CentOS release 6.6 (Final)
$ uname  -r
2.6.32-504.30.3.el6.x86_64

当前系统ruby版本是1.8.7

$ ruby --version
ruby 1.8.7 (2013-06-27 patchlevel 374) [x86_64-linux]

ruby有自己的软件包管理器gem,就像python的pip,在当前环境使用gem会报错:

$ gem install innodb_ruby
-bash: gem: command not found

在这种情况下,我们需要在centos6上安装高版本的ruby。

第一步-CentOS SCLo库

添加CentOS SCLo软件集合存储库

$ yum -y install centos-release-scl-rh centos-release-scl

第二步-安装ruby2.4

添加SCLo存储库后,我们可以使用以下命令安装Ruby2.4

$ yum install rh-ruby24

随着版本的更新,这个命令可能也会改变,如果提示没有rh-ruby24软件包,可以尝试rh-ruby25,rh-ruby26

第三步-设置环境变量

在CentOS6上安装Ruby2.4之后,只需设置如下所述的环境变量

$ scl enable rh-ruby24 bash

$ ruby --version
ruby 2.4.6p354 (2019-04-01 revision 67394) [x86_64-linux-gnu]

现在就可以用gem安装ruby软件了:

$ gem install innodb_ruby
Fetching: bindata-1.8.3.gem (100%)
Successfully installed bindata-1.8.3
Fetching: digest-crc-0.4.1.gem (100%)
Successfully installed digest-crc-0.4.1
Fetching: innodb_ruby-0.9.16.gem (100%)
Successfully installed innodb_ruby-0.9.16
Parsing documentation for bindata-1.8.3
Installing ri documentation for bindata-1.8.3
Parsing documentation for digest-crc-0.4.1
Installing ri documentation for digest-crc-0.4.1
Parsing documentation for innodb_ruby-0.9.16
Installing ri documentation for innodb_ruby-0.9.16
Done installing documentation for bindata, digest-crc, innodb_ruby after 3 seconds
3 gems installed

绕过网管搭建公司内网vpn

武汉肺炎期间搭建的公司内网vpn

以前在外网vps上搭建过openvpn,过程很复杂,主要是验证这一块,各种秘钥证书让openvpn架设起来很不友好。可是最后搭建成功后被防火长城严重干扰,基本上没法用。近期武汉的肺炎导致公司不能正常开工,公司的网管又不能及时搭建vpn,为了大家能够有一个良好的远程办公体验,我绕过了网管控制的公司外网出口路由器自行搭建了部门的vpn。

OpenVPN

总体的思路大概是上图这样,在部门10.0.17.0/24网段内的一台内网服务器10.0.17.48搭建openvpn服务,采用tap模式将内网ip地址分配给openvpn客户端,并通过外网端口转发将内网的服务映射到外网vps上

OpenVPN的安装

网上大把openVPN的安装教程文章,基本上都是手动配置各种证书和秘钥,非常的繁琐烦人,当然不止我觉得烦,所以已经有人做了自动安装的脚本,github上有两个好几K星星的安装脚本,我选了其中一个:https://github.com/angristan/openvpn-install
按照脚本提供的使用方法,直接运行以下命令即可:

curl -O https://raw.githubusercontent.com/angristan/openvpn-install/master/openvpn-install.sh
chmod +x openvpn-install.sh
./openvpn-install.sh

安装过程中需要手动输入一些配置,比如tcp/udp模式的选择,对外提供服务的ip和port,以及其他一些关于证书和秘钥的自定义参数。 我们这里只设置模式和ip端口,因为是内网服务器,脚本会直接帮你输入内网ip,这里我们要改为最后对外提供服务的外网ip,上图中的120.100.1.1,端口随便定义。
安装结束后会让你输入第一个用户的用户名,之后为这个用户创建ovpn权限文件。
如果想增加或者删除用户,再次运行./openvpn-install.sh 脚本即可。

OpenVPN的配置

这个脚本提供的是tun模式的服务器,使用tun模式的话,openvpn客户端会得到另外一个内网网段的地址而不能与公司内网10.0.17.0/24直接交互,如需交互就要配置复杂的路由表,操作起来稍微有些不太友好。最初我使用了tun模式,10.0.17.48启动tun模式后会虚拟一张拥有10.8.0.1地址的网卡,并自动成为10.8.0.0/24网段的网关,openvpn客户端会得到10.8.0.0/24网段的ip地址,如果公司内的电脑想要访问10.8.0.0/24网段的机器,比如一个服务器开发人员远程通过openvpn连入公司内网,得到ip地址10.8.0.5,在自己的开发机上启动了一个服务器程序,处于公司内的测试人员想要访问该远程开发人员的服务器程序就需要在本地配置路由表:

linux:route add -net 10.8.0.0 netmask 255.255.255.0 gw 10.0.17.48 eth0 (对应的删除命令route del -net 10.8.0.0 netmask 255.255.255.0,显示路由命令route)
windows:oute add 10.8.0.0 mask 255.255.255.0 10.0.17.48(对应的删除命令route delete 10.8.0.0,显示路由命令route print)

这样公司内网10.0.17.0/24网段的机器才知道如何找到10.8.0.0/24网段。 但是我们有一些服务器没有在10.0.17.0/24网段,而是在10.0.30.0/24网段,尝试在10.0.30.0/24网段的机器上配置路由后发现不能双向通信,没有再接着深入研究,决定放弃tun模式使用tap模式,而使用tap模式需要首先创建网桥。

进行桥接配置

同样的使用tap模式的教程文章一大把,看起来都非常的繁琐也写的不清楚,最后还是求助于OpenVPN官网的教程:https://openvpn.net/community-resources/ethernet-bridging/

按照官方教程,我简单总结了一下使用方法:

首先找一台有两张网卡的服务器(一张网卡不知道行不行,没有深入研究),选其中一张做桥接。按照自己的网络情况修改bridge-start.sh中的参数。

执行这些防火墙规则(不开防火墙不知道需不需要执行,反正我执行了,也许其实没啥用):
iptables -A INPUT -i tap0 -j ACCEPT
iptables -A INPUT -i br0 -j ACCEPT
iptables -A FORWARD -i br0 -j ACCEPT

每次启动和关闭openvpn服务器都要按照这样的顺序:
run bridge-start
run openvpn
stop openvpn
run bridge-stop

官方给出的创建网桥脚本(经过修改参数适用于我的网络环境):

sample-scripts/bridge-start

#!/bin/bash

#################################
# Set up Ethernet bridge on Linux
# Requires: bridge-utils
#################################

# Define Bridge Interface
br="br0"

# Define list of TAP interfaces to be bridged,
# for example tap="tap0 tap1 tap2".
tap="tap0"

# Define physical ethernet interface to be bridged
# with TAP interface(s) above.
eth="ens18"
eth_ip="10.0.17.48"
eth_netmask="255.255.255.0"
eth_broadcast="10.0.17.255"

for t in $tap; do
    openvpn --mktun --dev $t
done

brctl addbr $br
brctl addif $br $eth

for t in $tap; do
    brctl addif $br $t
done

for t in $tap; do
    ifconfig $t 0.0.0.0 promisc up
done

ifconfig $eth 0.0.0.0 promisc up

ifconfig $br $eth_ip netmask $eth_netmask broadcast $eth_broadcast

官方给出的停止网桥脚本:

sample-scripts/bridge-stop

#!/bin/bash

####################################
# Tear Down Ethernet bridge on Linux
####################################

# Define Bridge Interface
br="br0"

# Define list of TAP interfaces to be bridged together
tap="tap0"

ifconfig $br down
brctl delbr $br

for t in $tap; do
    openvpn --rmtun --dev $t
done

通过外网端口转发将内网的服务映射到外网vps上

/usr/sbin/openvpn –cd /etc/openvpn/ –config server.conf 启动openvpn服务后,10.0.17.48就开始提供vpn服务了,但是不在公司内的外网工作人员并不能访问内网地址,就需要一个固定的外网地址进行转发,因为一直有使用ssh的端口转发功能,所以最先想到了ssh,由于ssh端口转发只支持tcp,所以只能使用tcp模式启动openvpn:

根据网络情况修改参数:ssh -fN -R19028:127.0.0.1:19028 root@120.100.1.1 -p22

这样客户端通过连接120.100.1.1:19028 就可以访问vpn服务了。

至此整套vpn就搭建完了,即使不在公司内,在家也能接入公司的网络使用svn,redmine等等开发工具了。但是在使用过程中发现一个问题,vpn经常会断开,不是某个客户端自己断开,而是整个服务器停止服务,所有客户端全部都会掉线。阅读了官网关于tcp/udp模式的介绍:https://openvpn.net/faq/why-does-openvpn-use-udp-and-tcp/ 原来tcp模式并不是正常的模式,官方的原话是tcp并不稳定,并不推荐使用tcp,但是本着不好用总比没有强还是支持了tcp模式,因为tcp-over-tcp会造成tcp meltdown,两层tcp造成的超时叠加重传会导致tcp熔断,整个连接就over了。
又回到寻找转发工具的路上,经过比较后选择了frp:https://github.com/fatedier/frp 这个国人开发的工具(貌似codis也是同一个人开发的)支持udp的转发,并支持底层传输使用kcp(kcp基于udp),这样整个链路连通后最多只有一层tcp协议,不会发生tcp meltdown。frp的使用也很简单,需要在外网vps上启动frps:./frps -c frps.ini,内网openvpn服务器启动frpc:./frpc -c frpc.ini

frps.ini:

[common]
bind_port = 19027
kcp_bind_port = 19027

frpc.ini:

[common]
server_addr = 120.100.1.1
server_port = 19027
protocol = kcp
[openvpn]
type = udp
local_ip = 127.0.0.1
local_port = 19028
remote_port = 19028

这样frp通过19027作为底层传输19028端口的内容,实现udp协议的转发。

最后把openvpn的配置贴一下备忘 /etc/openvpn/server.conf:

port 19028
proto udp
dev tap0
user nobody
group nobody
persist-key
persist-tun
keepalive 10 120
topology subnet
server-bridge 10.0.17.48 255.255.255.0 10.0.17.150 10.0.17.253
ifconfig-pool-persist ipp.txt
push "dhcp-option DNS 114.114.114.114"
push "dhcp-option DNS 114.114.114.115"
push "route 10.0.0.0 255.255.0.0"
client-to-client
dh none
xxx
xxx
xxx
...

客户端权限文件xxx.ovpn:

client
proto udp
remote 120.100.1.1 19028
dev tap
resolv-retry infinite
nobind
persist-key
persist-tun
remote-cert-tls server
verify-x509-name server_B5pwaF04ayWqXebM name
auth SHA256
auth-nocache
cipher AES-128-GCM
tls-client
tls-version-min 1.2
tls-cipher TLS-ECDHE-ECDSA-WITH-AES-128-GCM-SHA256
verb 3
xxx
xxx
xxx
...

MySQL表分区的实验

MySQL自带一项表分区的功能,可以将一张表的表文件拆分为多个文件,提高大表的IO效率,最近做了一些实验,验证了一下表分区的可用性
MySQL官方文档:

关于分区的全面介绍
分区键的约束,看了很多网上的文章没看懂,看了官方的解释才明白是怎么回事

什么是MySQL表分区

mysql数据库中的数据是以文件的形式存在磁盘上的,一张表主要对应着两个个文件,一个是frm存放表结构的,一个是ibd存放表数据和索引的。如果一张表的数据量太大的话,那么idb就会变的很大,查找数据就会变的很慢,这个时候我们可以利用mysql的分区功能,在物理上将这一张表对应的两个个文件,分割成许多个小块,这样呢,我们查找一条数据时,就不用全部查找了,只要知道这条数据在哪一块,然后在那一块找就行了。如果表的数据太大,可能一个磁盘放不下,这个时候,我们可以把数据分配到不同的磁盘里面去。

MySQL表分区实验

我首先根据mysql的表分区功能创建了两个实验表,结构一样,只是一个表分区,另一个不分区。

不分区的表tbl_test_nosub:

CREATE TABLE `tbl_test_nosub`  (
`fld_record_id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '记录号',
`fld_create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间(local_time)',
`fld_modif_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '最后修改时间',
`fld_deleted` tinyint(4) NOT NULL DEFAULT 0 COMMENT '删除标记(非0表示已经删除了)',
`fld_value_str` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL DEFAULT '',
`fld_value_int` int(255) NOT NULL DEFAULT 0,
`fld_value_key` int(255) NOT NULL DEFAULT 0,
PRIMARY KEY (`fld_record_id`) USING BTREE,
INDEX `idx_key`(`fld_value_key`) USING BTREE
) ENGINE = InnoDB  CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Compact;

分区的表tbl_test_sub:

CREATE TABLE `tbl_test_sub`  (
`fld_record_id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '记录号',
`fld_create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间(local_time)',
`fld_modif_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '最后修改时间',
`fld_deleted` tinyint(4) NOT NULL DEFAULT 0 COMMENT '删除标记(非0表示已经删除了)',
`fld_value_str` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL DEFAULT '',
`fld_value_int` int(255) NOT NULL DEFAULT 0,
`fld_value_key` int(255) NOT NULL DEFAULT 0,
PRIMARY KEY (`fld_record_id`) USING BTREE,
INDEX `idx_key`(`fld_value_key`) USING BTREE
) ENGINE = InnoDB  CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Compact PARTITION BY HASH (fld_record_id)
PARTITIONS 4
(PARTITION `p0` MAX_ROWS = 0 MIN_ROWS = 0 ,
PARTITION `p1` MAX_ROWS = 0 MIN_ROWS = 0 ,
PARTITION `p2` MAX_ROWS = 0 MIN_ROWS = 0 ,
PARTITION `p3` MAX_ROWS = 0 MIN_ROWS = 0);

通过对两张表插入同样的递增数据5000万条来测试查询效率,用我自己写的python脚本分别对两张表查询1万次,tbl_test_sub耗时8秒左右,而tbl_test_nosub耗时5秒左右。

"SELECT * FROM `%s` WHERE `fld_record_id` = %d;" (fld_record_id随机1到5000w)

按照mysql官方的文档,表分区后,这种简单的唯一条件查询(而且条件是主键)应该会触发所谓的分区修剪,只查询相对应的分区,减少查询时间。可是在我的测试中分区表的查询速度反而降低了。
尝试重建了分区,将分区数量加大:

Alter table tbl_test_sub partition by key(fld_record_id)  partitions 24; //重建分区表大概花费了700秒

然后尝试用python脚本分别对两张表查询10万次,tbl_test_sub耗时87s左右,而tbl_test_nosub耗时81s左右。分区后的表查询速度还是慢的。
为了减少python本身对mysql效率的影响,我又改为用存储过程来测试查询效率:

DROP PROCEDURE IF EXISTS test_select;

DELIMITER && 
CREATE PROCEDURE load_part()
BEGIN
    DECLARE i INT;
    SET i=1;
    WHILE i<100000
    DO
        SELECT * FROM tbl_test_sub WHERE fld_record_id = i*100;
    SET i=i+1;
    END WHILE;
END&&
DELIMITER ;

CALL test_select;

我的python脚本每次查询的条件 fld_record_id 都是随机的,想在存储过程中也使用这种方法,但是不知为何mysql自己的RAND()函数执行效率非常差,执行一次需要30s,所以改为定数查询,这样应该也更客观一点。可能由于innodb缓存的作用,每查询一次速度就会快一些,三四次之后查询时间会稳定在一个值,tbl_test_sub五次查询的时间分别是 36s,37s,22s,24s,22s、tbl_test_nosub五次查询的时间分别是 31s,32s,21s,21s,21s。差别并不是很大,但是分区之后的查询还是慢一些。如果把条件从fld_record_id分区键更换为普通索引fld_value_key的话,tbl_test_sub五次查询的时间分别是 51s,43s,42s,42s,42s、tbl_test_nosub五次查询的时间分别是 51s,27s,25s,25s,25s。可以看到差距还是非常大的,因为分区键首先进行了分区定位,进行了分区修剪,只会在数据存在的分区内进行查找,而非分区键数据列虽然也做了索引,但是不能定位分区,最坏的情况下会在所有的分区中进行查找。通过explain的结果可以证实这一点:

EXPLAIN PARTITIONS SELECT * FROM tbl_test_sub WHERE fld_value_key = 1;
EXPLAIN PARTITIONS SELECT * FROM tbl_test_nosub WHERE fld_value_key = 1;
EXPLAIN PARTITIONS SELECT * FROM tbl_test_nosub WHERE fld_record_id = 1;
EXPLAIN PARTITIONS SELECT * FROM tbl_test_sub WHERE fld_record_id = 1;

MySQL官方文档中关于表分区的优点是这样说的(来源于https://dev.mysql.com/doc/refman/5.6/en/partitioning-overview.html):

1,表分区功能可以使一张表中存储的数据比单个磁盘或文件系统分区中存储的数据更多。

2,通过删除仅包含该数据的一个或多个分区,通常可以轻松地从分区表中删除失去其用途的数据。相反,在某些情况下,通过添加一个或多个用于专门存储该数据的新分区,可以大大简化添加新数据的过程。

3,满足以下条件的某些查询可以大大优化:满足给定WHERE子句的数据只能存储在一个或多个分区上,这会自动从搜索中排除任何剩余的分区。由于可以在创建分区表之后更改分区,因此您可以重新组织数据以增强在首次设置分区方案时可能不会经常使用的频繁查询。排除不匹配分区(以及因此包含的任何行)的能力通常称为分区修剪。

关于查询效率的解释上,我的实验应该符合官方的说法,但是并没有提高查询效率,可能是数据文件拆分后从小块文件中查询得到的效率提升并没有定位分区消耗的效率大。据说一颗3层的B+树可以承载的数据是2000W,超过2000W应该会增加到4层,但是通过查询MySQL的数据我的实验数据表已经达到了5000W条,依然是3层:

B+树PAGE_NO

SELECT
b.name,a.name,index_id,a.space,a.PAGE_NO
FROM
information_schema.INNODB_SYS_INDEXES a,
information_schema.INNODB_SYS_TABLES b
WHERE
a.table_id = b.table_id and a.space <> 0;

使用innodb_space查看数据表所在页面的B+树高度(innodb_space在centos6上如何安装):

# innodb_space -f ./tbl_test_nosub.ibd space-index-pages-summary | head -n 10
page        index   level   data    free    records 
3           179     2       3298    12860   194     
4           180     2       2100    14122   100     
5           179     0       7680    8496    152     
6           179     0       15047   1067    293     
7           179     0       15037   1077    292     
8           179     0       15077   1037    292     
9           180     0       15283   521     899     
10          180     0       15283   521     899     
11          179     0       309     15943   6       
# innodb_space -f ./tbl_test_sub#P#p0.ibd space-index-pages-summary | head -n 10
page        index   level   data    free    records 
3           273     2       289     15959   17      
4           274     2       168     16082   8       
5           273     0       16011   125     310     
6           273     0       9424    6760    182     
7           273     0       13832   2322    267     
8           273     0       9423    6749    182     
9           274     0       9010    7054    530     
10          274     0       13685   2285    805     
11          273     0       6625    9579    128 

page_no为3和4的page_level是2,因为根level是0,所以这个树高度是3层;

所以在我的实验中,5000W的数据表虽然进行了分区,但是B+树的层数依然是3层,在IO次数上没有变化,所以IO上提升的效率基本没有,但是由于定位分区需要开销,所以总体来说查询效率是下降的。我的结论是,也许数据量超过一定数量级后B+树会变为4层,这样一次分区表的查询会减少一次IO操作,也许会大于定位分区带来的开销,这样总体查询效率可能有提升。但是表分区总体解决的是表容量的问题,适合按时间分区,比如按不同年份分区来区分热点数据和非热点数据。

记一次MySQL远程备份恢复,并重新设置主从过程

远程备份

安装percona-xtrabackup:

yum install https://repo.percona.com/yum/percona-release-latest.noarch.rpm
yum install perl-DBI perl-DBD-MySQL perl-Digest-MD5 perl-IO-Socket-SSL perl-TermReadKey
yum install percona-xtrabackup-24

备份,将本地的数据库数据备份到远程机器

innobackupex  --defaults-file=/etc/my.cnf  --defaults-group=mysqld4306 --user=backup --socket=/tmp/mysql_4306.sock  --no-lock  --no-timestamp  --stream=tar ./ | ssh root@120.1.100.100 -p22 \ "cat - > /data/service/mysql_4306.tar"

在远程机器上解压,然后恢复数据(恢复完之后会得到binlog文件的名字和当前binlog序号,下面要用)

tar -xvf mysql_4306.tar
innobackupex --user root --password 'your_mysql_pwd'  --apply-log /data/service/mysql_4306

把数据移动到mysql并修改文件夹用户属性

mv /data/service/mysql_4306/* /data/mysql_db/4306/
chown -R mysql:mysql /data/mysql_db/4306/

启动mysql,并设置主从

/etc/rc.d/init.d/mysqld_multi start 4306

用得到的binlog信息在mysql中重新设置主从(InnoDB: xtrabackup: Last MySQL binlog file position 202221999, file name mysql-bin.071479)

CHANGE MASTER TO MASTER_HOST = '172.31.0.1', MASTER_PORT =4306, MASTER_USER='slave_user', MASTER_PASSWORD='slave_user', MASTER_LOG_FILE='mysql-bin.071479', MASTER_LOG_POS=202221999;

启动从服务

start slave;

查看从库状态

show slave status;

看到两个yes表示主从已经正常了。

记一次线上亿级表数据库合服

合服

我们的项目线上运营了三年多,经历了很多次开新服,合服的过程,至此有一些服已经多次合服,并入了大量的数据,这些数据中大部分已经成为僵尸数据,已经影响了服务启动速度和普通玩家体验。现在运营要求将两个已经拥有海量数据的服合并,如果这种级别的两个库合并的话无疑会造成很多隐患,也许会无法支持业务,所以在跟运行协商后决定将库中一些只进行了新手阶段体验且没有进行充值的大量僵尸玩家删除,如果这些玩家回来,重新创建角色差别也不是很大。这不是最好的解决方法,但是鉴于项目整体条件和环境,也算是一个可行的方案。

由于一些历史遗留原因和设计与实际生产产生的偏差,单个数据库实例中某些表已经变的非常庞大,数据量超过亿条,已经出现一些200ms的慢查询。将两张亿级的数据表进行合并肯定不是一个好的选择,只能将一些僵尸数据进行转移或者删除,这里在跟运营协商后直接删除了,编写了python脚本在线上环境中删除数据,尽量保证删除过程不会对表进行长时间写锁定,保证线上项目的稳定。 长时间的数据删除结束后,表数据应该减少了3/4左右,但是数据库表文件的文件大小并没有改变。 原来mysql在删除数据后并不会进行磁盘整理,这些被删掉的数据空间会被下次写入时复用。 如果需要整理碎片要进行手动操作:

alter "ENGINE=InnoDB" D=db_name,t=tbl_name

表本身存储引擎就是InnoDB,执行这样一个类似空转的语句可以让mysql对数据表进行碎片整理。但是这个语句肯定会锁表,在维护期间执行的话时间恐怕也不够,所以用pt来做这件事情

安装和使用pt-osc

wget https://www.percona.com/downloads/percona-toolkit/3.0.13/binary/redhat/6/x86_64/percona-toolkit-3.0.13-1.el6.x86_64.rpm
yum install -y percona-toolkit-3.0.13-1.el6.x86_64.rpm


pt-online-schema-change --user=root --password='your_mysql_pwd' --socket=/tmp/mysql_4306.sock --alter "ENGINE=InnoDB" D=db_name,t=tbl_name --execute

经过漫长的等待后执行完毕,表文件占用空间减少了很多。解决了所有表的空间占用后,两个本身数据量非常大的库瘦身了很多,之后再合服压力就没那么大了,基本在可控范围内。

C++代码静态检查

代码静态检查

人生要不断完善自己,项目也一样,最近用到了代码静态检查工具来检测了一下项目代码,用了两个工具,linux下的cppcheck和windows下的TscanCode,用法都很简单,就不细说了,最后得出的结果还是有一些区别的,大部分是
真实存在的代码问题。

cppcheck的结果像这样:

[haizhan_src_2/ActivityTask/ActivityTask.cpp:48]: (warning) Member variable 'CActivityTask::tmExpire' is not initialized in the constructor.
[haizhan_src_2/ActivityTask/ActivityTask.cpp:48]: (warning) Member variable 'CActivityTask::bSendInsert' is not initialized in the constructor.
[haizhan_src_2/ActivityTask/ActivityTask.cpp:48]: (warning) Member variable 'CActivityTask::bDBInsert' is not initialized in the constructor.
[haizhan_src_2/Email/SystemEmailManager.cpp:407] -> [haizhan_src_2/Email/SystemEmailManager.cpp:404]: (error) Iterator 't_it' used after element has been erased.
[haizhan_src_2/userinfo_export/userinfo_export_serviceService.cpp:379]: (error) Dereferencing '_pmapRoleInfos' after it is deallocated / released

它会给出一些warning和error,包括未初始化的变量,数组越界使用,失效的迭代器,无效的函数等等等等。

TscanCode的结果:

<?xml version="1.0" encoding="UTF-8"?>
<results>
    <error file="E:\Work\Server\haizhan_src\ArenaMain\ArenaSceneDead.cpp" line="255" id="nullpointer" subid="dereferenceAfterCheck" severity="error" msg="[_pPlayerFlag.player_before] is dereferenced here after checking null at line 250, which implies that [ _pPlayerFlag.player_before ] may be null dereferenced." web_identify="{&quot;identify&quot;:&quot;_pPlayerFlag.player_before&quot;}" func_info="int CArenaSceneDead::check_FlagTimeValue ( int _unTimeInterval , TFlagPlayerInfo * _pPlayerFlag )"/>
    <error file="E:\Work\Server\haizhan_src\BattleMatch\BattleMapMatch.cpp" line="1265" id="nullpointer" subid="dereferenceAfterCheck" severity="error" msg="[_pOtherMatchTeam] is dereferenced here after checking null at line 1194, which implies that [ _pOtherMatchTeam ] may be null dereferenced." web_identify="{&quot;identify&quot;:&quot;_pOtherMatchTeam&quot;}" func_info="void CMapMatch::RepairBalance ( TMatchTempCampVec &amp; _stMatchTempCampVec , TMatchTempElo &amp; _stElo , bool _bUseOneTeam , bool _bUsePrioryTeam , CMatchTeamMap * _pOtherMatchTeam , int _eOtherMatchType )"/>
    <error file="E:\Work\Server\haizhan_src\Warship\ExtraPartPlugin.cpp" line="707" id="nullpointer" subid="dereferenceAfterCheck" severity="error" msg="[t_consume] is dereferenced here after checking null at line 700, which implies that [ t_consume ] may be null dereferenced." web_identify="{&quot;identify&quot;:&quot;t_consume&quot;}" func_info="void CExtraPartPlugin::deduct_RankUpConsumables ( int _consumeid , const char * _szReason , int _unReasonParam )"/>
    <error file="E:\Work\Server\haizhan_src\xmlloader\MatchBuyDataManager.cpp" line="30" id="logic" subid="uninitMemberVar" severity="warning" msg="Member variable &apos;TRecommData::pIStdMatchBuyMain,&apos; is not initialized in the constructor." web_identify="{&quot;identify&quot;:&quot;TRecommData::pIStdMatchBuyMain,&quot;}" func_info=""/>
    <error file="E:\Work\Server\haizhan_src\xmlloader\StdListener.cpp" line="110" id="logic" subid="SignedUnsignedMixed" severity="warning" msg="Unsigned to signed assignment occurs." web_identify="" func_info="void Prop::set_Param1 ( int param )"/>
</results>

同样会给出很多错误,但是比cppcheck更加详细,标明了错误的类型,子类型,错误内容。

总结

两种工具各有千秋,不过个人认为还是TscanCode更好用,报告的内容也更全,腾讯还是做点好事的。。。

跟某个云厂商的长期扯皮...

云服务器效率急降

从2017年6月开始,云服务器出现了不同程度的效率急降,通常是突然cpu能力和IO效率在几秒内降低5成,表现到程序上就是游戏服务器程序突然cpu100%,非常可怕,找到云厂商反应问题,得到的答复是他们没问题,要我自己检查自己的程序。每次出问题通过sar命令观察系统的效率,都可以看到明显的问题。这个问题断断续续在一年的时间中出现了不下10次,最近该厂商终于承认是自己的问题,他们的解释是同一台物理机上其他虚拟机抢占了我们虚拟机的资源,导致我们的云服务器效率下降。 记录一下几个关键sar命令,可以清楚的看到系统的各种统计信息,这些信息可以很清楚的表明,服务器的效率问题跟CP的服务器程序没有关系。


sar -B

sar -B

pgpgin/s:每s从磁盘换入的页的大小(KB)
pgpgout/s:每s换出到磁盘的页的大小(KB)
fault/s:每s发生的缺页错误的次数,包括minor fault和major fault。
majflt/s:每s发生的major fault的次数,major fault会导致从磁盘载入内存页(即使用了swap分区)。
pgfree/s:每s放入空闲列表中的页的个数。
pgscank/s:每s被kswapd后台进程扫描的页的个数。
pgscand/s:每s直接被扫描的页的个数。
pgsteal/s:为了满足内存要求,每s从cache(pagecache和swapcache)回收的页的个数。
%vmeff:等于pgsteal  /  pgscan,用于计算页回收(page reclaim)的效率。


sar -q

sar -q

runq-sz:处于运行或就绪的进程数量
plist-sz:现在进程的总数(包括线程).
ldavg-1:最近一分钟的负载.
ldavg-5:最近五分钟的负载.
ldavg-15:最近十分钟的负载.


sar -n DEV

sar -n DEV

IFACE:LAN接口
rxpck/s:每秒钟接收的数据包
txpck/s:每秒钟发送的数据包
rxbyt/s:每秒钟接收的字节数
txbyt/s:每秒钟发送的字节数
rxcmp/s:每秒钟接收的压缩数据包
txcmp/s:每秒钟发送的压缩数据包
rxmcst/s:每秒钟接收的多播数据包


通过一次redis防火墙配置来总结一下iptables

iptables

项目上线了很多国家和地区,不同地区的发行商提供的云服务器都不太一样,亚马逊云算好的,好多乱七八糟的地方云厂商提供的各种云服务器的配置都不太一样,很多机器拿来用的时候经常会遇到关于iptables的奇怪问题,时间久了我一般都是拿到机器后直接先把iptables stop 掉。 直到项目上使用了redis之后,我发现redis在安全性上并不强壮,必须要借助iptables来辅助一下,记录一下iptables的使用情况。

iptables 配置文件路径
/etc/sysconfig/iptables
#修改后重启服务
service iptables restart

redis服务器上(codis)的iptables配置:

# Generated by iptables-save v1.4.7 on Sat May  7 13:57:07 2016
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [1:140]
-A INPUT -s 120.1.1.1/32 -p tcp --dport 19000 -j ACCEPT
-A INPUT -s 120.2.2.2/32 -p tcp --dport 19000 -j ACCEPT
-A INPUT -s 120.3.3.3/32 -p tcp --dport 19000 -j ACCEPT
-A INPUT -p tcp --dport 19000 -j DROP
COMMIT
# Completed on Sat May  7 13:57:07 2016

这个配置的作用是,允许120.1.1.1,120.2.2.2,120.3.3.3这三台服务器访问19000端口,然后用-A INPUT -p tcp --dport 19000 -j DROP禁止其他所有地址对19000端口的访问,这样的话就只有这三台ip的机器可以访问redis的19000端口。为了实现这个需求走了一些弯路,一开始我的配置是这样的:

# Generated by iptables-save v1.4.7 on Sat May  7 13:57:07 2016
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [1:140]
-I INPUT -s 120.1.1.1/32 -p tcp --dport 19000 -j ACCEPT
-I INPUT -s 120.2.2.2/32 -p tcp --dport 19000 -j ACCEPT
-I INPUT -s 120.3.3.3/32 -p tcp --dport 19000 -j ACCEPT
-I INPUT -p tcp --dport 19000 -j DROP
COMMIT
# Completed on Sat May  7 13:57:07 2016

这个配置始终“不生效”,没有达到我想要的结果,其实我并没有搞懂-A和-I的区别,-A是添加一条命令到防火墙规则的后面,类似stl中的push_back,而-I则类似stl中的push_front。 而iptables是根据规则从前到后进行解释的,也就是说如果用-I的话相当于第一条就执行了-I INPUT -p tcp --dport 19000 -j DROP ,这样的话后面不管写什么规则都没有用了。。。


一些其他关于iptables的使用经验

允许来自外部的ping测试
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT
iptables -A OUTPUT -p icmp --icmp-type echo-reply -j ACCEPT

记一次协程中使用线程锁造成的问题以及各种锁的总结

我习惯把进程或线程同步互斥的控制方法称之为”锁”,自旋锁、互斥锁、信号量、临界区各种各样的锁一大堆,这些名字看起来都特别高大上,但是其实他们就是实现了线程间对共享数据的一个安全访问,有些锁是可以用在不同进程的线程间的,比如互斥锁和信号量。有些锁可以设置进入这个锁的次数,比如信号量。如果你的程序不需要在多个进程的线程之间加锁,那么使用临界区就可以了。


我们的项目使用的大部分是临界区,在codis和协程上线后出现了一个问题,由于每个玩家是一个单独的协程,在多线程处理一块共享数据的时候,不同的协程进入同一个函数进行加锁操作,因为函数的后续进行了redis的异步访问,这些协程可能在没有解锁的情况下被挂起,如果这个函数的访问次数增多,那么其他需要处理共享数据的线程就会等待,造成某些逻辑的卡死。由于第一时间就想到了该问题的解决办法,所以并没有继续深入研究导致程序假死的具体步骤。

记录一下出问题的函数以及解决后的函数:

///获得角色的摘要信息指针
TRoleData * CRoleDataService::FindRoleData(const std::string & _roleid)
{
    SVR_BASE::CSVRCriticalEnter t_autolock(&m_lock);
    RoleMap::const_iterator t_it = m_mapRoleByRoleId.find(_roleid);
    if(t_it == m_mapRoleByRoleId.end())
    {
        SVR_BASE::coroutine_t t_coroutine = SVR_BASE::co_current();
        MASSERT( t_coroutine!=NULL );
        TRoleData *    t_ptrRole = new TRoleData();
        t_ptrRole->strRoleId = "";
        if ( true == CRoleDataService::Instance._redis_command_cb( REDIS_CLT::HGETALL(ROLE_SUMMARY::_SUMMARY_ROLE_+_roleid), 
            std_tr1::bind(_co_rolesummary_callback, t_coroutine, t_ptrRole, std_tr1::_1 )))
        {
            SVR_BASE::co_wait_io_complete();
        }
        if("" != t_ptrRole->strRoleId)
        {
            m_mapRoleByRoleId.insert(std::make_pair(_roleid,t_ptrRole));
        }
        else
        {
            delete t_ptrRole;
            t_ptrRole = NULL;
        }
        return t_ptrRole;
    }
    else
    {
        return t_it->second;
    }
}

上面的函数使用了我们项目中自动释放的临界区对象SVR_BASE::CSVRCriticalEnter来对m_mapRoleByRoleId数据进行加锁,这个对象的生命周期到函数退出为止,但是 SVR_BASE::co_wait_io_complete() 会把该协程挂起,如果这个协程不再被激活,那么锁将永远不会被解开,其他线程中用到这个锁的地方就会一直处于等待状态。修改后的代码:

///获得角色的摘要信息指针
TRoleData * CRoleDataService::FindRoleData(const std::string & _roleid)
{
    m_lock.lock();
    RoleMap::const_iterator t_it = m_mapRoleByRoleId.find(_roleid);
    if(t_it != m_mapRoleByRoleId.end())
    {
        m_lock.unlock();
        return t_it->second;
    }
    m_lock.unlock();
    SVR_BASE::coroutine_t t_coroutine = SVR_BASE::co_current();
    MASSERT( t_coroutine!=NULL );
    TRoleData *    t_ptrRole = new TRoleData();
    t_ptrRole->strRoleId = "";
    if ( true == CRoleDataService::Instance._redis_command_cb( REDIS_CLT::HGETALL(ROLE_SUMMARY::_SUMMARY_ROLE_+_roleid), 
        std_tr1::bind(_co_rolesummary_callback, t_coroutine, t_ptrRole, std_tr1::_1 )))
    {
        SVR_BASE::co_wait_io_complete();
    }
    if("" != t_ptrRole->strRoleId)
    {
        m_lock.lock();
        t_it = m_mapRoleByRoleId.find(_roleid);
        if ( t_it!= m_mapRoleByRoleId.end() )
        {
            delete t_it->second;
            m_mapRoleByRoleId.erase(t_it);
        }
        m_mapRoleByRoleId.insert(std::make_pair(_roleid,t_ptrRole));
        m_lock.unlock();
    }
    else
    {
        delete t_ptrRole;
        t_ptrRole = NULL;
    }
    return t_ptrRole;
}

修改后的代码只在操作m_mapRoleByRoleId这块共享数据的时候进行了加解锁操作,保证了锁不会被协程挂起。

codis(下)

Codis 在程序中的使用

前两篇文章记录了codis的安装和启动,以及服务器集群的架设,这一篇记录一下codis在工程中的使用情况。

python测试程序

用python连接codis服务器的测试程序,测试了一些简单的命令和简单排行榜。连接的ip地址是codis组件中proxy的地址,proxy接受的命令并不是全部是redis命令,具体情况参考官方文档

https://github.com/CodisLabs/codis/blob/release3.2/doc/unsupported_cmds.md)


import redis   # 导入redis模块,通过python操作redis 也可以直接在redis主机的服务端操作缓存数据库

r = redis.Redis(host='127.0.0.1', port=6379, decode_responses=True)   # host是redis主机,需要redis服务端和客户端都启动 redis默认端口是6379

#r.set('name2', 'warship3')  # key是"foo" value是"bar" 将键值对存入redis缓存
# print(r['name2'])
# print(r.get('name2'))  # 取出键name对应的值
# print(type(r.get('name2')))

#r.hmset("myhash",{"field1":"Hello","field2":"World"}) 
# r.zadd("499-rank-100",5,111)
# r.zadd("499-rank-100",2,112)
# r.zadd("499-rank-100",30,113)
# r.zadd("499-rank-100",10,114)

#ZRANGE myzset 0 -1 WITHSCORES

# print(type(r.zrange("499-rank-100",0,3,withscores=True)))
# print(r.zrange("499-rank-100",0,3,withscores=True))
# print(r.zrevrange("499-rank-100",0,3,withscores=True))
# print(r.zrevrank("499-rank-100",10))

print(r.zrevrange("1014-rank-4",0,100,withscores=True))
print(r.zrevrank("1014-rank-4",10))

项目中的实际使用

首先我们的项目在没有引入codis之前,所有的数据全部存储在mysql中,玩家摘要和排行榜这些请求量比较大的数据都是在程序初始化的时候全部加载到内存中的,引入codis后,需要把这些数据全部在codis中做一份拷贝(用python做这类工作非常效率,因为我们的mysql数据中每一张数据表都有一个modify_time字段,标识了该条数据的上一次修改时间,所以只要根据这个字段就可以在不停服的情况下做前期的数据拷贝,停服后只需要同步最后一点时间内修改的数据就好了),工程中使用这些数据的时候需要把mysql和codis中的数据同步更新,由于mysql的数据更可信,所以在mysql和redis的数据出现不匹配的情况下,选择mysql中的数据,而大量对摘要和排行榜数据的请求全部交给codis处理,这样不仅分离了项目主程序和摘要排行榜数据,也加快了读取这些数据的速度。

上线后codis的表现比较好,没有出现过什么问题,如果各个group的内存使用不太平均,还可以使用codis网页工具提供的功能对slot进行动态转移,把内存量使用大的group中的slot分配到其它group中。

codis(中)

Codis Dashboard 的配置和使用

前一篇文章记录了codis的安装和启动,这一篇记录一下codis的使用。 codis的环境布置好之后需要用codis的网页来进行一系列配置,包括redis组的配置和代理地址的添加等等

管理页面的主页,显示了当前使用的内存量,连接数,redis中存储的key的数量和每秒请求数


主页


在 Proxy 栏可看到我们已经启动的 Proxy, 但是 Group 栏为空,因为我们启动的 codis-server 并未加入到集群 添加 NEW GROUP,NEW GROUP 行输入 1,再点击 NEW GROUP 即可 添加 Codis Server


代理


添加实例(即添加后端的redis服务器)
Add Server 行输入我们刚刚启动的 codis-server 地址,添加到我们刚新建的 Group,然后再点击 Add Server 按钮即可


redis组


对slot初始化要分组,之后可以动态调整slot的归属

codis(上)

Redis集群,Codis的安装和使用

项目上经过了多次合服和长达两年多的运营,单服玩家已经超过了600W,项目最原始的架构是将所有玩家的摘要数据在服务器启动时从数据库中读取,如果只有几万人或者及几十万人,启动速度基本上就是一两分钟的样子,没有什么压力,但是600W玩家的所有摘要数据全部加载需要的时间可能超过几十分钟,为此项目使用了Redis,将玩家摘要数据和排行榜数据全部都转移到了Redis中,我使用了Codis,基于go开发的redis集群,可以无限扩展Redis的内存大小。记录一下部署和使用Codis的全过程。

Codis

安装环境

codis-proxy相当于redis,即连接codis-proxy和连接redis是没有任何区别的,codis-proxy无状态,不负责记录是否在哪保存,数据在zookeeper记录,即codis proxy向zookeeper查询key的记录位置,proxy 将请求转发到一个组进行处理,一个组里面有一个master和一个或者多个slave组成,默认有1024个槽位,其把不同的槽位的内容放在不通的group。codis是基于go语言编写的,因此要安装go语言环境。每台服务器安装java环境和zookeeper,zookeeper集群最少需要3台服务器。

安装 zookeeper

wget http://219.238.7.67/files/205200000AF1D7B9/mirrors.hust.edu.cn/apache/zookeeper/zookeeper-3.4.11/zookeeper-3.4.11.tar.gz
tar -xzf zookeeper-3.4.11.tar.gz -C /data/service

安装 java

tar zxf jdk-8u131-linux-x64.gz
mv jdk1.8.0_131 /usr/local/

安装 go 编译环境

# cd /usr/local/src
[root@node1 src]# yum install -y gcc glibc gcc-c++ make git
[root@node1 src]# wget https://storage.googleapis.com/golang/go1.7.3.linux-amd64.tar.gz
[root@node1 src]# tar zxf go1.7.3.linux-amd64.tar.gz
[root@node1 src]# mv go /usr/local/
[root@node1 src]# mkdir /usr/local/go/work
[root@node1 src]# vim /root/.bash_profile

export PATH=$PATH:/usr/local/go/bin
export GOROOT=/usr/local/go
export GOPATH=/usr/local/go/work
path=$PATH:$HOME/bin:$GOROOT/bin:$GOPATH/bin
[root@node1 src]# source /root/.bash_profile

[root@node1 src]# echo $GOPATH
/usr/local/go/work
[root@node1 ~]# go version
go version go1.7.3 linux/amd64

zookeeper配置文件

# 中使用的基本时间单位, 毫秒值.
tickTime=2000
# follower和leader之间的最长心跳时间 initLimit * tickTime 毫秒超时
initLimit=10
# 
# follower和leader之间 同步数据的最大时间 syncLimit * tickTime 毫秒超时
syncLimit=5
# 数据目录. 可以是任意目录.
dataDir=/data/service/zookeeper-data/data
# log目录
dataLogDir=/data/service/zookeeper-data/logs
# 防止启动失败一个配置,不知道干啥的
quorumListenOnAllIPs=true
# 监听client连接的端口号.
clientPort=19021
# 客户端连接的最大数量。
#maxClientCnxns=60
# 维护相关的
#autopurge.snapRetainCount=3
#autopurge.purgeInterval=1

server.1 = 0.0.0.0:19022:19029
server.2 = 172.31.0.17:19022:19029
server.3 = 172.31.0.79:19022:19029

详细解释:
tickTime:这个时间是作为 Zookeeper 服务器之间或客户端与服务器之间维持心跳的时间间隔,也就是每个 tickTime 时间就会发送一个心跳。
dataDir:顾名思义就是 Zookeeper 保存数据的目录,默认情况下,Zookeeper 将写数据的日志文件也保存在这个目录里。
clientPort:这个端口就是客户端连接 Zookeeper 服务器的端口,Zookeeper 会监听这个端口,接受客户端的访问请求。
initLimit:这个配置项是用来配置 Zookeeper 接受客户端(这里所说的客户端不是用户连接 Zookeeper 服务器的客户端,而是 Zookeeper 服务器集群中连接到 Leader 的 Follower 服务器)初始化连接时最长能忍受多少个心跳时间间隔数。当已经超过 5个心跳的时间(也就是 tickTime)长度后 Zookeeper 服务器还没有收到客户端的返回信息,那么表明这个客户端连接失败。总的时间长度就是 106000=60 秒
syncLimit:这个配置项标识 Leader 与 Follower 之间发送消息,请求和应答时间长度,最长不能超过多少个 tickTime 的时间长度,总的时间长度就是 5
6000=30 秒
server.A=B:C:D:其中 A 是一个数字,表示这个是第几号服务器;B 是这个服务器的 ip 地址;C 表示的是这个服务器与集群中的 Leader 服务器交换信息的端口;D 表示的是万一集群中的 Leader 服务器挂了,需要一个端口来重新进行选举,选出一个新的 Leader,而这个端口就是用来执行选举时服务器相互通信的端口。如果是伪集群的配置方式,由于 B 都是一样,所以不同的 Zookeeper 实例通信端口号不能一样,所以要给它们分配不同的端口号。

启动zookeeper

/data/service/zookeeper-3.4.11/bin/zkServer.sh  /data/service/zookeeper-3.4.11/conf/zoo.cfg

安装部署codis

下载编译codis3.2版本

我直接使用了编译好的一个版本

codis配置文件


/codis/config/dashboard.toml
##################################################
#                                                #
#                  Codis-Dashboard               #
#                                                #
##################################################

# Set Coordinator, only accept "zookeeper" & "etcd" & "filesystem".
# for zookeeper/etcd, coorinator_auth accept "user:password" 
# Quick Start
#coordinator_name = "filesystem"
#coordinator_addr = "/tmp/codis"
coordinator_name = "zookeeper"
coordinator_addr = "172.31.0.84:19021"
#coordinator_auth = ""

# Set Codis Product Name/Auth.
product_name = "codis-warship"
product_auth = ""

# Set bind address for admin(rpc), tcp only.
admin_addr = "0.0.0.0:18080"

# Set arguments for data migration (only accept 'sync' & 'semi-async').
migration_method = "semi-async"
migration_parallel_slots = 100
migration_async_maxbulks = 200
migration_async_maxbytes = "32mb"
migration_async_numkeys = 500
migration_timeout = "30s"

# Set configs for redis sentinel.
sentinel_client_timeout = "10s"
sentinel_quorum = 2
sentinel_parallel_syncs = 1
sentinel_down_after = "30s"
sentinel_failover_timeout = "5m"
sentinel_notification_script = ""
sentinel_client_reconfig_script = ""

启动dashboard:

[root@node1 codis]# nohup ./bin/codis-dashboard --ncpu=1 --config=config/dashboard.toml --log=dashboard.log --log-level=WARN >> /var/log/codis_dashboard.log &

/codis/config/proxy.toml
##################################################
#                                                #
#                  Codis-Proxy                   #
#                                                #
##################################################

# Set Codis Product Name/Auth.
product_name = "codis-warship"
product_auth = ""

# Set auth for client session
#   1. product_auth is used for auth validation among codis-dashboard,
#      codis-proxy and codis-server.
#   2. session_auth is different from product_auth, it requires clients
#      to issue AUTH <PASSWORD> before processing any other commands.
session_auth = ""

# Set bind address for admin(rpc), tcp only.
admin_addr = "0.0.0.0:11080"

# Set bind address for proxy, proto_type can be "tcp", "tcp4", "tcp6", "unix" or "unixpacket".
proto_type = "tcp4"
proxy_addr = "0.0.0.0:19000"

# Set jodis address & session timeout
#   1. jodis_name is short for jodis_coordinator_name, only accept "zookeeper" & "etcd".
#   2. jodis_addr is short for jodis_coordinator_addr
#   3. jodis_auth is short for jodis_coordinator_auth, for zookeeper/etcd, "user:password" is accepted.
#   4. proxy will be registered as node:
#        if jodis_compatible = true (not suggested):
#          /zk/codis/db_{PRODUCT_NAME}/proxy-{HASHID} (compatible with Codis2.0)
#        or else
#          /jodis/{PRODUCT_NAME}/proxy-{HASHID}
jodis_name = "zookeeper"
jodis_addr = "172.31.0.84:19021,172.31.0.17:19021,172.31.0.79:19021"
jodis_auth = ""
jodis_timeout = "20s"
jodis_compatible = true

# Set datacenter of proxy.
proxy_datacenter = ""

# Set max number of alive sessions.
proxy_max_clients = 1000

# Set max offheap memory size. (0 to disable)
proxy_max_offheap_size = "1024mb"

# Set heap placeholder to reduce GC frequency.
proxy_heap_placeholder = "256mb"

# Proxy will ping backend redis (and clear 'MASTERDOWN' state) in a predefined interval. (0 to disable)
backend_ping_period = "5s"

# Set backend recv buffer size & timeout.
backend_recv_bufsize = "128kb"
backend_recv_timeout = "30s"

# Set backend send buffer & timeout.
backend_send_bufsize = "128kb"
backend_send_timeout = "30s"

# Set backend pipeline buffer size.
backend_max_pipeline = 20480

# Set backend never read replica groups, default is false
backend_primary_only = false

# Set backend parallel connections per server
backend_primary_parallel = 1
backend_replica_parallel = 1

# Set backend tcp keepalive period. (0 to disable)
backend_keepalive_period = "75s"

# Set number of databases of backend.
backend_number_databases = 16

# If there is no request from client for a long time, the connection will be closed. (0 to disable)
# Set session recv buffer size & timeout.
session_recv_bufsize = "128kb"
session_recv_timeout = "30m"

# Set session send buffer size & timeout.
session_send_bufsize = "64kb"
session_send_timeout = "30s"

# Make sure this is higher than the max number of requests for each pipeline request, or your client may be blocked.
# Set session pipeline buffer size.
session_max_pipeline = 10000

# Set session tcp keepalive period. (0 to disable)
session_keepalive_period = "75s"

# Set session to be sensitive to failures. Default is false, instead of closing socket, proxy will send an error response to client.
session_break_on_failure = false

# Set metrics server (such as http://localhost:28000), proxy will report json formatted metrics to specified server in a predefined period.
metrics_report_server = ""
metrics_report_period = "1s"

# Set influxdb server (such as http://localhost:8086), proxy will report metrics to influxdb.
metrics_report_influxdb_server = ""
metrics_report_influxdb_period = "1s"
metrics_report_influxdb_username = ""
metrics_report_influxdb_password = ""
metrics_report_influxdb_database = ""

# Set statsd server (such as localhost:8125), proxy will report metrics to statsd.
metrics_report_statsd_server = ""
metrics_report_statsd_period = "1s"
metrics_report_statsd_prefix = ""

启动代理

[root@node1 codis]# nohup ./bin/codis-proxy --ncpu=1 --config=config/proxy.toml --log=proxy.log --log-level=WARN >> /var/log/codis_proxy.log &

/codis/config/redis-6380.conf redis的配置主要是改这几个

bind 172.31.0.84
port 6380
pidfile "/tmp/redis_6380.pid"
maxmemory 12G
dir "/data/service/codis"
logfile "/tmp/redis_6380.log"

通过codis-server指定redis.conf文件启动redis服务,不能通过redis命令启动redis服务,通过redis启动的redis 服务加到codis集群无法正常使用:

[root@redis1 codis]# ./bin/codis-server ../config/redis_6380.conf 

使用codis的网页服务器配置codis服务器

启动codis-fe

nohup ./bin/codis-fe --ncpu=1 --log=fe.log --log-level=WARN --zookeeper=172.31.0.84:19021 --listen=172.31.0.84:8060

以上配置是我在实际使用中的配置,官方的教程在这里:

https://github.com/CodisLabs/codis/blob/release3.2/doc/tutorial_zh.md

记一次线上程序出现mysql死锁

线上程序出现死锁

过年期间的某一天,线上程序突然宕机一次,查看core文件发现是出现了数据库死锁,项目上线这么久还是第一次碰到死锁宕机的情况,我们的项目本身并没有使用事务,为什么会出现死锁我也很懵。通过命令在数据库上查看出现死锁的日志:

mysql   -h 127.0.0.1 -P 4306 -u root -p --execute="show engine innodb status \G" > ./mysql.log

查看mysql.log的内容发现有死锁内容:

SQL ERROR:Deadlock found when trying to get lock; try restarting transaction
SQL:UPDATE tbl_fleet_science SET fld_level='8', fld_level_exp='1052182' WHERE fld_science_id='1' AND fld_fleet_id='0006-00-20160828-001006-0000008';

一共有两个地方对两行这个表中的数据进行了操作,互相等待锁释放导致死锁,仔细查了一下代码中操作这个表的语句,使用的条件顺序是WHERE fld_science_id='1' AND fld_fleet_id='0006-00-20160828-001006-0000008',先匹配fld_science_id后匹配fld_fleet_id,但是tbl_fleet_science表结构中有一个索引,索引的顺序是(tbl_fleet_science,fld_fleet_id),这个顺序和代码中的条件顺序是相反的。 查询了一些资料并没有找到权威的解释,没有明确证据表明这种相反的顺序会导致死锁,但是能找到的唯一有疑点的地方只有这里了,所以把这个表的索引做了修改,改为(fld_fleet_id,tbl_fleet_science),与代码中的使用顺序一致。

更新之后确实也没有出现过类似的问题,但是这次死锁具体是不是因为这个顺序导致的,也不能完全确定,也许是某些不可知的问题,后面的工作中如果再遇到同类问题再来解决这个问题吧。

ssh登陆管理

ssh登陆管理

我们的项目本身有大量的云服务器,装机的时候用了统一的装机脚本,云服务器的密码都是一样的,因为早期只有几个核心程序员有登陆云服务器的权限,所以并没有对这方面做管理。直到有一个核心程序员离职,不得不管理一下手头的服务器

首先所有对服务器有操作权限的人员提供自己的ssh-key秘钥对,一般使用rsa秘钥,然后把这些秘钥对中的公钥(.pub)添加到服务器的/home/service/.ssh/authorized_keys文件中,新添加的公钥直接在该文件后面添加即可。

为了提升服务器的整体安全性,我们需要对ssh进行配置:

vim /etc/ssh/sshd_config          

修改配置文件并主要修改以下几个参数

PermitRootLogin no                       #禁止root登录
PubkeyAuthentication yes                 #启用公告密钥配对认证方式
RSAAuthentication yes                    #允许RSA密钥
PasswordAuthentication no                #禁止密码验证登录,如果启用的话,OpenSSH的 RSA认证登录就没有意义了。


修改后重启

service sshd restart

如果服务器上没有.ssh文件夹,那么我们需要自己来创建:

mkdir .ssh
chmod 700 .ssh
cd .ssh
touch authorized_keys
chmod 600 authorized_keys
#将公钥内容写入文件
cat /data/service/Identity.pub >> authorized_keys

这样如果有人离职,只需要把服务器上对应人员的公钥删除就可以了。

一些其他关于ssh的记录

# 123.206.76.219:4306 是远端的ip和port,2009是远端的SSH端口,service是远端的ssh账号 
# 44306:127.0.0.1是自己本地的port和ip 
ssh -fN -L44306:127.0.0.1:4306 service@123.206.76.219 -p2009    

这个命令特别“绕”,不记录一下根本记不住…它实现的功能是ssh的端口映射,通过ssh将一个远端ip:port映射到本地,适用于非同一局域网内的机器。
在我们的项目中因为腾讯的服务器必须使用腾讯云,跟阿里云不是在一个局域网内的,但是他们之间又要有mysql的访问需求,所以通过ssh做了这个端口映射,记录一下。

记一次使用gperftools优化线上程序

gperftools的实战使用经验

项目自上线之后一直有玩家反应战斗的时候会有不太流畅的情况出现,尤其是人数多,生成战斗对象多的情况下,经过对程序的监控并没有发现什么异常,主机资源的使用上也没发现什么问题,cpu和内存的使用率都不高,网络进出流量也并没有达到峰值,cpu使用率和网络的抖动从监控上看也看不出什么问题,这个问题困扰了我很久,一直苦于无法解决,不过通过我对cpu使用率的观察,感觉cpu虽然没有长时间的抖动,但是偶尔会有一个比较大的抖动出现,时间很短,但是抖动的范围还是比较大的,最后我找到了google的性能分析工具gperftools,因为我们的程序可以分布负载,可以用多个进程将一部分大的功能负载,这些进程本身是没有什么区别的,可以随意动态增加减少,所以我拿出了一个线上程序做了这次实验,把gperftools的相关库链接到了我的程序上并添加相关代码编译后放到线上环境中去运行,运行24小时候后停止程序并分析输出报告,然后解决报告中可见的问题,解决之后再次将新的程序放到线上运行24小时,如此循环一直到输出报告中看不到明显异常为止。 在这次实验中,确实发现了几个消耗cpu特别严重的函数调用,这些函数使用频繁,而且又非常耗费cpu,如果实例对象特别多的时候确实有可能造成cpu的抖动从而影响玩家正常体验。 把这些问题都完善之后确实收到了效果,反应战斗不流畅的玩家数量大幅减少,可以说这次优化还是比较成功的,记录一下gperftools的使用方法,gperftools可以分析cpu和内存的性能,下面分别写了例子和用法。

gperftools是google出品的一个性能分析工具,相关介绍可见:
https://github.com/gperftools/gperftools/wiki
gperftools性能分析通过抽样方法完成,默认是1秒100个样本,即一个样本是10毫秒,因此程序运行时间要长一些。

编译安装gperftools

https://github.com/gperftools/gperftools/releases 下载最新版本的gperftools源码包
解压到/usr/local/src目录,编译安装

cpu性能分析

#include <google/profiler.h>
#include <iostream>
using namespace std;
void func1() 
{
    for (int i = 0; i < 100000; ++i) 
    {
        ++i;
        char *p = (char*)malloc(1024);
        free(p);
    }  
}
void func2() 
{
    for (int i = 0; i < 200000; ++i) 
    {
        char *p = (char*)malloc(1024);
        free(p);
    }  
}
void func3() 
{
    for (int i = 0; i < 100000; ++i) 
    {
        func1();
        func2();
    }  
}

int main()
{
    ProfilerStart("test.prof");//开启性能分析并指定所生成的profile文件名
    func3();
    ProfilerStop();//停止性能分析并输出结果(如果这行代码不运行,将无法输出任何数据,只有一个空文件)
    return 0; 
}

编译上面的程序文件test.cpp,连接tcmalloc和profiler
g++ -I/work/trunk/ThirdParty/include/  -L/work/trunk/haizhan/Linux64_6.3.0_Debug/commonlib/ -Wl,-rpath,/work/trunk/haizhan/Linux64_6.3.0_Debug/commonlib/   -ltcmalloc -lprofiler   test.cpp -o test
运行程序,等待程序退出后输出分析
./text
将程序输出的分析结果解析成pdf,需要安装graphviz
yum install graphviz
/work/trunk/haizhan/Linux64_6.3.0_Debug/share_bin/pprof -pdf  test test.prof  > CpuProfiler.pdf

可以看到cpu在各个函数中的消耗,上面的百分比是本函数的消耗,下面的百分比是本函数所在堆栈所有函数的消耗,可以很清楚的分析出严重消耗cpu的问题函数。

CpuProfiler.pdf

我们线上项目的分析报告已经被我删掉了。。。很遗憾没法记录当时的程序运行情况和每天优化后重新运行的分析情况。


内存性能分析

#include <gperftools/heap-profiler.h>
#include <iostream>
using namespace std;
void func1() 
{
    for (int i = 0; i < 100000; ++i) 
    {
        ++i;
        char *p = (char*)malloc(1024);
        free(p);
    }  
}
void func2() 
{
    for (int i = 0; i < 200000; ++i) 
    {
        char *p = (char*)malloc(1024);
        free(p);
    }  
}
void func3() 
{
    for (int i = 0; i < 100000; ++i) 
    {
        func1();
        func2();
    }  
}

int main()
{
    HeapProfilerStart("test.prof");//开启内存分析并指定所生成的profile文件名
    func3();
    HeapProfilerStop();//停止内存分析并输出结果(如果这行代码不运行,将无法输出任何数据,只有一个空文件)
    return 0; 
}


编译,运行并输出pdf(同CPU性能分析),不同之处在于内存分析每产生1G的内存使用就会生成一个文件,生成pdf的时候需要包含所有生成的文件
/work/trunk/haizhan/Linux64_6.3.0_Debug/share_bin/pprof -pdf  test test.prof.*.heap  > out.pdf

MemoryProfiler.pdf

官方给出的建议是虽然gperftools对程序性能的影响不是非常大,也尽量不要在线上环境使用gperftools,但是经过我的使用发现gperftools大概会降低程序30%左右的效率,如果你的程序本身占用cpu并不高,而有些问题又必须在线上真实环境下才可能表现的出来,完全可以放到线上尝试一下。

内存池tcmalloc的使用

tcmalloc

由于项目引入了协程,之前使用的内存泄漏和越界检测工具asan就不能用了,asan官方说对协程的支持不够。 所以只能换一个工具,tcmalloc是一个不错的替代品,同时tcmalloc还提供了内存管理的功能,在一个2.8GHz 4核的机器上普通malloc和free一次耗时300ns,tcmalloc同样的功能只需要50ns(数据来源于网络)。 记录一个检测内存泄漏的例子:

#test.cpp

#include <iostream>
int test()
{
    char* p = new char[100];
    return 1;
}

int main()
{
    test();
    return 1;
}


g++ -g -o test test.cpp

#设置这三个环境变量可以让程序不用重新编译

export HEAPCHECK=normal
export LD_PRELOAD=./commonlib/libtcmalloc.so
export PPROF_PATH=./share_bin/pprof

./test

WARNING: Perftools heap leak checker is active -- Performance may suffer
Have memory regions w/o callers: might report false leaks
Leak check _main_ detected leaks of 100 bytes in 1 objects
The 1 largest leaks:
Using local file ./test.
Leak of 100 bytes in 1 objects allocated from:
        @ 40074a test
        @ 40075e main
        @ 3a5341ed5d __libc_start_main
        @ 4005f9 _start


If the preceding stack traces are not enough to find the leaks, try running THIS shell command:

pprof ./test "/tmp/test.17307._main_-end.heap" --inuse_objects --lines --heapcheck  --edgefraction=1e-10 --nodefraction=1e-10 --gv

If you are still puzzled about why the leaks are there, try rerunning this program with HEAP_CHECK_TEST_POINTER_ALIGNMENT=1 and/or with HEAP_CHECK_MAX_POINTER_OFFSET=-1
If the leak report occurs in a small fraction of runs, try running with TCMALLOC_MAX_

可以看到检测出了内存泄漏的堆栈信息,以及最上一层存在泄漏的函数test()

C++中的协程(下)

ucontext协程的封装

前面介绍了unix自带的ucontext相关函数,现在记录一下我们实际项目中的使用。

我们的项目中,每个玩家在登陆时创建玩家对象CHZUser,同时通过SVR_BASE::co_create函数创建一个协程上下文对象(后面简称协程对象),协程对象的执行函数是该玩家的消息循环函数CHZUser::message_loop,

enum CoroutineStatus_t
{
    emCoroutine_closed    =0,    ///< 关闭的
    emCoroutine_WaitRun,    ///< 等待执行
    emCoroutine_Runing,        ///< 正在执行
    emCoroutine_Message,    ///< 等着消息
    emCoroutine_IOComplete    ///< 等待IO完成
};

协程的五个状态

C++中的协程(上)

c++中协程的使用

我们的项目在逻辑中的异步操作并不太多,而且使用异步本身是一件很麻烦的事情,mysql也没有封装异步的方法,但是后期一些项目上的功能需要使用异步sql查询数据库,为了适应项目的发展和需求,引入了协程。

从原理上看,协程保存了程序执行的当前位置,后续可以切换回来,即便是在一个完整函数的中间,也可以切走再切回来,像是一个用户态的线程,但是和线程不同的是,协程之间的切换需要我们自己实现。协程的好处是可以用符合思维习惯的同步写法,写出具有异步功能的高效代码,而不用像传统异步开发那样,设置各种回调函数把代码割离,弄的支离破碎很难理解。不过协程的切换虽然不会切到内核态,完全由用户来控制切换,但是协程的切换造成的开销,还是会比异步回调的方法开销大一些,但是总体来说,同步的写法实现异步这种好处还是盖过了切换带来的效率损耗。

因为c++并没有标准的协程库,所以各种实现协程的方法很多,当前比较知名的是仿go语言实现的c++库libgo,腾讯的libco,boost的coroutine,context组件,unix系统自带的ucontext等等。 这些库我只看过libco和ucontext,腾讯的libco封装的太高级,其实用到自己项目中是不太灵活的,但是libco用汇编重写了切换协程的代码,在这方面的效率是比较高的,我们项目中用了unix系统的ucontext函数,实现了基本的协程功能,在实际使用中效率不太高,但是也足够用。 记录一下ucontext的原理和使用。

ucontext

先看一个简单的例子,来自维基百科:

#include <stdio.h>
#include <ucontext.h>
#include <unistd.h>

int main(int argc, const char *argv[])
{
    ucontext_t context;
    getcontext(&context);
    puts("Hello world");
    sleep(1);
    setcontext(&context);
    return 0;
}

这个程序的运行结果是这样的:

[shafeng@localhost Desktop]$ vim test.cpp
[shafeng@localhost Desktop]$ g++ test.cpp -o test
[shafeng@localhost Desktop]$ ./test 
Hello world
Hello world
Hello world
Hello world
Hello world
Hello world
Hello world
Hello world
Hello world
Hello world
Hello world
^C
[shafeng@localhost Desktop]$ 

getcontext得到了当前的上下文,然后输出Hello world,然后调用setcontext,又跳到了调用getcontext的位置,接着输出Hello world,一直循环下去。。。。


ucontext很简洁,头文件 <ucontext.h> ,两个结构体mcontext_tucontext_t
四个函数getcontext(),setcontext(),makecontext(),swapcontext(),这些内容就可以实现协程。

mcontext_t应该是系统需要传递的参数,是用户无关的,用户只需要关心ucontext_t结构和
getcontext(),setcontext(),makecontext(),swapcontext(),下面详细介绍一个结构体和四个函数:

typedef struct ucontext {
          struct ucontext *uc_link;
          sigset_t         uc_sigmask;
          stack_t          uc_stack;
          mcontext_t       uc_mcontext;
          ...
      } ucontext_t;

ucontext_tuc_link指向后继上下文,当前上下文如果执行结束会跳转到uc_link指向的上下文中运行,如果uc_linkNULL那么当前线程直接退出。
uc_stack 设置当前上下文的堆栈大小和位置。
其他参数暂时没用到,具体用到之后再补充

int getcontext(ucontext_t *ucp);

初始化ucp结构体,将当前的上下文保存到ucp中

int setcontext(const ucontext_t *ucp);

设置当前的上下文为ucpsetcontext的上下文ucp应该通过getcontext或者makecontext取得,如果调用成功则不返回。如果上下文是通过调用getcontext()取得,程序会继续执行这个调用。如果上下文是通过调用makecontext取得,程序会调用makecontext函数的第二个参数指向的函数,如果func函数返回,则恢复makecontext第一个参数指向的上下文第一个参数指向的上下文context_t中指向的uc_link.如果uc_linkNULL,则线程退出。

void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...);

makecontext修改通过getcontext取得的上下文ucp(这意味着调用makecontext前必须先调用getcontext)。然后给该上下文指定一个栈空间ucp->stack,设置后继的上下文ucp->uc_link.

当上下文通过setcontext或者swapcontext激活后,执行func函数,argcfunc的参数个数,后面是func的参数序列。当func执行返回后,继承的上下文被激活,如果继承上下文为NULL时,线程退出。

int swapcontext(ucontext_t *oucp, ucontext_t *ucp);

保存当前上下文到oucp结构体中,然后激活ucp上下文。

如果执行成功,getcontext返回0,setcontextswapcontext不返回;
如果执行失败,getcontext,setcontext,swapcontext返回-1,并设置相应的errno.

简单说来, getcontext获取当前上下文,setcontext设置当前上下文,
swapcontext保存当前上下文并切换上下文,makecontext创建一个新的上下文。

#include <ucontext.h>
#include <stdio.h>

void func1(void * arg)
{
    puts("func1");
}
void context_test()
{
    char stack[1024*128];
    ucontext_t child,main;

    getcontext(&child); //获取当前上下文
    child.uc_stack.ss_sp = stack;//指定栈空间
    child.uc_stack.ss_size = sizeof(stack);//指定栈空间大小
    child.uc_stack.ss_flags = 0;
    child.uc_link = &main;//设置后继上下文

    makecontext(&child,(void (*)(void))func1,0);//修改上下文指向func1函数

    swapcontext(&main,&child);//保存当前上下文到main,切换到child上下文
    puts("main");//如果设置了后继上下文,func1函数指向完后会返回此处
}

int main()
{
    context_test();

    return 0;
}

这个例子中,getcontext首先获得了当前的上下文,child参数相当于一个输出,通过getcontext函数把当前上下文的信息写入到child中。然后修改了child上下文的栈空间和大小,以及后继上下文,然后调用makecontext,这时child相当于输入,将child上下文的执行位置指向func1,最后调用swapcontextmainchild都是输入,将当前的上下文保存到main中,然后切换到child上下文(此时的child上下文位置是func1函数的起点)。 这样这个程序执行后的输出就是:

[shafeng@localhost Desktop]$ vim test.cpp
[shafeng@localhost Desktop]$ g++ test.cpp -o test
[shafeng@localhost Desktop]$ ./test 
func1
main
[shafeng@localhost Desktop]$ 

可以看到当程序执行到swapcontext后,程序通过child上下文跳转到了func1,先执行了puts("func1"); 函数执行结束后跳转到child的后继上下文main,也就是调用swapcontext时保存起来的上下文,这样程序又回到了swapcontext执行结束的位置(因为swapcontext执行成功不返回,swapcontext已经执行成功,所以跳转回来之后程序的位置在swapcontext的下一句)并执行puts("main");

上面的程序在执行完child上下文后能够跳到main上下文是因为设置了child的后继上下文child.uc_link = &main,如果我们把这一句改为child.uc_link = NULL,编译执行后得到如下结果

[shafeng@localhost Desktop]$ vim test.cpp
[shafeng@localhost Desktop]$ g++ test.cpp -o test
[shafeng@localhost Desktop]$ ./test 
func1
[shafeng@localhost Desktop]$ 

可以看到child上下文在执行结束之后就直接退出了线程。 所以上下文的后继上下文很重要,否则我们的程序可能直接退出。

总结

以上就是ucontext的全部内容,非常简洁。我们项目中使用的就是ucontext,后面再记录一下项目中的实际使用情况。

关于线上程序排错的一些命令 gstack,strace,gcore(Linux)

关于Linux程序运行中查看堆栈的一些命令

最近项目上遇到一些问题,服务器程序在运行的时候cpu特别高,尤其是逻辑线程,一度达到99%。为了查找问题,使用了一些之前没有用过的命令,加上我之前用过的几个命令,写一篇记录

gstack

gstack 线程id,例如:gstack 2939,可以查看到当前线程的堆栈信息,如果程序的当前线程占cpu特别高,可能会出现死循环或者其他症状,我们可以通过使用gstack命令查看当前线程的堆栈信息来诊断问题.

strace

strace -T -r -c -p 线程id,例如:strace -T -r -c -p 2939,可以得到当前线程的所有系统调用统计,停止统计的时候必须自己手动退出,会输出使用命令到停止统计期间的信息.

gcore

gcore 进程id,例如:gcore 12345,可以得到当前进程的所有线程的堆栈信息.

一些总结,通过以上这些命令可以间接观察到程序当前的运行状态和运行位置.给予程序开发者更多的讯息以诊断程序当前可能出现的问题,有一个需要注意的地方,gstack和strace在运行期间不会影响到程序本身的运行,而gcore会将程序暂停,影响程序的正常工作.

mysql 优化配置和一些参数的解释

Mysql的一些性能优化记录

项目上经过合服等操作,单服的玩家数量一度超过500W,一些排行榜数据和玩家摘要数据在程序启动时就要从数据库中加载,数据量的增大也延长了服务器程序的启动时间,一些必要的优化就在所难免了.贴一下数据的配置.做一下记录

[client]
socket = /tmp/mysql.sock


[mysqld_multi]
log        = /data/mysql_db/mysqld_multi.log
mysqld     = /usr/bin/mysqld_safe
mysqladmin = /usr/bin/mysqladmin
user       = shutdown_user

###########################################################################################
[mysqld4306]
port       = 4306
socket     = /tmp/mysql_4306.sock
pid-file   = /tmp/mysql_4306.pid
datadir    = /data/mysql_db/4306
default_storage_engine = InnoDB
sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES
skip-name-resolve
max_connections = 1000
innodb_flush_log_at_trx_commit = 2
innodb_flush_log_at_timeout     =1
innodb_buffer_pool_size = 24080M
innodb_log_buffer_size = 8M
innodb_log_file_size = 256M
log-bin=/data/mysql_binlog/4306/mysql-bin
binlog_format=MIXED
binlog_cache_size = 1M
max_binlog_size=512M
log_warnings
slow_query_log
long_query_time = 2
server-id = 1
expire_logs_days = 5
secure-file-priv=
tmpdir=/data/mysql_tmp

innodb_buffer_pool_size

这个选项调大可以增加innodb的内存大小,减少io操作

expire_logs_days

这个选项可以指定binlog保存的天数,binlog占用硬盘空间特别大,但是binlog又是恢复数据库时最宝贵的log,所以如何权衡这个问题还得结合自己项目的需要

secure-file-priv=

这个选项可以让其他用户写入数据,项目在合服时无法写入数据,加入这个配置可以降低数据库的安全性检查

tmpdir=/data/mysql_tmp

这个选项可以指定mysql在运行时使用的硬盘空间地址,默认的地址是在系统盘,而一般系统盘的空间较小,把目录指定到空间较大的目录可以有效提高数据库的io效率
如果表结构发生了改变,需要增加表列,而这个表本身的数据量非常大,那么mysql就会用到大量的硬盘空间,如果空间不足,在修改表结构的时候可能会报错Incorrect key file for table 'tbl_xxx'; try to repair it,
那么就必须把mysql的tmpdir指定到一个容量大的地方。
/data/mysql_tmp文件夹必须创建出来,并且给mysql用户添加权限 chown -R mysql:mysql mysql_tmp/

高版本gcc编译的程序在低版本gcc机器上运行

关于linux的程序发布

我们项目的服务器一直使用centos6.5,主要原因是当时各种云服务器厂商的主流稳定centos版本就是6.5,但是这个版本的系统自带的gcc版本是4.4.7,gcc版本过低导致很多gcc新版本的功能无法使用,比如c++11还有之前我介绍过的ASan。 一开始我们自己做了妥协,把编译机从centos7降到了centos6.5,很多c++11的代码也做了修改。 编译机和线上环境系统版本的统一保证了程序的稳定性。

但是随着项目的发展,一些gcc的新功能也确实特别诱人,为了升级gcc研究了一下这个问题。 其实只要把编译机上的libstdc++.so文件与我们自己的程序一起发布就可以了,编译机上可以升级gcc到最新版本,程序编译发布时将升级过的libstdc++.so文件一同发布,让我们自己的程序在运行的时候动态链接编译机上的libstdc++.so(使用g++的-Wl,-rpath命令)

好像是gcc的某一个版本之前libstc++.so是依赖于libc.so的,也就是说当时如果只发布libstc++.so可能程序也跑不起来,之后某个版本又解除了这个依赖,不知道我说的对不对。。。记录一下。

顺便记录几个命令

查看libstdc++.so的版本

strings /usr/lib/libstdc++.so.6 | grep GLIBCXX  

GLIBCXX_3.4
GLIBCXX_3.4.1
GLIBCXX_3.4.2
GLIBCXX_3.4.3
GLIBCXX_3.4.4
GLIBCXX_3.4.5
GLIBCXX_3.4.6
GLIBCXX_3.4.7
GLIBCXX_3.4.8
GLIBCXX_3.4.9
GLIBCXX_3.4.10
GLIBCXX_3.4.11
GLIBCXX_3.4.12
GLIBCXX_3.4.13
GLIBCXX_FORCE_NEW
GLIBCXX_DEBUG_MESSAGE_LENGTH


查找系统的全部名字是libstdc++.so.6的文件
find / -name "libstdc++.so.6"

/data/service/share_server_trunk/commonlib/libstdc++.so.6
/usr/lib64/libstdc++.so.6

ASan(Linux),gcc4.8以上版本自带的内存检查工具

惊天大霹雳,超级好用的Linux程序内存检查工具,AddressSanitizer (ASan)

最近线上的程序总是莫名其妙崩溃,因为我们的项目使用了分布负载的机制,对于玩家的影响其实很小,但是我肯定是忍不了的…程序崩溃的core文件里面完全找不到问题所在,初步分析应该是野指针导致,仔细分析程序之后并没有发现内存释放后没有置null的情况,很可能是多线程导致的,然而代码量太大,大海捞针实在是无法找到问题所在,所以我找到了asan,项目之前一直用gcc4.4编译,完全不知道asan的存在,asan是gcc4.8以上gcc自带的内存检测库,开发自google,在gcc4.8版本以上编译链接的时候加入指定的参数即可,非常的方便好用,而且对程序的性能影响并不大,如果你的程序平时耗费的cpu和内存资源不超过50%,那么完全可以把加入asan的程序放到线上去跑.

首先在编译机上升级gcc,我直接升级到了最新版本gcc6.3版本(写这篇记录的时候最新版已经到gcc7.1了),升级后系统库中会新加入libasan,我们使用asan的功能都是来自于这里.
然后对代码进行编译和链接,编译时使用 -g -O2 -fsanitize=address -fno-omit-frame-pointer .-fsanitize=address命令就是将asan编译进来,这样编译的.o文件在运行时stack上申请的内存都会被asan接管,如果出问题asan会第一时间输出报告,如内存越界和各种非法访问. -fno-omit-frame-pointer可以防止一些优化导致指针丧失可读性. 链接时使用-fsanitize=address -fno-omit-frame-pointer选项可以让程序在heap上申请的内存被asan接管,这样asan会监控new和delete来输出内存泄漏的报告.

大功告成,使用asan编译和链接的程序生成好之后放到了线上环境运营,很快出现了一次宕机,asan会直接把自己的输出当做error输出,你可以查看到当前导致宕机的内存位置和变量名,文件行数,变量来源,线程信息等等非常全面的信息,得到这些信息后很快就分析出原来崩溃来自一个多线程共享的模块,而当前模块在多线程竞争资源的地方没有加锁,导致同一块内存被写坏. 问题完美解决,程序再也不会宕机了.

总结一下,asan在检测程序内存方面的功能实在是强大,之前也用过valgrind来检查内存泄漏,但是valgrind对性能的影响实在是太大,完全不能放到线上环境这样真实的环境下测试,而且asan给出的问题报告相当详细,基本上看一次错误报告就能解决当前导致宕机的问题,而core文件的信息由于优化和其他程序上下文的差异会导致提供的信息基本上没什么作用.宕机十次,十个core文件也无法准确定位某些野指针错误的问题.对于asan,只有一个词能表达我的心情,那就是相见恨晚…asan你值得拥有,谁用谁知道.

记一次使用mysqlbinlog恢复数据库

mysqlbinlog

记得项目还没上线的时候因为一次误操作清空了数据库,不是非常要命但是如果手动恢复的话还是很麻烦的,所以就用了mysql的binlog机制进行了数据库的恢复,因为没有上线,binlog并不大,也没有删除过binlog,所以很容易就恢复了,不过没有做记录,前几天另外一个项目组的数据库数据也因为误操作被清空了,正好又实践了一次…这次把mysqlbinlog的使用记录下来。


首先mysql启动前要配置一些字段才能使binlog生效。

vi /usr/local/mysql/etc/my.cnf


配置文件中配置如下的binlog相关配置

log-bin=/data/mysql_binlog/4306/mysql-bin
binlog_format=MIXED
binlog_cache_size = 1M
max_binlog_size=512M
expire_logs_days = 5  #我这里只记录了五天,因为binlog特别大,容易将硬盘占满,需要定时清理,但是如果binlog不全是没法恢复数据库的。

binlog中保存了修改数据库的所有语句(增删改查中,除了selec语句的其他所有语句),完全按照顺序写入binlog,如果数据库不幸被删了,那么从binlog的第一条记录开始恢复就会恢复binlog保存的所有数据。
使用binlog恢复数据的命令:

mysqlbinlog mysql-bin.000001 --start-position=4 --stop-position=7128197 -d db_launcher_hz | mysql -uroot -h 127.0.0.1 -P 4306 -D db_launcher_hz

命令中指定了要恢复的binlog文件 mysql-bin.000001,开始位置和结束位置,要恢复的数据库名字,用户名密码等等。开始位置和结束位置可以通过查看binlog内容来确定,查看binlog内容的命令:

mysqlbinlog mysql-bin.000001

会输出binlog的内容:

.......
SET TIMESTAMP=1519733350/*!*/;
BEGIN
/*!*/;
# at 261731
#180227 20:09:10 server id 1  end_log_pos 262082 CRC32 0x69ff7fd5       Query   thread_id=31923 exec_time=0     error_code=0
SET TIMESTAMP=1519733350/*!*/;
UPDATE tbl_activity SET fld_channel='', fld_status='1', fld_show_status='1', fld_start_time='2014-12-31 16:00:00', fld_stop_time='2015-01-01 16:00:00', fld_show_start_time='2014-12-31 16:00:00', fld_show_stop_time='2015-01-01 16:00:00' WHERE fld_id='7393'
/*!*/;
# at 262082
#180227 20:09:10 server id 1  end_log_pos 262184 CRC32 0x243a0c9d       Query   thread_id=31923 exec_time=0     error_code=0
SET TIMESTAMP=1519733350/*!*/;
COMMIT
/*!*/;
# at 262184
#180227 20:09:10 server id 1  end_log_pos 262285 CRC32 0xad2078ed       Query   thread_id=31923 exec_time=0     error_code=0
.......

我们可以把binlog的内容输出到文本中寻找想要恢复的的起止位置。

两次通过binlog恢复数据库内容的操作都是在项目早期,数据量并不多,binlog没有删除过,所以恢复起来并没有什么压力,但是binlog占用的硬盘空间非常大,如何管理binlog以保证数据库数据的安全性是之后需要考虑的一个问题。

AI行为树

用行为树来控制AI行为

项目上线的时候有很多竞品压力,关于AI方面并没有做太多的规则,为了快速上线服务器只是实现了AI的移动和攻击两个行为,其移动规则只是随机寻点移动,攻击行为也只是攻击最近的敌人。随着项目的发展,策划对AI的智能程度提出了要求,所以我们对AI的功能进行了加强。 我在之前的RPG项目中也做过AI,是基于状态机的,简单的RPG野外怪物使用状态机完全可以满足需求,但是目前的项目对AI行为的要求比较细致,所以使用了行为树。记录一下行为树的基本原理和使用情况

行为树的基本组成

最简单的AI使用if else语句就可以实现,但是随着AI行为的复杂度越来越高,要提供给策划配置等等需求的出现,必须使用一种更为方便清晰的方法来实现AI,行为树就是一个不错的选择。要实现一个行为树,首先要把怪物的各种行为模块化,比如巡逻行为,追击行为,普通攻击行为,技能攻击行为,各种条件判断行为(自身血量是否不足30%,队友人数是否少于1个…)等等,之后利用行为树的原理将这些模块进行组合,以达到行为的多样性,这种多样性的组合是基于行为数量指数级增加的,所以提供给策划配置的灵活性非常好。一棵行为树表示一组AI逻辑,要执行这组AI逻辑,需要从根节点开始遍历执行整棵树,遍历执行的过程中,父节点根据其自身类别选择需要执行的子节点并执行,子节点执行完后将执行结果返回给父节点, 一棵普通的行为树主要由四种节点组成,我把他们分为两类,枝干节点和叶子节点。枝干节点肯定不是行为树的末端,它们主要的功能是对自己枝干下的节点进行“管理”,如何执行自己枝干下的节点是枝干节点需要做的事情。叶子节点一定是行为树的末端,它们通常是一个动作,如攻击,移动等等。

–顺序节点(Sequence):枝干节点,顺序执行子节点,只要碰到一个子节点返回false,则返回false;否则返回true。

–选择节点(Selector):枝干节点,顺序执行子节点,只要碰到一个子节点返回true,则返回true;否则返回false。

–条件节点(Condition):叶子节点,执行条件判断,返回判断结果。

–动作节点(Action):叶子节点,执行设定的动作,一般返回true。

下面是一棵简单的普通行为树,这棵行为树的根节点是一个选择节点(Selector),根节点下有两个顺序节点的枝干,每个顺序节点下面是叶子节点的条件和动作。把这课行为树用语言描述一下就是,怪物开始行动,执行根节点,首先选择移动还是攻击,顺序执行移动(11)和攻击(12),当执行移动(11)的条件中战场内无敌人(111)为true,那么继续执行下面的条件自身范围内是否有友方(112),如果返回true那么就执行定点移动行为(113),执行113结束后整棵行为树执行结束,从根节点开始再次执行。如果111或者112返回false,那么执行攻击(12),如果视野范围内有敌人,那么就攻击距离最近的敌人(122),如果没有敌人,那么整棵行为树执行结束,从根节点再次开始执行。


普通行为树


项目中的代码实现:

//////////////////////////////////////////////////////////////////////////
///
///    @file     Behaviour.h
///
///    @date    2016-11-22  20:02:52
///
///    @brief    行为树
///
//////////////////////////////////////////////////////////////////////////
#ifndef __behaviour__Behaviour__
#define __behaviour__Behaviour__
#include <string>
#include <vector>
#include <_BaseGameObject.h>
namespace BehaviorTree
{
    //每个行为的状态
    enum Status
    {
        BH_INVALID, //无效
        BH_SUCCESS,    //成功
        BH_FAILURE,    //失败
        BH_RUNNING,    //执行中
    };
    //行为基类
    class Behavior
    {
    public:
        virtual Status update(uint32& delay){return BH_SUCCESS;};
        virtual void onInitialize(){}
        virtual void onTerminate(Status) {}
        virtual void addBehavior(Behavior* node){}
        virtual    void init(){};

        Behavior():
            m_eStatus (BH_INVALID)
            ,action_id_(0)
        {};
        virtual ~Behavior()
        {};
        Status tick(uint32& delay);
        Status getState()
        {
            return m_eStatus;
        }
        bool setState(uint32 _eStatus)
        {
            m_eStatus = (BehaviorTree::Status)_eStatus;
            return true;
        }
    private:
        Status m_eStatus;
    public:
        uint32 action_id_;
        uint32 std_id_;
        typedef std::vector <Behavior*> Behaviors;
        Behaviors m_Children;
    };
    //枝类
    class Composite : public Behavior
    {
    public:
        virtual void onTerminate(Status) {}
        virtual void addBehavior(Behavior* node)
        {
            m_Children.push_back(node);
        }
    };
    //顺序节点
    class Sequence : public Composite
    {
    public:

        virtual void onInitialize();
        virtual Status update(uint32& delay);

        Behaviors::iterator m_CurrentChild;
    };
    //选择节点
    class Selector : public Composite
    {
    public:
        virtual void onInitialize();
        virtual Status update(uint32& delay);
        virtual void onTerminate();
        Behaviors::iterator m_CurrentChild;
    };
    //并行节点
    class Parallel : public Composite
    {
    public:
        virtual void onInitialize();
        virtual Status update(uint32& delay);
        Behaviors::iterator m_CurrentChild;
    };

    //并行节点(全部节点执行完成结束)
    class ParallelEx : public Composite
    {
    public:
        virtual void onInitialize();
        virtual Status update(uint32& delay);
        Behaviors::iterator m_CurrentChild;
    };
}
#endif //__behaviour__Behaviour__




//////////////////////////////////////////////////////////////////////////
///
///    @file     Behaviour.cpp
///
///
///    @brief    行为树
///
//////////////////////////////////////////////////////////////////////////

#include "Behaviour.h"
namespace BehaviorTree
{
    Status Behavior::tick(uint32& delay)
    {
        if (m_eStatus == BH_INVALID)
        {
            onInitialize();
        }
        m_eStatus = update(delay);
        if (m_eStatus != BH_RUNNING)
        {
            onTerminate(m_eStatus);
        }
        return m_eStatus;
    }

    void Sequence::onInitialize()
    {
        m_CurrentChild = m_Children.begin();
    }
    Status Sequence::update(uint32& delay)
    {
        while (true)
        {
            Status s = (*m_CurrentChild)->tick(delay);
            if (s != BH_SUCCESS) {
                return s;
            }

            // 最后一个节点,执行完了
            if(++m_CurrentChild == m_Children.end())
            {
                m_CurrentChild = m_Children.begin();
                return BH_SUCCESS;
            }
        }
        return BH_INVALID;
    }

    void Selector::onInitialize()
    {
        m_CurrentChild = m_Children.begin();
    }
    Status Selector::update(uint32& delay)
    {
        while (true)
        {
            Status s = (*m_CurrentChild)->tick(delay);

            if (s != BH_FAILURE) {

                return s;
            }

            if(++m_CurrentChild == m_Children.end())
            {
                m_CurrentChild = m_Children.begin();
                return BH_FAILURE;
            }
        }
        return BH_INVALID;
    }

    void Selector::onTerminate()
    {
        setState(BH_INVALID);
    }


    void Parallel::onInitialize()
    {
        m_CurrentChild = m_Children.begin();
    }

    Status Parallel::update( uint32& delay )
    {
        return BH_INVALID;
    }


    void ParallelEx::onInitialize()
    {
        m_CurrentChild = m_Children.begin();
    }

    Status ParallelEx::update( uint32& delay )
    {
        while (true)
        {
            bool b_break = true;
            uint32 delay_prev=delay;
            for (auto ite = m_Children.begin();ite!=m_Children.end();++ite)
            {
                Status s = (*ite)->tick(delay);
                if (delay<delay_prev)
                {
                    delay_prev = delay;
                }
                if (BH_RUNNING==s)
                {
                    b_break = false;
                }
            }
            delay = delay_prev;
            //if ( delay<100 )
            //{
            //    delay = 100;
            //}
            if (true == b_break)
            {
                return BH_SUCCESS;
            }
            else
            {
                return BH_RUNNING;
            }
        }
        return BH_INVALID;
    }
}

上面的代码是行为树的基类,四种节点中的叶子节点(条件节点,动作节点)可以直接继承Behavior类,顺序节点是Sequence类,选择节点是Selector,我的代码里还加入了一种扩展的节点,并行节点Parallel类,这种枝干节点会同时执行枝干下的节点,不论这些节点返回什么,加入这种节点主要是为了实现同时移动同时攻击这种动作。我们项目中继承自Behavior类的部分代码(一个动作节点,一个条件节点):

//定点循环移动行为
class CActionMoveLoop : public BehaviorTree::Behavior
{
public:
    CActionMoveLoop(IHZArenaPlayer* player,uint32 action_id);
    ~CActionMoveLoop();
public:
    virtual BehaviorTree::Status update(uint32& delay);
    virtual void onInitialize();
    virtual void onTerminate(BehaviorTree::Status){setState(BehaviorTree::BH_INVALID);};
private:
    World3DPosition            GetoutOnePoint(bool& result);
    int32                    point_index_;
    int32                    order_type_;//0顺时针,1逆时针
    std::vector<WorldPosition>        path_point_;        ///< 路径
    void                    init();
    IPlayer*                player_;
    CFightAIPlugin*            ai_plugin_;
    World3DPosition            target_point_;
};


//检测条件,属性检测
class CConditionProp : public BehaviorTree::Behavior
{
public:
    CConditionProp(IHZArenaPlayer* player,uint32 action_id);
    ~CConditionProp(){};
public:
    virtual BehaviorTree::Status update(uint32& delay);
    virtual void onInitialize();
    virtual void onTerminate(BehaviorTree::Status){};
    void                    init();
private:
    uint32                    compare_type_;
    uint32                    prop_value_;
    uint32                    prop_percent_;
    IPlayer*                player_;
    CFightAIPlugin*            ai_plugin_;
};

策划通过绘制行为树逻辑图来梳理逻辑,然后通过逻辑图输出逻辑配置到xml中,生成一棵行为树的代码:

BehaviorTree::Behavior* CFightAIPlugin::create_tree_root( const StdTreeRoot_data::IStdRoot* std_root )
{
    switch (std_root->get_RootType())
    {
        //顺序
    case 1:
        {
            BehaviorTree::Sequence* rootseq = new BehaviorTree::Sequence();
            for (uint32 i = 0; i<std_root->StdChild_size(); i++) 
            {
                auto std_child = std_root->get_StdChild(i);
                if (std_child->get_NodeType()==1)
                {
                    auto std_node = get_StdTreeRoot_manager()->find_StdRoot_byRootID(std_child->get_ChildID());
                    if (NULL == std_node)
                    {
                        continue;
                    }
                    rootseq->m_Children.push_back(create_tree_root(std_node));
                }
                else
                {
                    auto std_node = get_StdTreeAction_manager()->find_StdAction_byActionID(std_child->get_ChildID());
                    if (NULL == std_node)
                    {
                        continue;
                    }
                    rootseq->m_Children.push_back(create_tree_leaf(std_node));
                }
            }
            return rootseq;
        }
        //选择
    case 2:
        {
            BehaviorTree::Selector* rootsel = new BehaviorTree::Selector();
            for (uint32 i = 0; i<std_root->StdChild_size(); i++) 
            {
                auto std_child = std_root->get_StdChild(i);
                if (std_child->get_NodeType()==1)
                {
                    auto std_node = get_StdTreeRoot_manager()->find_StdRoot_byRootID(std_child->get_ChildID());
                    if (NULL == std_node)
                    {
                        continue;
                    }
                    rootsel->m_Children.push_back(create_tree_root(std_node));
                }
                else
                {
                    auto std_node = get_StdTreeAction_manager()->find_StdAction_byActionID(std_child->get_ChildID());
                    if (NULL == std_node)
                    {
                        continue;
                    }
                    rootsel->m_Children.push_back(create_tree_leaf(std_node));
                }
            }
            return rootsel;
        }
        //并行
    case 3:
        {
            BehaviorTree::ParallelEx* rootpara = new BehaviorTree::ParallelEx();
            for (uint32 i = 0; i<std_root->StdChild_size(); i++) 
            {
                auto std_child = std_root->get_StdChild(i);
                if (std_child->get_NodeType()==1)
                {
                    auto std_node = get_StdTreeRoot_manager()->find_StdRoot_byRootID(std_child->get_ChildID());
                    if (NULL == std_node)
                    {
                        continue;
                    }
                    rootpara->m_Children.push_back(create_tree_root(std_node));
                }
                else
                {
                    auto std_node = get_StdTreeAction_manager()->find_StdAction_byActionID(std_child->get_ChildID());
                    if (NULL == std_node)
                    {
                        continue;
                    }
                    rootpara->m_Children.push_back(create_tree_leaf(std_node));
                }
            }
            return rootpara;
        }
        //动作或者条件
    case 4:
        {
            RLOG(MINFO)<<"root can not be action!!!";
            break;
        }
    default:
        break;
    }
    RLOG(MINFO)<<"root wrong:"<<std_root->get_RootID();
    return NULL;
}


BehaviorTree::Behavior* CFightAIPlugin::create_tree_leaf( const StdTreeAction_data::IStdAction* std_action )
{
    switch (std_action->get_ActionType())
    {
    //移动行为
    case 1:
        {
            return create_move_action(std_action);
            break;
        }
    //条件检测
    case 2:
        {
            return create_check_action(std_action);
            break;
        }
    //攻击行为
    case 3:
        {
            return create_attack_action(std_action);
            break;
        }
    //其他行为
    case 4:
        {
            return create_other_action(std_action);
            break;
        }
    default:
        {
            break;
        }

    }
    RLOG(MINFO)<<"No This Big Type:"<<std_action->get_ActionSubType()<<" id:"<<std_action->get_ActionID();
    return NULL;
}


BehaviorTree::Behavior* CFightAIPlugin::create_move_action( const StdTreeAction_data::IStdAction* std_action )
{
    switch (std_action->get_ActionSubType())
    {
    // 多坐标点顺序移动:到过的坐标点会被移除
    case 1:
        {
            CActionMovePath *move_path = new CActionMovePath(m_pPlayerObj,std_action->get_ActionID());
            return move_path;
        }
    // 多坐标点循环移动:反复循环
    case 2:
        {
            CActionMoveLoop *move_loop = new CActionMoveLoop(m_pPlayerObj,std_action->get_ActionID());
            return move_loop;
        }
    // 坐标点半径巡逻
    case 3:
        {
            CActionMovePatrol *move_patrol = new CActionMovePatrol(m_pPlayerObj,std_action->get_ActionID());
            return move_patrol;
        }
    // 指定id移动
    case 4:
        {
            CActionMoveToAI *move_ai = new CActionMoveToAI(m_pPlayerObj,std_action->get_ActionID());
            return move_ai;
        }
    // 最近敌人移动
    case 5:
        {
            CActionMoveToCloseEnemy *move_close_enemy = new CActionMoveToCloseEnemy(m_pPlayerObj,std_action->get_ActionID());
            return move_close_enemy;
        }
    // 占旗点巡逻
    case 6:
        {
            CActionMoveFlag *move_patrol_flag = new CActionMoveFlag(m_pPlayerObj,std_action->get_ActionID());
            return move_patrol_flag;
        }
    default:
        {
            break;
        }

    }
    RLOG(MINFO)<<"No This Move Action Type:"<<std_action->get_ActionSubType()<<" id:"<<std_action->get_ActionID();
    return NULL;
}

这些代码片段包含了行为树的创建,通过配置表来生成一棵复杂的行为树。然后我们还需要在一个循环中执行这棵树的逻辑:

uint32 CFightAIPlugin::AIUpdateBT()
{
    if (behavior_tree_==NULL)
    {
        return 0;
    }

    if (BehaviorTree::BH_RUNNING != behavior_status_)
    {
        behavior_now_ = behavior_tree_;
        behavior_now_->onInitialize();
        RLOG(MINFO)<<"----------loop---------";//执行结束,从树根重新开始执行
    }
    uint32 delay = 1000;
    behavior_status_ = behavior_now_->update(delay);//update当前的节点
    RLOG(MINFO)<<"ai delay:"<<delay;
    return delay;
}

AIUpdateBT()函数会在定时器下每间隔一段时间(100~1000ms,根据自己的需求调整每一次update的时间)update一次,来驱动整棵树的执行。

掌握了行为树的基本原理就可以实现各种各样复杂的AI逻辑并且条理清晰,便于管理,不会因为逻辑的复杂而使代码混乱,下面的图片展示了一个比上面例子更复杂的逻辑,你完全可以根据自己项目的需要来配置一个更大的行为树,它会严格按照你的配置来执行。


行为树


Linux上vsftpd的搭建和使用

Linux上vsftpd的搭建和使用

1.查看是否安装vsftp

rpm -qa | grep vsftpd

如果出现:

vsftpd-3.0.2-10.el7.x86_64

说明已经安装了vsftpd,如果没有安装
使用命令安装:

yum -y install vsftpd

2,配置vsftpd

whereis vsftpd

出现:

vsftpd: /usr/sbin/vsftpd /etc/vsftpd /usr/share/man/man8/vsftpd.8.gz

到 /etc/vsftpd 目录下配置 vsftpd.conf

pam_service_name=vsftpd             //设置服务名字,防火墙设置里要用
userlist_enable=YES                 //访问限制打开
tcp_wrappers=YES                    //防火墙打开
local_root=/haizhan                 //ftp的根目录
listen_port=19008                   //修改ftp的默认端口2119008
listen=YES                      
pasv_min_port=19010                 //pasv模式分配给客户端可以连接的端口范围
pasv_max_port=19020
pasv_promiscuous=YES                //关闭pasv的安全检查
pasv_enable=YES                     //使用pasv模式
pasv_addr_resolve=YES               //设置外网地址开关
pasv_address=120.120.120.120        //如果这台机器需要对外网提供ftp的pasv服务,需要在这里设置本机外网地址
listen_ipv6=NO                      //关闭ipv6才能正常工作

上面这些配置全部是必要配置,必须全部配置。

3,修改FTP系统默认端口
修改 /etc/services

ftp             19008/tcp
ftp             19008/udp

4,配置访问限制
1.修改/etc/hosts.deny
加入 :

vsftpd:ALL

2.修改/etc/hosts.allow
加入白名单ip,如:

vsftpd:192.168.0.1
vsftpd:120.1.1.1
vsftpd:192.168.0.0/255.255.255.0   //限定网段的,没有试过。

5,添加用户

useradd -d /zhanjian -s /sbin/nologin haizhan
passwd haizhan

C++中的explicit和extern

explicit

C++提供了关键字explicit,可以阻止不应该允许的经过转换构造函数进行的隐式转换的发生。声明为explicit的构造函数不能在隐式转换中使用
C++中, 一个参数的构造函数(或者除了第一个参数外其余参数都有默认值的多参构造函数), 承担了两个角色。 1 是个构造器 ,2 是个默认且隐含的类型转换操作符。
explicit构造函数是用来防止隐式转换的。请看下面的代码:

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
class Test1
{
public:
Test1(int n)
{
num=n;
}//普通构造函数
private:
int num;
};
class Test2
{
public:
explicit Test2(int n)
{

num=n;
}//explicit(显式)构造函数
private:
int num;
};
int main()
{

Test1 t1=12;//隐式调用其构造函数,成功
Test2 t2=12;//编译错误,不能隐式调用其构造函数
Test2 t2(12);//显式调用成功
return 0;
}

只要是构造函数中只有一个可变参数的C++类,都要避免隐式调用。

extern

extern可置于变量或者函数前,以表示变量或者函数的定义在别的文件中,提示编译器遇到此变量或函数时,在其它模块中寻找其定义。

下面这段代码没有使用任何 extern 关键字,编译中会报错。

1
2
3
4
5
6
7
//A.cpp
int i;
int main()
{

}
//B.cpp
int i;

添加extren关键字后可以编译通过,提示编译器i变量定义在其他模块中

1
2
3
4
5
6
7
8
//A.cpp
extern int i;
int main()
{

i=100;//试图使用B中定义的全局变量
}
//B.cpp
int i;

extern也可用来进行链接指定。

C++支持函数重载,而过程式语言C则不支持。函数被C++编译后在符号库中的名字与C语言的不同。例如,假设某个函数的原型为:
void foo( int x, int y );该函数被C编译器编译后在符号库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字
(不同的编译器可能生成的名字不同,但是都采用了相同的机制,生成的新名字称为“mangled name”)。_foo_int_int这样的名字
包含了函数名、函数参数数量及类型信息,C++就是靠这种机制来实现函数重载的。例如,在C++中,函数void foo( int x, int y )
与void foo( int x, float y )编译生成的符号是不相同的,后者为_foo_int_float。同样地,C++中的变量除支持局部变量外,
还支持类成员变量和全局变量。用户所编写程序的类成员变量可能与全局变量同名,我们以”.”来区分。而本质上,编译器在进行编译时,
与函数的处理相似,也为类中的变量取了一个独一无二的名字,这个名字与用户程序中同名的全局变量名字不同。
在C++中引用C语言中的函数和变量,在包含C语言头文件(假设为cExample.h)时,需进行下列处理:

1
2
3
4
extern "C"
{
#include "cExample.h"
}

epoll的各种框架

epoll的各种框架

了解了epoll的基本用法之后,再来看看别人都是怎么用的,很多应用广泛的开源项目中都可以看到epoll的不同用法,总结一下各种用法的优缺点,以及我们项目中的用法(文中所有QPS数据都来源于网络,本人并没有做实验验证过)

首先回顾一下epoll使用中的几个函数.

epfd = epoll_create(...);             // 创建指定大小的epoll句柄epfd

后文描述中的 listen 部分:

listen_fd = socket(...);              // 创建listen_fd
bind(listen_fd, addr);                // 把listen_fd绑定到本地地址addr
listen(listen_fd, ...);               // 监听listen_fd 

后文描述中的 accept 部分:

fd = accept(listen_fd, ...);          // 从listen_fd接受一个新接进来的连接套接字fd
epoll_ctl(epfd, op, fd, event);       // 对epfd做op操作,操作涉及监听fd的event事件
                                    // 比如op是EPOLL_CTL_ADD,意思是把 “对fd监听event事件” 注册到 epfd里面

后文描述中的 epoll_wait 部分:

num = epoll_wait(epfd, events, ...);  // 监听句柄epfd上的事件,并把事件通过event数组返回,num返回值是发生事件的个数

各家不同的使用方法:

一、epoll 1线程(listen+accept+epoll_wait+处理) 模型

代表开源产品:redis

基本原理:
这种模型基本就是教科书上的epoll使用方式:
socket -> bind -> listen -> epoll_wait -> accept或者处理读写事件 -> epoll_wait ……
redis基本遵循这样循环处理 网络事件和定时器事件

echo server测试:10万QPS

优点:
1)模型简单。这个模型是最简单的,代码实现方便,适合计算密集型应用
2)不用考虑并发问题。模型本身是单线程的,使得服务的主逻辑也是单线程的,那么就不用考虑许多并发的问题,比如锁和同步
3)适合短耗时服务。对于像redis这种每个事件基本都是查内存,是十分适合的,一来并发量可以接受,二来redis内部众多数据结构都是非常简单地实现

缺点:
1)顺序执行影响后续事件。因为所有处理都是顺序执行的,所以如果面对长耗时的事件,会延迟后续的所有任务,特别对于io密集型的应用,是无法承受的

二、epoll 1线程(listen+accept+epoll_wait) + 1队列通知 + n线程(处理) 模型

代表开源产品:thrift-nonblocking-server

基本原理:
1)在这种模型中,有1+n个线程。
2)有1个线程执行端口的listen并把listen_fd加入该线程的epoll_set,然后循环去做如下事情:1)epoll_wait监听新连接的到来,2)调用accept获得新到的fd,3)把fd放入队列,4) 回到 “1)” 继续epoll_wait
3)另外有n个工作线程,从队列里面获取文件描述符,然后执行:1)读取数据,2)执行任务

echo server测试:6万QPS

优点:
1)模型简单。这种模型的代码实现也是非常方便的
2)并发能力强。对于任务耗时或者IO密集的服务,可以充分利用多核cpu的性能实现异步并发处理
3)适合生产环境。虽然QPS没有特别高,但是对于目前大部分大型网站的吞吐量,这种网络处理能力也是足够了的,这也是为什么thrift nonblocking server可以用这种模型的原因
4)负载均衡。在这个模型,每个工作工作线程完成任务之后就会去队列里主动获取文件描述符,这个模型天然地就实现了负载均衡的功能。原因有2,一来是只有空闲的线程会拿到任务,二来是所有fd的事件监听都是由监听线程完成

缺点:
1)队列是性能瓶颈。
俗话说,不怕锁,只怕锁竞争。这个模型在运行过程中,n+1个线程竞争于队列之上,所以队列的访问是需要加锁的。对于echo server这种每次任务耗时都极短的服务,每次处理完任务就很快就会回到队列锁的争抢行列。大量的锁竞争直接拖垮了QPS。
不过好在常见的生产环境web服务都不是echo server,每次请求都会是毫秒级的,不会在锁竞争上产生问题。

三、epoll 1线程(listen+accept+epoll_wait) + n队列通知 + n线程(处理) 模型

代表开源产品:memcached

基本原理:
这种模型基本类似于 上一种模型,区别在于 把1个队列换成n个队列,每个工作线程绑定一个队列,每个工作线程从自己的队列消费数据,其他的保持一致

echo server测试:20万QPS

优点:
1)并发能力更强。相比于单队列的模型,多队列的好处是减少了队列的锁竞争。对于短耗时任务能得到比较多的提升,很适合缓存类应用

缺点:
1)有可能导致负载不均。因为监听线程是不会去根据不同线程的处理速度决定把任务分配给哪个线程的,如果每个任务的耗时不均衡,那么就可能导致有些线程累死,有些线程饿死

memcached对该模型改进:
memcached是多线程且是缓存类应用,非常适合这个模型。改进如下:
1)工作线程拿到的fd,这个fd会加到本线程的epoll_set里面,这个fd的后续读写事件都由该线程处理
2)工作线程和监听线程之间建立了管道,工作线程的管道fd也加入到工作线程的epoll_set里面,那么就可以让 ‘新fd到来’和‘旧fd可读写’ 这两种事件都由epoll_set监听,减少调度的复杂性
3)因为memcached的任务基本是查内存的工作,耗时短而且相对均匀,所以对负载问题不是很敏感,可以使用该模型

四、epoll 1进程(listen) + n进程(accept+epoll_wait+处理) 模型(epoll 1线程(listen) + n线程(accept+epoll_wait+处理) 模型)

代表开源产品:nginx

基本原理:(依据nginx的设计分析)
1)master进程监听新连接的到来,并让其中一个worker进程accept。这里需要处理惊群效应问题,详见nginx的accept_mutex设计
2)worker进程accept到fd之后,把fd注册到到本进程的epoll句柄里面,由本进程处理这个fd的后续读写事件
3)worker进程根据自身负载情况,选择性地不去accept新fd,从而实现负载均衡

echo server测试:后续补充

优点:
1)进程挂掉不会影响这个服务
2)和第二种模型一样,是由worker主动实现负载均衡的,这种负载均衡方式比由master来处理更简单

缺点:
1)多进程模型编程比较复杂,进程间同步没有线程那么简单
2)进程的开销比线程更多

五、epoll 1线程(listen+accept) + n线程(epoll_wait+处理) 模型

对应开源产品:无

基本原理:
1)1个线程监听端口并accept新fd,把fd的监听事件round robin地注册到n个worker线程的epoll句柄上
2)如果worker线程是多个线程共享一个epoll句柄,那么事件需要设置EPOLLONESHOT,避免在读写fd的时候,事件在其他线程被触发
3)worker线程epoll_wait获得读写事件并处理之

echo server测试:200万QPS(因为资源有限,测试client和server放在同一个物理机上,实际能达到的上限应该还会更多)

优点:
1)减少竞争。在第四种模型中,worker需要去争着完成accept,这里有竞争。而在这种模型中,就消除了这种竞争

缺点:
1)负载均衡。这种模型的连接分配,又回到了由master分配的模式,依然会存在负载不均衡的问题。可以让若干个线程共享一个epoll句柄,从而把任务分配均衡到多个线程,实现全局更好的负载均衡效果

我们的使用

我们项目中的epoll模型基本上是第五种模型的方式,一线程负责listen+accept,accept到的fd通过负载情况分配到负载最低的n线程中的一个线程中去,在n线程初始化中调用epoll_create,所以n线程都有自己的epoll对象,n个worker线程自己用
自己的epoll对象来epoll_wait分配给自己的fd,处理读写事件。 最后n线程将整合好的消息结构push到主逻辑线程的队列中,主逻辑线程进行逻辑处理。因为游戏服务器都是长连接,基本上每个玩家造成的访问量基本上差别不大,所以在负载上基本上
可以认为是均衡的,而且这种模型没有竞争,QPS高,非常适合我们的游戏项目。

总结和启示

1、除了第一个模型,其他模型都是多线程/多进程的,所以都采用了“1个master-n个worker”结构
2、如果accept由master执行,那么master就需要执行分配fd的任务,这种设计会存在负载不均的问题;但是这种情况下,accept由谁执行不会存在竞争,性能更好
3、如果accept让worker去执行,那么同一个listen_fd,这个时候由哪个worker来执行accept,便产生了竞争;产生了竞争就使得性能降低
4、对于一般的逻辑性业务,选择由master去accept并分配任务更合适一些。因为大部分业务请求耗时都较长(毫秒级别),而且请求的耗时不尽相同,所以对负载均衡要求也较高;另外一般的业务,也不可能到单机10万QPS的量级
5、这几个模型的变化,无非就是三个问题的选择:
1)多进程,多线程,还是单线程?
2)每个worker自己管理事件,还是由master统一管理?
3)accept由master还是worker执行?
6、在系统设计中,需要根据实际情况和需求,选择合适的网络事件方案

epoll的基本原理和简单使用

epoll

epoll作为目前linux处理网络io的“唯一”选择,几乎在所有的互联网服务器项目中被使用,各种不同的知名项目对epoll模型的设计都有一些区别,我们的项目也是epoll,记录一下,加深记忆。


首先epoll的基本操作很简单,只有几个函数和一个结构体:

头文件:
#include <sys/epoll.h>

创建epoll对象:

int epoll_create(int size);

epoll_create() 可以创建一个epoll实例。在linux 内核版本大于2.6.8 后,这个size 参数就被弃用了,但是为了兼容老版本,传入的值必须大于0,在 epoll_create() 的最初实现版本时, size参数的作用是创建epoll实例时候告诉内核需要使用多少个文件描述符。内核会使用 size 的大小去申请对应的内存(如果在使用的时候超过了给定的size, 内核会申请更多的空间)。epoll_create() 会返回新的epoll对象的文件描述符。这个文件描述符用于后续的epoll操作。如果不需要使用这个描述符,请使用close关闭。

设置epoll事件:

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epoll_ctl() 可以控制通过epoll_create()创建的epfd,通过op增删改文件描述符(比如socket)fd上的事件。

op值:
EPOLL_CTL_ADD 在epfd中注册指定的fd文件描述符并能把event和fd关联起来。
EPOLL_CTL_MOD 改变fd和event之间的联系。
EPOLL_CTL_DEL 从指定的epfd中删除fd文件描述符。在这种操作中event是被忽略的,并且为可以等于NULL。

event这个参数是用于关联制定的fd文件描述符的。它的定义如下:

typedef union epoll_data {
    void        *ptr;
    int          fd;
    uint32_t     u32;
    uint64_t     u64;
} epoll_data_t;

struct epoll_event {
    uint32_t     events;      /* Epoll events */
    epoll_data_t data;        /* User data variable */
};

events是传入参数,是一个字节的掩码构成的。下面是可以用的事件:

`EPOLLIN` - 当关联的文件可以执行 read ()操作时。
`EPOLLOUT` - 当关联的文件可以执行 write ()操作时。
`EPOLLET` - 设置指定的文件描述符模式为边缘触发,默认的模式是水平触发。

LT(level-triggered)水平触发是缺省的工作方式,并且同时支持block和no-block socket。在这种模式下,内核告诉你一个fd是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你,直到这个fd的数据被读完。

ET(edge-triggered)边缘触发是高速工作方式,只支持no-block socket。在这种模式下,当fd从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(直到你读取了全部数据或者fd遇到错误)。

上面这些事件是经常用到的,下面这几个事件目前理解还不够深,先放着吧。。。

EPOLLRDHUP - (从 linux 2.6.17 开始)当socket关闭的时候,或者半关闭写段的(当使用边缘触发的时候,这个标识在写一些测试代码去检测关闭的时候特别好用)
EPOLLPRI - 当 read ()能够读取紧急数据的时候。
EPOLLERR - 当关联的文件发生错误的时候,epoll_wait() 总是会等待这个事件,并不是需要必须设置的标识。
EPOLLHUP - 当指定的文件描述符被挂起的时候。epoll_wait() 总是会等待这个事件,并不是需要必须设置的标识。当socket从某一个地方读取数据的时候(管道或者socket),这个事件只是标识出这个已经读取到最后了(EOF)。所有的有效数据已经被读取完毕了,之后任何的读取都会返回0(EOF)。
EPOLLONESHOT - (从 linux 2.6.17 开始)设置指定文件描述符为单次模式。这意味着,在设置后只会有一次从epoll_wait() 中捕获到事件,之后你必须要重新调用 epoll_ctl() 重新设置。

data是传入参数,主要使用int fd;来标记文件描述符,也可以使用void *ptr;标记自定义的数据格式,包含文件描述符和自定义的数据。

等待epoll事件:

int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);

epoll_wait()这个系统调用是用来等待epfd中的事件。events是传入参数,一组空的epoll_event对象,用来接收epoll操作返回数据。maxevents是epoll_event数组的大小,必须要大于0(这里的epoll_event数组大小可以理解为一次epoll_wait调用操作的最大fd数量)。

timeout这个参数是用来制定epoll_wait会阻塞多少毫秒,会一直阻塞到下面几种情况:

1.一个文件描述符触发了事件。
2.被一个信号处理函数打断,或者timeout超时。

当timeout等于-1的时候这个函数会无限期的阻塞下去,当timeout等于0的时候,就算没有任何事件,也会立刻返回。

一个精简版的epoll伪代码:

#define MAX_EVENTS 10
struct epoll_event  ev, events[MAX_EVENTS];
int         listen_sock, conn_sock, nfds, epollfd;


/* Code to set up listening socket, 'listen_sock',
* (socket(), bind(), listen()) omitted */

epollfd = epoll_create1( 0 );
if ( epollfd == -1 )
{
    perror( "epoll_create1" );
    exit( EXIT_FAILURE );
}

ev.events   = EPOLLIN;
ev.data.fd  = listen_sock;
if ( epoll_ctl( epollfd, EPOLL_CTL_ADD, listen_sock, &ev ) == -1 )
{
    perror( "epoll_ctl: listen_sock" );
    exit( EXIT_FAILURE );
}

for (;; )
{
    nfds = epoll_wait( epollfd, events, MAX_EVENTS, -1 );
    if ( nfds == -1 )
    {
        perror( "epoll_wait" );
        exit( EXIT_FAILURE );
    }

    for ( n = 0; n < nfds; ++n )
    {
        if ( events[n].data.fd == listen_sock )
        {
            conn_sock = accept( listen_sock,
                        (struct sockaddr *) &local, &addrlen );
            if ( conn_sock == -1 )
            {
                perror( "accept" );
                exit( EXIT_FAILURE );
            }
            setnonblocking( conn_sock );
            ev.events   = EPOLLIN | EPOLLET;
            ev.data.fd  = conn_sock;
            if ( epoll_ctl( epollfd, EPOLL_CTL_ADD, conn_sock,
                    &ev ) == -1 )
            {
                perror( "epoll_ctl: conn_sock" );
                exit( EXIT_FAILURE );
            }
        } else {
            do_use_fd( events[n].data.fd );
        }
    }
}

这段代理里省略了服务器socket的创建过程和接收到客户端socket读数据事件处理的do_use_fd函数,比较精简。

下面是一个完整可运行的简单epoll例子:

#include <stdio.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <netdb.h>
#include <errno.h>

#define MAX_EVENT 20
#define READ_BUF_LEN 256

/**
* 设置 file describe 为非阻塞模式
* @param fd 文件描述
* @return 返回0成功,返回-1失败
*/
static int make_socket_non_blocking (int fd) {
    int flags, s;
    // 获取当前flag
    flags = fcntl(fd, F_GETFL, 0);
    if (-1 == flags) {
        perror("Get fd status");
        return -1;
    }

    flags |= O_NONBLOCK;

    // 设置flag
    s = fcntl(fd, F_SETFL, flags);
    if (-1 == s) {
        perror("Set fd status");
        return -1;
    }
    return 0;
}

int main() {
    // epoll 实例 file describe
    int epfd = 0;
    int listenfd = 0;
    int result = 0;
    struct epoll_event ev, event[MAX_EVENT];
    // 绑定的地址
    const char * const local_addr = "192.168.0.45";
    struct sockaddr_in server_addr = { 0 };

    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == listenfd) {
        perror("Open listen socket");
        return -1;
    }
    /* Enable address reuse */
    int on = 1;
    // 打开 socket 端口复用, 防止测试的时候出现 Address already in use
    result = setsockopt( listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on) );
    if (-1 == result) {
        perror ("Set socket");
        return 0;
    }

    server_addr.sin_family = AF_INET;
    inet_aton (local_addr, &(server_addr.sin_addr));
    server_addr.sin_port = htons(8080);
    result = bind(listenfd, (const struct sockaddr *)&server_addr, sizeof (server_addr));
    if (-1 == result) {
        perror("Bind port");
        return 0;
    }
    result = make_socket_non_blocking(listenfd);
    if (-1 == result) {
        return 0;
    }

    result = listen(listenfd, 200);
    if (-1 == result) {
        perror("Start listen");
        return 0;
    }

    // 创建epoll实例
    epfd = epoll_create1(0);
    if (1 == epfd) {
        perror("Create epoll instance");
        return 0;
    }

    ev.data.fd = listenfd;
    ev.events = EPOLLIN | EPOLLET /* 边缘触发选项。 */;
    // 设置epoll的事件
    result = epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);

    if(-1 == result) {
        perror("Set epoll_ctl");
        return 0;
    }

    for ( ; ; ) {
        int wait_count;
        // 等待事件
        wait_count = epoll_wait(epfd, event, MAX_EVENT, -1);

        for (int i = 0 ; i < wait_count; i++) {
            uint32_t events = event[i].events;
            // IP地址缓存
            char host_buf[NI_MAXHOST];
            // PORT缓存
            char port_buf[NI_MAXSERV];

            int __result;
            // 判断epoll是否发生错误
            if ( events & EPOLLERR || events & EPOLLHUP || (! events & EPOLLIN)) {
                printf("Epoll has error\n");
                close (event[i].data.fd);
                continue;
            } else if (listenfd == event[i].data.fd) {
                // listen的 file describe 事件触发, accpet事件

                for ( ; ; ) { // 由于采用了边缘触发模式,这里需要使用循环
                    struct sockaddr in_addr = { 0 };
                    socklen_t in_addr_len = sizeof (in_addr);
                    int accp_fd = accept(listenfd, &in_addr, &in_addr_len);
                    if (-1 == accp_fd) {
                        perror("Accept");
                        break;
                    }
                    __result = getnameinfo(&in_addr, sizeof (in_addr),
                                        host_buf, sizeof (host_buf) / sizeof (host_buf[0]),
                                        port_buf, sizeof (port_buf) / sizeof (port_buf[0]),
                                        NI_NUMERICHOST | NI_NUMERICSERV);

                    if (! __result) {
                        printf("New connection: host = %s, port = %s\n", host_buf, port_buf);
                    }

                    __result = make_socket_non_blocking(accp_fd);
                    if (-1 == __result) {
                        return 0;
                    }

                    ev.data.fd = accp_fd;
                    ev.events = EPOLLIN | EPOLLET;
                    // 为新accept的 file describe 设置epoll事件
                    __result = epoll_ctl(epfd, EPOLL_CTL_ADD, accp_fd, &ev);

                    if (-1 == __result) {
                        perror("epoll_ctl");
                        return 0;
                    }
                }
                continue;
            } else {
                // 其余事件为 file describe 可以读取
                int done = 0;
                // 因为采用边缘触发,所以这里需要使用循环。如果不使用循环,程序并不能完全读取到缓存区里面的数据。
                for ( ; ;) {
                    ssize_t result_len = 0;
                    char buf[READ_BUF_LEN] = { 0 };

                    result_len = read(event[i].data.fd, buf, sizeof (buf) / sizeof (buf[0]));

                    if (-1 == result_len) {
                        if (EAGAIN != errno) {
                            perror ("Read data");
                            done = 1;
                        }
                        break;
                    } else if (! result_len) {
                        done = 1;
                        break;
                    }

                    write(STDOUT_FILENO, buf, result_len);
                }
                if (done) {
                    printf("Closed connection\n");
                    close (event[i].data.fd);
                }
            }
        }

    }
    close (epfd);
    return 0;
}

总结

先记录一下epoll的基本原理和基本使用方法,后面记录一下不同的epoll框架和我们自己项目中使用的情况。

关于C++继承的二义性

关于C++继承的二义性

天气太热写不下去代码,就翻柜子,找到c++primer翻了一下,好久不看书,记一下翻到的内容,稍微巩固下…
给定下面的类层次,从 VMI 类内部可以限定地访问哪些继承成员?哪些继承成员需要限定?解释你的推理。

class Base {
public:
    bar(int);
protected:
    int ival;
};
class Derived1 : virtual public Base {
public:
    bar(char);
    foo(char);
protected:
    char cval;
};
class Derived2 : virtual public Base {
public:
    foo(int);
protected:
    int ival;
    char cval;
};
class VMI : public Derived1, public Derived2 { };

从这个继承层次看,VMI类内部访问哪些没有二义性,哪些成员有二义性呢?
从VMI类内部可以不加限定地访问继承成员bar和ival:bar在共享基类Base和派生类Derived1中都存在,但特定派生类实例的优先级高于共享基类实例,所以在VMI类内部不加限定地访问bar,则访问到的是Derived1中的bar实例。ival在共享基类Base和派生类Derived2中都存在,同理,在VMI类中不加限定地访问ival,访问到的是Derived2中的ival实例。
继承成员foo和cval需要限定:二者在Derived1和Derived2中都存在,Derived1和Derived2均为Base的派生类,访问优先级相同,所以,如果在VMI类内不加限定地访问foo和cval,则会出现二义性。

C++11中bind,function实战记录

C++11中bind和function的使用

最近在做的项目中有一个需求,我负责向客户端推送一组数据,这些数据需要另一个服务器程序员填充,
我需要调用他的一个函数,并把我的填充函数传递给他,然后在他的函数中填充这些数据.

首先我用普通函数试验了一下,因为c++11中的bind和function并没有使用过太多.
``` c++

#include <stdio.h>
#include <functional>
struct Info;
typedef std::function<Info*()> NewInfo;
typedef std::function<void (int key,int value)> PropExecute;
using namespace std::placeholders;

struct Info
{
    int key;
    int value;
};

void test(PropExecute func)
{
    func(1,1);
}
void prop(int key,int value,NewInfo f)
{
    Info* info = f();
    info->key = key;
    info->value = value;
}
Info* new_info()
{
    return new Info;
}

int main()
{
    test(bind(prop,_1,_2,new_info));
    return 1;
}

这样写就基本实现了该功能,InfoPropExecute函数类型由我公开给服务器同事,
同事提供test函数给我调用,并在该函数内使用我传递过去的函数将数据填充(这里我用1,1填充).

好了,现在修改一下,把所有的普通函数都修改为类成员函数,因为项目中这些函数都是类成员函数.

#include <stdio.h>
#include <functional>
struct Info;
typedef std::function<Info*()> NewInfo;
typedef std::function<void (int key,int value)> PropExecute;
using namespace std::placeholders;
class Attr
{
public:
    Attr():key(1),value(1){}
    ~Attr(){};
    void get_attr(PropExecute func)
    {
        func(key,value);
    }
    int key;
    int value;
};
class Info
{
public:
    Info():key(0),value(0){};
    ~Info(){};
    Info* new_info()
    {
        return new Info;
    }
    int key;
    int value;
};
class MyClass
{
public:
    MyClass(){}
    ~MyClass(){}
    void set_prop(int key,int value,NewInfo f)
    {
        Info* info = f();
        info->key = key;
        info->value = value;
    }
};


int main()
{
    Info info;
    Attr attr;
    MyClass myclass;
    NewInfo newinfo = std::bind(&Info::new_info,&info);
    attr.get_attr(bind(&MyClass::set_prop,&myclass,_1,_2,newinfo));
    return 1;
}

修改为类成员函数后就是这样了,这里遇到一个问题,main函数中调用get_attr时分成了两条代码,
因为写成这样编译报错attr.get_attr(bind(&MyClass::set_prop,&myclass,_1,_2,std::bind(&Info::new_info,&info)));
我估计可能是类型不匹配之类的错误.总之分成两条代码就OK了.

orm-odb(c++),一种对象关系映射数据操作方式

基于ORM机制的C++数据库操作开源库ODB

最近在项目里遇到一个问题,关于数据库读取大量数据的,这部分代码项目里的思路是,对数据表做索引,然后依据索引排序,取出最上面的max_size(5000)条数据,然后基于后面的数据继续排序,取出最上面max_size(5000)条,依此循环直到取出最后一条数据. 开始我觉得这样可能效率会比较低,因为每次操作都要进行一次排序,我觉得用limit 0,max_size .limit 0+max_size,max_size+max_size这样的方法要快一些,后来想了一下问题的关键应该是在每次取第一条数据的时候,比如先排序再去取的话,因为建立了索引,所以排序是很快的,而且第一条马上就找到了,但是用limit 0,max_size这样的方法的话,每次查找第一条数据都要顺序数到位置,所以先排序再取数据的方法是稍微好一点的.


不过正是因为这个问题,偶然发现了一个c++的数据库操作库,基于ORM机制,google了一下ORM,全称是对象关系映射(Object Relational Mapping),简单来说就是使用这个库可以让程序员避免直接操作sql语句,数据的存储和对象直接相关,多说无益,记下使用方法以便日后用得到.


我是在windows平台上编译和测试的.这里记下windows下的使用方法:
首先从http://www.codesynthesis.com/products/odb/ 下载到源码,主要是the Common Runtime Library和the Database Runtime Library,前者是ODB的基本库,后者是基于不同数据库的扩展库,后者要基于前者.我用的是mysql,所以要下载mysql的扩展库.前者没有任何依赖可以直接编译,后者要基于前者和mysql的库,我编译的时候总是不成功,后来发现我机器上安装的mysql是64位的,编译ODB的时候把win32改成X64后编译通过,以后要注意下这个问题.使用编译好的库启动官方的例子程序”hello”,一切正常.
简单的用法如下:

这是一个简单的person类.
// person.hxx

#include <string>

class person
{
public:
  person (const std::string& first,
          const std::string& last,
          unsigned short age);

  const std::string& first () const;
  const std::string& last () const;

  unsigned short age () const;
  void age (unsigned short);

private:
  std::string first_;
  std::string last_;
  unsigned short age_;
};

使用ODB后修改如下:

// person.hxx

#include <string>

#include <odb/core.hxx>     // (1)

#pragma db object           // (2)
class person
{
  ...

private:
  person () {}              // (3)

  friend class odb::access; // (4)

  #pragma db id auto        // (5)
  unsigned long id_;        // (5)

  std::string first_;
  std::string last_;
  unsigned short age_;
};

使用这个sql文件建立数据表
//This file was generated by ODB, object-relational mapping (ORM) compiler for C++.

DROP TABLE IF EXISTS `person`;

CREATE TABLE `person` (
  `id` BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
  `first` TEXT NOT NULL,
  `last` TEXT NOT NULL,
  `age` SMALLINT UNSIGNED NOT NULL)
 ENGINE=InnoDB;

测试例子如下,执行commit函数后三个person类对象的数据就写入了数据库中,真的非常方便简洁.
//driver.cxx

#include <memory>   // std::auto_ptr
#include <iostream>

#include <odb/database.hxx>
#include <odb/transaction.hxx>

#include <odb/mysql/database.hxx>

#include "person.hxx"
#include "person-odb.hxx"

using namespace std;
using namespace odb::core;

int
main (int argc, char* argv[])
{
  try
  {
    //auto_ptr<database> db (new odb::mysql::database (argc, argv));
    auto_ptr<database> db (new odb::mysql::database("dll","dll","odb_test","localhost",3306));
    unsigned long john_id, jane_id, joe_id;

    // Create a few persistent person objects.
    //
    {
      person john ("John", "Doe", 33);
      person jane ("Jane", "Doe", 32);
      person joe ("Joe", "Dirt", 30);

      transaction t (db->begin ());

      // Make objects persistent and save their ids for later use.
      //
      john_id = db->persist (john);
      jane_id = db->persist (jane);
      joe_id = db->persist (joe);

      t.commit ();
    }
  }
  catch (const odb::exception& e)
  {
    cerr << e.what () << endl;
    return 1;
  }
}

这是ODB官方自带的例子,我们在使用ODB的时候需要自己生成person类的C++文件,使用官方的ODB Compiler,下载后将ODB的bin目录添加到系统的环境变量中,通过person.hxx文件即可得到ODB需要的其他c++文件,类似protobuf的使用.
odb -d mysql –generate-query –generate-schema person.hxx
这条命令即可生成person-odb.hxx, person-odb.ixx, person-odb.cxx这三个文件,这样配合编译好的库文件就可以操作数据库了.

Windows下编译libcurl

Windows下libcurl静态库的编译和使用

最近在玩一个游戏,这游戏的服务器是http通信的,就想伪造一些数据看看.想起了curl,于是去下载,结果编译半天总是出错,又浪费了好多时间,现在成功了,记下来.


首先从这里下载最新的源代码
http://curl.haxx.se/download.html
libcurl是源码,curl是一个http的工具,反正我用libcurl的源码就够了.

打开找到相应的VC版本工程用VS打开,我用的是VS2012,相应的版本是VC11.

一开始编译出错,因为这个版本里包含了ssh和ssl的第三方库,我也用不着所以在预编译宏里把关于ssl和ssh的都去掉.

然后继续编译lib静态库成功了,新建测试工程加入头文件和生成的附加库libcurld.lib,发现有无法识别的外部符号错误,网上找了半天解决方法.

最后的解决方法是. libcurl的预编译设定为
WIN32
_DEBUG
BUILDING_LIBCURL
测试项目的预编译设置为
BUILDING_LIBCURL
并且在测试项目头文件中添加以下内容
#define CURL_STATICLIB
#if defined(_DEBUG)
#pragma comment(lib, "libcurld.lib")
#else
#pragma comment(lib, "libcurl.lib")
#endif
#pragma comment(lib,"winmm.lib" )
#pragma comment(lib,"ws2_32.lib" )
#pragma comment(lib,"wldap32.lib" )

再次编译,搞定.

xcode在os x下编译使用protobuf

os x系统下xcode中使用protobuf

最近在用cocos2dx做小游戏,希望用到网络部分,所以希望protobuf能在mac平台上工作,baidu和goole了一下后发现大部分都是重复的一两篇技术文章,转来转去,而且按照这两篇文章搞了一下都没有成功,后来发现了两篇实用的文章后才解决了问题,记录一下:

主要参考两篇在网上流传不广的文章.
http://blog.csdn.net/deep_coder/article/details/38055275
http://blog.csdn.net/rct1985/article/details/9340641

https://code.google.com/p/protobuf/downloads/list 这里下载最新的SourceCode工程, 我用的是2.5.0版本,下载完解压下指定目录下。

cd yourDir

./configure
make
make check
sudo make install

安装成功后在需要用到protobuf的xcode工程里:
a. 把解压完的目录下 protobuf-2.5.0/src/google整个目录拷贝到cocos2d-x工程下的libs目录下。
b. 把解压完的目录下 config.h 拷贝到 libs/google 目录下,主要是放到一些宏定义, 没办法,代码被引用了。
c. 删除编译多语言相关文件,google/protobuf/compiler 目录是用来编译多语言的,删除
d. 删除单元测试文件 所有 *unittest.cc 文件是测试用例(根据文件名猜的),删除, 还有两个tesst打着的文件夹

然后编译,会出现这个错误”#error Host architecture was not detected as supported by protobuf”

解决这个问题的方法是:

In platform_macros.h, Replace

#else
#error Host architecture was not detected as supported by protobuf
#endif


By:
#elif defined(__aarch64__)
#define GOOGLE_PROTOBUF_ARCH_ARM 1
#define GOOGLE_PROTOBUF_ARCH_64_BIT 1
#else
#error Host architecture was not detected as supported by protobuf
#endif

最后再编译就OK了!

gameloft puyo(一种类俄罗斯方块游戏的代码)

#头文件#
//tetris.h

#ifndef _TETRIS_H_
#define _TETRIS_H_

//#define _TETRIS_DEBUG

#include "include/glut.h"
#include "include/glpng.h"
#include <list>

#pragma comment(lib,"glpng.lib")

#define TETRIS_SCALE 2
#define TETRIS_SCREEN_WIDTH    ((GLint)(96*TETRIS_SCALE))
#define TETRIS_SCREEN_HEIGHT ((GLint)(192*TETRIS_SCALE))

#define TETRIS_INFO_WIDTH 120

#define TETRIS_KEY_ESCAPE 27
#define TETRIS_KEY_UP 72
#define TETRIS_KEY_DOWN 80
#define TETRIS_KEY_LEFT 75
#define TETRIS_KEY_RIGHT 77
#define TETRIS_KEY_SPACE  32
#define TETRIS_KEY_ENTER  13

#define TETRIS_TEXTURE_COUNT 4

#define TETRIS_UNIT_WIDTH 32
#define TETRIS_UNIT_HEIGHT 32

#define TETRIS_COLOR_BLUE 0
#define TETRIS_GREEN_BLUE 1
#define TETRIS_RED_BLUE 2
#define TETRIS_YELLOW_BLUE 3

#define TETRIS_UNITS_NUM (TETRIS_SCREEN_WIDTH/TETRIS_UNIT_WIDTH*TETRIS_SCREEN_HEIGHT/TETRIS_UNIT_HEIGHT)//72

#define TETRIS_COL_NUMS (TETRIS_SCREEN_WIDTH/TETRIS_UNIT_WIDTH)//6
#define TETRIS_ROW_NUMS (TETRIS_SCREEN_HEIGHT/TETRIS_UNIT_HEIGHT)//12

#define TETRIS_FREQUENCY_MAX 32
#define TETRIS_FREQUENCY_MIN 1

#define TETRIS_DELAY_TIME 100


using namespace std;

struct ImageRec {
    unsigned long sizeX;
    unsigned long sizeY;
    char *data;
};

typedef struct _unitData{
    GLubyte angle:2,color:2,fill:2,stat:2;
}unitData;

typedef struct{
    GLshort col,row;
}unitPos;

typedef struct
{
    GLint mainWindow; //which windown
    GLint textureCount; //number of texture the game use
    GLuint theTextures[TETRIS_TEXTURE_COUNT+1]; //to save information about texture
    unitData units[TETRIS_UNITS_NUM+TETRIS_COL_NUMS*2]; // 72+12 = 84
    unitData CurUnits[2];
    unitPos CurUnitsPos[2];
    GLint CurUnitsStat;
    GLint frequency; //if frequency be changed,the downing speed of object also be changed.
    GLint frequencymax; 
    GLint score; //score
    GLint scoreLastLevel; //score when you pass the last level
    GLint level; //level
    GLint gamecount; //counter
    GLboolean gameover;    // equal to 0: the game is running, equal to 1: game will finish
    GLboolean gamepause; //tag about pausing the game
    GLboolean updating;        //
    list<unitData*> pool;
    list<unitData*> poolex;
    GLint fontBase;
    GLboolean levelStart; 
    GLint delay; // to delay time 
    GLint updateLevel; // 
    GLint waitUserTextSy,waitUserTextSydir;
}tetrisMainData;


#endif


#源文件#
#include "tetris.h"

//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////

using namespace std;
const char *appName="Tetris";
const char *texturefiles[]={
"puyo_blue.png",
"puyo_green.png",
"puyo_red.png",
"puyo_yellow.png",
};
const GLint TexCoord[4][4][2]={
    0.0,0.0, 1.0,0.0, 1.0,1.0, 0.0,1.0,
        0.0,1.0, 0.0,0.0, 1.0,0.0, 1.0,1.0,
        1.0,1.0, 0.0,1.0, 0.0,0.0, 1.0,0.0,
        1.0,0.0, 1.0,1.0, 0.0,1.0, 0.0,0.0,
};

tetrisMainData tetris;

GLvoid Tetris_InitParam(void);


#ifdef _TETRIS_DEBUG

GLvoid tetris_debug(GLint t)
{
    switch(t)
    {
    case 0:
        break;
    case 1:
        break;
    case 2:
        break;
    }
}


#define debugSeg1() \
if(tetris.CurUnitsPos[0].col == tetris.CurUnitsPos[1].col\
   && tetris.CurUnitsPos[0].row == tetris.CurUnitsPos[1].row)\
   tetris_debug(1);

#define debugSeg2()\
if(tetris.CurUnitsPos[0].row > TETRIS_ROW_NUMS+1\
   || tetris.CurUnitsPos[0].row < 0) tetris_debug(2);\
if(tetris.CurUnitsPos[0].col > TETRIS_COL_NUMS - 1\
   || tetris.CurUnitsPos[0].col < 0) tetris_debug(2);\
if(tetris.CurUnitsPos[1].row > TETRIS_ROW_NUMS+1\
   || tetris.CurUnitsPos[1].row < 0) tetris_debug(2);\
if(tetris.CurUnitsPos[1].col > TETRIS_COL_NUMS - 1\
   || tetris.CurUnitsPos[1].col < 0) tetris_debug(2);\

#define debugSeg3()\
if(fabs(tetris.CurUnitsPos[0].col - tetris.CurUnitsPos[1].col) > 2 \
|| fabs(tetris.CurUnitsPos[0].row - tetris.CurUnitsPos[1].row) > 2)\
        tetris_debug(3);

#endif


//Read bitmap file data.
GLint ReadBMP(const char *filename, ImageRec *image) {

    FILE *file;
    unsigned long size;
    unsigned long i;
    unsigned short int planes;
    unsigned short int bpp;
    char temp;



    if ((file = fopen(filename, "rb"))==NULL) {
        printf("File Not Found : %s\n",filename);
        return 0;
    }

    fseek(file, 18, SEEK_CUR);

    if ((i = fread(&image->sizeX, 4, 1, file)) != 1) {
        printf("Error reading width from %s.\n", filename);
        return 0;
    }

    if ((i = fread(&image->sizeY, 4, 1, file)) != 1) {
        printf("Error reading height from %s.\n", filename);
        return 0;
    }

    size = image->sizeX * image->sizeY * 3;

    if ((fread(&planes, 2, 1, file)) != 1) {
        printf("Error reading planes from %s.\n", filename);
        return 0;
    }

    if (planes != 1) {
        printf("Planes from %s is not 1: %u\n", filename, planes);
        return 0;
    }

    if ((i = fread(&bpp, 2, 1, file)) != 1) {
        printf("Error reading bpp from %s.\n", filename);
        return 0;
    }

    if (bpp != 24) {
        printf("Bpp from %s is not 24: %u\n", filename, bpp);
        return 0;
    }

    fseek(file, 24, SEEK_CUR);

    image->data = (char *) malloc(size);
    if (image->data == NULL) {
        printf("Error allocating memory for color-corrected image data");
        return 0;
    }

    if ((i = fread(image->data, size, 1, file)) != 1) {
        printf("Error reading image data from %s.\n", filename);
        return 0;
    }

    for (i=0;i<size;i+=3) {
        temp = image->data[i];
        image->data[i] = image->data[i+2];
        image->data[i+2] = temp;
    }

    return 1;
}


//Load texture of font which the game used.
GLboolean LoadBmpTextures(char *file,GLuint *texture)                                  
{
    int Status=0;                              
    ImageRec *TextureImage=NULL; 

    TextureImage = (ImageRec*)malloc(sizeof(ImageRec));
    memset(TextureImage,0,sizeof(ImageRec));

    if (ReadBMP(file,TextureImage))
    {
        Status=1;                           
        glGenTextures(1, texture);             
        glBindTexture(GL_TEXTURE_2D, *texture);
        glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
        glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage->sizeX, TextureImage->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage->data);

    }

    if (TextureImage)                            
    {
        if (TextureImage->data)            
        {
            free(TextureImage->data);    
        }    
        free(TextureImage);
    }

    return Status;                                 
}


//Create font display list.
GLvoid BuildFont(GLuint *texture)                                
{
    float    cx;                                    
    float    cy;        
    tetris.fontBase=glGenLists(256);                                
    glBindTexture(GL_TEXTURE_2D, (*texture));            
    for (GLint loop=0; loop<256; loop++)                        
    {
        cx=float(loop%16)/16.0f;                        
        cy=float(loop/16)/16.0f;                        

        glNewList(tetris.fontBase+loop,GL_COMPILE);            
        glBegin(GL_QUADS);                            
        glTexCoord2f(cx,1-cy-0.0625f);        
        glVertex2i(0,0);                        
        glTexCoord2f(cx+0.0625f,1-cy-0.0625f);    
        glVertex2i(32,0);                        
        glTexCoord2f(cx+0.0625f,1-cy);            
        glVertex2i(32,32);                    
        glTexCoord2f(cx,1-cy);                    
        glVertex2i(0,32);                        
        glEnd();                                
        glTranslated(20,0,0);                        
        glEndList();                                    
    }                                                
}


//Delete the font
GLvoid KillFont(GLvoid)                                    
{
    glDeleteLists(tetris.fontBase,256);                            
}


//We can call the function below to display the content you want to...
GLvoid glPrint(GLint x, GLint y, char *string)    
{
    glBindTexture(GL_TEXTURE_2D, tetris.theTextures[TETRIS_TEXTURE_COUNT]);                                
    glPushMatrix();                                        
    glTranslated(x,y,0);                                
    glListBase(tetris.fontBase);                        
    glCallLists(strlen(string),GL_UNSIGNED_BYTE,string);                    
    glPopMatrix();                        
}

//
void Texture_Adjust(GLubyte r, GLubyte g, GLubyte b, GLubyte absolute)
{ 
    GLint width, height; GLubyte* pixels = 0; 

    glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &width);
    glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &height); 

    pixels = (GLubyte*)malloc(width*height*4); 

    if( pixels == 0 ) return; 

    glGetTexImage(GL_TEXTURE_2D, 0, GL_BGRA_EXT, GL_UNSIGNED_BYTE, pixels);     
    { 
        GLint i;
        GLint count = width * height; 
        for(i=0; i<count; ++i) 
        { 
            if( abs(pixels[i*4] - b) <= absolute
                && abs(pixels[i*4+1] - g) <= absolute 
                && abs(pixels[i*4+2] - r) <= absolute ) 
                pixels[i*4+3] = 0;
            else 
                pixels[i*4+3] = 255; 
        } 
    } 

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_BGRA_EXT, GL_UNSIGNED_BYTE, pixels); 
    free(pixels);
}

//
void Texture_SetAlpha(GLubyte r, GLubyte g, GLubyte b,GLubyte absolute,GLubyte alpha)
{ 
    GLint width, height; GLubyte* pixels = 0; 

    glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &width);
    glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &height); 

    pixels = (GLubyte*)malloc(width*height*4); 

    if( pixels == 0 ) return; 

    glGetTexImage(GL_TEXTURE_2D, 0, GL_BGRA_EXT, GL_UNSIGNED_BYTE, pixels);     
    { 
        GLint i;
        GLint count = width * height; 
        for(i=0; i<count; ++i){

            if( abs(pixels[i*4] - b) <= absolute
                && abs(pixels[i*4+1] - g) <= absolute 
                && abs(pixels[i*4+2] - r) <= absolute ) 
                pixels[i*4+3] = alpha;        
        }        
    } 

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_BGRA_EXT, GL_UNSIGNED_BYTE, pixels); 
    free(pixels);
}


//If the size of screen is changed,the function will be called.
//to readjust the screen param.
GLvoid changeWindow(GLsizei w, GLsizei h)
{
    if (h == 0) h = 1;
    glViewport(0, 0, w, h);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    if (w <= h){
        gluOrtho2D(0.0, TETRIS_SCREEN_WIDTH, 0.0, TETRIS_SCREEN_WIDTH * (GLfloat) h/(GLfloat) w);
        glScalef(1.0,((float)h/TETRIS_SCREEN_HEIGHT)/((float)w/TETRIS_SCREEN_WIDTH),1.0);

    }
    else{
        gluOrtho2D(0.0, TETRIS_SCREEN_HEIGHT * (GLfloat) w/(GLfloat) h, 0.0, TETRIS_SCREEN_HEIGHT);

        glScalef(((float)w/TETRIS_SCREEN_WIDTH)/((float)h/TETRIS_SCREEN_HEIGHT),1.0,1.0);
    }    
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();    
}

//load all resource will be used.
GLvoid LoadGLTextures()
{
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);    
    pngInfo info[4];
    for (int k=0; k < TETRIS_TEXTURE_COUNT; k++){
        glGenTextures(1,&tetris.theTextures[k]);
        tetris.theTextures[k] = pngBind(texturefiles[k], PNG_NOMIPMAP, PNG_ALPHA, &info[k], GL_CLAMP, GL_NEAREST, GL_NEAREST);
        Texture_Adjust(255,255,255,10);
    }
    LoadBmpTextures("buchstabenalpha.bmp",&tetris.theTextures[TETRIS_TEXTURE_COUNT]);
    Texture_Adjust(0,0,0,11);
    Texture_SetAlpha(133,133,133,122,128);    
    BuildFont(&tetris.theTextures[TETRIS_TEXTURE_COUNT]);        
}


//Draw a sprites
GLvoid render2Dsprite(GLint x,GLint y,GLint w,GLint h,GLint textureId,GLint angle=0)
{        
    const GLint (*p)[2]=TexCoord[angle];
    glBindTexture(GL_TEXTURE_2D,textureId);
    glBegin (GL_POLYGON);
    glTexCoord2f(p[0][0], p[0][1]);    
    glVertex2f (x, y);
    glTexCoord2f(p[1][0], p[1][1]);
    glVertex2f (x+w, y);
    glTexCoord2f(p[2][0], p[2][1]);
    glVertex2f (x+w, y+h);
    glTexCoord2f(p[3][0], p[3][1]);
    glVertex2f (x, y+h);
    glEnd ();    
}


//Draw the scene of the game.
GLvoid render()
{
    glutSetWindow(tetris.mainWindow);
    glClearColor(0.0, 0.0, 0.0, 1.0);
    glClear (GL_COLOR_BUFFER_BIT);
    glLoadIdentity();
    glPushMatrix();

    char str[32];

    glPushMatrix();
    glEnable (GL_BLEND);

        glLoadIdentity();
        str[0] = '\0';
        strcpy(str,"Tetris");
        glTranslatef(0.0,TETRIS_SCREEN_HEIGHT/2,0.0);
        glScalef(3.2,3.2,1.0);        
        glPrint(0.0,0.0,str);
        glDisable(GL_BLEND);        
    glPopMatrix();    
    glPushMatrix();    

        if(tetris.gameover){
            if(tetris.gamecount>>4&1){
            glTranslatef(30,TETRIS_SCREEN_HEIGHT/2.0,0.0);            
            glScalef(1.5,1.5,1.0);        
            glPrint(0.0,0.0,"Game Over!");    
            }
        }

        if(tetris.levelStart==1){
            if(tetris.gamecount>>4&1){
                glTranslatef(50,TETRIS_SCREEN_HEIGHT/2.0,0.0);
                str[0] = '\0';
                sprintf(str,"Level %d",tetris.level);
                glScalef(2.0,2.0,1.0);        
                glPrint(0.0,0.0,str);
            }
        }

        if(tetris.levelStart == 2)
        {

            if(tetris.gamecount>>4&1){
                glTranslatef(20.0,TETRIS_SCREEN_HEIGHT/2.0+tetris.waitUserTextSy,0.0);    
                glScalef(0.8,0.8,1.0);
                glPrint(0.0,0.0,"Press Enter to replay.");
            }

            if(tetris.waitUserTextSydir){
                tetris.waitUserTextSy += 2;
                if(tetris.waitUserTextSy > TETRIS_SCREEN_HEIGHT/2 - 32)
                    tetris.waitUserTextSydir = 0;
            }else{
                tetris.waitUserTextSy -= 2;
                if(tetris.waitUserTextSy < -(TETRIS_SCREEN_HEIGHT/2 - 32))
                    tetris.waitUserTextSydir = 1;
            }            
        }

        if(tetris.updateLevel)
        {
            tetris.updateLevel--;
            if(tetris.updateLevel>TETRIS_DELAY_TIME){
                if(tetris.gamecount>>4&1){
                    glTranslatef(35.0,TETRIS_SCREEN_HEIGHT/2.0,0.0);    
                    glScalef(0.8,0.8,1.0);
                    glPrint(0.0,0.0,"Congratulation!");
                    glTranslatef(-15.0,-20.0,0.0);
                    glPrint(0.0,0.0,"Level updating!");
                }
            }else{
                if(tetris.gamecount>>4&1){
                    glTranslatef(50,TETRIS_SCREEN_HEIGHT/2.0,0.0);
                    str[0] = '\0';
                    sprintf(str,"Level %d",tetris.level);
                    glScalef(2.0,2.0,1.0);        
                    glPrint(0.0,0.0,str);
                }
            }
        }
        glPopMatrix();

    unitData *p=tetris.units,*pTemp;
    for(int j=0;j<TETRIS_ROW_NUMS;j++){    
        for(int i=0;i<TETRIS_COL_NUMS;i++){
            pTemp = &p[j*TETRIS_COL_NUMS+i];
            if(pTemp->fill){                
                glPushMatrix();                
                render2Dsprite(i*TETRIS_UNIT_WIDTH,j*TETRIS_UNIT_HEIGHT,TETRIS_UNIT_WIDTH,TETRIS_UNIT_HEIGHT,tetris.theTextures[pTemp->color],pTemp->angle); 
                glPopMatrix();            
            }
        }    
    }

    glPushMatrix();    
        glEnable (GL_BLEND);
        glDisable(GL_ALPHA_TEST); 
        glLoadIdentity();
        glTranslatef(0.0,TETRIS_SCREEN_HEIGHT-32,0.0);                    
        glPushMatrix();
            str[0] = '\0';
            glScalef(0.8,0.8,1.0);
            sprintf(str,"Score:%d",tetris.score);
            glPrint(0.0,0.0,str);
        glPopMatrix();    
        glTranslatef(TETRIS_SCREEN_WIDTH*2/3,0.0,0.0);
        glPushMatrix();
            str[0] = '\0';
            glScalef(0.8,0.8,1.0);
            sprintf(str,"Level:%d",tetris.level);
            glPrint(0.0,0.0,str);
        glPopMatrix();

        glTranslatef(-TETRIS_SCREEN_WIDTH*2/3,-20.0,0.0);
        glPushMatrix();            
        glScalef(0.7,0.7,1.0);        
        glPrint(0.0,0.0,"Press Space to pause.");
        glTranslatef(0.0,-20.0,0.0);
        glPrint(0.0,0.0,"Press Esc to exit.");
        glPopMatrix();

        glDisable(GL_BLEND);
        glEnable (GL_ALPHA_TEST);
    glPopMatrix();    

    glEnd ();
    glPopMatrix();    
    glutSwapBuffers();    
}

GLvoid Tetris_SetSingleUnit(unitData *p,GLint color,GLint angle)
{
    p->fill = 1; p->color = color; p->angle = angle;
}


//Rotate the object.
GLvoid Tetris_TwoUnitRotate()
{
    if(tetris.CurUnitsStat != 1) return;

    unitData *pDest=NULL,*pMid=NULL,*pTemp;
    unitData (*pa)[TETRIS_COL_NUMS]=(unitData (*)[TETRIS_COL_NUMS])tetris.units;

    pTemp = &pa[tetris.CurUnitsPos[0].row][tetris.CurUnitsPos[0].col];

    if(tetris.CurUnitsPos[1].col == tetris.CurUnitsPos[0].col){        
        if(tetris.CurUnitsPos[1].row < tetris.CurUnitsPos[0].row){
            if(tetris.CurUnitsPos[1].col == TETRIS_COL_NUMS - 1) return;
            pMid = pTemp - TETRIS_COL_NUMS + 1;
            pDest = pTemp + 1;
        }else if(tetris.CurUnitsPos[1].row > tetris.CurUnitsPos[0].row){
            if(tetris.CurUnitsPos[1].col == 0) return;
            pMid = pTemp + TETRIS_COL_NUMS - 1;
            pDest = pTemp - 1;
        }else{
#ifdef _TETRIS_DEBUG
            tetris_debug(0);
#endif
        }
    }else if(tetris.CurUnitsPos[1].row == tetris.CurUnitsPos[0].row){
        if(tetris.CurUnitsPos[1].col < tetris.CurUnitsPos[0].col){
            if(tetris.CurUnitsPos[1].row == 0) return;
            pMid = pTemp - TETRIS_COL_NUMS - 1;
            pDest = pTemp - TETRIS_COL_NUMS;
        }else if(tetris.CurUnitsPos[1].col > tetris.CurUnitsPos[0].col){
            pMid = pTemp + TETRIS_COL_NUMS + 1;
            pDest = pTemp + TETRIS_COL_NUMS;
        }else{
#ifdef _TETRIS_DEBUG
            tetris_debug(0);
#endif
        }        
    }else {
#ifdef _TETRIS_DEBUG
        tetris_debug(0);
#endif
    }

    if(NULL == pDest || NULL == pMid) return;

    if(pDest->fill || pMid->fill) return;

    pa[tetris.CurUnitsPos[1].row][tetris.CurUnitsPos[1].col].fill = 0;    

    tetris.CurUnitsPos[1].col = (pDest - tetris.units) % TETRIS_COL_NUMS;
    tetris.CurUnitsPos[1].row = (pDest - tetris.units) / TETRIS_COL_NUMS;

    tetris.CurUnits[1].angle = (tetris.CurUnits[1].angle+1)%4;
    tetris.CurUnits[0].angle = (tetris.CurUnits[0].angle+1)%4;

    pa[tetris.CurUnitsPos[0].row][tetris.CurUnitsPos[0].col].angle = tetris.CurUnits[0].angle;

    Tetris_SetSingleUnit(pDest,tetris.CurUnits[1].color,tetris.CurUnits[1].angle);

}


//Call back function about keyboard.
GLvoid keys(unsigned char key, GLint x, GLint y)
{
    if(key == TETRIS_KEY_ESCAPE) exit(0);

    if(key == TETRIS_KEY_SPACE) { tetris.gamepause = (++tetris.gamepause)%2;}

    if(key == TETRIS_KEY_ENTER) {
        tetris.levelStart = 0;
        tetris.delay = TETRIS_DELAY_TIME;
        Tetris_InitParam();

    }

    if(tetris.CurUnitsStat != 1) return;
    if(tetris.gamepause) return;
    if(tetris.levelStart) return;
    if(tetris.gameover) return;

    unitData (*pa)[TETRIS_COL_NUMS]=(unitData (*)[TETRIS_COL_NUMS])tetris.units;
    unitData *pUnitTemp[2];

    GLint i,j,k;

    for(i=0;i<2;i++)
        pUnitTemp[i] = &pa[tetris.CurUnitsPos[i].row][tetris.CurUnitsPos[i].col];

    switch(key)
    {
    case TETRIS_KEY_SPACE:        
        break;
    case TETRIS_KEY_UP:
        Tetris_TwoUnitRotate();
        break;
    case TETRIS_KEY_DOWN:        
        tetris.frequency = TETRIS_FREQUENCY_MIN;
        break;
    case TETRIS_KEY_LEFT:
        j = tetris.CurUnitsPos[0].col <= tetris.CurUnitsPos[1].col;
        for(k=0;k<2;k++){
            if(j == 0) i = (k+1)%2;
            else i = k;        
            if(tetris.CurUnits[i].stat) continue;
            if(tetris.CurUnitsPos[i].col > 0
                && !(pUnitTemp[i]-1)->fill){
                tetris.CurUnitsPos[i].col--;
                pUnitTemp[i]->fill = 0;                 
                Tetris_SetSingleUnit(pUnitTemp[i]-1,tetris.CurUnits[i].color,tetris.CurUnits[i].angle);    
            }
        }
        break;
    case TETRIS_KEY_RIGHT:
        j = tetris.CurUnitsPos[0].col >= tetris.CurUnitsPos[1].col;
        for(k=0;k<2;k++){            
            if(j == 0) i = (k+1)%2;
            else i = k;
            if(tetris.CurUnits[i].stat) continue;
            if(tetris.CurUnitsPos[i].col < TETRIS_COL_NUMS - 1
                && !(pUnitTemp[i]+1)->fill){    
                tetris.CurUnitsPos[i].col++;
                pUnitTemp[i]->fill = 0;
                Tetris_SetSingleUnit(pUnitTemp[i]+1,tetris.CurUnits[i].color,tetris.CurUnits[i].angle);
            }
        }
        break;
    }
    glutPostRedisplay();
}



GLvoid specialKeysPressed(GLint key, GLint x, GLint y)
{
    switch(key)
    {
    case GLUT_KEY_UP:
        keys(TETRIS_KEY_UP,0,0);
        break;
    case GLUT_KEY_DOWN:
        keys(TETRIS_KEY_DOWN,0,0);        
        break;
    case GLUT_KEY_LEFT:
        keys(TETRIS_KEY_LEFT,0,0);
        break;
    case GLUT_KEY_RIGHT:
        keys(TETRIS_KEY_RIGHT,0,0);         
        break;
    }    
}

GLvoid Tetris_InitMem(void*start,GLint size)
{
    GLbyte *p=(GLbyte*)start;
    while(size--) (*p) = 0;
}

GLvoid Tetris_InitTwoUnits(unitData*p)
{
    GLint tmp=rand()%3;

    switch(tmp)
    {
    case 0:
        tmp = TETRIS_COL_NUMS / 2 - 1;
        tetris.CurUnitsPos[0].col = tmp;
        tetris.CurUnitsPos[1].col = tmp+1;
        tetris.CurUnitsPos[0].row = TETRIS_ROW_NUMS;
        tetris.CurUnitsPos[1].row = TETRIS_ROW_NUMS;        
        break;
    case 1:
        tmp = TETRIS_COL_NUMS / 2 - 1;
        tetris.CurUnitsPos[0].col = tmp;
        tetris.CurUnitsPos[1].col = tmp;
        tetris.CurUnitsPos[0].row = TETRIS_ROW_NUMS;
        tetris.CurUnitsPos[1].row = TETRIS_ROW_NUMS+1;
        break;
    case 2:
        tmp = TETRIS_COL_NUMS / 2;
        tetris.CurUnitsPos[0].col = tmp;
        tetris.CurUnitsPos[1].col = tmp;
        tetris.CurUnitsPos[0].row = TETRIS_ROW_NUMS;
        tetris.CurUnitsPos[1].row = TETRIS_ROW_NUMS+1;
        break;
    }

    Tetris_InitMem(&p[0],sizeof(unitData));
    p[0].color = rand()%4;
    Tetris_InitMem(&p[1],sizeof(unitData));
    p[1].color = rand()%4;
    tetris.CurUnitsStat = 1;
}

GLvoid Tetris_GameOver()
{
    memset(tetris.units,0,sizeof(tetris.units));
    tetris.pool.clear();
    tetris.poolex.clear();
}


//
GLboolean Tetris_TwoUnitsFunc(unitData *p)
{
    unitData (*pa)[TETRIS_COL_NUMS]=(unitData (*)[TETRIS_COL_NUMS])tetris.units;
    unitData *pUnitTemp[2],*ps;

    GLint i,j,k,cnt;

    for(i=0;i<2;i++)
        pUnitTemp[i] = &pa[tetris.CurUnitsPos[i].row][tetris.CurUnitsPos[i].col];    

    if(tetris.gamecount % tetris.frequency){ return 0;}
    else{    
        j = (tetris.CurUnitsPos[0].row <= tetris.CurUnitsPos[1].row);
        cnt = 0;
        for(k=0;k<2;k++){
            if(j == 0) i = (k+1)%2;
            else i = k;
            if(0 != p[i].stat) 
                continue;
            cnt++;
            ps = pUnitTemp[i] - TETRIS_COL_NUMS;
            if(ps >= tetris.units){    
                if(ps->fill < 2){
                    if(--tetris.CurUnitsPos[i].row < 0) 
                        tetris.CurUnitsPos[i].row = 0;                        
                    Tetris_SetSingleUnit(ps,p[i].color,p[i].angle);                    
                    pUnitTemp[i]->fill = 0;                        
                }else {
                    if(ps < &tetris.units[TETRIS_UNITS_NUM])
                    {
                        pUnitTemp[i]->fill = 2;
                        if(pUnitTemp[i] > &tetris.units[TETRIS_UNITS_NUM] - TETRIS_COL_NUMS )
                        {
                            tetris.gameover = 1;
                            tetris.delay = TETRIS_DELAY_TIME*2;
                            Tetris_GameOver();
                            return 1;
                        }
                    }
                    p[i].stat = 1;                
                }
            }else{
                p[i].stat = 1;                
                pUnitTemp[i]->fill = 2;                        
            }
        }
        if(0 == cnt) tetris.CurUnitsStat = 2;

        tetris.updating = 1;
        tetris.pool.clear();
        tetris.pool.push_back(&pa[tetris.CurUnitsPos[0].row][tetris.CurUnitsPos[0].col]);
        tetris.pool.push_back(&pa[tetris.CurUnitsPos[1].row][tetris.CurUnitsPos[1].col]);
    }
    return 1;
}



GLvoid Tetris_Update()
{
    GLint i,j,cnt=0;

    unitData (*pa)[TETRIS_COL_NUMS]=(unitData (*)[TETRIS_COL_NUMS])tetris.units;

    list<unitData*> *pool;
    list<unitData*>::iterator iter;    

    GLint col,row;

    pool = &tetris.poolex;

    for(cnt=0,iter=pool->begin();iter != pool->end(); iter++)
    {
        unitData *p,*q;

        p = q = (*iter);

        i = 0;
        p += TETRIS_COL_NUMS;
        while(p < &tetris.units[TETRIS_UNITS_NUM] && p->fill) { i++; p += TETRIS_COL_NUMS; }

        if(i > 0)
        {            
            p = (*iter);
            p -= TETRIS_COL_NUMS;
            while(p >= tetris.units && !p->fill) {p -= TETRIS_COL_NUMS; }    

            tetris.pool.push_back(p+TETRIS_COL_NUMS);

            j = 0;
            while(j++ < i){
                p += TETRIS_COL_NUMS;
                q += TETRIS_COL_NUMS; 
                (*p)=(*q);
                p->fill = 2;
                q->fill = 0;
            }    

            cnt++;
        }        
    }

    if(pool->size() > 0) pool->clear();

    if(cnt) return;

    pool = &tetris.pool;

    for(cnt = 0,iter=pool->begin();iter != pool->end(); iter++)
    {
        unitData *p,*pthis;

        GLint nums[4]={0},num=0; //bottom,left,right,above
        GLint thiscol,thisrow,index;

        p = pthis = (*iter);

        index = p - tetris.units;
        thiscol = index % TETRIS_COL_NUMS;
        thisrow = index / TETRIS_COL_NUMS;

        //bottom
        i = 0;
        row = thisrow - 1;
        col = thiscol;
        p = &pa[row][col];
        while(row >= 0 && p->fill && p->color == pthis->color) { nums[0]++; p = &pa[--row][col]; }

        //left
        i = 0;
        row = thisrow;
        col = thiscol - 1;
        p = &pa[row][col];
        while(col >= 0 && p->fill && p->color == pthis->color) { nums[1]++; p = &pa[row][--col]; }

        //right
        i = 0;
        row = thisrow;
        col = thiscol + 1;
        p = &pa[row][col];
        while(col < TETRIS_COL_NUMS && p->fill && p->color == pthis->color) { nums[2]++; p = &pa[row][++col]; }

        //above
        i = 0;
        row = thisrow + 1;
        col = thiscol;
        p = &pa[row][col];
        while(row < TETRIS_ROW_NUMS && p->fill && p->color == pthis->color) { 
            nums[3]++; p = &pa[++row][col]; 
        }

        num = nums[0] + nums[3] + 1;
        if(num >= 4){
            p = pthis - nums[0] * TETRIS_COL_NUMS;
            for(i = 0; i < num; i++){
                p->fill = 0;
                p += TETRIS_COL_NUMS;
            }

            tetris.score += 5*num;

            p = p-TETRIS_COL_NUMS;

            unitPos *pos=tetris.CurUnitsPos;

            if(p == &pa[pos[0].row][pos[0].col]
                || p == &pa[pos[0].row][pos[0].col]){}
            else{                
                tetris.poolex.push_back(p);
                cnt++;
            }
        }

        num = nums[1] + nums[2] + 1;
        if(num >= 4){            
            p = pthis - nums[1];        
            for(i = 0; i < num; i++){
                tetris.poolex.push_back(p);
                p->fill = 0;                
                p++;
            }    
            tetris.score += 5*num;
            cnt++;
        }
    }

    if((tetris.score - tetris.scoreLastLevel) > 50 + (tetris.level) * 10){
        tetris.updateLevel = 128;
        tetris.level++;
        tetris.scoreLastLevel = tetris.score;
        tetris.frequencymax -= 2;
        if(tetris.frequencymax < 1) tetris.frequencymax = 1;
    }

    if(0 == cnt) {
        tetris.updating = 0;
        for(i = 0; i < 2; i++)
            if(!pa[tetris.CurUnitsPos[i].row][tetris.CurUnitsPos[i].col].fill) 
                tetris.CurUnits[i].stat = 1;
    }
    else tetris.updating++;

}


//Timer, 
GLvoid OnTimer(int time)
{
    unitData *p=tetris.CurUnits;

    GLboolean flag=0;

    if(++tetris.gamecount == 30000)
        tetris.gamecount = 0;

    if(tetris.gamepause
        ||tetris.gameover
        || tetris.levelStart
        || tetris.delay) 
    {
        glutTimerFunc(5, OnTimer, 1);        
        glutPostRedisplay();
        if(tetris.delay > 0){
            tetris.delay--;
            if(tetris.delay == 0){
                if(tetris.levelStart==1){
                    tetris.levelStart = 0;
                    //tetris.gameover = 1;
                    //tetris.delay = TETRIS_DELAY_TIME;
                    //tetris.levelStart = 2;

                    return;
                }

                if(tetris.gameover){
                    tetris.gameover = 0;
                    tetris.levelStart = 2;
                    tetris.waitUserTextSy = 0;
                    return;
                }
            }
        }
        return;
    }

    switch(tetris.CurUnitsStat)
    {

    case 0:        
        Tetris_InitTwoUnits(p);
        break;
    case 1:
        if(!tetris.updating)
            flag = Tetris_TwoUnitsFunc(p);
        break;
    case 2:    
        tetris.CurUnitsStat = 0;    
        break;
    }

    glutPostRedisplay();    
    glutTimerFunc(5, OnTimer, 1);

    if(!flag && tetris.updating)        
        Tetris_Update();    
    tetris.frequency = tetris.frequencymax;    
}

GLvoid idle()
{

}

GLvoid Tetris_InitParam(void)
{
    tetris.level = 1;
    tetris.score = 0;
    tetris.levelStart = 1;
    tetris.frequencymax = TETRIS_FREQUENCY_MAX;
    tetris.frequency = tetris.frequencymax;
    tetris.delay = TETRIS_DELAY_TIME;
    tetris.CurUnitsStat = 0;
    memset(tetris.CurUnits,0,sizeof(tetris.CurUnits));
    memset(tetris.CurUnitsPos,0,sizeof(tetris.CurUnitsPos));
    memset(tetris.units,0,sizeof(tetris.units));
    tetris.scoreLastLevel = 0;
}


//Init game.
GLvoid initgame()
{
    LoadGLTextures();    
    glEnable(GL_TEXTURE_2D);
    glAlphaFunc(GL_GREATER, 0.1f);    
    glEnable (GL_BLEND);
    glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glShadeModel (GL_FLAT);    
    Tetris_InitParam();    
}

GLint main(GLint argc,char** argv)
{
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
    glutInitWindowPosition(100,100);
    glutInitWindowSize(TETRIS_SCREEN_WIDTH,TETRIS_SCREEN_HEIGHT);    
    tetris.mainWindow = glutCreateWindow(appName);    
    glutReshapeFunc(changeWindow);
    glutDisplayFunc(render);
    glutKeyboardFunc(keys);
    glutSpecialFunc(specialKeysPressed);
    glutSetCursor(GLUT_CURSOR_NONE);
    glutTimerFunc(5,OnTimer,1);
    glutIdleFunc(idle);

    initgame();

    glutMainLoop();    
    return 0;
}

无休止的加班:游戏行业从业人员出路在何方?

问题

  加班是互联网行业永远的痛,但普通的IT公司,产品起码有个间歇期,而在游戏行业,项目节点定下来后,为保证抢钱节奏,基本天昏地暗永无止境地加班,根本没有时间找女朋友。(本问题具有泛用性,请勿关闭我题目)

  • 工作状态:本人于某二线公司做端游策划2年了。基本一周内1次8点走,2次10点走,2次11点走的节奏,时不时来个通宵和周末加班。陪同赶进度的有程序、测试、合作方的运营团队等人。我认为我们是个成熟的团队,效率甚高,只是工作量之大是必然。

  • 福利回馈。加班费当然是没有。工资就二线水平,在广州一般般。赚钱了得看老板脸色,去年居然只发半个月奖金。(好歹也是1000W/M的流水啊)

  • 晋升途径。策划可升主策,程序可升主管,运营可升经理。但都是以有新开项目为前提,按照二线公司的尿性,找主管喜欢社招而不内部提升。个人认为跳槽到同为二线的公司谋求晋升是一条路,社招去企鹅、X易还是普通员工但待遇大幅提升也不错。

  • 路在何方?问题重点是:人总要成家立业,但做游戏,工作几乎占据了全部,没时间泡妞和进行其余的爱好,有些有老婆的人还因此闹离婚的事也见不少,总而言之生活质量很低,除非到了制作人或总监的级别(游戏圈子真不大,进进出出,最后转业的人很多)。究竟如何才能提高生活质量,路在何方?创业?呵呵。


回答

  我呢,6年策划经验,换了5家公司,跟过9个项目,上线5个,大成有2;做过执行,拼过关卡,搭过系统,算过数值,客串过两场主策;踩过潮流的节点,也曾逆流而动;页游入行,转回端游,又跳进手游;待过大公司,见过土老板,窝过小外资,曾在风投热捧的大佬手下干活,还和人鼓捣过创业计划……以上这些吹牛全不重要,关键是,有一天我突然想明白了,所以,现在我已经不做游戏了。(至于原因我会在下面的生涯理想中提到)

  翻过上面46个回答,没看见有跳出行业的人来现身说法,作为离局者,我提供自己几年的思见作为一个参考视角,这里我对业内的情况不多做细说,只谈一些共通性的问题,希望能有所帮助。

  我能领会题主所说的那种痛苦与彷徨,因为我也曾这样一路走来。题目中的疑问,我们分开来说,先说工作,再谈生活。

那些年加班伤不起的工作

  工作,这是人生中完全无法回避的问题,你要花大半辈子时间去熟悉它,理解它,掌握它,运气好可能会爱上它。下面,我们从简单说起,由低到高三个层次:日常工作、职业规划、生涯理想。

  • 日常工作

  刚入行的那会,我总是埋头干活,各色需求总是来者不拒,一心想着都是尽快的自我提升,7*12的日子也没少过,直到把生活压榨到极限,才发现想跟上事务的累积速度是永远不可能完成的任务。这时候,我开始学会明确岗位的职责,定位自己的位置,规划工作的事项,降低沟通的成本,改进工作方式,甚至学会适当拒绝。当你从一个混乱无序的状态,到分清轻重缓急之后,工作强度和生活追求之间的对立,也会随之慢慢转为在你个人意志支配之下的共处。

  前面五点我就不细说,每个人基本都能做到。对于拒绝,大部分人要么过度拒绝,要么不会拒绝。为什么要拒绝。因为有些事务是临时的、不合理的、在你职责之外的、他人转嫁的、扰乱你工作安排、妨害你日常生活的……那么,你有拒绝的权利。这里,拒绝的目的不是为了逃避工作量,而是为了减少被频繁打断插入的次数,从而提高由此造成的效率低下和计划时间的超支。工作中我们要尊重他人的时间,同时也需要让他人学会尊重我们,适当的拒绝可以帮助双方形成一个更加有效的合作方式。这也是常在游戏机制中提到的负反馈的规范作用。
拒绝怎样的事情才是适当的。所有在前三点定义之外或与之产生冲突的,且不会对项目造成严重影响的事情,都是可以考虑拒绝掉的。这些可以拒绝掉的事件中,侵占时间最明显的要数:无必要的加班和冗长的会议。陪加班也许是策划最常经历的事件,美其名曰程序和美术遇上问题后好及时沟通,其实这里头包含的问题是你文档结构混乱、讲解说明不清。初始情况思路不周全,没有考虑可执行性细节才有后来的临时沟通需求。如果前期工作做好,逻辑明确,流程图清晰,程序员哥哥们还是很给力的,完全不需要我们随时伺候在侧。会议冗长则基本在于主持者的不作为,没有归纳统合意见,拒绝拍板决断,引导进程不力,导致在某一阶段反复拉锯,大家磨到饭点趁势散会,碰上这种人主持的会议,能逃就逃,逃不掉就提上本本进去办公吧。说这两点,是说可以用完备的前期来拒绝无效的后期;可以用经验来规避低效的风险。

  为什么要强调拒绝。因为对于业内大多数无知无良的PM和土老板来说,他们对于时间的贪婪永远不会满足,你不学会拒绝,就别提你想要的生活。当然拒绝也要有礼有节有据。不要笼统地说这个不能做,那个没时间。条分理析地说明黑字部分前三点原因,正常的领导会和你寻求解决方案而不是一味刁难。

  对于奇葩,自然另当别论。我就曾遇到个纱布PM,年终谈绩效的第一句就是:我看你似乎不喜欢加班。心里暗骂一句,嘴上摆开自己的岗位职责、工作事项、负责内容、进度规划、项目的进展、与之的配合。末了,补上一句“我在正常的时间点内正常完成了正常项目规划的正常任务,没有拖慢,没给他人造成困扰,这是我效率的体现。自认100%配得上我的岗位和工资,你有什么意见。如果你对我有其他职责之外的要求,请先拿出相应的诚意,别扯假大空地忽悠我。无效加班这种事除了浪费公司的电力、能蹭个免费工作餐外,唯一的价值就是让老大偶尔路过时,觉得你手下工作上心你有面子,反正这种群众演员不差我一个。”那位在我目前生涯中奇葩度暂居榜首的PM唯喏半晌说不出话来,几个月后,我便跳槽高就去了。说这个,是为了说明日常工作的最后一点:遇见傻逼的时候,甭客气,别陷在里头陪他玩儿。

  • 职业规划

  一般说到职业规划时,我们谈的总是在当前岗位上的上行通道。这条路,大家都很明晰了,不需赘言。稍微提上两句的就是:当你想往上升的时候,不要只看到职位以及所能带来的权益。而应该尝试使用目标职位的思路,来考虑对待你当下所从事的工作,以及对全局走势的一个看法。多想一些再与实际相印证总不会有坏处。很多事情其实是想的时候指点江山容易,真轮到你干也许更加坑爹。比如在看到全线飘红红的加班安排时,你有没有看到项目层的压力,以及公司态度的微妙转变,当你觉得通过某个改动很傻逼的时候,有没有算过其中的风险系数。当然你觉得某项决定真心傻逼时,你有没有考虑过可能会造成的影响,以及是不是该预备两手补救措施。如果只着眼手上的事务,那么成长将变得缓慢。

  接下来,我们尝试把视野更开阔一些,不要只关注直接对应的上升通道,可以关注与其他行业交叉结合处的工作可能,可以考虑跨行业优势所在和短板补足,或者当你的业余爱好远强于工作储备时,是不是考虑换个更擅长的更有意思的来做。这世界上有很多更好玩的事情,不要被手上的工作局限了双眼。这些说起来都很简单,但实践起来相当困难。当你在一个行业内累积相当经验后,想要换个方向面对未知的环境和不可预知的风险时,对谁都是一个艰难的决定。我身边离开游戏圈的朋友大部分选择回老家当安稳的公务员,也有一些跳去做产品经理的。而我呢,一度想去做TMT分析,现在暂时是跑去做电商,不过两年后肯定又会是另一个职业。对于未来,我有一个算是明确但不明晰的想法,走着走着也许就走通了。

  到这里,我想返回头说下有关规划的个人观点:我认为规划实际上是一个在学习中进步,让能力匹配上目标的过程。规划的重点在于建立、明确、完善一套属于自己的方法论和世界观,而非仅是简单职业的成长。当你手中不仅是握着实操的具体经验,而是一套行之有效的处理问题的方式方法与学习能力之后,我想,就算跳到一个十万八千里外的行业中,也能很快成长起来。我一直相信一句话:凡所经历,皆是加持。这里加持的并非具体的事件,而是通过事件总结出的经验教训。以此为基础产生的认识,对未来的期许,才算得上是规划。

  • 生涯理想

  每个人入行时,都对着工作抱有着憧憬。这些理想是入行的动力,也是维系工作的热情。就算最不受待见的公务员,也是希望能过上朝九晚五喝茶看报的舒坦日子。但理想和现实总存在着出入,有时候差异可能很大。这种落差从工作内容到工作目标上都无时无刻地在消蚀你的热情。这时候,就需要重塑和纠正最初的理想。

  我不敢说每个入行的游戏从业者都是抱着对游戏一腔热血而入行的,比如我就见过啥也不懂游戏都不玩,光听说游戏业赚钱就想进来当策划混口饭吃的90后;也见过许多美术和程序漠不关心项目,只看分配到自己工作。但是我还一直抱有一个理想,希望有朝一日能带出一款自己满意、开发团队喜欢、玩家热爱的产品;如果还有可能,我希望通过游戏传递快乐的同时,能带给玩家一个相对客观、正向的价值导向;我的野望,或者说狂妄也就到此为止了。

  但是现在,我却已不再做游戏了。我一直认为,策划的本质就是将快乐的方法通过游戏传递出去。而现在,我看到的和做过的大多游戏传递而出的却是一种赤裸裸的逐利意识。我不反对赚钱,我只是本能反感这种简单粗暴毫无美感的圈钱方式。刚开始的时候,我曾因为宝箱的机制和人吵过,为耐久的扣点和人吵过,为加星的爆率和人吵过,为了各自匪夷所思的收费陷阱和人理论……随着后来的整个游戏圈的约定俗成,我逐步妥协,却还是觉得应该坚持一个不作恶的底线。当我看到,越来越多的所谓玩点其实就是收钱的坑点的时候,玩游戏的玩家越花钱越不开心,我感到我的工作也越来越不开心。当我的开心依附于玩家砸入用于购买开心的钱化入公司的户头再转为个人工资的时候,我感到这份工作已经失却了最初的理想。

  曾待过一个公司,会让策划定期去听客服录音,有天晚上便听到一个小姑娘质问:为什么我花了六千块钱还洗不出一个完美的宠物宝宝!关于这个问题的内部回答是什么呢?呵,理论上这是两万块钱才会出现的概率哦。嗯,两万软妹币换得200%的成长模板和两个紫红耀眼的大字。是的,这钱不偷、不抢、不蒙、不骗、不犯法,但是对于三观懵懂的小盆友来说,你导出这样的价值取向给他真的大丈夫吗?而那些我被人砍了要花多少钱买什么才能报仇这样的客服问答我就不提了。关于这些,我不喜欢,真的不喜欢。

  于是,我又重新思考了下自己的职业理想,赚大钱并不在我的首位,我更想做的是些好玩有趣,为人们提供欢乐,便利人们生活,不作恶、正能量的互联网科技项目。最后,我就离开了。

  当在离职原因一栏写下:“不爱了”三个字与游戏圈作别时,心底其实涌过一阵轻松:我终于敢放下这个我且算精通,将要出头,却早已麻木的工作。《Fight Club》里说“It’s only after we’ve lost everyting that we’re free to do anyting.”我觉得我正逼近这个状态。

  总之呢,道不同不相为谋。如果不放弃眼下,便无法收获未来。如果你不去尝试,永远不知道自己适合什么。窄门未必能上天堂,但自己走得舒坦就成。当你对工作感到彷徨、失意和痛苦的时候,就想想最初的理想,可忍耐的忍耐,该离开时离开。这些都是我的个人观点,也许我生来就是一个不安分的人,觉得折腾也并非是坏事。

谁不想有个明媚的生活

  扯完工作,来说生活。很多时候,我们抱怨工作繁重,仅剩生存。但真给你一个假期,你除了用来睡觉、吃饭、发呆之外,可曾用来真正的活一下?我见过不少策划,下班之后宁愿领份加班盒饭,坐在电脑前上网、聊天、打游戏也不愿下班。就算这样,他们也会抱怨自己没有生活。你口中的生活仅仅是一段可有可无仅供浪费的空闲时间,还是一段用于支撑兴趣爱好的必备时间?如果是前者,因为没有规划没有权重自然随时能被压榨;如果是后者,你有所坚持自然能有保留。所以怎么能说得那么绝对,没有生活呢。时间,挤挤总会有的。上班的时间真是百分百就在干活吗?就算工作时长真的无法缩短的情况下,你难道不能将生活反向渗透到工作中吗,时间再紧,也压缩不到拉屎吧(曾经一上厕所就开始背单词这种事情我会拿出来乱说吗)。总之,以我的观点,你真心需要一段时间的时候,无论如何你都会为它留出位置。如果仅限于口头抱怨,那么就做好继续抱怨到老的觉悟。所谓生活质量,不是有时间就有质量,首先你得有想法、有行动才有质量。

  关于特别提出的女朋友。我想先问一句:你是真想找个携手到老的伴侣呢,还是仅仅想享受女朋友所带来的好处。我认识的单身死宅们,大都停留在后一个的幻想阶段。在一地狼藉的屋子里光着膀子打着dota,一边喊着为什么我没有女盆友这种人活该孤单到老。没想法、没追求、没行动,总幻想有一天有个白来的妹子,呵呵呵呵,祝你成功。对于真心想找妹子的小伙伴呢,就送9个字啦:“高筑墙、广积粮、缓称王”。翻译过来就是:自我提升、扩大圈子、别急着确立关系(这里说的不是屯备胎和玩暧昧,是说对待感情要慎重啦,不能仅仅是因为寂寞就在一起,因为长得好就在一起,因为有车有房就在一起……不然,出来混迟早要还的。)至于哪里找,怎么追,如何相处都是后话。首先你得摆正态度。大家都是做项目的,自然知道执行才是关键,守株待兔、怨天尤人是从来没有结果的。别说前面没路,当你想走时,自然就有路了。

  我一再强调行动与态度,否定主观的归因,大抵是因为我本身是一个偏执的行动派吧。从来觉得与其花时间来抱怨不如着手改善,所有事情认真去做总有解决方案,代价大小的问题。(为了证明这一大段不是信口开河空口大话,我随便自曝一下,EX就是我在7*12的工作强度下遇上的,是一个和我工作与日常交际中完全不搭边的圈子,细节也不说了,每人都有自己擅长的方法,总之,俩人很开心,一块处了三年。虽然最后分开了,现在想来还是惘然。)

via:知乎

这片文章很喜欢,作者的价值观跟我非常像,昨天看86届奥斯卡的颁奖礼,《12 Years a Slave》的男主切瓦特·埃加福特(Chiwetel Ejiofor)在片中有这样一句台词:“I don’t want to survive. I want to live”

关于 gdb 的一些命令使用

Linux下gdb的使用和命令

一般来说,GDB主要帮忙你完成下面四个方面的功能:

1、启动你的程序,可以按照你的自定义的要求随心所欲的运行程序。

2、可让被调试的程序在你所指定的调置的断点处停住。(断点可以是条件表达式)

3、当程序被停住时,可以检查此时你的程序中所发生的事。

4、动态的改变你程序的执行环境。

(命令行的调试工具却有着图形化工具所不能完成的功能)


下面介绍gdb常用的命令

1. 显示源代码,必须在编译的时候使用 -g 参数才可以

list:

(gdb) list
显示当前运行位置的前5行和后5行代码

(gdb) list function
(gdb) list filename:function
显示所指向的本文件的函数或者其他文件内函数的前2行和后8行代码

(gdb) list -
往前显示代码

(gdb) list +
往后显示代码

这些命令默认都是显示10行,可以用set listsize 20 修改显示的行数为20行,用show listsize查看当前设置的显示数量.

list命令还有下面的用法:

(gdb) list <first>, <last>
显示从first行到last行之间的源代码。

(gdb) list , <last>
显示从当前行到last行之间的源代码。

2. 查看运行时数据

print:

在你调试程序时,当程序被停住时,你可以使用print命令(简写命令为p)print命令的格式是:
(gdb) print n

打印出变量n的值,可以按照指定的进制查看变量的值,比如 int n=5 可以使用 print/x n 命令得到 $26 = 000000101结果
(gdb) whatis p
type = int*
显示某个变量的类型

(gdb) print Findfuc(1,0)
对程序中函数的调用

(gdb) print *pCTable
$8={e=reference=’/000’,location=0x0,next=0x0}
数据结构和其他复杂对象

(gdb)print h@10
$13=(-1,345,23,-234,0,0,0,98,345,10)
查看内存中在变量h后面的10个整数,一个动态数组的语法如下所示:base@length

display:

还没怎么接触过,留空.

backtrace:

打印当前的函数调用栈的所有信息。如:
(gdb) bt
#0 func (n=250) at tst.c:6
#1 0x08048524 in main (argc=1, argv=0xbffff674) at tst.c:30
#2 0x400409ed in __libc_start_main () from /lib/libc.so.6
从上可以看出函数的调用栈信息:__libc_start_main --> main() --> func()

frame up down:

切换栈
(gdb) frame 1
切换到bt显示出来的栈序号为1的栈中去.
比如:frame 0,表示栈顶,上面的例子中就是func所在栈,一般当前程序执行的位置的栈就是栈顶,frame 1,表示栈的第二层,
main函数所在栈

(gdb) up n
表示向栈的上面移动n层,可以不打n,表示向上移动一层。

(gdb) down n
表示向栈的下面移动n层,可以不打n,表示向下移动一层。

3. 断点

break:

break命令(可以简写为b)可以用来在调试的程序中设置断点.
(gdb) break filename:line-number
在指定文件的指定行上设置一个断点

(gdb) break filename:function-name
在指定文件的指定函数入口处设置一个断点

(gdb) break line-or-function if expr
在指定行号或者指定函数内设置一个断点,当expr条件成立时断住程序,如:break 46 if dwCount==100

(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep y 0x000028bc in CApp:Init at CApp.cpp:155
2 breakpoint keep y 0x0000291c in main at GameApp.cpp:168
显示断点信息

(gdb) delete breakpoint 1
删除编号为1的断点

(gdb) delete breakpoint
删除所有断点

watch :

设置观察点,还没怎么用过,留空

catch :

设置捕捉点,还没怎么用过,留空

4. 执行

run continue next step finish until:

程序执行

(gdb) set args
可指定运行时参数。(如:set args 10 20 30 40 50

(gdb) show args
查看设置好的运行参数

(gdb) run
执行程序

(gdb) continue
执行到下一个断点 相当于vs里的F5快捷键

(gdb) next
执行下一句代码 相当于vs里的F10快捷键

(gdb) step
执行下一句代码,如果下一句代码调用了函数,那么进入该函数,相当于vs里的F11快捷键

(gdb) finish
跳出当前函数,相当于vs里的shift+F11快捷键

(gdb) until
跳出当前while或者for循环(这个貌似在vs里是没有的,求大牛指点)

jump:

跳转执行,还没怎么用过,留空

call:

强制调用函数,留空

5. 其他

还有一些关于”信号”,”搜索”等没怎么接触过的命令和用法,有待以后工作和学习中接触到之后再填补.

const和static的用法

const和static的用法总结

const的用法

1. const修饰变量和指针

const修饰变量和指针即表示该变量和指针不能被修改,一般在遇到const char* 的时候会
产生歧义.const char* 这种结构的定义其实只要找到const修饰的是哪种变量类型就可以
知道究竟哪个量是不可修改的了.比如, const (char*) pBuffer const修饰的是 char*
类型 ,所以pBuffer作为一个char*类型的变量被const,pBuffer的值也就是这个指针的值是
不能被修改的.而 const (char) *pBuffer const修饰的是char类型,所以*pBuffer的char
内容是不能被修改的.所以以后要是有人给你出这种题目,抓住const修饰的类型就好了.

2. const修饰函数参数

这种情况主要是为了保护实参在函数内不被改变,比如 void function(const char *p),p所
指向的内容是不能在函数中被修改的,否则编译不通过.还有 int function(const A &a)这种
用法,避免拷贝构造函数的调用和析构函数的调用,采用传引用的方法,加const修饰可以避免
函数内部对a对象的修改.

3. 用const修饰函数的返回值

如果给以“指针传递”方式的函数返回值加const修饰,那么函数返回值(即指针)的内
容不能被修改,该返回值只能被赋给加const修饰的同类型指针。
例如函数:
1
const char * GetString(void);
如下语句将出现编译错误:
1
char *str = GetString();
正确的用法是
1
const char *str = GetString();

4. const成员函数

任何不改变成员变量值的成员函数都应该被定义为const成员函数,const放在函数声明后面

1
2
3
4
5
6
7
8
9
10
11
12
class CStack
{
public:
int GetCount() const;
private:
int m_dwNum;
}

CStack::GetCount() const
{
m_dwNum++; //编译错误,不能对成员变量进行修改;
}

总之,const的作用就是定义只读,在一定程度上控制程序员的失误操作,保证程序的健壮和稳定.


static的用法

  1. 隐藏

    在两个源文件中,如果定义了全局的变量和函数,两个源文件中都可以使用这些全局变量和函数,
    如果想让变量和函数只在当前文件中起作用,就要加static前缀将函数隐藏在本源文件中.

  1. 类的静态成员

    类的静态成员只属于类本身,不属于任何一个类实例,所以没有this指针.如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class CStack
    {
    public:
    static int m_dwCount;
    static int GetCount() const;
    }
    int CStack::m_dwCount = 10;
    CStack::GetCount()
    {
    return m_dwCount;
    }
    //静态成员函数不能访问类的私有成员,只能访问类的静态成员,
    //实际上,它就是增加了类的访问权限的全局函数

    静态成员必须要在定义中初始化.