《elisp 每日一练》——定时器与进程调用综合练习

《elisp 每日一练》——定时器与进程调用综合练习

Masimaro
2026-10-26 / 0 评论 / 1 阅读 / 正在检测是否收录...

在前面一系列文章中,我们讨论了定时器以及同步和异步方式调用进程。本篇是回顾和综合练习篇

回顾

首先我们介绍了两种定时器:

  • run-at-time 创建普通定时器
  • run-with-idle-timer 创建空闲定时器

接着我们讨论了同步进程调用的方式:

  • shell-command-to-string
  • process-lines
  • call-process

后面我们讨论了异步调用进程的方式

早期版本使用 start-process 新版本使用控制粒度更细的 make-process

最后介绍了两种解析json的方式:早期版本使用纯elisp实现的 json-read 和 json-read-from-string。后来新版本使用的接口是基于 Jansson 实现的 json-parse-string、json-parse-buffer

练习

本节的综合练习,我们来实现一个基于 curl 的异步http 调用功能。

虽然 Emacs原生有一个 url-retrieve 的方案可以支持http的异步访问,但是它的问题也比较明显:相比较curl来说,它的使用显得十分诡异,异步函数 url-retrieve 在某些复杂场景并不能做到真正的异步。所以在一些对网络请求有较高要求的场景通常是通过调用 curl 程序实现通用的外部http请求。

它的调用方式如下:

(let ((target-url "https://api.ipify.org?format=json"))
  (my-async-http-request
   :url target-url
   :method "GET"
   :data nil
   :headers '(:Accept "application/json")
   :response
   (lambda (response)
     (condition-case err
         (let* (((json json-parse-string response :object-type 'alist))
                (ip (alist-get 'ip json)))
           (message "解析成功:您的ip是 :%s" ip))
       (error
        (message "json 解析失败"))))))

header 是一个 alist / plist 结构,最终需要将这种风格的键值对转化为字符串形式的header参数,用来调用 curl。我们先封装一个函数用于将alist / plist 结构的header转化为对应的字符串

(defun my--plist-to-curl-args(headers)
  "把 HEADERS(形如 (:Accept \"application/json\" :X-Foo \"bar\") 的 plist,
或 alist ((. \"Accept\") . \"application/json\") 结构)转换成 curl -H 参数列表。"
  (let ((args))
    (cond
     ;; plist风格 (:Key "val" :Key2 "val2")
     ((and headers (keywordp (car headers)))
      (let ((rest headers))
        (while rest
          (let ((k (car rest))
            (v (cdr rest)))
            (push "-H" args)
            (push (format "%s: %s" (substring (symbol-name k) 1) v) args))
          (setq rest (cddr rest)))))
     ;; alist 风格 ((\"Accept\" . \"application/json\") ...)
     ((listp headers)
      (dolist (pair headers)
        (push "-H" args)
        (push (format "%s: %s" (car pair) (cdr pair)) args))))
    (nreverse args)))

对于 alist 风格的数据转化比较好理解,它实际上就是一个列表,列表的每一项都是一个cons cell。所以我们只需要遍历它的每一子项,针对子项分别调用 car 和 cdr 就可以获取每一项。

但是对于 plist 形式的数据处理的稍微复杂一些。我们在前面提到过 plist 本身也是一个个list, '(:key value) 的数据实际上是一个拥有两个成员的list,list的第一个元素是符号 :key,第二个是数据 value。所以我们可以使用通过 car cdr 分别取它的键和值,对于下一个键值对可以使用 cddr 获取。这里也是采用循环的思路获取每一项,需要注意的是 key 符号本身包含了最左边的 : 所以我们需要去掉它

关于调用 curl 来说,跟我们之前编写的cmake调用的思路类似,这里就不多啰嗦了。

(defun my-async-http-request (&rest param)
  "向远程接口发起异步HTTP请求
参数:
  :url 目标url (必填)
  :method HTTP 请求方法字符串,默认 \"GET\"
  :headers plist 或者 alist 结构,每一项对应着http header 中的一个键值对
  :data 请求体
  :response 接收响应 body 字符串的回调函数
"
  (let* ((url (plist-get param :url))
         (method (or (plist-get param :method) "GET")) ;;默认是get请求
         (headers (plist-get param :headers))
         (data (plist-get param :data))
         (callback (plist-get param :response))
         (buf-name (format "*Async-HTTP-%s*" (format-time-string "%H%M%S-%3N")))
         (output-buf (get-buffer-create buf-name))
         (curl-args (list "-s" "-X" method url)))

    ;;拼接header 头部
    (setq curl-args (append curl-args (my--plist-to-curl-args headers)))

    ;; 如果有请求的data,追加 -d 参数
    (when data
      (setq curl-args (append curl-args (list "-d" data))))

    ;;初始化输出缓冲区
    (with-current-buffer output-buf
        (erase-buffer))
    (message "正在向 [%s] 发起异步%s请求..." url method)
    (message "curl-args= %s" curl-args)
    ;;正式使用curl 发起请求
    (make-process
     :name "my-async-curl-proc"
     :buffer output-buf
     :command (append '("curl") curl-args)
     :filter nil

     ;; 退出时根据退出状态负责调用用户回调
     :sentinel
     (lambda (proc event)
       (when (eq (process-status proc) 'exit)
         (let ((exit-code (process-exit-status proc))
               (buf (process-buffer proc)))
           ;;正常则读取返回的http响应包
           (if (and (zerop exit-code) (buffer-live-p buf))
               (let ((response-str (with-current-buffer buf (buffer-substring (point-min) (point-max)))))

                 ;; 存在回调函数则调用回调
                 (when callback
                   (funcall callback response-str))

                 ;;清理临时buffer
                 (kill-buffer buf))
             (message "请求失败,错误码:%d" exit-code)
             (when (buffer-live-p buf)
               (kill-buffer buf)))))))))

这里我们将buffer的所有数据都通过一个参数传给回调函数,更加合理的设计是传入三个参数: status_code、response_header、response_data。调用者就不需要自己拆分http响应体的内容了。但是这部分的工作就交给各位读者去尝试了

0

评论 (0)

取消