IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    Common Lisp使用iolib进行网络编程

    levin发表于 2012-01-04 08:38:24
    love 0

    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的其它参考文档

    [1] http://common-lisp.net/project/iolib/manual/
    [2] http://pages.cs.wisc.edu/~psilord/blog/data/iolib-tutorial/tutorial.html


沪ICP备19023445号-2号
友情链接