近期网站业务量比较大,用户注册量突破20万,用户的附件也到达几十个GB,服务器的硬盘已经满了,但是出于一些原因,没有及时更换服务器,服务器磁盘大小就成了一个瓶颈,于是我做了以下几个改进来解决这个问题:
1 服务器会储存近3天的日志,每天日志大约5G左右,改为只储存近1天的日志,可以节约大约10G的磁盘空间
2 文件数据库大约占据30G的空间,将这一部分一并转移到另一个分区,可以腾出来大约30G磁盘空间
3 用户上传的附件也变成了比较占磁盘空间的数据,这部分数据被访问的不频繁,甚至一些旧用户的数据基本不会被访问到,所以要把这一部分数据搬到其他服务器上,而且还要能被访问到,于是我想到了nsf(网络文件系统)来解决这个问题。

数据搬运到另一台服务器,用户访问和之前一样的url,使用nginx重定向请求到网络磁盘中的文件,因此访问时间也会变长。因为用户附件数据有一个比较大的特点,即越久的数据,被访问的概率越小。所以将比较旧的数据搬运到另一个服务器上,这样可以让绝大多数请求时间不会受影响。

因为这个系统的程序被重写过,两次附件的命名规则有较大的不同,旧版本上传的附件命名格式为5xxxxxxxx,新版本的附件命名格式是14xxxxxxxx。于是将5xxxxxxx的文件移动到另外一台服务器(备用服务器)。

在备用服务器(Centos6)上安装nfs服务器软件

yum install nfs-utils rpcbind

在/etc/exports在添加一行

/home/wwwbackup/server48 xxx.xxx.xxx.xxx(ro)

xxx.xxx.xxx.xxx 是开放监听的地址,可以是IP,域名等
格式是

<输出目录> [客户端1 选项(访问权限,用户映射,其他)] [客户端2 选项(访问权限,用户映射,其他)]

具体配置参考

http://www.cnblogs.com/mchina/archive/2013/01/03/2840040.html

启动 rpcbind 和 nfs

service rpcbind start
service nfs start

设置开机启动

chkconfig --level 35 rpcbind on
chkconfig --level 35 nfs on

在客户机上安装nfs软件

yum install nfs-utils

挂载网络磁盘

mkdir /mnt/server48
mount -t nfs xxx.xxx.xxx.xxx:/home/wwwbackup/server48 /mnt/server48

不报错的话说明挂载成功了,如果报错Linux NFS Mount: wrong fs type, bad option,说明没安装nfs-utils

试着ls一下目录文件

ls -l /mnt/server48

成功,说明挂载成功,如果需要每次开机自动挂载,那么需要在/etc/fstab中增加一行

xxx.xxx.xxx.xxx:/home/wwwbackup/server48 /mnt/server48 nfs 0 0

重启系统生效

于是整个nfs服务器端和客户端就配置好了。

下面配置nginx,使访问5xxxxxx的附件的请求从nfs上取文件,在nginx配置中增加

location ~ ^\/upload\/attachment\/5(.*) {
    root /mnt/server48/old-attachment/;
    expires 12h;
}

nginx的location匹配规则是
1. =前缀的指令严格匹配这个查询。如果找到,停止搜索。
2. 所有剩下的常规字符串,最长的匹配。如果这个匹配使用^〜前缀,搜索停止。
3. 正则表达式,在配置文件中定义的顺序。
4. 如果第3条规则产生匹配的话,结果被使用。否则,如同从第2条规则被使用。
(参考http://www.nginx.cn/115.html)

所以这条规则尽量靠前放置。
重启nginx

访问

http://jl.haitou.cc/upload/attachment/523314e462ba5.jpg

测试可以访问,于是判断整套配置正确。

还是有一些问题值得反思的
1 本例中nfs被配置成只读模式,如果是读写模式会带来什么问题
2 nfs的认证安全性问题,

参考链接:

http://linux.vbird.org/linux_server/0330nfs.php

http://www.cnblogs.com/mchina/archive/2013/01/03/2840040.html

http://ubuntuforums.org/showthread.php?t=1900450

http://blog.chinaunix.net/uid-26393988-id-3268972.html

web应用会把数据持久化到磁盘,但是以何种方式持久化,有很深的学问。
刚开始学编程的时候,老师们叫我们把数据以文件的形式储存在磁盘中,这似乎是最简单的持久化方式,所有数据持久化的最终操作都是写磁盘文件。
写文件是最原始的数据持久化方式,也是效率最高的方式,但是有些逻辑因为数据组织过于复杂,需要关系型数据库来储存数据,近几年流行的非关系型数据库也是是一个不错的选择。

不管关系型数据库,还是非关系数据库,都会提供一个接口,用户无需关心数据到底存在磁盘的哪个位置,但是直接写文件不便于管理,于是开发了一个接口,提供了读,写,删的接口,用户无需关心数据存在哪里,只要提供桶名和文件id即可找到文件。

首先要下载一份php的源码,这里以php-5.5为准。

cd php/php-5.5.xx/ext
./ext_skel --extname=filebase
cd filebase
vi config.m4

修改其中的

PHP_ARG_ENABLE(filebase, whether to enable filebase support,
Make sure that the comment is aligned:
[  --enable-filebase           Enable filebase support])

把每行之前dnl删除掉。

接下来修改php-filebase.h中,删除以下一行

PHP_FUNCTION(confirm_filebase_compiled);	/* For testing, remove later. */

接下来修改filebase.c中,删除以下一部分

/* Remove the following function when you have successfully modified config.m4
   so that your module can be compiled into PHP, it exists only for testing
   purposes. */
……

https://github.com/gerpayt/php-filebase/commit/ff7512792903e0aa3fd3c76f71ad28c91fb6a8cd

接下来,增加第一个函数,叫做filebase_get,暂时就完成一个输出“hello world”的功能吧。

/* {{{ PHP_FUNCTION
*/
PHP_FUNCTION(filebase_get)
{
	php_printf("Hello World!\n");
}
ZEND_BEGIN_ARG_INFO(arginfo_filebase_get, 0)
ZEND_END_ARG_INFO()
/* }}} */

php_printf 就是c语言中的printf。

接下来,改进一点,不是直接输出这条信息,而是当做返回值交给php。

PHP_FUNCTION(filebase_get)
{
	char out[13];
	php_sprintf(out, "Hello World!\n");
	RETURN_STRING(out, 1);
}

用php_sprintf来生成一个字符串,使用RETURN_STRING来返回一个字符串。

接下来让函数读入一个参数,返回Hello xxx! 这样的字符串。

PHP_FUNCTION(filebase_get)
{
	char *name;
	int name_len;
	if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &name, &name_len) == FAILURE)
	{
		return;
	}

	char out[10 + name_len];
	php_sprintf(out, "Hello %s!\n", name);
	RETURN_STRING(out, 1);
}

