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

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

回顾

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

  • 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请求。

它的调用方式如下:

emacs-lisp 复制代码
(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转化为对应的字符串

emacs-lisp 复制代码
(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调用的思路类似,这里就不多啰嗦了。

emacs-lisp 复制代码
(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响应体的内容了。但是这部分的工作就交给各位读者去尝试了

相关推荐
aluluka1 天前
《elisp 每日一练》——Process Sentinel 与 json集成
json·emacs·lisp·elisp
Masimaro4 天前
《elisp 每日一练》—— 同步进程调用
emacs
aluluka5 天前
《elisp 每日一练》——定时器机制
emacs·lisp·elisp
aluluka6 天前
《elisp 每日一练》——hook 与 advice 综合练习
emacs·lisp·elisp
Masimaro9 天前
《elisp 每日一练》——mode hook
emacs
智购科技自动售货机工厂9 天前
省电40%的底层逻辑:冷热双温售货机基于SaaS的智能温控算法~YH
单片机·嵌入式硬件·百度·架构·emacs
Masimaro12 天前
《elisp》 每日一练——hook 机制详解
emacs
Masimaro12 天前
《elisp 每日一练》——major mode 综合练习
emacs
aluluka15 天前
《elisp 每日一练》—— syntax table 与 font lock
emacs·lisp·elisp