Published: 2019-01-27

CommonLisp UDP库usocket使用心得

本文记录了自己使用usocket创建一个UDP over RPC框架时遇到的一个问题。

当我刚开始做的时候,是这样的流程:

;; server 先bind本地一个port
(usocket:socket-connect nil :protocol :datagram
                            :local-host server-host
                            :local-port server-port)
-> server-socket

;; 然后server调用receive来监听这个socket
(usocket:socket-receive server-socket nil data-max-length) ;; 等待直到有数据
-> return-buffer return-length client-host client-port


;; client 通过connect来连接到server的host port
(usocket:socket-connect server-host server-port
                        :protocol :datagram
                        :local-host client-host
                        :local-port client-port)
-> client-socket


;; client 监听 server的返回
(usocket:socket-receive client-socket nil data-max-length) ;; 等待直到有数据

-> return-buffer return-length server-host server-port

;; 然后client 发送数据
(usocket:socket-send client-socket buffer (length buffer))


;; server收到数据后,发送回数据
(usocket:socket-send server-socket buffer (length buffer)
                         :host client-host
                         :port client-port)


采用上面的收发逻辑是可以实现一个UDP服务器和客户端交互的, 然而总觉得怪怪的,因为server和client的非对称性,而UDP协议中两者是“对称”的。

现在比如说有多个节点s1, s2, s3…, 其中每个节点同时要充当server相应其它节点的请求,和充当client在自己有需要时请求其它节点。 按照上面的流程,当其中的一个节点比如s1先充当server bind到一个port后(调用一次 socket-connect ),当它要向s2发送消息时(充当client),又需要调用 socket-connect 指定s2的地址端口和自己的地址端口时, 会产生错误“端口已被使用”(因为两次生成的socket绑定到同一个port),而我需要对于每个节点来说收发都在同一个port上(因为其他节点根据收到你消息的port来给你发信息)。

我之所以陷入这种困境是受这个例子的影响,限制了思维,经过一番研究发现更为合适的流程是下面这样的,而且更为“美观”:

;; server 先bind本地一个port 建立一个socket
(usocket:socket-connect nil :protocol :datagram
                            :local-host server-host
                            :local-port server-port)
-> server-socket

;; 然后server调用receive来监听这个socket
(usocket:socket-receive server-socket nil data-max-length) ;; 等待直到有数据
-> return-buffer return-length client-host client-port


;; client 也先bind到本地一个port 建立一个socket
(usocket:socket-connect server-host server-port
                        :protocol :datagram
                        :local-host client-host
                        :local-port client-port)
-> client-socket


;; client 监听 socket上的数据
(usocket:socket-receive client-socket nil data-max-length) ;; 等待直到有数据

-> return-buffer return-length server-host server-port


;; 如果client 需要发送数据
(usocket:socket-send client-socket buffer (length buffer)
                     :host server-host
                     :port server-port )


;; server 收到数据后,发送回数据
(usocket:socket-send server-socket buffer (length buffer)
                     :host client-host
                     :port client-port)

可以看到这个流程是对称的,对比之前的步骤差异就在建立client socket的时候没有绑定到另外一端到哪个地址,而是在发送的时候指定地址,并且同时可以监听这个socket上的消息。

这样可以解决上面举的例子的困境,因为节点充当server或者client的时候其实用的是同一个socket,在上面收发而已。

也就是说,之前的client在创建socket的时候就指定了另一端(server)的方向,和server创建时不指定相冲突,而现在client创建的socket是不指定另一端的,而在发送时指定地址, 该socket同时也能作为server使用,这样端口不冲突。其实从receive/send的角度来考虑会比server/client的方式要清晰。

理清楚其实很简单。可惜我一开始在上面困住了时候看不透,究其原因:

  1. 自己对于socket网络编程的不清晰(UDP的具体流程和受TCP方式的影响)
  2. common lisp的usocket库 API说明不清晰,或者设计有些许不合理
  3. 缺乏common lisp UDP 相关文档示例

故此做个小记。

Author: Nisen

Email: imnisen@163.com