zend_parse_parameters这个函数用来读入参数

https://github.com/gerpayt/php-filebase/commit/6303011a4b7f498b14c986de99880d9632af3c48

让函数从ini读取参数,并打印这个参数

PHP_INI_BEGIN()
PHP_INI_END()

函数中间插入

    PHP_INI_ENTRY("filebase.root", "filebase", PHP_INI_ALL, NULL)

filebase.c中加入

const char *root = INI_STR("filebase.root");
const int root_len = strlen(root);

char out[10 + root_len];
php_sprintf(out, "Hello %s!\n", root);

https://github.com/gerpayt/php-filebase/commit/dabf00934477a471be783b70933c94d38636f0a0

从文件中读取内容
在filebase.c中加入

php_stream *stream;
char filename[root_len + bucket_len + path_len + 5 ];
char subpath[2];
subpath[0] = path[path_len - 2];
subpath[1] = path[path_len - 1];
subpath[2] = '\0';
 
php_sprintf(filename, "%s/%s/%s/%s", root, bucket, subpath, path);

int options = ENFORCE_SAFE_MODE | STREAM_MUST_SEEK;
	
stream = php_stream_open_wrapper(filename, "r", options, NULL);

if (!stream) {
	RETURN_FALSE;
}
int file_size;
php_stream_seek(stream, 0, SEEK_END);
file_size = php_stream_tell(stream);
php_stream_seek(stream, 0, SEEK_SET);
char buf[file_size+1];
php_stream_read(stream, buf, file_size);
buf[file_size] = '\0';
php_stream_free(stream, PHP_STREAM_FREE_CLOSE_PERSISTENT);

RETURN_STRING(buf, 1);

https://github.com/gerpayt/php-filebase/commit/77ca69a4a1913ceaf3c71fc0a67a8af4f7ab1e65

加入写入函数

PHP_FUNCTION(filebase_put)
{
	char *bucket, *path, *content;
	int bucket_len, path_len, content_len;
	if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "sss", &bucket, &bucket_len, &path, &path_len, &content, &content_len) == FAILURE)
	{
		return;
	}

	const char *root = INI_STR("filebase.root");
	const int root_len = strlen(root);

	php_stream *stream, *stream_dir;
	char filename[root_len + bucket_len + path_len + 5 ];
	char dirname[root_len + bucket_len + path_len + 5 ];
	char subpath[2];
	subpath[0] = path[path_len - 2];
	subpath[1] = path[path_len - 1];
	subpath[2] = '\0';

	php_sprintf(filename, "%s/%s/%s/%s", root, bucket, subpath, path);
	php_sprintf(dirname, "%s/%s/%s", root, bucket, subpath);

	int options = ENFORCE_SAFE_MODE | STREAM_MUST_SEEK;

	stream_dir = php_stream_opendir(dirname, options, NULL);
	if (!stream_dir) {
		int res_mkdir;
		res_mkdir = php_stream_mkdir(dirname, 0755, options, NULL);
		if (!res_mkdir) {
			RETURN_FALSE;
		}
	}
	stream = php_stream_open_wrapper(filename, "w+", options, NULL);

	if (!stream) {
		RETURN_FALSE;
	}

	int file_size;
	file_size = php_stream_write(stream, content, content_len);
	php_stream_free(stream, PHP_STREAM_FREE_CLOSE_PERSISTENT);

	RETURN_LONG(file_size);
}

增加删除函数

PHP_FUNCTION(filebase_del)
{
	char *bucket, *path;
	int bucket_len, path_len;
	const char *root = INI_STR("filebase.root");
	const zend_bool debug = INI_BOOL("filebase.debug");
	const int root_len = strlen(root);
	int result;

	char *filename;
	char subpath[3];

	if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss", &bucket, &bucket_len, &path, &path_len) == FAILURE)
	{
		return;
	}

	filename = (char*)emalloc((root_len + bucket_len + path_len + 5)*sizeof(char));

	subpath[0] = path[path_len - 2];
	subpath[1] = path[path_len - 1];
	subpath[2] = '\0';

	php_sprintf(filename, "%s/%s/%s/%s", root, bucket, subpath, path);

	if(debug){
		php_printf("DEBUG: filename:%s\n", filename);
	}

	// TODO use php wrapper function
	result = unlink(filename);
	if (result)
	{
		RETURN_FALSE;
	}

	efree(filename);
	RETURN_TRUE;
}

差不多就这个三个函数,接下来是编译这个扩展,因为使用的是linux系统,编译相对容易一些。只需要以下几步

phpize
./configure
make
make install

