Common Lisp进行网络编程可用的库还是挺多的,比较常用的库有usocket和iolib,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的其它参考文档