存档

作者存档

Common Lisp使用iolib进行网络编程

一月 4th, {2012 没有评论 455 人阅读过  

Common Lisp进行网络编程可用的库还是挺多的,比较常用的库有usocketiolib,usocket我简单了解了一下没有真正拿来用,它的API比较简单,文档写得比较全面,相比之下,iolib要比usocket强大的多,但缺点是文档太少,官方的文档可用的内容非常少,但如果能阅读一下iolib的相关源码,就会发现其实iolib是一个很强大的网络编程库,其中包含了DNS解析,socket基本操作(bind,listen等等),IO多路复用以及通常用来做IPC的socketpair,而且iolib的multiplex用起来有种libevent的感觉,用iolib可以实现一般的应用层网络编程,至于是否支持raw socket,我还没仔细研究,不过感觉应该问题不大。

1.iolib的安装
使用asdf-install可以在线安装iolib,但貌似asdf-install不会自动解决包的依赖问题,最近才发现原来asdf-install其实已经是一个废弃的项目,官方已经不推荐使用了,在cliki的asdf-install首页最开头就有一句醒目的提示语:

ASDF-install is OBSOLETE. DO NOT USE ASDF-INSTALL, EVER. DO NOT ASK AROUND ABOUT HOW TO GET IT RUNNING. IT IS O-B-S-O-L-E-T-E. Not working. Not maintained. Please use quicklisp instead.

取而代之的是quicklisp,之前就有人跟我推荐过quicklisp我还没来得及尝试,这几天试了下确实非常方便,可以自动地下载程序包及其依赖的相关程序包,无需手工解决依赖问题,让我想到debian的apt-get,关于quicklisp的安装和使用都非常简单,在它首页上都有使用说明。而且quicklisp几乎每个月都会在官方blog上放出过去的几个月程序包的下载排行(如:Project download stats for November),可以在选择程序包的时候有个参考。

2.创建passive socket

当创建一个用于充当server角色的程序时通常需要创建passive socket,用来监听客户端的连接,关于socket编程的基本步骤已经是大家所熟知的了,create socket,bind,listen,accept等等,iolib是使用cffi(The Common Foreign Function Interface)通过调用linux系统调用来实现的,因此和用C语言编程几乎是一个套路,方法如下:

(setq socket
      (make-socket
       :connect :passive
       :address-family :internet
       :type :stream
       :external-format '(:utf-8 :eol-style :crlf)))
 
(bind-address socket
              (ensure-address "127.0.0.1")
              :port 1086
              :reuse-addr t)
 
(listen-on socket :backlog 10)
 
(setq client (accept-connection socket))
 
(multiple-value-bind (who port) (remote-name socket)
      (format t "Client ~A:~D connected.~%" who port))
 
(close socket)

几个操作一目了解,更细节的操作(比如如何创建UDP套接字)就去翻下源码好了,bind-address这个这个函数第二个参数用ensure-address把字符串转换成所需要的address类型,iolib也定义了一系列形如+ipv4-unspecified+的静态变量,类似于C语言里面的INADDR_ANY,最后一个参数reuse-addr相当于用setsockopt对套接字设置SO_REUSEADDR选项。

对于iolib的passive我一直有一个问题未能解决,当程序作为server在监听客户端连接时,在C语言中可以使用CTRL+C给程序发送SIGINT信号让程序终止,排除TIME_WAIT等这些情况,程序再次启动时仍可以bind到同一个指定端口(即使没有显示地调用close关闭套接字),但在slime中使用C-c C-c终止程序,并确保在slime-selecter中已经结束掉所有的用户线程之后,再次启动程序绑定同一个端口便会提示端口已被占用,除非结束掉lisp进程(sbcl/ccl等),我也就直接选择重启emacs,这个问题一直未能解决,困扰我很长时间,所以我只能在程序运行中不断地插入close来在不该关闭的地方临时关闭套接字。

3. 创建active socket

通常使用active socket的程序是作为客户端的角色,下面是我写的一段简单的示例代码,用于发送一个http请求:

(let (socket ip http)
  (setf socket (make-socket
              :connect :active
              :address-family :internet
              :type :stream
              :external-format '(:utf-8)
              :ipv6 nil))
 
  (setf ip (lookup-hostname "basiccoder.com"))
  (format t "IP of ~a is: ~a~%" host ip)
 
  (connect socket ip :port 80 :wait t)
  (format t "Connected to ~a via ~a:~a to ~a:~a~%"
          host (local-host socket) (local-port socket)
          (remote-host socket) (remote-host socket))
 
  (setf http (make-http-request "GET" "/" host
               (("Connection" "Closed")
                ("User-Agent" "Mozilla"))))
 
  (format t "send: ~A~%" http)
 
  (format socket http)
  (finish-output socket))

使用lookup-hostname来解决IP地址,通过connect来向远程服务器进行连接,make-http-request是我写的一个生成http请求头的一个宏,返回http请求字符串;创建的socket对象其实是一个流对象,因此可以使用format向流中写入数据,写入的数据会保存在缓冲区中,当调用(finish-output socket)函数时开始执行数据的发送操作。iolib也提供了send-to函数,不详细讨论了。

4. 从流中读取数据
由于socket对象是一个流对象,因此可以用任何从流中读出数据的方法来从socket中接收数据,如read-line,read-byte等等,但read-line存在一个问题,当使用read-line读取数据时,当数据中存在非ASCII字符中便会抛出异常,它在读取的过程中会对数据进行ASCII解码,而read-byte则不会存在这个问题,因为它读出的是二进制的字节,它不关心编码方式,但read-byte我感觉很多情况下是不太适用的,因为它一次只能读取一个字节,一般情况下多次执行这样的操作效率不会太高,当然,iolib也提供了receive-from函数,间接地调用了系统调用recvfrom(),是一种带缓冲区的接收方式,也比较符合C语言的编程习惯。

receive-from的使用方法如下:

(multiple-value-bind (buf-vector rbytes)
          (receive-from socket :buffer buf-vector
                               :start 0 :end 4096
                               :size 4096)

receive-from返回的是values,主返回值是包含接收到的数据的vector,另一个返回值是读取到的字节数,函数调用的参数里面:buffer不是必须的,当:buffer未指定时,则需要指定:size参数,这时receive-from会自动创建一个指定大小的vector并将数据填充后返回。receive-from返回的vector中保存的是字节码值,并不是字符串,可以使用octets-to-string函数将其转换成string,具体可以参考下我的这篇日志:Common Lisp为Babel添加GBK支持

5. IO多路复用

iolib提供了multiplex机制,原理也是对epoll/poll/kqueue进行了封装,我在linux下测试默认是用的epoll,使用方法和libevent非常相似,首先要创建一个全局的event base:

(setf *http-event-base*
        (make-instance 'iomux:event-base))
</lisp>
 
将要进行利用的socket对象添加到该event base中,使用set-io-handler函数:
<pre lang="lisp">
(set-io-handler *http-event-base*
                (socket-os-fd socket)
                :read
                (make-http-event-loop conn client)
                :one-shot t)

第三个选项:read表示监听套接字是否有数据可读,同类的选项还有:write和:error,第四个参数是事件发生是要执行的回调函数,由于lisp中没有类似于C语言中的void*这种方式,不会像C语言一样给回调函数通过一个指针来传递相关的参数,但lisp的高阶函数使用传递额外参数更加方便了,上述代码中的make-http-event-loop函数的返回值是一个lambda函数,用来作为set-io-handler的回调函数,而 conn和clinet两个参数可以通过make-http-event-loop传递给lambda函数:

(defun make-http-event-loop (conn client)
  (lambda (fd event exception)
     (format t "event ~A on fd(~D) with connection
:~A client :~A" event fd conn client)))

最后,调用event-dispatch函数来进入事件循环:

(event-dispatch *http-event-base*)
(when *http-event-base*
  (close *http-event-base*))

6. iolib的其它参考文档

分类: Lisp 标签: , ,

Common Lisp为Babel添加GBK支持

十二月 22nd, {2011 没有评论 526 人阅读过  

前段时间在学Common Lisp,接触新语言我干的第一件事一般是通过HTTP抓取某个web页面,因为对网络编程比较感兴趣,而且平时写的程序也多是网络相关的,所以比较关心这方面的用法,于是用IOLib写了一个简单的小程序尝试着抓取了几个大门户网站的页面代码,关于IOLib的基本用法改天我也写篇日志记录一下,也算是和大家分享一下,毕竟能找到的中文资料比较少,而且文档也不是特别全,就像这篇文章里面说的:”Such is the nature of open source documentation. “,于是大多数的用法都得通过hack源代码来弄明白,言归正传,在写这个小程序的时候我遇到了一些问题,关于字符编码的问题,下面慢慢道来吧。

IOLib的receive-from方法是通过调用recvfrom来进行的,这种带缓存的接收方式很符合其它语言进行编程的套路,但它所接收到的buffer数据是需要存储在一个vector ‘(unsigned-byte 8)中的,虽然字符串在本质上也是向量,但对于字符串的很多操作不能直接应用于vector,而且vector中的元素都是每个字符的unicode编码,而不是确定的字符,于是便需要进行转换,最初我使用的办法:

(map 'string #'code-char buffer)

这个方法简单粗暴,直接在对一个vector上的每一个元素应用code-char函数,然后将输出映射为string,首先需要了解code-char/char-code这对函数的用法,大多数的Common Lisp实现都使用Unicode字符编码,当然,Unicode向下兼容ASCII和ISO-8859-1,所以这一对函数的作用就是在字符和它的Unicode码之间相互转换,如:

CL-USER> (char-code #\中)
20013
CL-USER> (code-char 20013)
#\U4E2D
CL-USER> (format t "~d" #x4E2D)
20013
CL-USER> (format t "~a" #\U4E2D)

字符“中”的Unicode码是20013,用code-char将其转换成字符之后,REPL并不是将可视的字符直接显示出来,而是显示#\U4E2D,表示这是一个Unicode字符,后面的4E2D是该字符的16进程Unicode码,换成10进制也便是20013,将该这了符直接打印出的话便可以得到字符“中”了。

当抓取的页面编码不是Unicode(一般情况下都不是的),直接使用这种办法转换成字符串就会导致除ASCII字符以外的其它字符乱码。

Google搜索下在stackoverflow上发现这个问题的解决办法,babelflexi-streams的octets-to-string函数可以实现由vector向string的转换,而vector必须是(make-array element-length :element-type ‘(unsigned-byte 8))类型,在使用该函数在抓取新浪/网易等门户网站的数据的时候会抛出异常:

代码:

(babel:octets-to-string buffer :encoding :utf-8)

异常:

Illegal :UTF-8 character starting at position 437.
 [Condition of type BABEL-ENCODINGS:INVALID-UTF8-CONTINUATION-BYTE]

起初我对这个问题百思不得其解,后来在水木还有StackOverflow上提出这个问题之后,我才发现原来我的问题是那么弱,也非常感谢大家的热心解答,确实如字面意思,有非法的UTF-8字符,也就是说字符串并非使用UTF-8编码的,而我习惯性地以为这些大网站理应都是UTF-8编码才对,谁知跟我想的正好想反,他们大多都不是UTF-8,有GBK甚至有GB2312,关于GBK和UTF-8我在推特上提起过这个问题,有推友说大网站仍在延用GB2312是为了节省流量,这个我可以接受,也有推友说使用GBK是因为它比UTF-8可能多几个字符,这个我也可以接受,但有的推友却跑过来说GB*是国标,用UTF-8什么的那是崇洋媚外,这个我表示无论如何也接受不了,互联网是没有国界的存在,在这样一个大的开放平台上你搞个国标有个毛用,当然我们的互联网也不开放是真的。

GB2312是GBK的子集,GBK向下兼容GB2312,使用GB2312就意味着能使用的字符数要远小于GBK,这两者相对于UTF-8对于中文的优点就是它们对于汉字的编码是两个字节的,而UTF-8是三个字节的,当然,我说的仅是指汉字,UTF-8是变长编码的,它也支持2字节和4字节甚至更多。

babel是不支持GBK的,sbcl的sb-ext:octets-to-string和Closure的ccl:octets-to-string都支持GBK,我个人比较较真,想给babel写个GBK的patch,以便可以实现平台无关的转码。

可能是我搜索技术不行,搜到的关于GBK的中文正式文档不多,只在维基百科上找到了关于GBK的介绍,虽然没有涉及到具体的GBK到Unicode转码规则,但它提供的关于GBK的介绍也是很有帮助的,发现原来GBK与Unicode的转换并不像UTF-8与Unicode之间的转换那样有固定的规则,而是需要通过查表实现,于是乎我在网上下载到了一个包含所有GBK符号的码表,排列顺序也是按GBK的相关规则来的,解码的过程我参考了这篇文章:从GBK到Unicode的中文字符映射,编码的话就是反其道而行之,原理很简单,参考维基百科上的两个图就可以推算出来。

对于lisp而言,宏是这门语言一个很大的亮点,通过宏甚至可以定制语言的特性,babel写义了几个宏作为编解码的接口,最重要的有下面四个:

define-octet-counter
define-code-point-counter
define-encoder
define-decoder

前两个宏首先对要进行编/解码的数据进行预处理,分别计算出编/解码后的数据所占用的长度,从而预先分配存储空间。后两个宏分别定义对应的编码和解码算法,这两个宏具体的使用方法都是参考了enc-unicode.lisp这个文件里面关于UTF-8的编解码代码。

添加了GBK支持的babel我推送到github上去了(https://github.com/levin108/babel/),需要的同学可以下载,有什么写得不对的或者不好的欢迎提出来,我是新手需要学习。

分类: Lisp 标签: , ,

使用ASDF构建Common Lisp程序包

十二月 13th, {2011 6 条评论 879 人阅读过  

在切入正题之前先写点不相关的,工作确定之后便开始忙论文的事,忙里偷闲总想搞点什么以做娱乐,不得不说,腾讯面试官说过的要精通两到三门不同的语言我印象很深刻,自己也想尝试一下新东西,VIM让我审美疲劳了,也想尝试一下Emacs,机缘巧合由田春老师翻译的《实用Common Lisp编程》刚上市不久,Emacs和Lisp也有不少渊源,再加上Lisp作为一门生命持久的元老级别的语言,至今仍然能倍受广大黑客的推崇,我相信它一定有学习的价值,而且Hadoop的MapReduce据说也是受Lisp的map和reduce函数的启发而来,相信对于Lisp的学习肯定不会是浪费时间,尽管将来工作中应用Lisp的机会可能很少,但深入学习的话肯定会对自己有一定的启发和帮助。

于是几乎同一时间我开始尝试使用Emacs并在卓越上订购了中文版的《实用Common Lisp编程》,抽空阅读尝试。总起来说这本书是非常不错的,几乎是面面俱到,但有些我认为也很有用的宏如defstruct,deftype,check-type等书中没有给出相关介绍,另外关于cl的package书中有一章节专门讲了定义的规则,但对于package的管理及安装并没有提及,我个人觉得如果是practical编程的话提一下cl中重要的ASDF包管理工具还是很有必要的,既然书中没有提到就得自己通过其它的渠道去了解学习,这方面中文的资料相对较少,大多数的资料都是在外文网站上查到的,当然也包括到stackoverflow上的提问。

ASDF全称是Another System Definition Facility,asdf这个组合很有意思,正好和左手的键盘基本按键重合,当初还以为是作者很有个性地起了这个随意的名字呢。我个人的理解是ASDF是类似于automake或者cmake的工具,提供一种程序包的管理方法和工具,因此也没什么复杂的地方,只不过有一些基本的规则需要遵守,了解了就没什么了。

程序包中一般会有一个.asd文件,该文件定义了程序包中源码文件的组织方式及依赖关系,类似于cmake中的CMakelists.txt,当然该文的编码方式是使用lisp风格的。

1. asdf工具的安装配置。
目前存在的common lisp实现有很多,在这篇文章中有介绍:Common Lisp Implementations: A Survey,免费的common lisp实现中性能比较好使用也比较简单的应该属sbcl了,《practical common lisp》也推荐了sbcl,作为一个新手我第一选择当然也选了sbcl,在sbcl中已经集成了asdf工具,无需再手动安装,但安装方法也很简单,可以参考ASDF Manual,该手册对asdf有详细的介绍。

2. asdf包的编译加载。
asdf是一个工具集,可以对包进行各种操作,其中包括编译,加载等。asdf对于包的编译和加载等操作都需要基于.asd文件,也就是说编译某个包asdf需要先找到该包对应的.asd文件,该文件一般是存放在源码根目录中的,asdf对于该文件的寻址有两种方式,new style和old style,下面分别简单介绍下这两种方式:

old style是为了兼容旧版本的程序的,目前已经不推荐新程序使用,其方法是将程序包的.asd文件所存在的路径添加到asdf的*central-registry*变量中,可使用如下语句完成该操作:

(push "/home/levin/lisp/spider/" asdf:*central-registry*)

上述操作便可将/home/levin/lisp/spider/(注意结尾的’/')这个路径添加到*central-registry*变量中,这样asdf便可对该包进行寻址,但在REPL中执行的该操作只对当前的会话有效的,REPL重启后需要重新添加路径到该变量中,未免有些繁琐,可以将该语句添加至common lisp实现的启动文件中,如sbcl即为.sbclrc这个文件,这样在REPL启动之后该路径便会自动添加到*central-registry*变量中。

new style是ASDF2所提倡使用的方法,其方法是在~/.config/中创建common-lisp目录用于存放相关的配置文件,和该主题相关的配置文件需要创建一个名为source-registry.conf.d的目录,在该目录下可以创建文件名任意的文件,将下面的语句添加至该文件中即可,文件如:

~/.config/common-lisp/source-registry.conf.d/01-spider-source.conf

(:directory "/home/lisp/lisp/spider/")

如上表示在/home/lisp/lisp/spider/这个目录中查找.asd文件,也可以将:directory替换成:tree,使asdf在目录中递归寻找.asd文件:

(:tree "/home/lisp/lisp/spider/")

完成对asdf包的寻址操作之后,便可以使用asdf对程序包进行编译加载,编译和加载包分别可使用:

CL-USER> (asdf:compile-system :spider)
CL-USER> (asdf:load-system :spider)

也可以使用asdf的operate对包进行编译和加载:

CL-USER> (asdf:oos 'asdf:compile-op :spider)
CL-USER> (asdf:oos 'asdf:load-op :spider)

对于一个未经编译完成的包在编译完成会自动加载,同样,未编译完成的包在加载时也会自动先进行编译。

3. asdf系统的构建:

之前提到的.asd文件是asdf的很重要的文件,该文件最简单的形式如下:

(defpackage :spider-system
  (:use :cl :asdf))
(in-package :spider-system)
 
(defsystem spider
  :name "spider"
  :author "levin li"
  :version "0.0.1"
  :license "MIT"
  :description "A spider program."
  :depends-on (:iolib)
  :components ((:file "package")
               (:file "spider" :depends-on ("package"))))

最开头的包定义是为了防止system名与其它包冲突,定义的包中包含一些基本信息,:depends-on声明了该程序将要依赖的包的,:components定义了包中的组件,即源代码,上述的源码包中只有两个文件,package.lisp和spider.lisp,package.lisp是定义一个cl package,可以理解为其它语言中的namespace,package.lisp定义了一个package,之后的程序如spider.lisp可能都会包含在该包中,因此:depends-on(“package”)这句话就非常重要,它会在编译spider.lisp之前先加载已经编译的好的package.lisp,若未声明spider依赖package,则在一次编译完成后REPL重启并对spider.lisp进行修改,而package.lisp未加改动,则asdf只会编译修改过的spider.lisp,这时候系统会提示spider包未找到,因为asdf发现package.lisp的目标文件是最新的,默认不会对其进行编译和加载,这也是我刚开始遇到的问题之一,在认真读了ASDF Manual后才解决了这个问题,关于该文件的一些详细的定义可以参考ASDF Manual。

package.lisp的内容如下:

(in-package :cl-user)
 
(defpackage :spider
  (:use :cl :iolib)
  (:export :send-request
           :test))

其中定义了包的名称,包的依赖的其它包,及要导出的符号等,关于package的更多介绍还是参考《practical common lisp》等资料。而spider.lisp是代码文件,开头需要有一行:

(in-package :spider)

表明当前程序包在spider包中定义,继而可以在spider.lisp中编写其它的逻辑代码,定义函数,宏或变量,所有的这些定义都被包含在包spider中,其中只有在package.lisp中声明为:export的符号才可以被其它包所引用,如上定义,该包中只有send-request和test两个函数可以被其它外部包引用。

4. asdf包的安装

绝大多数的common lisp包都是使用asdf组织的,可以使用asdf-install工具安装软件包,asdf-install使用之前需要先加载:

CL-USER> (asdf:oos 'asdf:load-op :asdf-install)

之后便可以使用asdf-install安装软件包,一般而言有三种方式:

1. 通过包的名字进行安装:

CL-USER> (asdf-install:install "iolib")

这时asdf-install会自动从cliki.net上下载可用的包并安装,http://www.cliki.net/asdf-install这个页面列出了在线可用的软件包的列表。

2. 通过包的url进行安装:

CL-USER> (asdf-install:install "http://weitz.de/files/cl-ppcre.tar.gz")

3. 通过包的本地路径进行安装:
CL-USER> (asdf-install:install “/home/levin/lisp/iolib.tar.gz”)

更加详细的安装方法可以参考asdf-install turorial.

分类: Lisp 标签: , ,

毕业求职经历

十一月 2nd, {2011 24 条评论 2,184 人阅读过  

前天去淘宝把三方交了,找工作就到此为止了,找工作的经历虽然不算复杂漫长,但也觉得很累很辛苦。

正式开始投简历找工作应该是从九月份开始的,九月中旬注册了几家招聘网站,陆陆续续地开始往外投简历,目标也比较明确,虽然专业一直是通信,但却想去互联网公司做后端开发,服务器或者分布式系统相关的开发,所以通信类的公司没有怎么投简历,只投了华为的云计算方向和爱立信的软件研发方向。国企没有投,片面地觉得国企是最没有战斗力的公司,还是想到互联网行业的私企做些有挑战性的工作。

第一家面试的公司是IBM研究院,在上地的钻石大厦,离北邮超级远,先坐车到清华西门,然后转车到西北旺,下车后发现右手边是农田,左手边是树林,没有路人,没有出租车,于是完全迷路了,后来沿着树林走了一段时间后偶然发现树林对面有座大楼冒出个头来,于是穿过树林过去问了下那楼的保安,说那就是钻石大厦,OMG,让我好找。IBM CRL没有笔试,师兄推荐的部门,因为是第一次面试没什么经验,也没有准备英文自我介绍,还有些紧张,做了一个简单的slide,没有被问到复杂的算法问题,因为时间比较短,所以挑了一两个重点的项目讲了讲,结束后感觉表现真的很烂,也可能是师兄推荐的原因最后让我拿到了二面的机会,二面安排在十一前,仍然是讲项目,在师兄的指点下改了slide,而且也提前准备了英文自我介绍,这中间也参加过几次面试,也有了点经验,所以感觉还不错,结束后几天也拿到了互联网公司的offer,IBM CRL感觉不太适合我,那边偏向于研究一些,像我这种做工程的在那边确实不是太好发展,于是联系师兄把情况说清楚了,已经浪费了大家两次面试的时间了,如果有第三次面试机会我还是不去了,这样IBM的面试也就到此为止了。

第二家面试的单位应该是华为了,之前参加过一个华为的机试,出奇的简单,就只有一道题补全一个函数,功能是求数组里面小于平均数的数的个数,两分钟就写完了然后在机房里面刷推想等下同学,于是就被赶出去了。过了不久通知面试,在知春路某酒店,记得通知的是12点,十一点半赶过去,等了一会儿面试官让先去吃饭,吃完饭回来继续等,直到等到下午将近一点半实在有点不耐烦了,去找hr简单表达了一下不满,于是过了十分钟大约一点半左右被安排了面试。虽然面试的是云计算,但我相信给我面试的那个姐姐应该对云计算也没有什么深入的了解,而且对我的开源项目表现出不屑,只对简历上写的实验室的863项目感点兴趣,说是因为是国家项目,其实学校里面做的国家项目大家都懂的,当时自己心情也确实没那么有耐心了,毕竟已经在那里浪费了两个小时的时间了,一个多小时后结束,第二天居然收到了二面通知,没有去参加二面,华为就这样结束了。

考虑到华为的这种情况,通信公司我已经完全不想考虑了,爱立信的机试我也没有参加。

网易有道经过实验室师兄内推搞到了一个提前参加笔试的机会,清华科技园创业大厦,笔试是在晚上,题量不大但做得很烂,但很幸运地收到了面试通知,一面的时候和面试官比较投缘,问的问题我也大都了解,tcp/ip,socket,epoll等等的,都比较熟,算法问题也都答的差不多,很顺利进了二面,二面表现比较差,问了几个系统设计的题,微博的推送机制,搜索引擎的搜索提示,这些我之前还真没去了解过,想了想把就重点说了下cache,结合着kv系统简单设计了下,面试官也没说好坏,开始问的两个算法题记不清是什么了,但印象中是没有什么好的思路的,那一次面试感觉非常差,但因为没有收到拒信也纠结了好久,终于在十一过后的某一天收到了三面的通知,有点意外,三面的大哥没有问算法,问我的软件的架构,分布式缓存系统设计等等,感觉答的还不错,感觉至少要比二面好,等了好久,却收到了有道的拒信,有道成了第一家拒掉我的公司,找工作需要实力也需要机缘,有道的面试我应该是实力和机缘都不够吧。

淘宝的笔试在华为二面的那天晚上,没有参加华为下午的二面也是怕他们太拖拉耽误了淘宝的笔试,笔试现场很混乱,因为没有记名,霸面的同学把屋子挤得满满的,记不清当时是怎么协调的了,出着汗答到时间结束,最后一个算法大题居然没来得及想,随便写了个简单算法,后来面试的时候瞅了一眼笔试的试卷,那个题只得了3分,后面的附加题到是抽时间做了,线程进程,锁,网络编程都是比较熟悉的东西。在北理参加360笔试的时候收到了淘宝的面试通知,东三环嘉泰金融大厦,离北邮也确实有点远,淘宝当天面试的人很多,但还算比较准时的,一面问的基础都比较熟悉,每次一被问到STL我就怂了,这东西只会简单的用用,不过跟面试官表达过长时间使用C之后,他问的关于STL的都是怎么用的方面,这些我还都是搞得定的,后来让写了个递归的小程序,也很简单,结束后让我去休息去等一会儿,过了一会儿又把我叫到一间小会议室里面,我问这是要干嘛,说是面试啊,原来是连着面的,于是自我介绍讲项目,二面的面试官感觉很犀利,问C10K,prefork/worker-master在C10K问题上的差别,我之前理解的也不太对,面试官也都给我解释清楚了,后来问了很多,关于读的书,平时的爱好等等,还出了两个智力题,只想出来了一个,本来有不少问题要问的,关于淘宝的开源产品等等的问题,后来HR在外面示意时间应该是超了,我就没仔细问,于是又去休息区等着了。中午淘宝请吃了个午饭,下午一点多三面,见到了大牛行癫,被问到了Memcached和Redis的哈希表的处理方式的问题,异步的expand,但Memcached其实我表达错了,Memcached是异步来rehash的,而Redis因为是单线程,它的rehash是嵌在各种操作(lookup,insert)中进行的,都不是一下全部完成的,因为大数据量时rehash会相当耗时,当然,我理解不对的地方行癫也都给我解释清楚了,最后还把二面关于淘宝基础产品的问题都问了,之后就是回来等消息了,几天后收到offer,没有传说中的几十w年薪,选择淘宝是因为在淘宝能做想做的方向,@colyli帮忙推荐到淘宝的虚拟化团队做分布式存储,这个方向我很感兴趣,所以拿到淘宝offer之后找工作就基本接近尾声了。

360的面试是在淘宝后的几天,北大博雅大酒店,打车杀了过去,360也是三面连着的,前两面是技术面,最后一面是hr面,一面的技术面问了很多基础知识,甚至还问了vim的操作,这些都比较熟悉,另外也问了C10K的问题,其它的都是在聊天了,面试官人很好,给我讲hadoop,分布式系统等等,二面也差不多的流程,感觉面试官主要是想看我能不能把心思都放到工作上,可能是因为简历上我在工作之外做的自己的项目比较多,当然,这个肯定不是问题,三面过后也同样拿到了360的offer,和淘宝几乎是同一时间,感觉非常对不起360,没有决定去却接受了人家的offer,这是非常损rp的,所以在把三方交给淘宝之后赶紧写邮件给360的hr拒掉了这个offer。

腾讯的笔试应该是在清华,比较晚了,因为当时拿到了淘宝的offer,其实已经不太想再折腾了,因为感觉选择越多最后纠结就越多,淘宝的工作我已经很满意了,但后来觉得腾讯是中国互联网老大,不去试试也不甘心,笔试还不错,题基本上都会,一面是在知春路的京仪大酒店,两点钟面试官非常准时,问的东西都很常细,线程池,锁,网络编程的细节,算法,都是些很细节的问题,那次面试我才知道有pthread_cond_timewait()这个函数,惭愧,二面安排在腾讯北京分公司,苏州街银科大厦,面试过程比较随意,面试人很好,感觉就是聊天,问了一些关于项目的问题,最后问了一个智力题,当时感冒还发着烧,脑子实在是转不动,于是就没想出来,于是就这样结束了,三面是hr面,hr叔叔人也很好,问了一下工作中遇到的问题,offer的期待等等,二十多分钟,最后送我走的时候还给我介绍了腾讯北京这边的情况,给我介绍了腾讯演播室,带我看了很多名星的签名,照片,走的时候还看到了超级正典腾讯18层的前台MM,突然觉得去腾讯工作也不错哈。hr说要11月1号到10号之间才会发offer,30号我就需要把三方交给淘宝,跟hr说过这个情况,说自己没有太多选择,希望能尽快得到腾讯的消息好做决定,但30号也实在太仓促只有两天的时间,于是最后只能选择了淘宝,今天是2号仍然没有收到腾讯的消息,有可能是挂了,即便是有幸拿到offer现在也去不了了。

参加过的笔试还有微软,EMC这两家笔试完都没消息了,微软笔试答得很烂,可以理解,EMC笔试感觉还不错的,结果也一样挂了,当时都没怎么重视笔试,以为笔试一般不会挂掉,但事实上我大多数都是挂在了笔试。

之前也参加过谷歌的笔试,安排在了清华的技科楼,当时顶着巨大的堵车压力勉强按时赶到清华,但诺大一个清华居然没有人知道技科楼怎么走,问了好久都没有找到路,最后好不容易找到个地图才发现了那位置,找到考场的时候已经开始考了将近20分钟了,最近我还很嚣张地提前交了试卷,其实感觉答得还算可以,不过肯定是过不了的,后来果然收到了谷歌的拒信,不过能有机会参加一次谷歌的笔试也很满足啦,嗯。

参加过笔试面试的差不多就这几家,没有广撒网式的投简历,之前也投过新浪搜狐亚马逊等互联网公司的简历,最后都放弃了笔试,当然,绝不是看不上这些公司,只是觉得有个差不多的工作就可以了,选择越多纠结越多,而且最后拿到别人的offer最后又不去给人家公司也会带来麻烦,影响人家招聘。

找工作的经历基本上就这些了,抓紧时间把毕业论文搞出来,也希望能有时间可以去淘宝实习几天,提前了解下要做的东西。

分类: My Life 标签:

Redis的事件循环与定时器模型

十月 7th, {2011 1 条评论 1,127 人阅读过  

假期的最后一天,简单翻阅了下Redis的源码,读一款server软件的源码我一般是从进程/线程模型开始的,Redis让我有些诧异,它采用了单进程单线程的模型,一般的server软件都会采用多进程或者多线程再或者多线程多进程混合的模型来设计,从而充分利用多核处理器的并行计算能力来提高软件的性能,Redis这种模型我只能推断程序的可并行化程度不高,顺序计算反而能省去多线程同步和维护线程池/进程池的开销,我对于数据库server端的设计没有什么经验也没有太多的理解,如有谬误欢迎大家指正。

当然,这里要写的不是关于Redis的进程模型,而是Redis的事件模型和定时器模型。

Redis没有依赖libevent,而是自己通过IO多路复用的方式来实现了事件循环和定时器,不像nginx或者apache有多种多路复用方式可供选择,Redis只采用了三种:epoll/kqueue/select,默认采用epoll,在linux环境下最优的方式当然是epoll,当在FreeBSD平台下epoll不存在时则使用kqueue,当然若两种方式都未定义则使用性能最差的select,我只阅读了跟epoll相关的代码。

main()函数的最后调用了aeMain()这个函数进入Redis的事件循环,这个函数的很简单,循环调用aeProcessEvents()来对事件进行处理:

void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);
        aeProcessEvents(eventLoop, AE_ALL_EVENTS);
    }
}

在此之前Redis做了很多初始化的工作,这些工作大多是在initServer()这个函数中执行的,初始化一些相关的list,dict等,调用aeCreateEventLoop()初始化eventloop,这个函数初始化eventloop相关的数据结构,并最终调用了epoll_create()函数,对epoll上下文进行初始化。紧接着Redis创建了用于listen的socket对象,并调用aeCreateFileEvent()把该socket描述符的读事件加入到事件池中去,另外,还调用了aeCreateTimeEvent()函数来初始化一下定时器,定期地执行serverCron()这个函数,接下来看一下aeCreateFileEvent()aeCreateTimeEvent()这两个函数。

aeCreateFileEvent()这个函数初始化aeFileEvent结构(该结构保存事件的一些状态,以及事件的文件描述符等),并调用aeApiAddEvent()函数将描述符相关的事件添加到事件池中,对于epoll它的实现如下:

static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
    aeApiState *state = eventLoop->apidata;
    struct epoll_event ee;
    /* If the fd was already monitored for some event, we need a MOD
     * operation. Otherwise we need an ADD operation. */
    int op = eventLoop->events[fd].mask == AE_NONE ?
            EPOLL_CTL_ADD : EPOLL_CTL_MOD;
 
    ee.events = 0;
    mask |= eventLoop->events[fd].mask; /* Merge old events */
    if (mask & AE_READABLE) ee.events |= EPOLLIN;
    if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
    ee.data.u64 = 0; /* avoid valgrind warning */
    ee.data.fd = fd;
    if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;
    return 0;
}

非常简洁,这个函数只不过是把epoll_ctl()相关的操作做了一下封装,至此描述符已经加入到事件池中进行监听了,接着看aeCreateTimeEvent()这个函数。

long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds,
        aeTimeProc *proc, void *clientData,
        aeEventFinalizerProc *finalizerProc)
{
    long long id = eventLoop->timeEventNextId++;
    aeTimeEvent *te;
 
    te = zmalloc(sizeof(*te));
    if (te == NULL) return AE_ERR;
    te->id = id;
    aeAddMillisecondsToNow(milliseconds,&te->when_sec,&te->when_ms);
    te->timeProc = proc;
    te->finalizerProc = finalizerProc;
    te->clientData = clientData;
    te->next = eventLoop->timeEventHead;
    eventLoop->timeEventHead = te;
    return id;
}

同样是初始化数据结构,但没有调用aeApiAddEvent()这个函数,当然,定时器又不需要文件描述符,当然不需要添加相关事件,定时器的实现只是使用了epoll_wait()的定时功能,aeAddMillisecondsToNow()这个函数顾名思义是把当前时间加上一个给定的毫秒数,然后算出一个when_sec和when_ms,eventloop对象的timeEventHead实际上是一个单向链表,它用于保存所有的定时器事件,当添加一个定时器事件时其实只是向该链表头中插入了一个元素,其会后由aeProcessEvents()这个函数遍历该链表取出超时的事件进行处理,接着我们看下这个事件处理里面最核心的函数。

int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
    int processed = 0, numevents;
 
    /* Nothing to do? return ASAP */
    if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;
 
    /* Note that we want call select() even if there are no
     * file events to process as long as we want to process time
     * events, in order to sleep until the next time event is ready
     * to fire. */
    if (eventLoop->maxfd != -1 ||
        ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
        int j;
        aeTimeEvent *shortest = NULL;
        struct timeval tv, *tvp;
 
        if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT))
            shortest = aeSearchNearestTimer(eventLoop);
        if (shortest) {
            long now_sec, now_ms;
 
            /* Calculate the time missing for the nearest
             * timer to fire. */
            aeGetTime(&now_sec, &now_ms);
            tvp = &tv;
            tvp->tv_sec = shortest->when_sec - now_sec;
            if (shortest->when_ms < now_ms) {
                tvp->tv_usec = ((shortest->when_ms+1000) - now_ms)*1000;
                tvp->tv_sec --;
            } else {
                tvp->tv_usec = (shortest->when_ms - now_ms)*1000;
            }
            if (tvp->tv_sec < 0) tvp->tv_sec = 0;
            if (tvp->tv_usec < 0) tvp->tv_usec = 0;
        } else {
            /* If we have to check for events but need to return
             * ASAP because of AE_DONT_WAIT we need to se the timeout
             * to zero */
            if (flags & AE_DONT_WAIT) {
                tv.tv_sec = tv.tv_usec = 0;
                tvp = &tv;
            } else {
                /* Otherwise we can block */
                tvp = NULL; /* wait forever */
            }
        }
 
        numevents = aeApiPoll(eventLoop, tvp);
        for (j = 0; j < numevents; j++) {
            aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
            int mask = eventLoop->fired[j].mask;
            int fd = eventLoop->fired[j].fd;
            int rfired = 0;
 
	    /* note the fe->mask & mask & ... code: maybe an already processed
             * event removed an element that fired and we still didn't
             * processed, so we check if the event is still valid. */
            if (fe->mask & mask & AE_READABLE) {
                rfired = 1;
                fe->rfileProc(eventLoop,fd,fe->clientData,mask);
            }
            if (fe->mask & mask & AE_WRITABLE) {
                if (!rfired || fe->wfileProc != fe->rfileProc)
                    fe->wfileProc(eventLoop,fd,fe->clientData,mask);
            }
            processed++;
        }
    }
    /* Check time events */
    if (flags & AE_TIME_EVENTS)
        processed += processTimeEvents(eventLoop);
 
    return processed; /* return the number of processed file/time events */
}

这其中的aeApiPoll()这个函数其实就是对epoll_wait()操作的一个封装,epoll_wait()的最后一个参数是一个毫秒级的超时时间,Redis充分利用了这个时间在对IO事件进行监听的同时来实现了定时。这个函数的前面一大部分代码是在计算这个超时时间的,它调用aeSearchNearestTimer()这个函数来获取最近要超时的一个定时器对象,如何获取的呢?就是遍历刚才的提到的那个timeEventHead链表,来找出时间值最小的一个,注意是遍历,因为链表中的定时器也是无序的,不过我相信作者有一天会把它换成红黑树或者其它的数据结构吧。如果找到一个将要超时的定时器,则将它与当前时间进行比较,如果当前时间大于定时器时间则表示定时器已超时,将超时时间设为0,若当前时间小于定时器时间,则将超时时间设为两者之差。如果定时器队列为空,或者说没有任何定时器事件,则可以根据AE_DONT_WAIT这个标志来决定epoll_wait()是non-blocking立即返回,还是一直阻塞在那里。

aeApiPoll()函数返回时,有两种情况,一种是IO事件被触发,另一种是定时器超时,当IO事件被触发时,遍历所有活跃描述符并调用相关的回调函数对其进行处理。当没有IO事件被触发,而是超时时,则返回值numevents为0,函数会转向processTimeEvents()来遍历定时器列表,调用定时器回调函数处理定时器事件,当IO事件被触发而并没有定时器超时时,如果设置了AE_TIME_EVENTS标志则也会对定时器列表进行遍历,主循环便是如此,我认为这会多少对效率有一定的影响,当然可能现在的Redis定时器列表并不太大,所以效率问题也可以忽略。

以上是简单地对今天的工作做的总结,欢迎大家批评指教。

分类: NoSql 标签: , ,

Memcached内存管理机制浅析

九月 8th, {2011 3 条评论 3,159 人阅读过  

Memcached的内存管理在网上也可以搜集到不少不错的文章,新浪的这篇《Memcached深度分析》讲得不错,读别人的文章还是不如自己直接去读源码分析源码来得直接,这里写一下我阅读Memcached源码时对于Memcached内存管理机制的理解。

Memcached的代码结构很简单,从main()函数入口进去之后便是几个模块的初始化函数,和内存管理相关的主要有两个函数,一个是assoc_init(),这个是用来初始化哈希表的,关于这个哈希表的作用留在外面讨论,另一个是slabs_init(),该函数用来初始化slab,下面先来讨论一下slab机制。

1. 内存slab的管理
1.1 slabs的初始化

Memcached把内存分为一个个的slab,每个slab又分成一个个的chunk,系统会定义一个slab_class数组,其中每个元素是都是一个对该slab的描述,包括这个slab里面的每个chunk的大小,这个slab里面包含多少个chunk等信息,先把slab分布情况打印出来看看,对Memcached的内存分配有个大体的认识,然后再去读代码可能会好一些。

$ memcached -vv
slab class 1: chunk size 80 perslab 13107
slab class 2: chunk size 104 perslab 10082
slab class 3: chunk size 136 perslab 7710
slab class 4: chunk size 176 perslab 5957
slab class 5: chunk size 224 perslab 4681
slab class 6: chunk size 280 perslab 3744
slab class 7: chunk size 352 perslab 2978
slab class 8: chunk size 440 perslab 2383
slab class 9: chunk size 552 perslab 1899
slab class 10: chunk size 696 perslab 1506
slab class 11: chunk size 872 perslab 1202
slab class 12: chunk size 1096 perslab 956
slab class 13: chunk size 1376 perslab 762
slab class 14: chunk size 1720 perslab 609
slab class 15: chunk size 2152 perslab 487
slab class 16: chunk size 2696 perslab 388
slab class 17: chunk size 3376 perslab 310
slab class 18: chunk size 4224 perslab 248
slab class 19: chunk size 5280 perslab 198
slab class 20: chunk size 6600 perslab 158
slab class 21: chunk size 8256 perslab 127
slab class 22: chunk size 10320 perslab 101
slab class 23: chunk size 12904 perslab 81
slab class 24: chunk size 16136 perslab 64
slab class 25: chunk size 20176 perslab 51
slab class 26: chunk size 25224 perslab 41
slab class 27: chunk size 31536 perslab 33
slab class 28: chunk size 39424 perslab 26
slab class 29: chunk size 49280 perslab 21
slab class 30: chunk size 61600 perslab 17
slab class 31: chunk size 77000 perslab 13
slab class 32: chunk size 96256 perslab 10
slab class 33: chunk size 120320 perslab 8
slab class 34: chunk size 150400 perslab 6
slab class 35: chunk size 188000 perslab 5
slab class 36: chunk size 235000 perslab 4
slab class 37: chunk size 293752 perslab 3
slab class 38: chunk size 367192 perslab 2
slab class 39: chunk size 458992 perslab 2
slab class 40: chunk size 573744 perslab 1
slab class 41: chunk size 717184 perslab 1
slab class 42: chunk size 1048576 perslab 1

这是Memcached的默认配置,chunk size是按照CHUNK_ALIGN_BYTES对齐的,chunk size相比于前一个slab中的chunk size有一个上升因子factor,1.4.7里面factor的默认值是1.25,我们可以看到按默认配置slab总共分成了42类。
先给出一个我用Dia画的Memcached的内存分配图,Dia不如Visio好用,凑合着画了一个,如果有理解不对的地方欢迎大家指出。

接下来看一下slabs_init()的代码,还是只保留关键代码,节省版面。

void slabs_init(const size_t limit, const double factor, const bool prealloc) {
    int i = POWER_SMALLEST - 1;
    unsigned int size = sizeof(item) + settings.chunk_size;
 
    mem_limit = limit;
    if (prealloc) {
        /* Allocate everything in a big chunk with malloc */
        mem_base = malloc(mem_limit);
        if (mem_base != NULL) {
            mem_current = mem_base;
            mem_avail = mem_limit;
        } 
    }
    memset(slabclass, 0, sizeof(slabclass));
    while (++i < POWER_LARGEST && size <= settings.item_size_max / factor) {
        /* Make sure items are always n-byte aligned */
        if (size % CHUNK_ALIGN_BYTES)
            size += CHUNK_ALIGN_BYTES - (size % CHUNK_ALIGN_BYTES);
 
        slabclass[i].size = size;
        slabclass[i].perslab = settings.item_size_max / slabclass[i].size;
        size *= factor;
    }
    power_largest = i;
    slabclass[power_largest].size = settings.item_size_max;
    slabclass[power_largest].perslab = 1;
#ifndef DONT_PREALLOC_SLABS
    {
        char *pre_alloc = getenv("T_MEMD_SLABS_ALLOC");
 
        if (pre_alloc == NULL || atoi(pre_alloc) != 0) {
			printf("prealloc memory.\n");
            slabs_preallocate(power_largest);
        }
    }
#endif
}

prealloc这个参数其实是跟后面的DONT_PREALLOC_SLABS这个宏是相关的,表示是否要在初始化的时候为slabs分配内存,如果需要预先为slabs分配内存,则先跟系统申请mem_limit字节的内存,之后的slab都是从这块内存上分配的,这块内存大小默认是64M,说起来当时犯了个很低级的错误,当时看到这里的时候发现mem_limit的默认值是1024 * 1024 * 64,于是断点在这里,发现malloc()没有返回NULL,当时想我一个2G的机器申请64G的内存到底是怎么分配成功的,纠结了好久才发现不是64G,是64M,所以读代码/写代码的时候还真得保持头脑清醒才行。。。

然后再说prealloc,如果没有定义DONT_PREALLOC_SLABS这个宏的话,初始化的时候会先申请64M的内存,接着调用preallocate这个函数,看下这个函数的注释,然后我要吐槽一下我的英语,前两天被它的那句注释搞晕了。

#ifndef DONT_PREALLOC_SLABS
/* Preallocate as many slab pages as possible (called from slabs_init)
on start-up, so users don't get confused out-of-memory errors when
they do have free (in-slab) space, but no space to make new slabs.
if maxslabs is 18 (POWER_LARGEST - POWER_SMALLEST + 1), then all
slab types can be made. if max memory is less than 18 MB, only the
smaller ones will be made. */
static void slabs_preallocate (const unsigned int maxslabs);
#endif

这句话的大体意思应该是:在启动的时候尽可能多地分配slabs页,所以用户不要因为内存足够却获得OOM而感到郁闷。。。

我当时的理解时:在启动的时候尽可能多地分配slabs页,这样用户就不会因为内存足够却被提示OOM而感到郁闷了。。。

我仔细地查阅源代码,发现如果prealloc,那么64M的内存用光之后并不会再去malloc新内存,跟我当时理解的这句注释的意思正好相反,仔细研究代码发现代码没有什么问题之后我反过来看了一眼这句注释,我觉得是我把这句英文理解错了吧。。。

OK,也就是说如果开启了prealloc功能的话,那么很有可能在有空闲内存的情况下分配内存失败,另外提前为slabs分配内存也有可能会造成内存的浪费,有可能所有的item都不会使用某个slab class,这样这个slab class里面分配的内存就浪费掉了,DONT_PREALLOC_SLABS在1.4.7里面是默认定义的,也就是说prealloc功能是默认关闭的,于是就不考虑先prealloc了。

slabs_init()接下来的代码就很简单了,对每个slab的chunk size进行对齐然后设置该slab class的相关成员变量的值。

1.2 slabclass_t的结构介绍
typedef struct {
    unsigned int size;      /* sizes of items */
    unsigned int perslab;   /* how many items per slab */
    void **slots;           /* list of item ptrs */
    unsigned int sl_total;  /* size of previous array */
    unsigned int sl_curr;   /* first free slot */
    void *end_page_ptr;         /* pointer to next free item at end of page, or 0 */
    unsigned int end_page_free; /* number of items remaining at end of last alloced page */
    unsigned int slabs;     /* how many slabs were allocated for this class */
    void **slab_list;       /* array of slab pointers */
    unsigned int list_size; /* size of prev array */
    unsigned int killing;  /* index+1 of dying slab, or zero if none */
    size_t requested; /* The number of requested bytes */
} slabclass_t;
static slabclass_t slabclass[MAX_NUMBER_OF_SLAB_CLASSES];

size和perslab这两个字段已经说过了,slots这里存放的是空闲的slab列表,当调用do_slabs_free()这个函数之后,要释放的chunk就被放到这个数组的尾部,sl_curr数组尾部开始的第一个空闲的chunk,sl_total表示数组的总大小,当sl_curr大小等于sl_total的时候数组会通过realloc()进行扩容,容易是旧容量的2倍。

end_page_ptr这个字段表示该slab里面的当前空闲的chunk地址,end_page_free,表示该slab中剩余的空闲chunk的数目,其它的几个字段按注释都很容易理解了。

1.3 创建新的slab

前面提到的slabs_preallocate()函数只不过是对每一个已初始化的slab_class调用do_slabs_newslab()函数为其分配一块slab内存空间,看下这个函数的代码。

static int do_slabs_newslab(const unsigned int id) {
    slabclass_t *p = &slabclass[id];
    int len = p->size * p->perslab;
    char *ptr;
 
    if ((mem_limit && mem_malloced + len > mem_limit && p->slabs > 0) ||
        (grow_slab_list(id) == 0) ||
        ((ptr = memory_allocate((size_t)len)) == 0)) {
        return 0;
    }
 
    memset(ptr, 0, (size_t)len);
    p->end_page_ptr = ptr;
    p->end_page_free = p->perslab;
 
    p->slab_list[p->slabs++] = ptr;
    mem_malloced += len;
 
    return 1;
}

slab_class的slab_list字段保存的是已分配的slabs列表,该列表实际上是个数组,当数组中没有空闲位置时则会调用grop_slab_list()对数组进行扩容,接下来便会调用memory_allocate()给slab分配内存,这个函数会检测是否已经开始了prealloc功能,如果开启了便会在预分配的内存块上申请一块内存,当这块预分配的内存用完时并不会对其进行扩容,于是便返回分配内存失败,这也就造成了系统明明有剩余内存,Memcached却提示SERVER_ERROR out of memory,当然,如果没有开启prealloc功能,这个函数便会直接调用malloc()分配内存,接下来对各个指针进行初始化。刚分配的空闲slab,它的end_page_str指针是指向slab内存首部的,end_page_free字段代表slab内存中包含中的chunk数。

1.4 在slab上分配内存

在某个slab_class上分配size大小的内存的函数是do_slabs_alloc(),这个函数有两个参数,要分配的内存字节数size,和该内存应该存在于哪个slab class上的slab class id. 这两个参数在是有相关性的,在调用该函数的时候class id一般是通过size来计算得出来的,先看一下这个函数:

static void *do_slabs_alloc(const size_t size, unsigned int id) {
    slabclass_t *p;
    void *ret = NULL;
 
    if (id < POWER_SMALLEST || id > power_largest) {
        return NULL;
    }
 
    p = &slabclass[id];
 
    /* fail unless we have space at the end of a recently allocated page,
       we have something on our freelist, or we could allocate a new page */
    if (! (p->end_page_ptr != 0 || p->sl_curr != 0 ||
           do_slabs_newslab(id) != 0)) {
        /* We don't have more memory available */
        ret = NULL;
    } else if (p->sl_curr != 0) {
        /* return off our freelist */
        ret = p->slots[--p->sl_curr];
    } else {
        /* if we recently allocated a whole page, return from that */
        ret = p->end_page_ptr;
        if (--p->end_page_free != 0) {
            p->end_page_ptr = ((caddr_t)p->end_page_ptr) + p->size;
        } else {
            p->end_page_ptr = 0;
        }
    }
 
    if (ret) {
        p->requested += size;
    }
 
    return ret;
}

1.如果end_page_ptr等于0,并且sl_curr等于0,则表示slab中已经没有空闲内存,并且回收的chunk free list里面也没有可用内存了,于是这时候需要调用do_slabs_newslab()创建新的slab。

2.如果sl_curr不等于0,则表示chunk free list中还有可用的内存,直接返回一个可用的chunk即可。

3.如果chunk free list里面没有可用内存,而slab中还有空闲内存,则直接从slab中申请一个chunk的内存,然后将end_page_ptr后移。

2. 对象item的管理
2.1 item对象的分配

存入系统的每个key-value对都会被转换成一个item,这个item中保存了相关的状态标志信息,当服务器收到一个set请求时便需要在内存中创建一个item,item的内存理所当然是在上面讨论过的slab分配器上分配的。item的存储使用了LRU的方法,把item链入一个链表中,其中全局变量heads[LARGEST_ID]tails[LARGEST_ID]这两个数组保存各个slab class所对应的item链表的表头和表尾,item创建的函数do_item_alloc()太长,就不把代码贴出来了,描述一下它的过程。

在这里先提一下前面提到的哈希表,哈希表是用来把item通过key散列到哈希表上的,这样就可以通过key来快速地定位item,在do_item_unlink()这个函数中,首先要把该item从哈希表中删除,然后再从list中移除,最后检测该item的refcount,如果refcount是0,则调用item_free()释放内存,item_free()再调用底层的slab_free()去释放内存,slab_free()只是do_slab_free()的线程安全版本,它在内部先加锁随后调用do_slab_free(),再之后解锁。

OK,接着看item的分配过程,首先会从链表的尾开始往前找,如果某节点的item设置了过期时间并且该item已过期,则回收该item,调用do_item_unlink()把它从链表中取出来,刚才说过do_item_unlink()这个函数在refcount为0的时候会释放掉这个item,所以为了防止这个item内存被释放,先将它的refcount设置为1,若向前查找50次都没有找到符合要求的item,则循环断开。

如果没有找到可以回收的item,然后就调用slabs_alloc()分配内存,如果内存也分配失败,就尝试着从链表尾开始向前找出一些没有人用的item(refcount=0),把它do_item_unlink()掉,这时候因为refcount=0,所以它相关的内存也会被释放还给slab分配器,这个尝试又从尾向前尝试50次,OK,slab分配器中可能又有可用内存了,再用slabs_alloc()分配内存,如果还失败。。。好吧,这次只能从链表中删除一些正在引用但过期时间小于current_time – CURRENT_REPAIR_TIME的节点,这个尝试又从尾向前尝试50次,OK,再做最后一次尝试再去slabs_alloc()分配内存,如果这次还是失败,那就彻底放弃了,内存分配失败。。。

3. Memcached的哈希表
3.1 Memcached用到的哈希算法

Memcached用到的哈希算法比较复杂,算法地址在http://burtleburtle.net/bob/hash/doobs.html,Memcached维护了两个哈希表,primary_hashtable和old_hashtable,primary_hashtable是当前正在使用的哈希表,当表没有进行扩张时从这张表中插入或者查找,old_hashtable用于哈希表扩张的时候使用,它指向旧的哈希表,当哈希表中的item数大于表的大小的3/2时,则哈希表进行扩张,此时插入和查找等操作都是在old_hashtable中进行的。

3.2 数据项插入哈希表

数据项插入哈希表时用了assoc_insert()这个函数,下面看下它的代码

int assoc_insert(item *it) {
    uint32_t hv;
    unsigned int oldbucket;
 
    assert(assoc_find(ITEM_key(it), it->nkey) == 0);  /* shouldn't have duplicately named things defined */
 
    hv = hash(ITEM_key(it), it->nkey, 0);
    if (expanding &&
        (oldbucket = (hv & hashmask(hashpower - 1))) >= expand_bucket)
    {
        it->h_next = old_hashtable[oldbucket];
        old_hashtable[oldbucket] = it;
    } else {
        it->h_next = primary_hashtable[hv & hashmask(hashpower)];
        primary_hashtable[hv & hashmask(hashpower)] = it;
    }
 
    hash_items++;
    if (! expanding && hash_items > (hashsize(hashpower) * 3) / 2) {
        assoc_expand();
    }
 
    MEMCACHED_ASSOC_INSERT(ITEM_key(it), it->nkey, hash_items);
    return 1;
}

如果expanding是true,哈希表正在扩张,则把item插入到old_hashtable中,否则则插入到primary_hashtable中,然后检测item数是否大于hashsize * 3 / 2,如果是,则进行扩张,哈希表的查找删除等操作也大致类似,不拿出来说了。

3.3 哈希表的扩张

哈希表的扩张其实是异步进行的,Memcached在初始化时在main()函数中会调用start_assoc_maintenance_thread()函数来开启一个线程对哈希表进行定期维护,线程函数通过对条件变量的wait进行睡眠,当被激活时发现expanding为true,则对哈希表进行扩张,把旧表的元素复制到新表中,然后释放旧表的内存空间,搞定后再睡去。。。触发哈希表扩张事件的函数是assoc_expand()

static void assoc_expand(void) {
    old_hashtable = primary_hashtable;
 
    primary_hashtable = calloc(hashsize(hashpower + 1), sizeof(void *));
    if (primary_hashtable) {
        if (settings.verbose > 1)
            fprintf(stderr, "Hash table expansion starting\n");
        hashpower++;
        expanding = true;
        expand_bucket = 0;
        pthread_cond_signal(&maintenance_cond);
    } else {
        primary_hashtable = old_hashtable;
        /* Bad news, but we can keep running. */
    }
}

这个函数让把old_hashtable指向primary_hashtable,之后给primary_hashtable重新分配内存空间,然后把expanding标志设为true,接着激活maintenace_cond信号,maintenace线程被唤醒开始异步地把old_hashtable中的元素拷贝到primary_hashtable中来。

OK,这是我对Memcached内存管理机制的一个简单的探索和了解,如有谬误的地方,欢迎大家批评指正。

分类: NoSql 标签: ,

VIM复制粘贴的那些事

九月 7th, {2011 8 条评论 1,714 人阅读过  

说起vim的复制粘贴一直是个比较困扰我的问题,之前一直用gvim,跟系统剪贴板之间的复制粘贴都没有问题,gvim毕竟还需要再开个窗口,麻烦,而且不如vim那样快捷,但vim里面最让我头痛的是复制粘贴问题,想把vim里面的内容复制到其它的地方貌似怎么也不行,粘贴进来的话免强可以,但格式可能会很乱,折腾来折腾去,今天先是发现了个往外复制比较蹩脚的办法:

:set mouse=v

这样鼠标就可以变成文本选择指针的样子,可以选择选择字体,然后点右键选复制,或者CTRL+SHIFT+C,但如果mouse=a这种模式下的话选择之后,右键的复制是灰色的,当然这种情况复制如果有行号的话行号也会被复制进去,所以在复制前先把行号关了,复制完再打开,这办法貌似也能凑合,但着实太不专业,而且太麻烦,远不如用gvim的“+y这种来得方便,“+y这个很多推友表示是可以的,但在我这里不可以,刚经@multiple1902指点,有可能是vim编译的问题。

查看了一下vim版本相关信息:

vim --version

发现clipboardxterm_clipboard这些选项都是不可使用的功能,这样看来果然是编译问题了,slackware讲求KISS,编译的时候没有加入该选项也可以理解,于是从vim官网svn上check下来源码(话说我为啥check下来的是7.2呢,难道7.3还没有stable…),重新编译。

编译的时候必须要选上的选项一个是–enable-multibyte,如果没加这个选项的话,中文应该就会乱码了。
另外关于剪贴板相关的选项,具体我也不清楚是哪一个,看了下几个相关的选项觉得最有可能是的是–enable-xim,只加了这两个选项之后编译完测试发现vim已经可以和系统剪贴板之间共享数据了,“+y可以使用了,世界一下子就清爽了很多,于是重新加完整选项编译:

./configure --prefix=/usr \
            --sysconfdir=/etc \
            --enable-tclinterp \
            --enable-pythoninterp \
            --enable-perlinterp \
            --enable-rubyinterp \
            --enable-cscope \
            --enable-multibyte \
            --enable-xim \
            --enable-gtk2-check \
            --enable-fontset \
            --with-x

OK,编译完成之后removepkg vim删掉之前slackware安装包中提供的vim,然后用makepkg打包安装,哎呀,说起包管理方式我还是最喜欢slackware的tgz包,虽然没有debian的apt那么华丽,但简洁就是美啊。

vim安装完之后已经可以用了,但存在一个问题,vim退出之后终端便会乱码,到网上搜了半天各种关于ubuntu下的解法,拿到slackware上来完全不适用,命令啦路径啦什么的根本找不到,最后搜到一个非常简单的解决办法,说是这个问题是因为gnome-terminal标题名称的不标准,我矁了一眼标题写着“终端”两个字,给出的解决办法是在前后各加一个空格,让系统让为它是英文,我直接把它改成了Terminal,然后再启动vim然后退出就没有问题了。

总算解决了我对于vim的怨念,一直以为是vim的问题,没想到竟然是slackware打包的问题。

分类: Linux 标签: ,

Memcached的线程模型及状态机

九月 5th, {2011 6 条评论 1,293 人阅读过  

Memcached是一种应用较广泛的分布式内存对象缓存系统,应用之余总想了解它的实现机理,这也就是开源的好处,以至于每接触一款优秀的开源软件都有去阅读它源代码的冲动,Memcached-1.4.7的代码量还是可以接受的,只有10K行左右,我比较关心的两个方面还是它的进程(线程)管理机制和内存管理机制,这里先简单写一下我对Memcached进程管理方面的理解。

Memcached使用libevent实现事件循环,libevent在Linux环境下默认采用epoll作为IO多路复用方法,这个不重要,接下来要讨论的是Memcached的进程管理模型。

Memcached采用了很典型的Master-Worker模型,采用的是多线程而不是多进程,而线程之间没有冗余的共享数据,这样便降低了多线程进行线程同步的开销,核心的共享数据是消息队列,主线程会把收到的事件请求放入队列,随后调度程序会选择一个空闲的Worker线程来从队列中取出事件请求进行处理。

在main()函数里面,Memcached为主线程调用event_init()创建了一个libevent base对象,随后调用thread_init()来初始化线程,我们来看下这个函数的实现。

void thread_init(int nthreads, struct event_base *main_base) {
    int         i;
 
    pthread_mutex_init(&cache_lock, NULL);
    pthread_mutex_init(&stats_lock, NULL);
 
    pthread_mutex_init(&init_lock, NULL);
    pthread_cond_init(&init_cond, NULL);
 
    pthread_mutex_init(&cqi_freelist_lock, NULL);
    cqi_freelist = NULL;
 
    threads = calloc(nthreads, sizeof(LIBEVENT_THREAD));
    if (! threads) {
        perror("Can't allocate thread descriptors");
        exit(1);
    }
 
    dispatcher_thread.base = main_base;
    dispatcher_thread.thread_id = pthread_self();
 
    for (i = 0; i < nthreads; i++) {
        int fds[2];
        if (pipe(fds)) {
            perror("Can't create notify pipe");
            exit(1);
        }
 
        threads[i].notify_receive_fd = fds[0];
        threads[i].notify_send_fd = fds[1];
 
        setup_thread(&threads[i]);
    }
 
    /* Create threads after we've done all the libevent setup. */
    for (i = 0; i < nthreads; i++) {
        create_worker(worker_libevent, &threads[i]);
    }
 
    /* Wait for all the threads to set themselves up before returning. */
    pthread_mutex_lock(&init_lock);
    while (init_count < nthreads) {
        pthread_cond_wait(&init_cond, &init_lock);
    }
    pthread_mutex_unlock(&init_lock);
}

代码很简单,初始化几个全局锁和一个条件变量,init_lock和init_cond这一对锁/条件变量的作用很明显,它们的作用是用来等待所有的worker线程启动完毕后,thread_init()才可以继续执行,见该函数最后的几句,非常通用的用法,等待init_count数达到预定的线程数后主线程方可继续执行,否则就一直在wait,每创建一个worker线程就会让init_count的值加1,创建worker线程的工作是在setup_thread()函数中进行的,这个后面讨论。

thread_init()在初始化完全局锁并为线程池分配分配空间之后便开始对线程池中的每个线程进行初始化,哦,在这之前把全局的调度线程设置成为主线程,OK,接下来就开始遍历线程池,每个线程都有一对notify_fd,它们通过管道连接,这个管道便是主线程对线程池中的工作线程进行调度的接口,看一下setup_thread()这个函数的实现(节省版面,只保留了关键代码)。

static void setup_thread(LIBEVENT_THREAD *me) {
    me->base = event_init();
    if (! me->base) {
        fprintf(stderr, "Can't allocate event base\n");
        exit(1);
    }
 
    /* Listen for notifications from other threads */
    event_set(&me->notify_event, me->notify_receive_fd,
              EV_READ | EV_PERSIST, thread_libevent_process, me);
    event_base_set(me->base, &me->notify_event);
 
    if (event_add(&me->notify_event, 0) == -1) {
        fprintf(stderr, "Can't monitor libevent notify pipe\n");
        exit(1);
    }
 
    me->new_conn_queue = malloc(sizeof(struct conn_queue));
    if (me->new_conn_queue == NULL) {
        perror("Failed to allocate memory for connection queue");
        exit(EXIT_FAILURE);
    }
    cq_init(me->new_conn_queue);
}

首先调用event_init()为该线程初始化一个事件池,我们知道libevent的事件池(event base)不是线程安全的,所以每个线程需要有自己的事件池,所有的IO事件都由这个事件池来处理,在这个函数里面,首先将notify_receive_fd添加到事件监听循环中,设置回调函数thread_libevent_process(),当主线程向该worker线程通过pipe IPC发送消息时,便会触发该worker线程执行thread_libevent_process()函数。随后setup_thread()函数创建connection队列并将其初始化。

接着回过头看thread_init()函数,初始化线程池结束以后,所需要做的工作便是为线程池创建对应的线程,也就是调用了create_worker()函数,这个函数只不过把pthread_create()做了一个简单的封装,至少到目前为止里面没有什么特别的代码,线程的执行函数是worker_libevent(),该函数将全局线程数加一,随后便调用event_base_loop(),完全将事件循环交给了libevent,当然到目前为止libevent的事件池中只有用来进行IPC的管道文件描述符(说到这里,当时我还在奇怪呢,代码执行这里还并没有添加socket描述符到事件池里面,那event_loop岂不一执行就结束了,后来才反应过来里面有IPC管道文件描述符了),那么这些描述符是在什么时候添加的呢?刚才已经讨论过了,是在setup_thread()函数里面,它的事件回调函数是thread_libevent_process(),OK,接下来我们看看这个函数吧。

static void thread_libevent_process(int fd, short which, void *arg) {
    LIBEVENT_THREAD *me = arg;
    CQ_ITEM *item;
    char buf[1];
 
    if (read(fd, buf, 1) != 1)
        if (settings.verbose > 0)
            fprintf(stderr, "Can't read from libevent pipe\n");
 
    item = cq_pop(me->new_conn_queue);
 
    if (NULL != item) {
        conn *c = conn_new(item->sfd, item->init_state, item->event_flags,
                           item->read_buffer_size, item->transport, me->base);
        if (c == NULL) {
            close(item->sfd);
        } else {
            c->thread = me;
        }
        cqi_free(item);
    }
}

函数的一开始便从fd中读取一个字节,不在乎这一个字节是什么,只是主线程发过来的一个单字节(实际上是个\0),用来触发fd的READ事件,主线程通过往worker线程的notify_write_fd中写入一个单字节来实现对worker线程的调度,因此这个函数里面首先要先把这一个字节读取出来,否则会影响以后的事件循环。紧接着从connection队列中弹出一个item,然后根据这个item的信息再创建一个connection,这里的conn还是不要理解为连接的好,理解成任务更好一些,conn对象中有读写缓冲区,状态(用来实现Memcached的有限状态机)等,这conn_new()这个函数里面会创建一个conn对象并对它进行初始化,但最重要的操作是设置对socketfd的事件监听函数,event_handler(),这个函数又调用drive_machine(),其中就实现了Memcached的有限状态机,通过对state的不同值来进行不同的操作,具体的状态如下:

const char* const statenames[] = { "conn_listening",
						      "conn_new_cmd",
						        "conn_waiting",
							"conn_read",
							"conn_parse_cmd",
							"conn_write",
							"conn_nread",
							"conn_swallow",
							"conn_closing",
							"conn_mwrite" };

关于状态机是怎么运行的就不讨论了,代码篇幅过长,涉及到的细节太多,不展开说了,要说的一点是状态机在哪里进入的conn_listening状态,这是某个socketfd的起始状态,socket需要先监听然后accept,再之后才可以进行read,write等操作,listen必然会在程序初始化的过程中调用,我们可以看到在main()函数里面线程初始化结束以后,便开始对socket描述符进行初始化,获取系统存在几个可用的地址信息,然后创建socket描述符,再然后bind(),再之后调用listen(),这些操作都是在server_socket()函数中执行的,该函数初始化监听socket,并针对服务器做一些socket options的设置,最后最关键的一步是针对该socket调用了conn_new(),但事件池是main_base,也就是说accept()事件是由主线程接收到的,accept()是发生在主线程内,这样也就避免了多线程accept()的惊群,accept()函数的执行也是在状态机循环中执行的,drive_machine()函数中,accept()完成后,主线程便调用dispatch_conn_new()对该描述符进行调度,状态机的代码太长,不列出来了,看一看dispatch_conn_machine()的代码吧。

void dispatch_conn_new(int sfd, enum conn_states init_state, int event_flags,
                       int read_buffer_size, enum network_transport transport) {
    CQ_ITEM *item = cqi_new();
    int tid = (last_thread + 1) % settings.num_threads;
 
    LIBEVENT_THREAD *thread = threads + tid;
 
    last_thread = tid;
 
    item->sfd = sfd;
    item->init_state = init_state;
    item->event_flags = event_flags;
    item->read_buffer_size = read_buffer_size;
    item->transport = transport;
 
    cq_push(thread->new_conn_queue, item);
 
    MEMCACHED_CONN_DISPATCH(sfd, thread->thread_id);
    if (write(thread->notify_send_fd, "", 1) != 1) {
        perror("Writing to thread notify pipe");
    }
}

创建一个item并压入队列,然后通过一个简单的哈希从线程池中找出一个线程,写一个空字符来激活这个线程(前面讨论过了),接着被激活的线程就会从conn_queue中弹出一个item来进行处理。

OK,关于Memcached的线程模型要说的就这些吧,我自己读源码时对它的浅显的理解,欢迎大家批评讨论。

分类: NoSql 标签: , ,

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

八月 5th, {2011 14 条评论 1,966 人阅读过  

近两天给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 30 条评论 2,686 人阅读过  

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

之前的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++ 标签: ,