windows下的编译是小羊帮我完成的,我这里就不叙述了。

编译好之后,会生成一个filebase.so的文件,在php.ini中引用这个扩展,并设置filebase.root的参数

[filebase]
extension=filebase.so
filebase.root="/home/filebase"

重启apache服务器,如果在phpinfo中找到相关信息,则表示filebase扩展安装成功。

尽情来玩耍吧,少年!

$dirname = dirname(__FILE__);
ini_set('filebase.root', $dirname.'/filebase');
ini_set('filebase.debug', '1');

$article = filebase_get('article', '12345');
if ($article)
{
    echo $article;
}
else
{
    echo "Error\n";
}

$article = "This is a very long text";
$length = filebase_put('article', '54321', $article);
echo "write len {$length}\n";

$res = filebase_del('article', '54321');
if ($res)
{
    echo "Success\n";
}
else
{
    echo "Failure\n";
}

github地址:https://github.com/gerpayt/php-filebase

———-效率测试———

可想而知,这个插件的性能应该高于file_get_contents(),事实证明,比file_get_contents快10%左右。
其实不是为了提高这10%的性能而编写这个扩展,而是为了方便管理,备份,迁移,符合大数据的思想。

对于一个新闻发布系统,一篇新闻的正文储存有很多种方式,最常见的就是随着标题等信息储存在关系型数据库中,如mysql;另一种方案是使用nosql存储,如mongodb;还有一种简单粗暴的办法就是直接吧文章正文存入一文件的形式存进磁盘。

本文主要测试mongodb和直接存文件的性能。
大约有5万篇文章,平均大小约为50kb。
测试机 Ubuntu 14.04 8核8G内存,5400RPM机械硬盘

测试过程也简单粗暴,随机读取1万个记录,然后输出文章长度,最后计算运行时间。
使用python和php两种语言进行测试。
mongodb的集合带有索引。

python版

import time
import random
import pymongo

print 'filedatabase ...'
start = time.time();
for i in range(10000):
    id = random.randint(0,50000)

    filename = "article/infotext/%d/%d.html" % (id % 100, id)
    try:
        fp = open(filename)
        content = fp.read()
        print len(content),
    except:
        pass
end = time.time();

print "filedatabase\n", end-start


print 'mongo ...'

start = time.time();
for i in range(10000):
    id = random.randint(0,50000)

    client = pymongo.MongoClient("localhost", 27017)
    db = client.files
    doc = db.article.find_one({'_id': id})
    if doc:
        print len(doc['content']),

end = time.time();

print "mongo\n", end-start

php版

<?php 

print "filedatabase ...\n";
$start = microtime(true);
for ($i=0; $i<10000; $i++)
{
    $id = rand(0,50000);

    $filename = sprintf("xjh/infotext/%d/%d.html", $id % 100, $id);
    $content = @file_get_contents($filename);
    print strlen($content).' ';
}

$end = microtime(true);

print "filedatabase\n". ($end-$start) . "\n";

print "mongo ...\n";

$start = microtime(true);
for ($i=0; $i<10000; $i++)
{
    $id = rand(0,50000);

    $mongo = new MongoClient();
    $db = $mongo->files;
    $collection = $db->article;
    $cursor = $collection->findOne(array('_id'=>$id));
    print strlen($cursor['content']).' ';
}
$end = microtime(true);

print "mongo\n". ($end-$start) . "\n";

测试前压缩一下内存

echo 1 > /proc/sys/vm/drop_caches

下面是测试结果:

python

cf@cf-Ubuntu:~/tmp$ python filedatabasevsmongo.py 
filedatabase ...
filedatabase
34.4837019444
mongo ...
mongo
15.6313991547
cf@cf-Ubuntu:~/tmp$ python filedatabasevsmongo.py 
filedatabase ...
filedatabase
27.576843977
mongo ...
mongo
15.4162368774

php

cf@cf-Ubuntu:~/tmp$ php filedatavasevsmongo.php
filedatabase ...
filedatabase
35.346391916275
mongo ...
mongo
0.85509300231934
cf@cf-Ubuntu:~/tmp$ php filedatavasevsmongo.php
filedatabase ...
filedatabase
29.030472993851
mongo ...
mongo
0.83656191825867

对结果进行一定的分析
1运行同一个程序,第二次运行笔第一次运行,在直接读文件上有速度的提升,是因为操作系统会把常用文件缓存在内存中,再次读取时,不必在硬盘里读取。
2php和python在直接读文件方面速度相当python略快
3python的mongo读操作笔php对mongo的读操作要缓慢很多,对于这个现象,只能怀疑是pymongo这个库的效率问题了。
4当反复运行测试程序,基本上会把所有文件缓存进内存,到最后测试结果显示,mongo的性能不如直接读文件。

cf@cf-Ubuntu:~/tmp$ python filedatabasevsmongo.py 
filedatabase ...
filedatabase
0.186345100403
mongo ...
mongo
14.8158619404
cf@cf-Ubuntu:~/tmp$ php filedatavasevsmongo.php
filedatabase ...
filedatabase
0.15131092071533
mongo ...
mongo
0.85830688476562

本文未对并发性能进行测试……
EOF

redis 是一款基于内存的非关系型数据库,目前发展比较成熟,使用redis的公司也比较多,鉴于redis的数据全部存在内存中,并且速度特别快,所以redis适合做一些经常变化,而且数据量比较小的数据存储,比如帖子点击量,用户经验,游戏中的数据等。
本文介绍了使用redis作为点击量统计的全部过程。

安装redis和redis的php驱动
本文写作时,redis最新的版本是2.8.13,redis的php扩展phpredis的最新版本是2.2.5。

安装redis

