存档

‘Lisp’ 分类的存档

Common Lisp使用iolib进行网络编程

一月 4th, {2012 2 条评论 15,284 人阅读过  

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 1 条评论 11,529 人阅读过  

前段时间在学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 7 条评论 14,229 人阅读过  

在切入正题之前先写点不相关的,工作确定之后便开始忙论文的事,忙里偷闲总想搞点什么以做娱乐,不得不说,腾讯面试官说过的要精通两到三门不同的语言我印象很深刻,自己也想尝试一下新东西,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 标签: , ,