存档

‘C/C++’ 分类的存档

Hybrid开发手记之聊天窗口的WebKit支持

八月 5th, {2011 16 条评论 9,930 人阅读过  

近两天给Hybrid(https://github.com/levin108/hybrid)的聊天窗口加上了WebKit支持,之前没有实际用过WebKit,而且Web前台开发功力也不强,草草做了一个界面,但相比用GtkTextView来实现看上去还是要舒服好多,先上个图吧:

本篇没有什么高深的东西,作为一个简单的开发文档。

一,主题组件化的方法

聊天窗口的显示区域已经组件化,并没有进行深层次的模块化,代码还是在一起编译的,只是逻辑上组件化了。

之前是固定的由GtkTextView实现,在加入GtkWebKit的时候同时也保留了GtkTextView的实现,这两者是可选的,不管是GtkWebKit还是GtkTextView都需要实现四个最基本的函数:

typedef GtkWidget* (*text_create)(void);
typedef void (*text_append)(GtkWidget *, HybridAccount *,
							HybridBuddy *,	const gchar *, time_t);
typedef void (*text_notify)(GtkWidget *, const gchar *, gint);
typedef void (*theme_set_ops_func)(void);

前三个函数是组件的操作函数,功能分别是创建聊天区域,向聊天区域中添加消息,向聊天区域中添加提示消息,最后一个是设置操作集合的钩子函数。

对于这两种不同的实现分别定义了两个文件,chat-textview.c和chat-webkit.c,这两个文件里面分别是两者各自的实现,而它们对外的接口只是一个GtkWidget,这得利于GOBJECT的这种类似多态的特性。

对于不同的实现会定义操作集变量:

static HybridChatTextOps webkit_ops = {
	hybrid_chat_webkit_create,
	hybrid_chat_webkit_append,
	hybrid_chat_webkit_notify
};

聊天窗口当前使用的聊天区域实现方式全由该操作集来确定,而使用哪个操作集可以由两种方式各自的theme_set_ops_func函数来设置。

我们可以把两种组件看成两个不同的主题,在聊天窗口文件中定义了该主题的列表:

struct _HybridChatTheme {
	const gchar *name;
	theme_set_ops_func func;
};
 
static HybridChatTheme theme_list[] = {
#ifdef USE_WEBKIT
	{
		"webkit",
		hybrid_chat_set_webkit_ops
	}, 
#endif
	{
		"textview",
		hybrid_chat_set_textview_ops
	}, {
		NULL, NULL
	}
};

运行时程序会根据用户的当前配置情况来选择使用哪种主题。

二,WebKit遇到的问题

关于WebKit有几点小问题,第一次用难免会碰到些小问题,不过幸好还是解决掉了。

1. undefined @1: ReferenceError: Can’t find variable

WebKit外部来操作DOM模型主要是通过从外部调用webkit_web_view_execute_script()来实现的,当然我看最新的GtkWebKit API里面已经支持直接操作DOM了,但貌似手头的系统上安装的版本都还没有这个API函数,为了兼容性的考虑还是采用了传统的方法。在HTML模块中用Javascript定义了函数appendMessage(html),通过这个函数向WebKit中定义聊天信息,但当收到消息自动弹出的时候会提示undefined @1: ReferenceError: Can’t find variable appendMessage(),这种情况的原因很简单,函数是定义了但找不到,原因只能是因为模板字符串还没有加载完成便调用了appendMessage()函数,因此会出现这样的错误,因此在对WebKit进行脚本操作之前首先要等它初始化完成,也就是等它load_finished之后,WebKit提供了load_finished事件,但这个事件目前已经Deprecated了,替代的方法是使用load_status属性,属性和事件的使用方法明显不一样,关于WebKit deprecate load_finished事件的原因我没去仔细想,load_status属性的方法只能是轮询,在收到消息时检测load_status,或未加载完成,则调用g_timeout_add()将操作延时后执行,执行中再检测load_status,若仍未完成则继续延时,直到加载完成为止,实际上在实现的时候我给g_timeout_add的第一个参数写了0,事实证明在进入加调函数时需要的那个简单的HTML模板就已经加载完成了,这时候再去调用webkit_web_view_execute_script()去添加一条新消息。

2. Message: console message: undefined @1: SyntaxError: Parse error

同样是在调用webkit_web_view_execute_script()遇到了上面的错误,通过对比和分析发现我传入的字符串参数中夹带了\n字符,把这个字符换成空格后便没有这个问题了,但换行在HTML中应该是
,于是需要把\n替换成
,C语言以及glibc都没有提供形如replace()这种方便的函数,于是我用GString,把字符一个一个的贴过去,遇到\n就贴一个
在后面,效率是低点,但实现起来比较方便,代码如下:

static gchar*
escape_string(const gchar *str)
{
	GString *res;
 
	res = g_string_sized_new(strlen(str));
 
	while (str && *str) {
		switch (*str) {
			case 13:
				res = g_string_append(res, "<br/>");
				break;
			case '\"':
				res = g_string_append(res, "\\\"");
				break;
			case '\t':
				break;
			default:
				res = g_string_append_c(res, *str);
				break;
		}
		str ++;
	}
 
	return g_string_free(res, FALSE);
}

以上大致完成了文章开头图片的样式,已经可以正常使用,但字体在我这slackware系统上还有些不太完美,也可能是我系统字体设置有问题,这个等后期再处理。

分类: C/C++ 标签: ,

新开源项目Hybrid开发手记

七月 30th, {2011 31 条评论 22,938 人阅读过  

博客有两个月没更新了吧,先说说这段时间都在做些什么吧,六月初的时候实验室项目验收完,以为可以轻松下来了,于是开了一个新的开源项目在做,这个稍后再说,没过多久就被拉去给实验室的新项目做设计文档去了,这活做起来比硬编码要麻烦得多,于是大多数时间都在为这个事情心烦,稍微有点闲下来的时间就去写点代码,接下来说下最近在做的这个开源项目。

之前的Openfetion虽然也受到了一些开源社区朋友的好评,但软件质量怎么样我心里比谁都明白,在twitter上我也公开承认过Openfetion的代码质量以及软件架构都非常差,说到代码质量,最初在开发Openfetion的时候没有想过会把它作为一个通用的软件拿出来给大家用,而是自己纯粹在写着玩,于是写得很随意,当然这也得怪我这种恶劣的编程习惯,专业的coder即便是写一个测试用的小代码也会严格按规范来,这是一种习惯,我承认之前做得不够好。再说到软件架构,对于IM软件我之前一样没有什么经验,在没有经过调研的情况就盲目开始编码,完全没有参考现有的开源软件架构,甚至没有个合适的事件循环,所以Openfetion目前的状态是勉强能用。

之前考虑过重构Openfetion,但其实重构的成本要远远高于重写,于是我决定终止Openfetion项目,不再为Openfetion增加/修改代码,也不鼓励其它人对它进行修改。

作为Openfetion的替代品,我发起了新的开源项目Hybrid(https://github.com/levin108/hybrid),这同样是一款IM软件,我把它定位为类似于Pidgin/Empathy的IM框架,但即没有使用libpurple也没有使用telepathy,这两个库使用起来都还是有很多限制,我喜欢自由自在的写代码,我不太想考虑太多关于Hybrid这个软件能走多远,能有多少人用,我需要一款这样的软件,我想让它支持飞信和Gtalk,这是我最需要的两种通信协议,这就够了。

Hybrid同样也使用了GTK作为UI库,纯C语言开发,用了OpenSSL处理加密逻辑,使用glib的GIOChannel来实现了异步IO事件循环,同时把libxml2的API做了封装,xml的处理代码看起来简洁多了(libxml2的API命名规则看起来实在太难受了,而且还需要经常对它的自定义数据类型进行转换)。软件的整体架构上参考了一个pidgin,之前的Openfetion用多线程来处理并发逻辑,线程之间的同步开销很大,现在经常还会出现的崩溃问题大多来源于线程同步没有做好。现在采用的方法是采用单线程,将处理逻辑都交给GUI线程来进行处理,当然所有的IO都必须是非阻塞IO,通过异步IO的方法来检测IO事件,当前所有的操作都必须是非阻塞的,包括SSL/TLS握手等,但目前还存在的一个问题是DNS解析过程仍然是阻塞的,我还没有去研究如何实现非阻塞DNS解析,这个会加入TODO List中。至于模块化也是采用了glib中的gmodule来实现的,没有直接使用dlopen的方法,这样应该会提高一点可移植性吧。

关于协议模块,飞信的协议模块在现有的pidgin插件的基础上重写了,使用了封装过的xml接口,同样也使用了一致的编码风格,但在之前的基础上写代码效率就高了很多,很快就完成了。另外关于Gtalk的协议模块也没有花太多时间,用了一个周末的时间就把基础框架弄好了,因为用的是XMPP,开放的协议实现起来非常简单,只需要读一遍协议然后照着开发就好了,一路写下来都很顺,没有遇到什么大的麻烦。

总之到目前为止,这款软件已经基本能满足我的需要了,但还是有很多需要完善的地方,如好友的搜索,webkit的聊天界面,以及自定义请求对话框等,这些等以后慢慢再更新,现在面临毕业找工作,时间相对也较少,有空的时候就更新一点。

还没有想好什么时候要发布一个正式版,有一点点追求完美,总想把它做得更好一点,有感兴趣的同学可以去把代码clone下来玩玩,有用过的同学欢迎反鐀意见。

分类: C/C++ 标签: ,

Apache MPM Prefork设计方法浅析

五月 16th, {2011 2 条评论 9,576 人阅读过  

最近几天翻阅了apache的MPM(Multi-Processing Module)机制相关的代码,虽然还有很多细节没有搞明白,但对apache的服务器模型有了一个大体的概念,对于不同的操作系统,apache提供了不同的默认MPM模型,下表是不同操作系统默认的MPM模型:

BeOS beos
Netware mpm_netware
OS/2 mpmt_os2
Unix prefork
Windows mpm_winnt

Unix平台则对应着prefork模型,prefork从名字上看意思是预先生成子进程,所以这种模型大致上是怎么工作的我们心里差不多有些认识了,prefork是一种很重要的服务器程序设计模型,对应的还有prethread,prefork一般应用在Unix平台上,因为在服务器启动时需要预告fork出一些空闲的子进程,由它们共同监听客户端的请求,这样来实现快速高并发的特性,这种机制之所以不适合Windows等平台,是因为在Windows等平台上进程的代价太高。

apache的进程管理中有一个叫做scoreboard(记分牌)的概念,主进程在进入MPM循环以前会先在进程池中创建一个scoreboard对象,该对象定义如下:

typedef struct {
    global_score *global;
    process_score *parent;
    worker_score **servers;
    lb_score     *balancers;
} scoreboard;

global_score保存主进程的状态,process_score则是一个数组插槽,每个插槽保存一个子进程的状态,worker_score则是一个二维数组插槽,用来保存每个子进程创建的线程状态,根据这个结构主进程可以对子进程以及相关的线程进行管理,apache按照最大化的原则来分配内存,比如会按配置中允许最多的进程数目来为parent分配内存空间。

MPM初始化还有一个很重要的方面是创建一个进程锁,fork()出来的子进程与父进程并不共享内存空间,多进程之于多线程的优势在于多进程可以省去多线程进行线程同步的开销,而这里创建的进程锁,主要作用是为了给accept()加锁,为了避免thundering herd问题。apache实现了五种类型的进程锁,使用flock()或fcntl()实现的文件锁,Posix信号量或System V信号量,以及使用pthread线程库实现的互斥锁。我理解的是文件锁的效率会低于其它类型的锁,因为文件锁要涉及到文件系统的IO操作。我只阅读了跟pthread相关的代码,和一般的多进程程序实现方式一致,因为子进程与父进程以及子进程之间不共享内存空间的,所以不可能像多线程程序一样将互斥锁定义为全局变量 ,因此使用共享内存机制,将互斥锁变量存放到共享内存里面,并设置共享属性。然后便可以使用该互斥锁对子进程中的accept()过程进行加锁。

初始化最后需要开始创建预定个数的子进程,调用startup_children()函数创建指定个数的子进程,该函数会检查scoreboard的空闲插槽,在空闲插槽上调用make_child()函数来在该插槽位置处创建一个子进程,该函数设置scoreboard中进程的状态,并fork()一个子进程,将子进程的pid写入到scoreboard对应的插槽处,子进程创建之后设置SIGHUP和SIGTERM信号,这两个信号对应的回调函数均为clean_child_exit()函数,该函数销毁内存池然后退出子进程。

make_child()函数执行成功后进行child_main()函数,即子进程的主循环。该函数看起来比较复杂,其实做的事情也很简单,首先是创建相关的内存池,对进程锁进行初始化(对于pthread进程锁对应是一个空函数,即无需进行初始化),将socket描述符加入到pollset中,这里的pollset也是apache抽象出来的概念,它的实现可以是kqueue/port/epoll/poll/select,具体采用哪种方式也是配置可选的。这里是我不太明白的地方,经常看到评论说nginx效率高于apache,当问起nginx效率高于apache的主要原因时,得到的答案很多都是nginx采用kqueue和epoll实现了高并发,其实感觉这个理由并不充分,我们可以看到apache同样也实现了kqueue和epoll的多路复用,如果这因为这个的话那apache没有理由会比nginx效率低多少的,另外也看到有说apache的进程管理机制占用内存过高,而且时常需要进行进程切换从而占用了CPU时间,这个说法可以接受,现在非常想去读下nginx的源码,想看看它到底是采用了什么样的机制带来了它如此之多的好评,下一步就可始阅读下nginx的源码,对比着apache,探索一些高并发服务器设计的最优方法。

子进程进入主循环之后会调用accept()方法,这个方法是需要进行加锁的,之后创建一个新的连接对象,并调用HOOK函数对连接进行处理,HOOK机制是apache模块化很重要的一种机制,在主程序中调用HOOK函数,具体的实现由具体的模块来定义。

父进程在创建完子进程之后也进行主循环,监控活动子进程的数目,并通过一定的调度使用子进程数目维护一个平衡,父进程使用waitpid()函数来检测子进程的退出情况,如果有进程退出,则创建一个新的进程来替代已结束的进程从而维持总数的一个平衡。

当然apache还有平稳启动机制,关于平衡启动的代码我暂时略过了,没有细读,以后有时间再回过头来仔细研究。

分类: C/C++ 标签:

Openfetion for ubuntu messaging menu开发手记

五月 3rd, {2011 7 条评论 11,506 人阅读过  

自从ubuntu11.04发布之后Openfetion就遇到了一个比较麻烦的问题,把Openfetion飞信最小化到托盘之后就找不到了,没用过ubuntu11.04,不过据说它的unity桌面貌似没有status icon这回事,所以把Openfetion塞进Messaging Menu也成了一个很重要的任务,在这里把开发过程和大家分享。

首先要感谢@YunQiangSu提供的关于ubuntu messaging menu的资料,是在ubuntu wiki上关于Messaging Menu的介绍,链接在此:https://wiki.ubuntu.com/MessagingMenu/,这篇文章是关于Messaging Menu的一个介绍,以及它的行为和样式的一个指南,虽然没有涉及到具体的开发细则,但也不失为一个很重要的参考。很惭愧地说,在这之前我甚至不知道在ubuntu右上角看到的那个信封到底叫什么名字,后来知道它原来是一个Indicator,ubuntu的status icon区域很多软件都是用libappindicator来实现的,所以它们的行为和其它的发行版不太一致,比如Dropbox和Transimission左键点击Status Icon就可以弹出菜单,而在Slackware里面就只能用右键才能弹出菜单,这就是它们的不同,一个是Indicator,一个是普通的Gnome Status Icon,而这里面提到的右上角的那个信封便是Indiactor的一种,名字叫做Messaging Menu。好吧,我相信很多ubuntu用户会过来鄙视我的,写下这些给那些和我一样的小白扫扫盲,有误请指正。

我们可以把软件安装到Messaging Menu里面,这样即使软件没有启动也可以在Messaging Menu里面找到该软件,并可以从那里启动该软件,方法很简单。下面是pidgin的做法:

mkdir -p debian/pidgin/usr/share/indicators/messages/applications
echo /usr/share/applications/pidgin.desktop > \
		 debian/pidgin/usr/share/indicators/messages/applications/pidgin

这两句话的功能一眼就看地出来,无须解释了。

Messaging Menu相关的开发需要用到libindicate这个库,首先得确保在系统里面安装了这个库的开发版本:

sudo apt-get install libindicate-dev

首先我们需要获取默认IndicateServer对象的引用,并对其进行初始化:

IndicateServer *server = indicate_server_ref_default();
/* 这句话给软件分类,主要是保存位置不同 */
indicate_server_set_type(server, "message.openfetion");
/* 这句话比较重要,它会让已安装在Messaging Menu里面的软件项前面带一个箭头,
 * 表示该软件当前正在运行,效果图见下面 */
indicate_server_set_desktop_file(server, DESKTOP_DIR"openfetion.desktop");
indicate_server_show(server);
g_signal_connect(G_OBJECT(server), INDICATE_SERVER_SIGNAL_SERVER_DISPLAY, 
		G_CALLBACK(server_display), fxmain);

当收到好友发送过来的消息时可以在该项后面显示哪个好友发送过来的消息,以及显示未读消息的条数,效果图如下:

这时候就需要给当前的这个IndicateServer对象添加一个IndicateIndicator对象,并为该对象设置相关的属性,如name(即显示Indicator上显示的名字),count(未读消息的数目),time(消息发送的时间),icon(像如发送消息好友的头像),draw-attention(这个属性可以随便设置一个非空字符串,以使Indicator高亮提示用户有消息到达,同理设置空字符串可取消高亮)。
上面的几个属性除了time和icon之外都可以用indicate_indicator_set_property()函数来搞定,该函数设置的属性值都是字符串,time是一个时间值,需要使用indicate_indicator_set_property_time()函数来进行设置,至于icon属性的设置需要用到另外一个库libindicate-gtk,这个库仅提供了两个函数,其中一个便是indicate_indicator_set_proper_icon()用来设置icon属性,属性值是一个GdkPixbuf对象。下面看Openfetion上收到一条消息时Messaging Menu所对的动作:

/* no indicator found, create one :) */
indicator = indicate_indicator_new();
/* add it to the global indicator list */
indicators = g_slist_append(indicators, indicator);
indicate_server_add_indicator(server, indicator);
 
indicate_indicator_set_property(indicator, INDICATE_INDICATOR_MESSAGES_PROP_COUNT, "0");
indicate_indicator_set_property(indicator, "sid", sid);
/* set icon */
snprintf(portrait, sizeof(portrait) - 1, "%s/%s.jpg", fxmain->user->config->iconPath, sid);
pixbuf = gdk_pixbuf_new_from_file(portrait, NULL);
indicate_indicator_set_property_icon(indicator, INDICATE_INDICATOR_MESSAGES_PROP_ICON, pixbuf);
g_object_unref(pixbuf);
 
g_signal_connect(G_OBJECT(indicator), INDICATE_INDICATOR_SIGNAL_DISPLAY,
		G_CALLBACK(message_display), NULL);
 
indicate_indicator_show(indicator);

首先我们需要确定在Messaging Menu中是否已经有该用户对应的Indicator存在,如果有就没有必要再创建一个新的造成重复,而检测重复性这个问题费了我不小的力气,libindicate没有提供获取列表的API,但查了一下它的源码,发现它确实实现了一个获取列表的函数,它把它定义为虚函数,推荐用户从这个类继承,我对GTK的C语言面向对象的机制不太了解,不知道该怎么去继承,但既然有这个函数就可以想办法拿来调用,能获取到Indicator列表,但这个列表简直让我抓狂,一个GArray列表,里面存放的是Indicator的id,一个毫无用处的数字,通过它根本获取不到Indicator对象,更不用说Indicator对象的属性了,因此我只能用自己的方法来实现了,其实很简单,就是每创建一个Indicator对象就把它塞到一个GSList里面,创建之前搜索该链表,如果其中有indicator的sid值与要创建的sid相同,则表示已存在,无需再重新创建,把它的count值加1即可。

有一点需要注意的是libindicate-gtk新旧版本的pc文件名是不同的(感谢@riku的测试),在10.04上是旧版pc文件名是indicate-gtk.pc,因此编译选项可以是

pkg-config --cflags --libs indicate-gtk

而在ubuntu 11.04中文件名是indicate-gtk-0.5.pc,编译选项便是:

pkg-config --cflags --libs indicate-gtk-0.5

我在CMakeLists.txt做了一些处理来让它自动识别这两个版本的不同。

分类: C/C++ 标签: ,

Apache基础数据结构(tables)代码浅析

四月 29th, {2011 1 条评论 7,255 人阅读过  

提到Apache社区脑子里立马会呈现出一系列的Java项目集合,尽管@gnawux师兄教导我不要纠结于语言,但对于Java的抵制还是很难一下子就消失的,所幸Apache社区最重量级的项目Apache开源HTTP服务器httpd的源代码是完全使用C语言开发的,尽管近年来涌现出种种轻量级高性能Web服务器,Apache仍以它的功能广泛和真正的高性能而处于无可取代的位置。

Apache也经常受人诟病,矛头直指它的低效,我没有太多这方面的经验,因此没有过多的发言权,看到一些大牛们对这个问题的评论是觉得Apache低效是因为对Apache缺少了解。我个人也觉得这样一款多年风行Web服务器领域的软件肯定有它存在的理由,我们认为它不好可能是我们对它的了解不够。

在网上看到过关于Apache源码是否值得阅读的讨论,很多人认为Apache源码组织结构不好,不太适合阅读,更多人推荐nginx等源码,其实我觉得像Apache这种项目正是”重剑无锋,大巧不工“,Linux源码也有人评价说组织的不好,可读它的代码也仍然非常有意义。我个人认为阅读Apache源码一方面能加深对Apache的了解,在使用的时候真正发挥出它的高性能,另一方面也能借鉴它作为大型服务器软件设计的方法。当然,如果要编写扩展模块来实现一些附加功能的话那阅读源码甚至是必须的工作。

以上都是我个人的理解,最近简单地翻阅了部分代码,长时间没有更新博客,这里就写一些Apache里面最基本的tables数据结构(和其它模块关联较小,相对比较独立,当然我也觉得读任何源码都应该先搞清楚它最基础的数据结构)。

C语言里面没有类似于STL的vector这样的数据结构,动态数组肯定是需要自己来实现的。看一下apache里面关于动态数组结构的定义:

struct apr_array_header_t {
    /** The pool the array is allocated out of */
    apr_pool_t *pool;
    /** The amount of memory allocated for each element of the array */
    int elt_size;
    /** The number of active elements in the array */
    int nelts;
    /** The number of elements allocated in the array */
    int nalloc;
    /** The elements in the array */
    char *elts;
};

pool是内存池,apache有自己的内存管理机制,它在内存池中分配内存,而不是直接使用malloc分配,内存池由apache统一进行管理,这是后话,现在只需要知道在apache中如果要申请一块内存空间就必须有一个对应的内存池。

elt_size是数组中每个元素的大小。

nelts是数组中活跃元素的个数。何为活跃元素?相对于nalloc来看,nalloc是数组中分配空间的元素个数,它在数组创建的时候会先分配nalloc个元素的空间,而这些内存空间仅仅分配而未进行初始化,也就是nelts为0,当往数组中插入元素时,nelts会递增,nelts也就是数组中实际存在的元素个数。当nelts个数大于nalloc时,则表明数组中已分配的内存空间不够,则需要再从内存池中申请额外的空间来扩充数组的容量。

elts便指向元素列表的起始地址。

创建一个数组:

APR_DECLARE(apr_array_header_t *) apr_array_make(apr_pool_t *p,
						int nelts, int elt_size)
{
    apr_array_header_t *res;
 
    res = (apr_array_header_t *) apr_palloc(p, sizeof(apr_array_header_t));
    make_array_core(res, p, nelts, elt_size, 1);
    return res;
}
 
static void make_array_core(apr_array_header_t *res, apr_pool_t *p,
			    int nelts, int elt_size, int clear)
{
    /*
     * Assure sanity if someone asks for
     * array of zero elts.
     */
    if (nelts < 1) {
        nelts = 1;
    }
 
    if (clear) {
        res->elts = apr_pcalloc(p, nelts * elt_size);
    }
    else {
        res->elts = apr_palloc(p, nelts * elt_size);
    }
 
    res->pool = p;
    res->elt_size = elt_size;
    res->nelts = 0;		/* No active elements yet... */
    res->nalloc = nelts;	/* ...but this many allocated */
}

上面的两个函数功能都很清晰,没有什么需要解释的,只是分配内存用的是apr_palloc()和apr_pcalloc()两个函数从内存池中分配内存,这两个函数的区别是后者在分配了内存之后会先清零,这也就是那个clear参数的作用了。

往数组中压入元素:

APR_DECLARE(void *) apr_array_push(apr_array_header_t *arr)
{
    if (arr->nelts == arr->nalloc) {
        int new_size = (arr->nalloc <= 0) ? 1 : arr->nalloc * 2;
        char *new_data;
 
        new_data = apr_palloc(arr->pool, arr->elt_size * new_size);
 
        memcpy(new_data, arr->elts, arr->nalloc * arr->elt_size);
        memset(new_data + arr->nalloc * arr->elt_size, 0,
               arr->elt_size * (new_size - arr->nalloc));
        arr->elts = new_data;
        arr->nalloc = new_size;
    }
 
    ++arr->nelts;
    return arr->elts + (arr->elt_size * (arr->nelts - 1));
}

上面提到如果数组中活跃元素已经占满了所有的已分配的内存空间之后,便需要向内存池申请额外的内存才能便新元素得已插入,apache的做法也很简单,申请双陪的内存,将旧内存中的内容copy到新内存中的前半部分,后半部分清零,或不做处理(apr_array_push_noclear()函数)。然后就是增加活跃元素数,返回最新元素的内存地址。

还有一系列关于数据操作的函数,apr_array_pop()从数组中弹出元素,apr_array_cat()连接两个数组,apr_array_copy()拷贝一个数组,原理都很简单,不再一一赘述,详情可见srclib/arp/tables/apr_tables.c

接下来看apache里面另一个很重要的数据结构,一个类似于数据字典的table,但它允许有多个相同健的元素存在。

struct apr_table_t {
    /* This has to be first to promote backwards compatibility with
     * older modules which cast a apr_table_t * to an apr_array_header_t *...
     * they should use the apr_table_elts() function for most of the
     * cases they do this for.
     */
    /** The underlying array for the table */
    apr_array_header_t a;
    /** Who created the array. */
    void *creator;
    /* An index to speed up table lookups.  The way this works is:
     *   - Hash the key into the index:
     *     - index_first[TABLE_HASH(key)] is the offset within
     *       the table of the first entry with that key
     *     - index_last[TABLE_HASH(key)] is the offset within
     *       the table of the last entry with that key
     *   - If (and only if) there is no entry in the table whose
     *     key hashes to index element i, then the i'th bit
     *     of index_initialized will be zero.  (Check this before
     *     trying to use index_first[i] or index_last[i]!)
     */
    apr_uint32_t index_initialized;
    int index_first[TABLE_HASH_SIZE];
    int index_last[TABLE_HASH_SIZE];
};

数据表中的元素是存放在变量名为a的数组中的。creator是该数据表的创建者,通常是个字符串。最后的三个字段是比较有学问的字段。TABLE_HASH_SIZE宏的大小定义为32,index_initialized变量也是一个32位的常数,该变量用于对每个key的hash值进行一个映射,key的hash算法很简单:

#define TABLE_HASH(key)  (TABLE_INDEX_MASK & *(unsigned char *)(key))

相当于把key的前8个字节转换成整形再与32求余,最后结果是一个小于32的整数。于是,当表中存在某个key时,假设key对应的hash值i,则index_initialized的第i位是被置位的,这样可以快速地检测某个key是否在表中存在。

至于index_first和index_last这两上数组中存储的是某个hash值所对应的元素在a数组中的索引范围(肯定是需要一个范围的,32个元素的hash表非常容易出现碰撞)。如果key对应的hash值为4,而index_first[4]=22,index_last[4]=26,则要查找的元素在a数组中第22个元素到第26个元素之间。至于index_last和index_first最面的值是怎么确定的接下来讨论。

需要提一下的是a数组中每一个元素都是一个apr_entry_t结构:

struct apr_table_entry_t {
    /** The key for the current table entry */
    char *key;          /* maybe NULL in future;
                         * check when iterating thru table_elts
                         */
    /** The value for the current table entry */
    char *val;
 
    /** A checksum for the key, for use by the apr_table internals */
    apr_uint32_t key_checksum;
};

我们看下是如何获取某个健值的:

APR_DECLARE(const char *) apr_table_get(const apr_table_t *t, const char *key)
{
    apr_table_entry_t *next_elt;
    apr_table_entry_t *end_elt;
    apr_uint32_t checksum;
    int hash;
 
    if (key == NULL) {
	return NULL;
    }
 
    hash = TABLE_HASH(key);
    if (!TABLE_INDEX_IS_INITIALIZED(t, hash)) {
        return NULL;
    }
    COMPUTE_KEY_CHECKSUM(key, checksum);
    next_elt = ((apr_table_entry_t *) t->a.elts) + t->index_first[hash];;
    end_elt = ((apr_table_entry_t *) t->a.elts) + t->index_last[hash];
 
    for (; next_elt <= end_elt; next_elt++) {
	if ((checksum == next_elt->key_checksum) &&
            !strcasecmp(next_elt->key, key)) {
	    return next_elt->val;
	}
    }
 
    return NULL;
}

程序遍历index_first[hash]和index_last[hash]区间内的链表,通过比对健值来获取要寻找的值。

接下来看设置某个健值的函数,稍微复杂一点:

APR_DECLARE(void) apr_table_set(apr_table_t *t, const char *key,
                                const char *val)
{
    apr_table_entry_t *next_elt;
    apr_table_entry_t *end_elt;
    apr_table_entry_t *table_end;
    apr_uint32_t checksum;
    int hash;
 
    COMPUTE_KEY_CHECKSUM(key, checksum);
    hash = TABLE_HASH(key);
    if (!TABLE_INDEX_IS_INITIALIZED(t, hash)) {
        t->index_first[hash] = t->a.nelts;
        TABLE_SET_INDEX_INITIALIZED(t, hash);
        goto add_new_elt;
    }
    next_elt = ((apr_table_entry_t *) t->a.elts) + t->index_first[hash];;
    end_elt = ((apr_table_entry_t *) t->a.elts) + t->index_last[hash];
    table_end =((apr_table_entry_t *) t->a.elts) + t->a.nelts;
 
    for (; next_elt <= end_elt; next_elt++) {
	if ((checksum == next_elt->key_checksum) &&
            !strcasecmp(next_elt->key, key)) {
 
            /* Found an existing entry with the same key, so overwrite it */
 
            int must_reindex = 0;
            apr_table_entry_t *dst_elt = NULL;
 
            next_elt->val = apr_pstrdup(t->a.pool, val);
 
            /* Remove any other instances of this key */
            for (next_elt++; next_elt <= end_elt; next_elt++) {
                if ((checksum == next_elt->key_checksum) &&
                    !strcasecmp(next_elt->key, key)) {
                    t->a.nelts--;
                    if (!dst_elt) {
                        dst_elt = next_elt;
                    }
                }
                else if (dst_elt) {
                    *dst_elt++ = *next_elt;
                    must_reindex = 1;
                }
            }
 
            /* If we've removed anything, shift over the remainder
             * of the table (note that the previous loop didn't
             * run to the end of the table, just to the last match
             * for the index)
             */
            if (dst_elt) {
                for (; next_elt < table_end; next_elt++) {
                    *dst_elt++ = *next_elt;
                }
                must_reindex = 1;
            }
            if (must_reindex) {
                table_reindex(t);
            }
            return;
        }
    }
 
add_new_elt:
    t->index_last[hash] = t->a.nelts;
    next_elt = (apr_table_entry_t *) table_push(t);
    next_elt->key = apr_pstrdup(t->a.pool, key);
    next_elt->val = apr_pstrdup(t->a.pool, val);
    next_elt->key_checksum = checksum;
}

TABLE_INDEX_IS_INITIALIZED(t,i)这个宏是检测index_initialized这个变量中的第i位是否被置位。如果没有被置位,则表示该key对应的节点已经在a数组中存在。如果存在则index_first[hash]和index_last[hash]都是存在的,为什么呢?先假设key对应的hash位未被置位,则新插入该key时,我们会把这个key对应的apr_entry_t结构放到a数组的最后,它的索引值便是a.nelts,这时候我们也把index_first[hash]和index_last[hash]的值都设置为a.nelts。当插入另一个key时,它的hash值可能与上一个key出现碰撞,此时该hash值对应的index_first[hash]的索引值不变,将index_last[hash]设置为新索引值,这样这两个key所对应的apr_entry_t结构便都存在于a[index_first[hash]]和a[index_last[a]]之间,我们只需要遍历找到该结构并将其val属性设置为我们要设置的值即可。同时由于该表允许多个相同健的存在,我们在设置健值之后也需要从表中移除其它的具有相同健的对象。与table有关的函数也有很多,大原理都比较简单,也不再赘述了。

本文纯属个人见解,如有谬误烦请指出。

分类: C/C++ 标签:

libofetion demo以及纯命令行飞信

十二月 20th, {2010 27 条评论 25,544 人阅读过  

之前一直有用户要求写一个libofetion的demo,再加上很多用户对于纯命令行版本飞信的强烈需求,于是我昨天简单地写了一个demo,把libofetion的API也做了一些修改,使它用起来更像是一个lib,不过对于第三方开发的话还是有很多很难理解的地方,因为最初并没有想把它当做一个lib来发布。到现在我对飞信的开发又要暂时先告一段落了,周末都在openfetion和娱乐中度过的,实验室项目和论文又要开始提上日程了,OK,先把code列出来,再做下简单地说明

/***************************************************************************
 *   Copyright (C) 2010 by lwp                                             *
 *   levin108@gmail.com                                                    *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program; if not, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
 ***************************************************************************/
 
#include <openfetion.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
#define BUFLEN 1024
 
int   password_inputed = 0;
int   mobileno_inputed = 0;
int   tono_inputed = 0;
int   message_inputed = 0;
User *user;
pthread_t th;
 
static void usage(char *argv[]);
 
int fx_login(const char *mobileno, const char *password)
{
	Config           *config;
	FetionConnection *tcp;
	FetionSip        *sip;
	char             *res;
	char             *nonce;
	char             *key;
	char             *aeskey;
	char             *response;
	int               local_group_count;
	int               local_buddy_count;
	int               group_count;
	int               buddy_count;
	int               ret;
 
	/* construct a user object */
 	user = fetion_user_new(mobileno, password);
	/* construct a config object */
	config = fetion_config_new();
	/* attach config to user */
	fetion_user_set_config(user, config);
 
	/* start ssi authencation,result string needs to be freed after use */
	res = ssi_auth_action(user);
	/* parse the ssi authencation result,if success,user's sipuri and userid
	 * are stored in user object,orelse user->loginStatus was marked failed */
	parse_ssi_auth_response(res, user);
	free(res);
 
	/* whether needs to input a confirm code,or login failed
	 * for other reason like password error */
	if(USER_AUTH_NEED_CONFIRM(user) || USER_AUTH_ERROR(user)) {
		debug_error("authencation failed");
		return 1;
	}
 
	/* initialize configuration for current user */
	if(fetion_user_init_config(user) == -1) {
		debug_error("initialize configuration");
		return 1;
	}
 
	if(fetion_config_download_configuration(user) == -1) {
		debug_error("download configuration");
		return 1;
	}
 
	/* set user's login state to be hidden */
	fetion_user_set_st(user, P_HIDDEN);
 
	/* load user information and contact list information from local host */
	fetion_user_load(user);
	fetion_contact_load(user, &local_group_count, &local_buddy_count);
 
	/* construct a tcp object and connect to the sipc proxy server */
	tcp = tcp_connection_new();
	if((ret = tcp_connection_connect(tcp, config->sipcProxyIP, config->sipcProxyPort)) == -1) {
		debug_error("connect sipc server %s:%d\n", config->sipcProxyIP, config->sipcProxyPort);
		return 1;
	}
 
	/* construct a sip object with the tcp object and attach it to user object */
	sip = fetion_sip_new(tcp, user->sId);
	fetion_user_set_sip(user, sip);
 
	/* register to sipc server */
	if(!(res = sipc_reg_action(user))) {
		debug_error("register to sipc server");
		return 1;
	}
 
	parse_sipc_reg_response(res, &nonce, &key);
	free(res);
	aeskey = generate_aes_key();
 
	response = generate_response(nonce, user->userId, user->password, key, aeskey);
	free(nonce);
	free(key);
	free(aeskey);
 
	/* sipc authencation,you can printf res to see what you received */
	if(!(res = sipc_aut_action(user, response))) {
		debug_error("sipc authencation");
		return 1;
	}
 
	if(parse_sipc_auth_response(res, user, &group_count, &buddy_count) == -1) {
		debug_error("authencation failed");
		return 1;
	}
 
	free(res);
	free(response);
 
	if(USER_AUTH_ERROR(user) || USER_AUTH_NEED_CONFIRM(user)) {
		debug_error("login failed");
		return 1;
	}
 
	/* save the user information and contact list information back to the local database */
	fetion_user_save(user);
	fetion_contact_save(user);
 
	/* these... fuck the fetion protocol */
	struct timeval tv;
	tv.tv_sec = 1;
	tv.tv_usec = 0;
	char buf[1024];
	if(setsockopt(user->sip->tcp->socketfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) == -1) {
		debug_error("settimeout");
		return 1;
	}
	tcp_connection_recv(user->sip->tcp, buf, sizeof(buf));
 
	return 0;
}
 
int send_message(const char *mobileno, const char *receiveno, const char *message)
{
	Conversation *conv;
	Contact      *contact;
	Contact      *contact_cur;
	Contact      *target_contact = NULL;
	int           daycount;
	int           monthcount;
 
	/* send this message to yourself */
	if(*receiveno == '\0' || strcmp(receiveno, mobileno) == 0) {
		/* construct a conversation object with the sipuri to set NULL
		 * to send a message to yourself  */
		conv = fetion_conversation_new(user, NULL, NULL);
		if(fetion_conversation_send_sms_to_myself_with_reply(conv, message) == -1) {
			debug_error("send message \"%s\" to %s", message, user->mobileno);
			return 1;
		}
	}else{
		/* get the contact detail information by mobile number,
		 * note that the result doesn't contain sipuri */
		contact = fetion_contact_get_contact_info_by_no(user, receiveno, MOBILE_NO);
		if(!contact) {
			debug_error("get contact information of %s", receiveno);
			return 1;
		}
 
		/* find the sipuri of the target user */
		foreach_contactlist(user->contactList, contact_cur) {
			if(strcmp(contact_cur->userId, contact->userId) == 0) {
				target_contact = contact_cur;
				break;
			}
		}
 
		if(!target_contact) {
			debug_error("sorry,maybe %s isn't in your contact list");
			return 1;
		}
 
		/* do what the function name says */
		conv = fetion_conversation_new(user, target_contact->sipuri, NULL);
		if(fetion_conversation_send_sms_to_phone_with_reply(conv, message, &daycount, &monthcount) == -1) {
			debug_error("send sms to %s", receiveno);
			return 1;
		}else{
			debug_info("successfully send sms to %s\nyou have sent %d messages today, %d messages this monthcount",
					receiveno, daycount, monthcount);
			return 0;
		}
	}
	return 0;
}
 
int main(int argc, char *argv[])
{
	int ch;
	char mobileno[BUFLEN];
	char password[BUFLEN];
	char receiveno[BUFLEN];
	char message[BUFLEN];
 
	memset(mobileno, 0, sizeof(mobileno));
	memset(password, 0, sizeof(password));
	memset(receiveno, 0, sizeof(receiveno));
	memset(message, 0, sizeof(message));
 
	while((ch = getopt(argc, argv, "f:p:t:d:")) != -1) {
		switch(ch) {
			case 'f':
				mobileno_inputed = 1;
				strncpy(mobileno, optarg, sizeof(mobileno) - 1);	
				break;
			case 'p':
				password_inputed = 1;
				strncpy(password, optarg, sizeof(password) - 1);
				break;
			case 't':
				tono_inputed = 1;
				strncpy(receiveno, optarg, sizeof(receiveno) - 1);
				break;
			case 'd':
				message_inputed = 1;
				strncpy(message, optarg, sizeof(message) - 1);
				break;
			default:
				break;
		}
	}
 
	if(!mobileno_inputed || !password_inputed || !message_inputed) {
		usage(argv);
		return 1;
	}
 
	if(fx_login(mobileno, password))
		return 1;
 
	if(send_message(mobileno, receiveno, message))
		return 1;
 
	fetion_user_free(user);
	return 0;
 
}
 
static void usage(char *argv[])
{
	fprintf(stderr, "Usage:%s -f mobileno -p password -t receive_mobileno -d message\n", argv[0]);
}

首先需要libofetion的支持,因为用到了最新的API,所以需要从hg中clone最新版本编译安装后才可编译该程序:

hg clone https://ofetion.googlecode.com/hg/ ofetion

编译方法如下:

gcc -o cli cli.c `pkg-config --cflags --libs ofetion`

有一个地方需要说明,请大家找到/* these… fuck the fetion protocol */这个句注释,它下面的几句话的作用是这样的,飞信在用户完成身份验证之后订阅相关信息之前会推送过来一条信令:

BN 406472150 SIP-C/4.0
N: SyncUserInfoV4
I: 2
Q: 1 BN
L: 124
 
<events><event type="SyncUserInfo"><user-info>
<score value="1261" level="2" level-score="678"/>
</user-info></event></events>

用它来更新用户的积分等级之类的玩意,像这种浮云般的信令都被我直接忽略掉了,但在这种命令行模式纯粹为了发短信的情况下,不需要新建线程监听服务器推送过来的信息,但这条推送过来的信令就有可能影响其它信令的交互,而且更让人蛋疼的是并不是每次登录都会推送这条信令,有时候有,有时候没有,所以就加了在登录完成之后加了一个recv,设定了1s的超时,来处理这句信令,如果在网络情况不好的情况下,大家可以自己把超时时间设长一些,这样也就意味着在发送一条短信时在recv那里要停留几秒钟,否则就有可能导致信息发送失败。

下载地址:fetion-demo

分类: C/C++ 标签: ,

openfetion cli功能开发手记

十二月 19th, {2010 11 条评论 32,118 人阅读过  

之前一直有用户提出的命令行短信功能终于被我落实了下来,不过却让很多网友失望,因为所谓的命令行并非是纯命令行,而是需要先有openfetion GUI版本登录以作为server,cli程序通过IPC将数据交由server转发,自己并不进行与sipc server的直接数据通信,也就是必须有X图形界面的支持,当然如果要把现在的openfetion server做成不需要图形界面支持的deamon server还存在很多问题,如deamon在收到短信时该怎么处理等,另外deamon server的开发应该也需要花些时间,这个暂时未做考虑,而基于目前的飞信协议如果要发送短信则必须进行身份验证,也就是纯命令行不需要deamon server支持的飞信必须在每次发短信之前都需要登录一次,考虑到这个问题就迟迟没有开发纯命令行的版本,主要是觉得这种形式的短信发送方式存在的意义不大,不仅速度慢而且需要经过复杂的身份验证,但很多用户想用它管理server的话那也就只能这样来实现了,技术上其实问题不大,调用libofetion的api很简单就能实现,过几天抽点时间写一个看看,今天先把已经实现的CLI功能的开发过程总结一下。

首先,先看一下CLI的使用方法:

程序很简单,只加了三个个文件,分别是src/fx_server.c,src/fx_cli.c和include/fx_server.h

服务器端初始化init_server函数来初始化server,函数如下:

int init_server(FxMain *fxmain)
{
	int   fifo;
	User *user = fxmain->user;
 
	char server_fifo[128];
	snprintf(server_fifo, sizeof(server_fifo) - 1, OPENFETION_FIFO_FILE, user->mobileno);
 
	if(mkfifo(server_fifo, FIFO_FILE_MODE) == -1
			&& errno != EEXIST) {
		debug_error("create fifo %s:%s\n", server_fifo, strerror(errno));
		return -1;
	}
 
	if((fifo = open(server_fifo, O_RDONLY, 0)) == -1) {
		debug_error("open fifo %s:%s\n", server_fifo, strerror(errno));
		return -1;
	}
 
	if((idlefifo = open(server_fifo, O_WRONLY, 0)) == -1) {
		debug_error("open fifo %s:%s\n", server_fifo, strerror(errno));
		close(fifo);
		return -1;
	}
 
	return fifo;
}

这个函数对IPC进行了一些初始化,首先创建以“openfetion_fifo_登录手机号 ”命名的命名管道文件,分别打开两次,一次为只读,用于监听client发来的IPC请求,另一个为只写,这个描述符打开之后从来没有使用过,这也是UNP第二卷里面提到的小技巧,当client关闭时会关闭打开的命名管道描述符,这里server中的read函数便会返回0,从而标识client关闭,这时server便需要关闭描述符重新打开关监听,为了避免这样一种情况,server自己以只写的方式打开这个通用描述符而不写入任何数据,这样server在收不到数据时read函数便会一直阻塞。

FIFO也是基于流的通信方式,所以需要自定义消息,没有什么复杂的数据需要传输,我就简单定义了两种消息,请求消息和应答消息:

struct fifo_mesg {
	unsigned short type;
	unsigned short length;
	unsigned int  pid;
};
 
struct fifo_resp {
	unsigned short code;
	unsigned short length;
};

请求消息中的pid字段为client进程的pid,client在向server发起请求之后会打开openfetion_fifo_pid命名的FIFO等待server返回响应,而server在收到请求之后可以提取出请求消息中的pid,从来找到client用于监听的命名管道文件,将返回消息通过命名管道再反馈给client。
目前请求信令的类型只有两种:

/* 发送短信 */
#define CLI_SEND_MESSAGE    1
/* 获取用户信息 */
#define CLI_GET_INFORMATION 2

应答信令的应答码也只有两种:

/* 操作已成功 */
#define CLI_EXEC_OK   200
/* 操作失败 */
#define CLI_EXEC_FAIL 400

请求消息体中为XML格式,应答消息体中为纯文本提示消息。请求信令消息体如下:

<r><m no="15200000000" bd="hello world" p="1" /></r>

其中no表示要发送的好友手机号码,注意该好友必须在好友列表中,并且必须对你已设置公开手机号,bd为要发送的短信,p为是否用直接发送到用户手机。该消息发送到server后,server发现请求的号码与自己的号码相同时,会将消息发送至用户自己的手机中。

OK,过程就这么多吧,没有超过1K行代码,也复杂不到哪里去,接下来看看时间来不来得及写一个纯命令行的版本,有需要的同学请关注。

分类: C/C++ 标签: , , ,

libvlc-gtk使用手记

十二月 5th, {2010 2 条评论 7,568 人阅读过  

前几天实验室这个破项目非要加上什么流媒体的功能,简单起见使用了VLC来实现,客户端这边就得需要把相关的播放界面整合到现有的界面里面来,之前的客户端UI我都是用GTK实现的,没办法,GTK用得比较多,相对熟练一些就用GTK来做了,没想到要把VLC整到GTK里面来那么麻烦,原生的libvlc是不支持GTK的,需要加一层libvlc-gtk,从网上好不容易下载到了libvlc-gtk的源码,从哪里下的也记不清了,反正就是零散地几个文件,没有README甚至连Makefile都没有,没办法首先得先写个Makefile把它编译一下,libvlc-gtk一共有八个文件,Makefile如下:

libvlc_objs=gtk-libvlc-media.o gtk-libvlc-instance.o gtk-libvlc-media-player.o
libvlc_cflags=`pkg-config --cflags gtk+-2.0 libvlc` -I../ -fPIC -shared
libvlc_ldflag=-fPIC -shared `pkg-config --libs gtk+-2.0 libvlc`
libvlc_so=libvlcgtk.so
 
%.o:%.c
	gcc -o $@ -c $< -g $(libvlc_cflags) 
 
$(libvlc_so):$(libvlc_objs)
	gcc $(libvlc_ldflag) -o $(libvlc_so) $(libvlc_objs)
 
install:
	install libvlcgtk.so /usr/local/lib/libvlcgtk.so
	ldconfig
 
clean:
	rm -f *.o *.so

当然这样在编译的时候还会有些问题,libvlc-gtk理所当然需要libvlc的支持,而libvlc各个版本之间的函数有一点点不一样,所以就需要进行条件编译,而libvlc-gtk把条件编译选项都写好了,用了自定义的几个版本号的宏来检测相关的版本,如果这几个宏没有定义便会在编译时报错,相关代码如下:

// Set default value for LIBVLC_VERSION_MAJOR
#ifndef LIBVLC_VERSION_MAJOR
#error "LIBVLC_VERSION_MAJOR must be defined."
#endif
 
// Set default value for LIBVLC_VERSION_MINOR
#ifndef LIBVLC_VERSION_MINOR
#error "LIBVLC_VERSION_MINOR must be defined."
#endif
 
// Set default value for LIBVLC_VERSION_REVISION
#ifndef LIBVLC_VERSION_REVISION
#error "LIBVLC_VERSION_REVISION must be defined."
#endif

检查了一下相关的函数后我自己在gtk-libvlc-include.h里面定义了这几个宏:

#define LIBVLC_VERSION_MAJOR    0
#define LIBVLC_VERSION_MINOR    9
#define LIBVLC_VERSION_REVISION 0

当然也可以通过其它的方式定义,如在Makefile里面,我这里图省事就直接在这里写死了。

libvlc-gtk的API使用起来也很简单,下面的一段代码是用来播放在5004端口接收到的UDP媒体流:

	#define MRL rtp://:5004
int player_init()
{
        GtkLibvlcInstance    *inst;
	GtkLibvlcMedia       *vlc_media;
	GtkWidget            *player;
	GError               *error = NULL;
	GtkWindow            *window;
 
	/* load the vlc instance */
	inst = gtk_libvlc_instance_new(NULL, NULL, &error);
	return_if_fail(error);
 
	/* creat a new media item */
	vlc_media = gtk_libvlc_media_new(MRL);
 
	/* creat a media player */
	player = gtk_libvlc_media_player_new(inst, &error);
	gtk_widget_set_usize(media->player, 600, 500);
	return_if_fail(error);
 
	gtk_container_add(GTK_CONTAINER(window), player);
	g_signal_connect(G_OBJECT(GTK_WIDGET(media->player)),
                              "expose_event",
                              G_CALLBACK(draw_able_expose),
                              NULL);
	/* the player widget must be realized before playing */
	gtk_widget_realize(media->player);
 
	/* add a media to the player */
	gtk_libvlc_media_player_add_media(
			GTK_LIBVLC_MEDIA_PLAYER(media->player),
			vlc_media);
 
	gtk_libvlc_media_player_play(
			GTK_LIBVLC_MEDIA_PLAYER(media->player),
			NULL, &error);
 
	g_object_unref(inst);
	g_object_unref(vlc_media);
        return 0;
}

不同版本的libvlc这之间MRL的格式也有所不同,0.23版本的MRL格式如:rtp://:5004,0.22版本的则如:rtp://@:5004

既然player被封装成一个widget,那么我们就可以用gdk的一些方法很容易地在它上面绘图,如我在播放前加上了一个logo:

g_signal_connect(G_OBJECT(GTK_WIDGET(media->player)),
						  "expose_event",
						  G_CALLBACK(draw_able_expose),
						  NULL);
 
static gboolean draw_able_expose(GtkWidget *widget,
			GdkEventExpose *e, gpointer data)
{
	GdkGC       *gc;
	GdkColormap *colormap;
	GdkPixbuf   *pixbuf;
	GdkColor     color;
	GdkDrawable *drawing;
	gint         width;
	gint         height;
 
	drawing = widget->window;
 
	pixbuf = gdk_pixbuf_new_from_file_at_size(
				"skin/mplayer.svg", 96, 96,NULL);
 
	gc=gdk_gc_new(drawing);
	colormap=gtk_widget_get_colormap(widget);
	gdk_color_parse("white",&color);
	gdk_color_alloc(colormap,&color);
	gdk_gc_set_foreground(gc,&color);
 
	gdk_drawable_get_size(drawing, &width, &height);
 
	gdk_draw_pixbuf(drawing, gc, pixbuf, 0, 0,
			(width - 96) / 2, (height - 96) / 2,
			96, 96, GDK_RGB_DITHER_NORMAL, 0, 0);
	g_object_unref(pixbuf);
}

最基本的呈现一个视频流的用法就这些了,像其它的一些API也很简单的,都是采用GTK的命名风格,即便没有注释也可以很简单地了解它们的用法,就不多说了。

分类: C/C++ 标签: ,

sqlite3使用手记

十月 23rd, {2010 5 条评论 16,531 人阅读过  

前几天为了解决openfetion登录速度过慢的问题,决定实现数据的本地化功能,以往采用二进制文件直接写入磁盘的形式效率和灵活性显然远远不够,于是毅然决定采用sqlite3来实现,用过之后才发现sqlite3果然是绝佳的选择,作为一种轻量型的数据库,sqlite3有着它独特的优势,简单易用,而且极为高效,当初在引入这个依赖库的时候还在犹豫,但完成后发现它所带来的用户体验绝对可以掩盖住安装时多一个小步骤的繁琐。

sqlite3第一次用,简单地把自己的使用过程写一下,以后再用到可以参考。

sqlite3对很多通过的SQL语句都支持,像SELECT,UPDATE,INSERT,DELETE等等都支持地很好,只要懂SQL语句就可以用sqlite3。

1,下面是几个比较重要的API函数:

/* 打开数据库,如果不存在则创建一个 */
int sqlite3_open(const char*, sqlite3**);
 
/* 关闭数据库 */
int sqlite3_close(sqlite3*);
 
/* 获取错误信息字符串 */
const char *sqlite3_errmsg(sqlite3*);
 
/* 执行SQL语句 */
int sqlite3_exec(sqlite3*, const char *sql, sqlite_callback, void*, char**);
 
/* 获取SQL查询结果,其实这个函数是不推荐使用的,一方面是因为它不安全,另一方面它不能处理BLOG类型的字段,当然,在这里我在查询的时候还是使用了它,因为没有涉及到多复杂多庞大的数据,这个使用起来又简单,所以就选用了这个方法 */
int sqlite3_get_table(
  sqlite3 *db,          /* An open database */
  const char *zSql,     /* SQL to be evaluated */
  char ***pazResult,    /* Results of the query */
  int *pnRow,           /* Number of result rows written here */
  int *pnColumn,        /* Number of result columns written here */
  char **pzErrmsg       /* Error msg written here */
);

还有复杂一些的API如下,我没仔细看,像sqlite3_get_table这些函数就是由下面这些API组合而成的

INT sqlite3_prepare(sqlite3*, const CHAR*, INT, sqlite3_stmt**, const CHAR**);
INT sqlite3_bind_double(sqlite3_stmt*, INT, DOUBLE);
INT sqlite3_bind_int(sqlite3_stmt*, INT, INT);
INT sqlite3_bind_int64(sqlite3_stmt*, INT, long long INT);
INT sqlite3_bind_null(sqlite3_stmt*, INT);
INT sqlite3_bind_text(sqlite3_stmt*, INT, const CHAR*, INT n, void(*)(void*));
INT sqlite3_bind_text16(sqlite3_stmt*, INT, const void*, INT n, void(*)(void*));
INT sqlite3_step(sqlite3_stmt*);

2,创建自增长类型的字段

CREATE TABLE history (id INTEGER PRIMARY KEY AUTOINCREMENT,
			name TEXT,userid TEXT,message TEXT,
			updatetime TEXT,issend INTEGER);

这个语句中创建的id字段即为自增长的字段,在插入记录的时候只需把id字段写为NULL即可实现其自增长,如:

INSERT INTO history VALUES(NULL,"levin","123456","hello sqlite3","2010-10-23",1);

3.sqlite3的限制读取条数

/* 在其它SQL数据库中SELECT指定条数的记录用如下语句:*/
SELECT TOP 10 * FROM history;
 
/* 而在sqlite3中使用下面语句:*/
SELECT * FROM history LIMIT 10

4.sqlite3的日期函数

sqlite3的日期函数还是很强大的,列举几个常用的。

/* 获取当前时间 */
SELECT DATETIME('now')
输出:2010-10-23 12:10:50
 
/* 获取当前时间偏移 */
SELECT DATETIME('now','+20 days');
输出:2010-11-12 12:12:54
 
/* 获取今年开始时间 */
SELECT DATETIME('now','start of year');
输出:2010-01-01 00:00:00
 
/* 获取本月开始时间 */
SELECT DATETIME('now','start of month');
输出:2010-10-01 00:00:00
 
/* 获取今天开始时间 */
SELECT DATETIME('now','start of day');
输出:2010-10-23 00:00:00

另外,sqlite3里面有个很重要的时间函数,strftime,这个跟POSIX里面的strftime函数很像,也是将日期类型格式化为字符串类型,如:

SELECT strftime('%Y|%m|%d','now');
2010|10|23

它的格式化字符和POSIX的strftime也完全一样,再例如我要查询本月的聊天记录,可以使用下面语句:

SELECT * FROM history WHERE 
strftime('%Y',updatetime) == strftime('%Y','now') AND
strftime('%m',updatetime) == strftime('%m','now') ;

5.写入较大数据时采用事务提高效率
我在应用sqlite3的时候其实只是写入了少量的数据,刚开始觉得效率不是什么大问题,后来有用户反馈说他的290+好友的飞信号在登录时要向磁盘写入几十S的数据,这个效率问题着实需要改善,于是采用事务来处理写入,事务的使用也非常简单,其实也就是下面的语句:

sqlite3_exec(db, "BEGIN TRANSACTION;", 0,0, &errMsg);
 
for(;;){
  insert into.....
}
 
sqlite3_exec(db, "COMMIT TRANSACTION;", 0, 0, &errMsg);

这样一系列地插入语句就可以被作为一个事务来执行,在COMMIT TRANSACTION的时候将插入操作写入磁盘,避免了每次插入记录时频繁地读写磁盘数据库,从而使效率大大提高,据说可以比单纯地插入快1000倍,这个我无从考证,不过我这里确实快了很多,几百条记录可以瞬间写入。

就在昨天一本科同学还问我会不会sqlite,我正好前两天也用了,就跟她交流了下,她问我怎么写入多条数据,呃..这个问题很简单,就是循环,在程序里面循环,想在SQL里面循环用sqlite3是做不到的,因为sqlite3忽略了很多数据库很重要的特性,它不支持存储过程,而且也没有其它数据库地高并发性,因此有的时候我多个线程同时访问同一个数据库文件的时候,便会报错说database is locked。

OK,以上只是我个人使用过程中的一点小小地总结,拿过来就用,也没做多少研究,sqlite3一些复杂的机制都没有去了解,把这些基础的东西写下来,给自己做个备份,说不定哪天还会再用到。

分类: C/C++ 标签: ,

Openfetion近期开发手记(相关功能实现技术)

十月 18th, {2010 29 条评论 8,252 人阅读过  

离上Openfetion上一个版本发布至今过了有将近一个月的时间了,上个版本放出的时候本以为已经解决了很多bug,但发布之后才发现用户遇到的问题还是很多,软件测试还是很重要的,当然有些协议上的问题只能交由用户去测试,我没有那么多飞信号,有些问题有很难遇到的,也很难复现的,所以就在这样不断地与用户沟通中解决问题。

其实做共享软件是件很开心的事情,不管你做得好不好,都会有人支持你,这个是很重要的,因为在最初开发openfetion的时候并没有想过要把它做为一个专业的飞信客户端放出来大家一起使用,可发布之后得到了广大linux用户的支持,即使那时候的bug比现在多得多,仍然有用户乐此不彼地帮我测试,反溃问题,提供建议,当然我也乐此不彼地修改程序,希望有朝一日它能让所有的用户都能稳定地运行,这也是自由软件的优势,如果当初这做为一个商业软件发布,我想毫无疑问,收到的会是一片骂声,然后这个项目也便会匆匆截止,而现在即便很忙也会拿出一些时间来加强它,因为总会有用户支持着它,也会有开源爱好者加入进来做一些贡献。

Openfetion下一个的版本号应该是2.0了,我想2.0应该和之前的1.x版本有所区别,功能上这个不是特别重要,因为之前的版本已经有了几乎所有的基本功能,其它的一些不常用的功能我也没考虑过,因为开发那些功能是一件性价比很低的事情,现在所能想到的2.0和1.x的区别应该是让2.0更加稳定,功能再丰富如果不能稳定运行这个软件就永远成不了优秀的软件,因为我在新版本里面做了大批量的代码修改,甚至包括以前一些很不专业的编码习惯,尽可能将所有潜在的问题都消灭掉,当然也加了一些用户一直以来要求的功能,下面简单地说一下。

首先,是在之前版本中,用户反映登录速度过慢,这个我承认,是因为在之前的版本中没有加入数据的本地缓存,每次用户登录的时候都会需要重新从服务器上请求自己相关的所有数据,包括用户列表和配置文件这样庞大到几个K甚至十几个K的数据,这不可避免地会导致登录过程过慢,甚至网络状况不好的时候,会导致在获取配置文件的时候卡在那里,这些问题都降低了用户体验,解决这些问题的方法毫无疑问是加入本地用户缓存,这也是网络软件所常用的方式,之前我把一些本地配置信息和聊天记录保存在本地所用的方法是直接使用二进制写入dat文件,那种方法灵活性非常差,而且效率也很低,所有就没有对其它的动态数据做缓存,现在在新版本中引用了sqlite3,这个轻量级的数据库无疑是实现这个功能的绝佳选择,使用起来很简单,而且灵活性也很高,基本的SQL语句几乎都支持,之前只是知道有这么个东西,但一直没用过,这次试了一下,发现使用起来也非常简单,于是毫不犹豫就把它给引入了,我想加入了这样一个依赖所带来的用户体验的提高是很大的,希望不会有用户抱怨引赖关系增多。

另外,在将数据进行本地缓存之后便为另一个功能的实现提供了基础,那就是离线登录功能,和IM的离线功能一样,就是在没有网络连接的情况下登录Openfetion,可以查看好友列表,当然这些好友信息都是存储在本地数据中的,通过sqlite3从数据库中提取出来的。

聊天记录改用sqlite3存储之后提取和写入也都方便了很多,而且还添加了删除聊天记录的功能,之前用二进制数据直接写入的方式保存聊天记录所带来的不便就是不能方便地删除聊天记录,如果要删除只能先把整个聊天记录都加载到内存中,然后从中删掉要删除的信息,之后再重新写入磁盘覆盖掉原来的文件,这样效率是非常低的,而用sqlite3直接可以用一条DELETE语句删掉想要删除的信息。

同样这次也加入了本地用户列表删除功能,用户登录完后记录在本地的用户名密码数据也同样都可以删除。

另一个很重要的功能是空闲时间自动离开功能,这个功能之前一直不知道该怎么实现,纠结于当焦点不在Openfetion中时,如何获取全局的鼠标键盘动作,而即便获取到了又如何检测是否空闲,这些问题都非常麻烦,后来查看了一下pidgin的源码,才发现其实IM软件所实现的空闲时间检测功能一般都是通过调用XScreenSaver来实现的,包括之前的evaqq也是之样实现的,过程很简单,下面的几行代码便是获取空闲时间的函数,通过周期性地检测空闲时间便可以判断出IM是否需要进行离开状态。

gint idle_timesec(void)
{
 
#ifdef USE_LIBXSS
	static XScreenSaverInfo *mit_info = NULL;
	static gint has_extension = -1;
	gint event_base, error_base;
 
	if (has_extension == -1)
		has_extension = XScreenSaverQueryExtension(
				GDK_DISPLAY(), &event_base, &error_base);
 
	if(has_extension){
		if (mit_info == NULL)
			mit_info = XScreenSaverAllocInfo();
 
		XScreenSaverQueryInfo(GDK_DISPLAY(), GDK_ROOT_WINDOW(), mit_info);
		return (mit_info->idle)/1000;
	}
#endif
	return 0;
}

这需要XScreenSaver的支持才行,于是加入了条件编译,在ubuntu或者debian中可以通过下面的命令安装XScreenSaver开发包:

sudo apt-get install libxss-dev

另外一个很重要的功能是断线自动离开功能,这个用tcp的相关特性来检测链路状态灵敏性太低,参考了一下pidgin和empathy,它们用的方法都是调用NetworkManager的相关API来实现的,NetworkManager是基于dbus的,之前在slackware13.1上因为安装NetworkManager失败,而导致之前同学写的基于nm和dbus的程序在我这里不能跑,从而对这个一直留有某种恐惧感,当然现在也意识到要让程序在网络状态改变的时候即刻感知到,最佳方法还是使用libnm,下面的函数便是初始化libnm的函数,它为网络状态改变的事件注册了回调函数nm_state_change()

void fx_conn_init(FxMain *fxmain)
{
#ifdef USE_NETWORKMANAGER
	GError *error = NULL;
        DBusGConnection *nm_conn = NULL;
        DBusGProxy *nm_proxy = NULL;
 
	nm_conn = dbus_g_bus_get(DBUS_BUS_SYSTEM, &error);
	if (!nm_conn) {
		debug_error("Error connecting to DBus System service: %s.\n", error->message);
	} else {
		nm_proxy = dbus_g_proxy_new_for_name(nm_conn,
		                                     NM_DBUS_SERVICE,
		                                     NM_DBUS_PATH,
		                                     NM_DBUS_INTERFACE);
		dbus_g_proxy_add_signal(nm_proxy, "StateChange", G_TYPE_UINT, G_TYPE_INVALID);
		dbus_g_proxy_connect_signal(nm_proxy, "StateChange",
		                        G_CALLBACK(nm_state_change), fxmain, NULL);
	}
#endif
}

下面的代码是回调函数的代码,在它里面能过检测各种网络状态而做出相应的动作:

static void
nm_state_change(DBusGProxy *proxy, NMState state, gpointer data)
{
	switch(state)
	{
		case NM_STATE_CONNECTED:
			debug_info("network is connected");
			break;
		case NM_STATE_ASLEEP:
			debug_info("network is sleeping...");
			break;
		case NM_STATE_CONNECTING:
			debug_info("network is connecting...");
			break;
		case NM_STATE_DISCONNECTED:
			debug_info("network is disconnected");
			break;
		case NM_STATE_UNKNOWN:
			debug_info("unknown network state");
		default:
			break;
	}
}

ubuntu或debian中NetworkManager开发包的安装方法如下:

sudo apt-get install libnm-glib-deb

要说的是,XScreenSaver和NetworkManager的使用都是可选项,为了避免不愿意引入这些库的用户抱怨,可以在configure的时候用–disable-screensaver和–disable-nm将其禁用。

另外也解决几个崩溃的bug,比如添加好友时崩溃,这个纯粹是我编码过程中出现的失误,还有群发短信时崩溃的问题,这个也是我编码的失误,都已经修改过来了,有时候收到的信息会显示发送失败,这个也修改好了,一个比较重要的bug是多人同时聊天在窗口切换的时候可能会崩溃的问题,这个问题现在也已经解决了。

有时候程序会出现 ”Program received signal SIGPIPE, Broken pipe.“这样的错误,这个信号一般是在服务器端主动关闭连接时客户端会收到的来自操作系统的信号,理论上服务器端主动关闭连接这个可能性不大,但它有时候确实会出现,之前没有对这个信号进行处理,这次把这个信号直接忽略了,然后在send和recv的时候就会返回-1,通过返回值就可以检测连接状态。

 struct sigaction sa;
 sa.sa_handler = SIG_IGN;
 sigaction( SIGPIPE, &sa, 0 );

还有用户提到在登录时登录按钮状态不变,这个之前没怎么在意,这次也修改过来了,不过没有加取消登录的功能,这个也考虑过,不过没想到实现的方法,因为登录线程用的是gthread库,而它没有像pthread一样提供取消线程的方法,一时间也不知道该怎么去实现,这个先暂时一放,希望有了解这个的朋友可以提供些帮助,现在的实现方法是像官方飞信一样在点击登录后改变登录界面,自己还用GIMP做了个登录正在进行的gif,就是让Openfetion的图标一直在转,哈哈,很有成就感,上两个登录界面的图吧。

image image

图片里面没有打码,这个飞信号是我测试用的小号,无所谓了,反正也不用。

这次新版本想多测试一段时间再发,肯定还存在问题,尽可能在发布之前能解决更多的问题,能让2.0正式版更稳定,能让用户更满意,同时也欢迎大家到svn上co最新的版本试用,并帮忙测试,如果你有问题,请向我反溃,这样我才能帮你解决问题。svn :

svn checkout http://ofetion.googlecode.com/svn/trunk/ ofetion-read-only

分类: C/C++ 标签: , , ,