wget http://download.redis.io/releases/redis-2.8.13.tar.gz
tar zxvf redis-2.8.13.tar.gz
cd redis-2.8.13.tar.gz
make
make install

redis没有第三方依赖包,没有configure过程,安装好之后,默认位置在 /usr/local/bin/redis-xxxx

安装完成后使用

redis-cli -v

来查看版本号

安装成功之后,接下来启动redis-server,启动参数有命令行读入和配置文件读入两种,使用配置文件读入,则首先需要创建redis的配置文件
安装包里面带了redis的默认配置文件redis.conf

cp redis.conf /etc

有兴趣可以看一下redis.conf 中的配置

#是否后台运行
daemonize yes
#进程号文件位置
pidfile /var/run/redis/redis-server.pid
#端口号
port 6379
#监听IP地址的请求
bind 127.0.0.1
#日志级别
loglevel notice
#日志储存位置
logfile /var/log/redis/redis-server.log
#数据库数量
databases 16
#内存向硬盘备份持久数据的周期
save 900 1
save 300 10
save 60 10000
#硬盘文件名
dbfilename dump.rdb
#硬盘文件所在路径
dir /var/lib/redis

接下来启动redis服务器

redis-server /etc/redis.conf

接下来就可以通过命令行工具操作redis了

redis-cli

redis有5中数据类型,每种数据类型都有自己对应的命令,所以命令比较多,这里只关注字符型的数据结构,常用的命令有

#设置某个键值
set some:thing foo
#读取某个键值
get some:thing
#读取键
keys some:*

更多命令参看http://redis.io/commands

接下来安装phpredis扩展

wget https://github.com/nicolasff/phpredis/archive/2.2.5.zip
phpize
./configure
make && make install

修改php.ini文件,添加redis扩展。
重启apache服务器,进入phpinfo.php,查看phpredis是否安装成功。

所有工具都安装好了,接下来用php进行点击量统计。
点击量统计有3个模块
1 帖子详细页面模块-点击量自增,并返回当前点击量
步骤比较简单,首先连接redis服务器,接下来发送一个incr命令
使用thread:click:{$id}的形式来命名redis的键

// Connect redis server
$redis = new redis();
$redis->connect('127.0.0.1', 6379);

// Perform increase action
$click = $redis->incr('thread:click:123');

2 帖子列表页,返回一页帖子所有的点击量
首先从mysql获得需要显示的帖子列表,然后从redis中获取需要显示的帖子点击数,两个点击量加起来即当前真实的点击量

// Connect redis server
$redis = new redis();
$redis->connect('127.0.0.1', 6379);

