存档

文章标签 ‘quicklisp’

Common Lisp使用iolib进行网络编程

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

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 标签: , ,