在前面一系列文章中,我们讨论了定时器以及同步和异步方式调用进程。本篇是回顾和综合练习篇
回顾
首先我们介绍了两种定时器:
run-at-time创建普通定时器run-with-idle-timer创建空闲定时器
接着我们讨论了同步进程调用的方式:
shell-command-to-stringprocess-linescall-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)