// Connect mysql server
$mysql = new PDO("mysql:host=localhost;dbname=thread","root","",array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8")); 
$rs = $mysql -> query("select * from thread limit 30");

while($row = $rs -> fetch()){ 
    $click = $redis->get('thread:click:'.$row['id']);
    echo $row['id']."	";
    echo $row['title']."	";
    echo $row['click'], "+", $click, "=", $row['click']+$click;
    echo "\n";
} 

3 隔一段时间把redis中的数据转存到mysql中,并清空redis
取出所有点击量的数据,更新mysql的点击量,并删除redis中与点击量相关的键

// Connect redis server
$redis = new redis();
$redis->connect('127.0.0.1', 6379);

// Connect mysql server
$mysql = new PDO("mysql:host=localhost;dbname=thread","root","",array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8")); 

// Fetch all clicks
$keys = $redis->keys('thread:click:*');
foreach ($keys as $key) {
    $id = substr($key,10);
    $click = $redis->get($key);
    $sql = "update thread SET `click` = `click` + $click where `id` = $id";
    echo $sql,"\n";
    $rs = $mysql -> query($sql);
}
// Delete clicks in redis
$redis->delete($keys);

redis管理工具的话,有两款不错,一个是桌面版的Redis Desktop Manager 一款是基于web的phpRedisAdmin
1 Redis Desktop Manager
官网 http://redisdesktop.com/
下载链接 http://redisdesktop.com/download
2 phpRedisAdmin
官网 https://github.com/ErikDubbelboer/phpRedisAdmin
下载地址 https://github.com/ErikDubbelboer/phpRedisAdmin/archive/v1.1.0.zip
安装
解压压缩包
也可以通过composer或者git来安装
curl -s http://getcomposer.org/installer | php
php composer.phar -s dev create-project erik-dubbelboer/php-redis-admin path/to/install
或者
git clone https://github.com/ErikDubbelboer/phpRedisAdmin.git

修改配置文件
include 目录下面有个config.sample.inc.php 改为 config.inc.php

安装第三方依赖库
cd phpRedisAdmin
git clone https://github.com/nrk/predis.git vendor

统计点击量的工作就做完了,还有一些问题值得探讨
使用redis来做一层缓存对mysql的性能到底有多大提升
redis占用的内存有多大
多久进行一次redis到mysql转存合适

参考

http://redis.io/

http://redis.readthedocs.org/en/latest/

https://github.com/nicolasff/phpredis/tree/2.2.5/

http://www.cnblogs.com/ikodota/archive/2012/03/05/php_redis_cn.html

http://blog.51yip.com/cache/1439.html

http://blog.51yip.com/cache/1440.html

https://github.com/ErikDubbelboer/phpRedisAdmin

phonegap是个很好的东西,他让html+css+js可以在手机上当做本地app来运行。
本文讲述了用Ubuntu14.04配置phonegap环境的过程
phonegap的安装是基于npm的,所以首先要把npm装上

sudo apt-get install npm

npm装好之后,要装phonegap

sudo npm install -g phonegap

这个要等一段时间。

完事后要下载安卓的sdk
去这个网址

http://developer.android.com/sdk/index.html

下载这个文件

http://dl.google.com/android/android-sdk_r23-linux.tgz

下载之后,解压。
运行tool/android这个程序,安装sdk。这个也要很久。。。

最后问题就来了
官网上说只需要三行就可以运行

phonegap create my-app
cd my-app
phonegap run android

我电脑上就报错,是因为环境变量没有配好造成的。
打开 .bashrc 添加以下环境变量

export ANDROID_HOME=/home/cf/Program/android-sdk-linux
export ANDROID_TOOLS=/home/cf/Program/android-sdk-linux/tools
export ANDROID_PLATFORM_TOOLS=/home/cf/Program/android-sdk-linux/platform-tools
export ANT_HOME=/usr/share/ant

PATH=$PATH:$ANDROID_HOME:$ANDROID_TOOLS:$ANDROID_PLATFORM_TOOLS:$ANT_HOME:.

因为我的java没有ant,所以要单独安装下ant

sudo apt-get install ant

此时,再运行

phonegap run android

不报错了,等待一段时间,安卓的模拟器就出来了。

当然,在模拟器出来之前,你要建立一个模拟器先。
可以用myeclipse+adt来建立一个模拟器。

安装myeclipse,下一个myeclipse-pro-2014-GA-offline-installer-linux.run 安装文件
安装过程中需要检测swap分区的容量,我的机子没有swap分区,所以不能通过验证,要临时建立一个swap分区,
参考http://www.cyberciti.biz/faq/linux-add-a-swap-file-howto/

安装好之后,下载一个adt,

http://dl.google.com/android/adt/adt-bundle-linux-x86_64-20140624.zip

在myeclipse的安装目录下建立android的目录,将压缩包中的 features和plugins解压到android中。
在myeclipse/dropins其中添加一个android.link文件,内容是“path=android”
重启myeclipse,即可按提示安装插件。

建立模拟器也比较容易,不多讲了。

最近推出了swift语言,使得ios的开发成为热门,于是也想了解一下相关方面的东西。
折腾的第一步就是要整一个Mac OS出来,我的计算机是i7第四代CPU,折腾起来费了不少功夫。

主要参考这个帖子http://wenzhixin.net.cn/2014/02/10/vbox_mac_install

我的机器是Lenovo Y410P 美版 系统是 Ubuntu 14.04LTS

所需要的资料在这个里面http://pan.baidu.com/s/1hqebqYw
在安装过程中遇到了一些问题,如下:

1 我本来是vbox4.3.10,安装过程中貌似出了点小问题
No interval found for Using 8000000
于是降级为4.3.6.
去这里找到需要的安装包,下下来还挺快的。

安装完之后,建立一个虚拟机,按照原帖的步骤来。

2 遇到一个比较常见的问题
hfs:could not initialize summary table for OS X Base System
博主也给了回答,说mac暂时不支持haskwell 架构CPU,要改CPUID。
于是我就查了一下其他CPU的CPUID,但是不知道换哪个型号的,就按楼主说的来吧。

vboxmanage modifyvm Mac --cpuidset 1 000306A9 02100800 7FBAE3FF BFEBFBFF

这里要说一下,原帖的命令提示符是“#”,于是我在运行的时候加了sudo,结果提示我找不到虚拟机,我以为虚拟机名字“Mac”填错了呢,我添了绝对路径,还是找不到。
网上还有说法要注册一下这个虚拟机

vboxmanage registervm /home/user/VirtualBox\ VMs/Mac/Mac.vbox

结果告诉我which has the same UUID as an existing virtual machine
最后前面不加sudo,就修改了成功了。

3遇到一个常见问题
missing Bluetooth controller transport
帖子里也有说法,但是我觉得说的不详细。
关机,选择光盘镜像“Hackboot_Mav.iso”。然后开机(不是重启)。
进入图形界面后,换光盘,选择“OSX Mavericks2.iso”,按下F5,然后按下“下箭头”按键,选择不要缓存的那个选项“Ignore Cache”,按回车。

之后的都比较顺利,但是安装内核扩展的时候,貌似不能粘贴到虚拟机里,所以手敲的比较累。
有位同学说了,安装内核复制之后,要修改属主,我就修改了属主。

chown -R root:wheel /Volumes/mnt/System/Library/Extensions

如果不加上这句话,会在系统里面报错,说XX失败。

其实还有问题,是电源管理什么的,参看了帖子里的回复。

cd /Volumes/mnt/System/Library/Extensions
mkdir intel_back
mv AppleIntelHD* AppleIntelF* intel_back/
touch ../Extensions

然后就可以了的,只不过启动好慢。。。

接下来是安装Xcode。。。。。。
把光驱里的磁盘换成Xcode.dmg 就会自动弹出一个界面,根据提示安装即可。

基友所在组织的服务器硬盘坏了,让数据恢复部门恢复了一下,结果没有恢复出来,但是意外发现硬盘里有一年之前的备份,所以尽管,最新的数据找不回来了,但是至少能找回一年之前的数据。

于是我们在有坏道的硬盘上找到一年之前的备份数据。

把硬盘装到服务器上,发现服务器不能正常进入windows,于是想各种办法,觉得用LiveCD启动系统再复制出来是一个不错的办法,于是接下来的问题是,LiveCD从哪里来。

LiveCD必须现场制作,于是需要U盘一个,镜像文件一个,刻盘工具软件一个。

U盘是有的,镜像文件和刻盘工具要从网上下载。

于是进入一台windows服务器,发现不能联网,于是打算从一太ubuntu上下载这两个文件。

刻盘工具用UltraISO,镜像用ubuntu14.04。前者只有2M,很快就下载下来了,但是后者有700M,官网的数据下的很慢。

于是想办法加快下载速度,首先想到了用百度云资源下载,但是linux下没有客户端,所以只能通过http下载,速度也不是特别快。突然我想到一个不错的下载站点,就是联创团队的镜像站,找到资源后,10M的速度,转眼就下完了。

接下来是把两个文件转移到windows上,于是拷贝到U盘上,但是速度比较慢,甚至比网络速度还慢。等待了一段时间,还是等完了。

接下来是安装刻盘软件,不费劲。

然后把镜像刻录到U盘里,也不费劲,但是时间上比较长。

成功后,把LiveCD插入到服务器上,进行启动,第一次还是从硬盘启动了,第二次,进系统之前选择启动方式,从U盘启动,然后就可以了。

进入系统之后,磁盘自动挂载了,但是很奇怪,图形界面下仍然打不开磁盘中的文件,于是用命令行进行操作,首先进入超级用户模式

sudo -i

sudo打了好几遍才拼对。。。。。

接下来进入 /media/ 目录下面,然后找到需要的文件夹,此时,想把文件转移到U盘的空余空间上,但是貌似不行,原因也没研究,就先暂存在 /home/ubuntu/ 下面,然后再导入到另一个U盘里面,导入的时候,U盘空间不够,于是就删除了几个日志文件,才勉强塞得下。

拷贝也是一件很长的过程,cp 要加 -r 选项 ,cp没有进度条,基友说是因为在之前,没有屏幕,是用打印机来显示的,需要把进度条都打印到纸上,浪费纸,所以linux下cp没有进度条。

拷贝完之后,基本工作就结束了,收拾东西离开机房,去另外一个地方把数据导出来到基友自己的U盘,也需要很长的时间,最后总算是吧备份数据库的内容给搞到了。

接下来的事情是恢复网站了。

虽然走了很多路,脚上都磨出来泡了,但是今天过的很充实。
想到基友晚上还要熬夜搞jsp作业,于是觉得这件事今天搞实在有点不合时宜,害得他还要熬夜赶作业。

小橙子小朋友是社实的,社实需要获得本科生的信息,学校也同意他们去hub系统上去导入,但是hub系统的外包公司不同意这件事情。
于是他就想绕过外面公司那层,直接获取hub系统的信息,至少学校也同意了,这就变成一件艹hub的游戏了,呵呵……
hub需要验证码才能登录,但是小橙子在这个事情上有些拙计,我就祝他一臂之力。
说到验证码破解,我在之前已经实现了,用的是tesseract-ocr这套开源的软件,在linux上搭建了一个示例,用来破解某些学校招生信息网的验证码。
后来这个系统没有得到实用,重装服务器的时候,装掉了。

现在重新拾起来,也不是一个难事。搭建服务器系统主要有一下几步:

安装前必备的包

yum install libpng12-dev
yum install libjpeg62-dev
yum install libtiff4-dev
yum install automake

wget  http://www.leptonica.org/source/leptonlib-1.67.tar.gz
tar -zxvf  leptonlib-1.67.tar.gz
cd leptonlib-1.67
./configure
make
make install

安装tesseract-ocr

wget http://tesseract-ocr.googlecode.com/files/tesseract-3.01.tar.gz
tar -zxvf  tesseract-3.01.tar.gz
cd tesseract-3.01
./autogen.sh
./configure
make
make install

不出意外的话应该可以编译成功的
在centos上总是出问题
之前是用的3.02版的,但就是编译不通过,谷歌坏了,所以网上找不到方案。
换用3.01版的,就不会有太多问题。

下载识别数据

wget http://tesseract-ocr.googlecode.com/files/tesseract-ocr-3.01.eng.tar.gz
tar xvf tesseract-ocr-3.01.eng.tar.gz
mv tesseract-ocr/tessdata/* /usr/local/share/tessdata/

测试一下tesseract,让tesseract识别这幅图片中的验证码
hubcode

wget http://blog.gerpayt.cn/wp-content/uploads/2014/06/hubcode.jpg
tesseract hubcode.jpg temp
cat temp.txt

得到72204,这说明tesseract-ocr安装成功了。

接下来写一个web接口。

<?php
//ini_set('display_errors','On');
//error_reporting(E_ALL);
if (@$_FILES['image'])
{
    $path = dirname($_SERVER["SCRIPT_FILENAME"]);
    move_uploaded_file(@$_FILES['image']['tmp_name'],$path.'/fuckhub.jpg');

    $imgsrc="data:".@$_FILES['image']['mime'].";base64,".base64_encode(file_get_contents("{$path}/fuckhub.jpg"));
    $ex = "{$path}/fuckhub.jpg {$path}/fuckhub";
    //print $ex;

    $socket = socket_create(AF_UNIX, SOCK_STREAM, 0);
    $connection =@socket_connect($socket,"/tmp/tesseract.sock");
    if ($connection)
    {
        socket_write($socket, $ex);
        $buffer = socket_read($socket, 1024);
        //socket_recvmsg($socket,$buffer);
        socket_close($socket);
        //print $buffer;
        $code =  trim(@file_get_contents($path.'/fuckhub.txt'));
    }
    else
    {
        print "Socket error!";
    }
    @unlink($path.'/fuckhub.jpg');
    @unlink($path.'/fuckhub.txt');
    if (@$_GET['api'])
    {
        echo $code;
        die();
    }
}
?>
<html>
<head>
</head>
<body>
fuckhub
<form action='' method='post' enctype="multipart/form-data">
<input type='file' name='image'/> <br />
<input type='submit' />
</form>
<img src="<?php print @$imgsrc ?>">
<input name="result" value="<?php print @$code; ?>">

</body>
</html>

因为php文件是由www用户来执行的,所以直接调用tesseract命令是失败的,处理方法与之前的一些类似,就是在后台增加一个常驻进程,进程的用户是root,网页通过unixsocket和常驻程序进行通信。于是就可以通过php来执行tesseract命令了。
后台常驻程序

from daemon import daemonize
import socket,os,commands,stat

def main():
    sockfile="/tmp/tesseract.sock"
    sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    if os.path.exists(sockfile):
        os.unlink(sockfile)
    sock.bind(sockfile)
    sock.listen(5)
    os.chmod(sockfile,stat.S_IRWXU|stat.S_IRWXG|stat.S_IRWXO)
    while True:
        connection,address = sock.accept()
        param = connection.recv(1024)
        (rc,rs) = commands.getstatusoutput('/usr/local/bin/tesseract ' + param)
        connection.send(rs)
    connection.close()

if __name__ == "__main__":
    daemonize()
    main()

以下是daemon.py

#!/usr/bin/python
#coding:utf-8

import sys
import os

def daemonize(stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
    try:
        pid = os.fork()
        if pid > 0:
            sys.exit(0)
    except OSError, e:
        sys.stderr.write("fork #1 failed: (%d) %s\n" % (e.errorno, e.strerror))
        sys.exit(1)

    os.chdir('/')
    os.umask(0)
    os.setsid()

    try:
        pid = os.fork()
        if pid > 0:
            sys.exit(0)
    except OSError, e:
        sys.stderr.write("fork #2 failed: (%d) %s\n" % (e.errorno, e.strerror))
        sys.exit(1)

    for f in sys.stdout, sys.stderr:
        f.flush()

    si = file(stdin, 'r')
    so = file(stdout, 'a+')
    se = file(stderr, 'a+', 0)
    os.dup2(si.fileno(), sys.stdin.fileno())
    os.dup2(so.fileno(), sys.stdout.fileno())
    os.dup2(se.fileno(), sys.stderr.fileno())

启动后台程序后,就可以通过访问php网页,从表单中提交验证码图片,来得到验证码中的数字了。

小橙子说这样还不过瘾,最好搞个接口,我就让他用curl模拟post,网址上带个?api=1的参数,就只返回结果了。
一个接口,在本地调用

<?php
$url="http://work.gerpayt.cn/decaptcha/fuckhub.php?api=1";
$data=array('image'=>'@test.jpg');
$ch = curl_init(); //初始化curl
curl_setopt($ch, CURLOPT_URL, $url);//设置链接
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);//设置是否返回信息
curl_setopt($ch, CURLOPT_POST, 1);//设置为POST方式
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);//POST数据
$response = curl_exec($ch);//接收返回信息
if(curl_errno($ch)){//出错则显示错误信息
    print curl_error($ch);
}
curl_close($ch); //关闭curl链接
echo $response;//显示返回信息

大功告成,以后艹hub就方便了。。。

参考

http://hub.hust.edu.cn/index.jsp

http://work.gerpayt.cn/decaptcha/fuckhub.php

http://blog.yorkgu.me/2012/01/30/compile-and-install-tesseract-orc-on-centos/

最近看很多人研究语音接口,于是我也来一发。

很早之前想做一个路由器说话的项目,我的路由器是一台X86架构的小型主机,带有音频输出接口,在装系统的时候驱动都装的很全,插上耳机即可放出音乐。

命令行下播放音乐是用mplayer

mplayer filename.mp3

即可。

接下来要做一个文字转语音的接口,text to speech,简称TTS。网上有很多,python也很容易安装,但是只有英文的。
中文语音处理做的不错的应该是科大讯飞了,科大讯飞也有自己的开放平台,上去注册一个帐号,下载一个开发工具。
工具里有appkey,貌似每个开发者下载的开发包都不一样。

打开Example文件夹下面的ttsdemo,make一下,然后有个可执行文件。执行可执行文件,会生成一个pcm文件,使用mplayer即可播放这个文件,发出声音。

下面把ttsdemo.c这个文件改一下:
改成不生成文件,而是直接从标准输出中输出音频流。注意apppkey要填写自己的。


#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>

#include "include/qtts.h"
#include "include/msp_cmn.h"
#include "include/msp_errors.h"


typedef int SR_DWORD;
typedef short int SR_WORD ;

//音频头部格式
struct wave_pcm_hdr
{
        char            riff[4];                        // = "RIFF"
        SR_DWORD        size_8;                         // = FileSize - 8
        char            wave[4];                        // = "WAVE"
        char            fmt[4];                         // = "fmt "
        SR_DWORD        dwFmtSize;                      // = 下一个结构体的大小 : 16

        SR_WORD         format_tag;              // = PCM : 1
        SR_WORD         channels;                       // = 通道数 : 1
        SR_DWORD        samples_per_sec;        // = 采样率 : 8000 | 6000 | 11025 | 16000
        SR_DWORD        avg_bytes_per_sec;      // = 每秒字节数 : dwSamplesPerSec * wBitsPerSample / 8
        SR_WORD         block_align;            // = 每采样点字节数 : wBitsPerSample / 8
        SR_WORD         bits_per_sample;         // = 量化比特数: 8 | 16

        char            data[4];                        // = "data";
        SR_DWORD        data_size;                // = 纯数据长度 : FileSize - 44
} ;

//默认音频头部数据
struct wave_pcm_hdr default_pcmwavhdr =
{
        { 'R', 'I', 'F', 'F' },
        0,
        {'W', 'A', 'V', 'E'},
        {'f', 'm', 't', ' '},
        16,
        1,
        1,
        16000,
        32000,
        2,
        16,
        {'d', 'a', 't', 'a'},
        0
};

int text_to_speech(const char* src_text ,const char* params)
{
        struct wave_pcm_hdr pcmwavhdr = default_pcmwavhdr;
        const char* sess_id = NULL;
        int ret = 0;
        unsigned int text_len = 0;
        char* audio_data;
        unsigned int audio_len = 0;
        int synth_status = 1;
        FILE* fp = NULL;

        printf("begin to synth...\n");
        if (NULL == src_text)
        {
                printf("params is null!\n");
                return -1;
        }
        text_len = (unsigned int)strlen(src_text);
        fp = stdout;
        if (NULL == fp)
        {
                printf("open stdout error\n");
                return -1;
        }
        sess_id = QTTSSessionBegin(params, &ret);
        if ( ret != MSP_SUCCESS )
        {
                printf("QTTSSessionBegin: qtts begin session failed Error code %d.\n",ret);
                return ret;
        }

        ret = QTTSTextPut(sess_id, src_text, text_len, NULL );
        if ( ret != MSP_SUCCESS )
        {
                printf("QTTSTextPut: qtts put text failed Error code %d.\n",ret);
                QTTSSessionEnd(sess_id, "TextPutError");
                return ret;
        }
        fwrite(&pcmwavhdr, sizeof(pcmwavhdr) ,1, fp);
        while (1)
        {
                const void *data = QTTSAudioGet(sess_id, &audio_len, &synth_status, &ret);
                if (NULL != data)
                {
                   fwrite(data, audio_len, 1, fp);
                   pcmwavhdr.data_size += audio_len;//修正pcm数据的大小
                }
                if (synth_status == 2 || ret != 0)
                break;
        }

        //修正pcm文件头数据的大小
        pcmwavhdr.size_8 += pcmwavhdr.data_size + 36;

        //将修正过的数据写回文件头部
        fseek(fp, 4, 0);
        fwrite(&pcmwavhdr.size_8,sizeof(pcmwavhdr.size_8), 1, fp);
        fseek(fp, 40, 0);
        fwrite(&pcmwavhdr.data_size,sizeof(pcmwavhdr.data_size), 1, fp);
        fclose(fp);

        ret = QTTSSessionEnd(sess_id, NULL);
        if ( ret != MSP_SUCCESS )
        {
        printf("QTTSSessionEnd: qtts end failed Error code %d.\n",ret);
        }
        return ret;
}

int main(int argc, char* argv[])
{
        ///APPID请勿随意改动
        const char* login_configs = " appid = xxxxxxxxx, work_dir =   .  ";
        const char* text  = argv[1];
        const char* param = "aue = speex-wb;3, vcn=xiaoyan,  spd = 50, vol = 50, tte = utf8";
        int ret = 0;
        char key = 0;

        //用户登录
        ret = MSPLogin(NULL, NULL, login_configs);
        if ( ret != MSP_SUCCESS )
        {
                printf("MSPLogin failed , Error code %d.\n",ret);
        }
        //音频合成
        ret = text_to_speech(text,param);
        if ( ret != MSP_SUCCESS )
        {
                printf("text_to_speech: failed , Error code %d.\n",ret);
        }
        //退出登录
        MSPLogout();
        return 0;
}

编译一下,改个名叫做say_stdout。say_stdout程序会把命令行参数转换为音频流,从标准输出中输出,接下来是把音频流播放出来。

mplayer具有播放音频流的功能
写一个shell程序,命名为say

#/bin/sh
say_stdout $1 | mplayer -cache 8192 - > /dev/null 2>&1

接下来使用say命令就能让路由器说话了。

say "我会讲话了!"

嘿嘿,还可以调节语速和音量参数。

路由器能说话之后,我让他来个正点报时的功能。
在crontab中增加一行

0 * * * * say "现在时刻`date +%I`点整" | mplayer -cache 8192 - > /dev/null 2>&1

完工!
参看http://open.voicecloud.cn/

======================================

忘了说了,会说话只让他正点报时肯定不爽,于是我又开发了一个自定义内容的说话接口,是一个python 写的 cgi程序。
因为cgi程序运行时不是root用户所以不能发出声音,具体原因我也不清楚,至少是不能通过cgi直接来发音的。
于是我做了一个客户端和一个服务器端,客户端是cgi程序,以www用户身份执行,服务器端是开机启动,并且一直在后台运行,两者之间通过unix的套接字通信。
cgi端(客户端)

#!/usr/bin/python
# coding:utf8
print "Content-type: text/html"
print
import cgi,socket,sys
try:
    form = cgi.FieldStorage()
    content = form.getvalue('saywhat')
except:
    content = ''

if content:
    content = content.replace(' ','_')
    content = content.replace('\t','_')
    content = content.replace('\r','_')
    content = content.replace('\n','_')
    try:
        sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        sock.connect("/tmp/say.sock")
        sock.send(content)
        print sock.recv(1024)
        sock.close()
    except:
        print "Error",sys.exc_type

print "<html>"
print "<head><title>路由器说话</title><meta charset='UTF-8'></head>"
print "<body>"
print "said:", content
print "<form method='post' action=''>"
print "    <input type='text' name='saywhat' />"
print "    <input type='submit' value='SAY' />"
print "</form>"
print "</body>"
print "</html>"

服务器端

#!/usr/bin/python
import socket,os,commands,stat

def main():
    sockfile="/tmp/say.sock"
    sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    if os.path.exists(sockfile):
        os.unlink(sockfile)
    sock.bind(sockfile)
    sock.listen(5)
    os.chmod(sockfile,stat.S_IRWXU|stat.S_IRWXG|stat.S_IRWXO)
    while True:
        connection,address = sock.accept()
        content = connection.recv(1024)
        if content:
            print 'say "%s"' % content
            (rc,rs) = commands.getstatusoutput('say ' + content)
            connection.send(rs)
        connection.close()

if __name__ == "__main__":
    main()

目前即可通过网页来命令路由器讲话了。